1206 lines
49 KiB
PHP
1206 lines
49 KiB
PHP
<?php
|
|
|
|
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
|
|
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
|
|
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
|
|
use Carbon\Carbon;
|
|
|
|
class Firewall extends Component
|
|
{
|
|
public bool $firewallEnabled = false;
|
|
public array $rules = [];
|
|
public array $aliases = [];
|
|
public array $ipsets = [];
|
|
public bool $loading = false;
|
|
public string $error = '';
|
|
public bool $showErrorModal = false;
|
|
public bool $showRuleForm = false;
|
|
public array $networkAdapters = [];
|
|
public array $adapterFirewallStates = [];
|
|
public array $firewallLogs = [];
|
|
public int $logLimit = 1000;
|
|
public array $availableIps = [];
|
|
public bool $showDeleteModal = false;
|
|
public ?int $deletingRulePos = null;
|
|
public string $logError = '';
|
|
|
|
// Rule creation/editing
|
|
public ?int $editingRulePos = null;
|
|
public string $ruleAction = 'ACCEPT';
|
|
public string $ruleSource = '';
|
|
public string $ruleDest = '';
|
|
public string $ruleProto = 'tcp';
|
|
public string $ruleDport = '';
|
|
public string $ruleSport = '';
|
|
public string $ruleComment = '';
|
|
public bool $ruleEnable = true;
|
|
public string $ruleMacro = '';
|
|
public string $ruleIface = '';
|
|
public string $ruleLog = 'nolog';
|
|
|
|
// Alias management
|
|
public string $aliasName = '';
|
|
public string $aliasCidr = '';
|
|
public string $aliasComment = '';
|
|
|
|
// IPSet management
|
|
public string $ipsetName = '';
|
|
public string $ipsetComment = '';
|
|
|
|
public function mount(Server $server): void
|
|
{
|
|
parent::mount($server);
|
|
$this->server->loadMissing('ipAddresses');
|
|
$this->loadAvailableIps();
|
|
$this->loadFirewallOptions();
|
|
$this->loadFirewallRules();
|
|
$this->loadNetworkAdapters();
|
|
$this->loadFirewallLogs();
|
|
}
|
|
|
|
protected function loadAvailableIps(): void
|
|
{
|
|
$allIps = collect();
|
|
if ($this->server->primaryIpv4) {
|
|
$allIps->push(['ip' => $this->server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary']);
|
|
}
|
|
if ($this->server->primaryIpv6) {
|
|
$allIps->push(['ip' => $this->server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary']);
|
|
}
|
|
foreach ($this->server->ipAddresses as $ipAddress) {
|
|
$allIps->push(['ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Assigned']);
|
|
}
|
|
$this->availableIps = $allIps->unique('ip')->values()->toArray();
|
|
}
|
|
|
|
/**
|
|
* Make a firewall API request using the dedicated firewall credentials
|
|
*/
|
|
protected function firewallRequest(string $url, string $method = 'get', array $data = []): \Illuminate\Http\Client\Response
|
|
{
|
|
$baseUrl = 'https://pve.virtnet.bond';
|
|
$user = 'root@pam';
|
|
$tokenId = 'firewall';
|
|
$secret = '0a439967-0a44-4b94-8906-c36b3a40cf68';
|
|
|
|
$fullUrl = rtrim($baseUrl, '/') . '/api2/json' . $url;
|
|
|
|
// Proxmox API token format: PVEAPIToken=USER@REALM!TOKENID=SECRET
|
|
$authHeader = 'PVEAPIToken=' . $user . '!' . $tokenId . '=' . $secret;
|
|
|
|
// For firewall log endpoint, accept text/plain as well
|
|
$acceptHeader = str_contains($url, '/firewall/log')
|
|
? 'application/json, text/plain, */*'
|
|
: 'application/json';
|
|
|
|
$request = Http::withHeaders([
|
|
'Authorization' => $authHeader,
|
|
'Accept' => $acceptHeader,
|
|
])->withOptions([
|
|
'verify' => true,
|
|
]);
|
|
|
|
// For PUT/POST requests, Proxmox API expects form data
|
|
if (in_array(strtolower($method), ['put', 'post']) && !empty($data)) {
|
|
return $request->asForm()->$method($fullUrl, $data)->throw();
|
|
}
|
|
|
|
// For GET requests, if data is provided, it should be query parameters
|
|
if (strtolower($method) === 'get' && !empty($data)) {
|
|
$queryString = http_build_query($data);
|
|
$fullUrl .= (str_contains($fullUrl, '?') ? '&' : '?') . $queryString;
|
|
return $request->$method($fullUrl)->throw();
|
|
}
|
|
|
|
return $request->$method($fullUrl)->throw();
|
|
}
|
|
|
|
public function loadFirewallOptions(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->firewallEnabled = isset($data['enable']) && $data['enable'] == 1;
|
|
$this->error = '';
|
|
$this->showErrorModal = false;
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
// Try to extract more detailed error from response if available
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to load firewall options: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
$this->firewallEnabled = false;
|
|
}
|
|
}
|
|
|
|
public function toggleFirewall(): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
// Store the current state before toggling
|
|
$currentState = $this->firewallEnabled;
|
|
// Calculate the new desired state (opposite of current)
|
|
$newState = $currentState ? 0 : 1;
|
|
$payload = ['enable' => $newState];
|
|
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options',
|
|
'put',
|
|
$payload
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
// Reload from API to get the actual state (don't assume it worked)
|
|
$this->loadFirewallOptions();
|
|
$this->notify('Firewall ' . ($this->firewallEnabled ? 'enabled' : 'disabled') . ' successfully', 'success');
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->notify('Failed to toggle firewall: ' . $message, 'error');
|
|
// Reload to ensure UI matches actual state
|
|
$this->loadFirewallOptions();
|
|
}
|
|
} catch (\Illuminate\Http\Client\RequestException $e) {
|
|
$errorMessage = $e->getMessage();
|
|
// Try to extract more detailed error from response
|
|
if ($e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to toggle firewall: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
// Reload to ensure UI matches actual state
|
|
$this->loadFirewallOptions();
|
|
} catch (\Exception $e) {
|
|
$this->error = 'Failed to toggle firewall: ' . $e->getMessage();
|
|
$this->showErrorModal = true;
|
|
// Reload to ensure UI matches actual state
|
|
$this->loadFirewallOptions();
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function loadFirewallRules(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->rules = is_array($data) ? $data : [];
|
|
$this->error = '';
|
|
$this->showErrorModal = false;
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
// Try to extract more detailed error from response if available
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to load firewall rules: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
$this->rules = [];
|
|
}
|
|
}
|
|
|
|
public function startCreateRule(): void
|
|
{
|
|
$this->resetRuleForm();
|
|
|
|
// Set default destination to first IP address
|
|
if (!empty($this->availableIps)) {
|
|
$this->ruleDest = $this->availableIps[0]['ip'];
|
|
}
|
|
|
|
// Set default interface to first network adapter
|
|
if (!empty($this->networkAdapters) && isset($this->networkAdapters[0]) && isset($this->networkAdapters[0]['name'])) {
|
|
$this->ruleIface = $this->networkAdapters[0]['name'];
|
|
}
|
|
|
|
$this->editingRulePos = null;
|
|
$this->showRuleForm = true;
|
|
}
|
|
|
|
public function startEditRule(int $pos): void
|
|
{
|
|
$rule = collect($this->rules)->firstWhere('pos', $pos);
|
|
if ($rule) {
|
|
$this->editingRulePos = $pos;
|
|
$this->showRuleForm = true;
|
|
$this->ruleAction = $rule['action'] ?? 'ACCEPT';
|
|
$this->ruleSource = $rule['source'] ?? '';
|
|
$this->ruleDest = $rule['dest'] ?? '';
|
|
$this->ruleProto = $rule['proto'] ?? '';
|
|
$this->ruleDport = $rule['dport'] ?? '';
|
|
$this->ruleSport = $rule['sport'] ?? '';
|
|
$this->ruleComment = $rule['comment'] ?? '';
|
|
$this->ruleEnable = !isset($rule['enable']) || $rule['enable'] == 1;
|
|
$this->ruleMacro = $rule['macro'] ?? '';
|
|
$this->ruleIface = $rule['iface'] ?? '';
|
|
$this->ruleLog = $rule['log'] ?? 'nolog';
|
|
}
|
|
}
|
|
|
|
public function resetRuleForm(): void
|
|
{
|
|
$this->ruleAction = 'ACCEPT';
|
|
$this->ruleSource = '';
|
|
$this->ruleDest = '';
|
|
$this->ruleProto = 'tcp';
|
|
$this->ruleDport = '';
|
|
$this->ruleSport = '';
|
|
$this->ruleComment = '';
|
|
$this->ruleEnable = true;
|
|
$this->ruleMacro = '';
|
|
$this->ruleIface = '';
|
|
$this->ruleLog = 'nolog';
|
|
}
|
|
|
|
public function saveRule(): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
// Build payload - always include action, enable, and type
|
|
// Type defaults to 'in' for incoming rules (can be 'in' or 'out')
|
|
$payload = [
|
|
'action' => $this->ruleAction,
|
|
'enable' => $this->ruleEnable ? 1 : 0,
|
|
'type' => 'in', // Default to incoming rule
|
|
];
|
|
|
|
// Add optional parameters only if they have values
|
|
// Note: Proxmox allows rules with just action, but some parameters may be required in certain contexts
|
|
if (!empty(trim($this->ruleSource))) {
|
|
$payload['source'] = trim($this->ruleSource);
|
|
}
|
|
// Destination is required, always include it
|
|
if (!empty(trim($this->ruleDest))) {
|
|
$payload['dest'] = trim($this->ruleDest);
|
|
}
|
|
if (!empty(trim($this->ruleProto))) {
|
|
$payload['proto'] = trim($this->ruleProto);
|
|
}
|
|
if (!empty(trim($this->ruleDport))) {
|
|
$payload['dport'] = trim($this->ruleDport);
|
|
}
|
|
if (!empty(trim($this->ruleSport))) {
|
|
$payload['sport'] = trim($this->ruleSport);
|
|
}
|
|
if (!empty(trim($this->ruleComment))) {
|
|
$payload['comment'] = trim($this->ruleComment);
|
|
}
|
|
if (!empty(trim($this->ruleMacro))) {
|
|
$payload['macro'] = trim($this->ruleMacro);
|
|
}
|
|
// Interface is required, always include it
|
|
if (!empty(trim($this->ruleIface))) {
|
|
$payload['iface'] = trim($this->ruleIface);
|
|
}
|
|
if (!empty(trim($this->ruleLog))) {
|
|
$payload['log'] = trim($this->ruleLog);
|
|
}
|
|
|
|
if ($this->editingRulePos !== null) {
|
|
// Update existing rule
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $this->editingRulePos,
|
|
'put',
|
|
$payload
|
|
);
|
|
} else {
|
|
// Create new rule
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules',
|
|
'post',
|
|
$payload
|
|
);
|
|
}
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Rule ' . ($this->editingRulePos !== null ? 'updated' : 'created') . ' successfully', 'success');
|
|
$this->resetRuleForm();
|
|
$this->editingRulePos = null;
|
|
$this->showRuleForm = false;
|
|
$this->loadFirewallRules();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to save rule: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
$fullErrorDetails = '';
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors'])) {
|
|
// Try to get the full error details
|
|
$errorMessages = [];
|
|
foreach ($errorData['errors'] as $error) {
|
|
if (is_array($error)) {
|
|
$errorMessages[] = json_encode($error);
|
|
} else {
|
|
$errorMessages[] = $error;
|
|
}
|
|
}
|
|
$fullErrorDetails = implode(' ', $errorMessages);
|
|
$errorMessage = !empty($fullErrorDetails) ? $fullErrorDetails : ($errorData['errors'][0]['message'] ?? $errorMessage);
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
if (isset($errorData['errors'])) {
|
|
$errorMessage .= ' ' . json_encode($errorData['errors']);
|
|
}
|
|
}
|
|
}
|
|
$this->error = 'Failed to save rule: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function toggleRule(int $pos): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
// Find the rule to get its current enable state
|
|
$rule = collect($this->rules)->firstWhere('pos', $pos);
|
|
if (!$rule) {
|
|
$this->error = 'Rule not found';
|
|
$this->showErrorModal = true;
|
|
$this->loading = false;
|
|
return;
|
|
}
|
|
|
|
// Toggle the enable state (0 = disabled, 1 = enabled)
|
|
$currentEnable = isset($rule['enable']) ? (int)$rule['enable'] : 1;
|
|
$newEnable = $currentEnable == 0 ? 1 : 0;
|
|
|
|
// Update only the enable field
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos,
|
|
'put',
|
|
['enable' => $newEnable]
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Rule ' . ($newEnable == 1 ? 'enabled' : 'disabled') . ' successfully', 'success');
|
|
$this->loadFirewallRules();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to toggle rule: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to toggle rule: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function confirmDeleteRule(int $pos): void
|
|
{
|
|
$this->deletingRulePos = $pos;
|
|
$this->showDeleteModal = true;
|
|
}
|
|
|
|
public function cancelDeleteRule(): void
|
|
{
|
|
$this->deletingRulePos = null;
|
|
$this->showDeleteModal = false;
|
|
}
|
|
|
|
public function deleteRule(): void
|
|
{
|
|
if ($this->deletingRulePos === null) {
|
|
return;
|
|
}
|
|
|
|
$pos = $this->deletingRulePos;
|
|
$this->loading = true;
|
|
$this->showDeleteModal = false;
|
|
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos,
|
|
'delete'
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Rule deleted successfully', 'success');
|
|
$this->loadFirewallRules();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to delete rule: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to delete rule: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
$this->deletingRulePos = null;
|
|
}
|
|
}
|
|
|
|
public function loadAliases(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->aliases = is_array($data) ? $data : [];
|
|
} catch (\Exception $e) {
|
|
$this->aliases = [];
|
|
}
|
|
}
|
|
|
|
public function createAlias(): void
|
|
{
|
|
if (empty($this->aliasName) || empty($this->aliasCidr)) {
|
|
$this->error = 'Name and CIDR are required';
|
|
$this->showErrorModal = true;
|
|
return;
|
|
}
|
|
|
|
$this->loading = true;
|
|
try {
|
|
$payload = [
|
|
'name' => $this->aliasName,
|
|
'cidr' => $this->aliasCidr,
|
|
];
|
|
if ($this->aliasComment) {
|
|
$payload['comment'] = $this->aliasComment;
|
|
}
|
|
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases',
|
|
'post',
|
|
$payload
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Alias created successfully', 'success');
|
|
$this->aliasName = '';
|
|
$this->aliasCidr = '';
|
|
$this->aliasComment = '';
|
|
$this->loadAliases();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to create alias: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to create alias: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function deleteAlias(string $name): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $name,
|
|
'delete'
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('Alias deleted successfully', 'success');
|
|
$this->loadAliases();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to delete alias: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to delete alias: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function loadIpsets(): void
|
|
{
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset'
|
|
);
|
|
$data = $response->json()['data'] ?? [];
|
|
$this->ipsets = is_array($data) ? $data : [];
|
|
} catch (\Exception $e) {
|
|
$this->ipsets = [];
|
|
}
|
|
}
|
|
|
|
public function createIpset(): void
|
|
{
|
|
if (empty($this->ipsetName)) {
|
|
$this->error = 'Name is required';
|
|
$this->showErrorModal = true;
|
|
return;
|
|
}
|
|
|
|
$this->loading = true;
|
|
try {
|
|
$payload = [
|
|
'name' => $this->ipsetName,
|
|
];
|
|
if ($this->ipsetComment) {
|
|
$payload['comment'] = $this->ipsetComment;
|
|
}
|
|
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset',
|
|
'post',
|
|
$payload
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('IP set created successfully', 'success');
|
|
$this->ipsetName = '';
|
|
$this->ipsetComment = '';
|
|
$this->loadIpsets();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to create IP set: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to create IP set: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function deleteIpset(string $name): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $name,
|
|
'delete'
|
|
);
|
|
|
|
if ($response->successful()) {
|
|
$this->notify('IP set deleted successfully', 'success');
|
|
$this->loadIpsets();
|
|
} else {
|
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
|
$this->error = 'Failed to delete IP set: ' . $message;
|
|
$this->showErrorModal = true;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to delete IP set: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function loadNetworkAdapters(): void
|
|
{
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
|
|
|
|
$this->networkAdapters = [];
|
|
$this->adapterFirewallStates = [];
|
|
|
|
foreach ($config as $key => $value) {
|
|
if (!is_string($key) || !str_starts_with($key, 'net')) {
|
|
continue;
|
|
}
|
|
|
|
$configString = (string) $value;
|
|
$segments = array_filter(array_map('trim', explode(',', $configString)));
|
|
$model = 'virtio';
|
|
$mac = null;
|
|
$firewallEnabled = false;
|
|
|
|
foreach ($segments as $segment) {
|
|
if (!str_contains($segment, '=')) {
|
|
continue;
|
|
}
|
|
|
|
[$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2));
|
|
$segmentKeyLower = strtolower($segmentKey);
|
|
|
|
if (in_array($segmentKeyLower, ['virtio', 'e1000', 'e1000e', 'rtl8139', 'vmxnet3', 'pcnet'], true)) {
|
|
$model = $segmentKeyLower;
|
|
$mac = $segmentValue;
|
|
} elseif ($segmentKeyLower === 'firewall') {
|
|
$firewallEnabled = ($segmentValue == '1' || $segmentValue === '1');
|
|
}
|
|
}
|
|
|
|
$this->networkAdapters[] = [
|
|
'name' => $key,
|
|
'model' => $model,
|
|
'mac' => $mac,
|
|
];
|
|
$this->adapterFirewallStates[$key] = $firewallEnabled;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->networkAdapters = [];
|
|
$this->adapterFirewallStates = [];
|
|
}
|
|
}
|
|
|
|
public function toggleAdapterFirewall(string $adapterName): void
|
|
{
|
|
$this->loading = true;
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
|
|
|
|
if (!isset($config[$adapterName])) {
|
|
$this->error = 'Network adapter not found: ' . $adapterName;
|
|
$this->showErrorModal = true;
|
|
$this->loading = false;
|
|
return;
|
|
}
|
|
|
|
$currentConfig = (string) $config[$adapterName];
|
|
$segments = array_filter(array_map('trim', explode(',', $currentConfig)));
|
|
$newSegments = [];
|
|
$firewallFound = false;
|
|
$currentFirewallState = $this->adapterFirewallStates[$adapterName] ?? false;
|
|
$newFirewallState = !$currentFirewallState;
|
|
|
|
foreach ($segments as $segment) {
|
|
if (!str_contains($segment, '=')) {
|
|
$newSegments[] = $segment;
|
|
continue;
|
|
}
|
|
|
|
[$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2));
|
|
|
|
if (strtolower($segmentKey) === 'firewall') {
|
|
$firewallFound = true;
|
|
if ($newFirewallState) {
|
|
$newSegments[] = 'firewall=1';
|
|
}
|
|
// If disabling, we skip adding firewall=1, effectively removing it
|
|
} else {
|
|
$newSegments[] = $segment;
|
|
}
|
|
}
|
|
|
|
// If firewall wasn't in the config and we're enabling it, add it
|
|
if (!$firewallFound && $newFirewallState) {
|
|
$newSegments[] = 'firewall=1';
|
|
}
|
|
|
|
$newConfig = implode(',', $newSegments);
|
|
|
|
$proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
|
|
'put',
|
|
[$adapterName => $newConfig]
|
|
);
|
|
|
|
$this->adapterFirewallStates[$adapterName] = $newFirewallState;
|
|
$this->notify('Firewall ' . ($newFirewallState ? 'enabled' : 'disabled') . ' for ' . $adapterName, 'success');
|
|
$this->loadNetworkAdapters(); // Reload to ensure consistency
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
|
} elseif (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
}
|
|
$this->error = 'Failed to toggle firewall for adapter: ' . $errorMessage;
|
|
$this->showErrorModal = true;
|
|
} finally {
|
|
$this->loading = false;
|
|
}
|
|
}
|
|
|
|
public function loadFirewallLogs(): void
|
|
{
|
|
$this->logError = '';
|
|
try {
|
|
// Proxmox API: GET /api2/json/nodes/{node}/qemu/{vmid}/firewall/log
|
|
// Documentation: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/firewall/log
|
|
// Returns: text/plain format with log entries, one per line
|
|
// Fetch logs in batches and collect all of them
|
|
$allRawLogs = [];
|
|
$start = 0;
|
|
$batchSize = 500;
|
|
$maxLogs = $this->logLimit;
|
|
$fetchedCount = 0;
|
|
$totalBodyLength = 0;
|
|
$lastContentType = '';
|
|
$lastIsJson = false;
|
|
|
|
// Keep fetching until we have all logs or reach the limit
|
|
do {
|
|
$response = $this->firewallRequest(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
|
|
'get',
|
|
[
|
|
'limit' => $batchSize,
|
|
'start' => $start
|
|
]
|
|
);
|
|
|
|
if (! $response->successful()) {
|
|
$errorData = [];
|
|
try {
|
|
$errorData = $response->json();
|
|
} catch (\Exception $e) {
|
|
// Response might not be JSON
|
|
}
|
|
$errorMsg = $errorData['message'] ?? 'HTTP ' . $response->status();
|
|
$this->logError = 'Failed to fetch logs: ' . $errorMsg;
|
|
\Log::warning('Firewall log API request failed', [
|
|
'status' => $response->status(),
|
|
'body' => substr($response->body(), 0, 500),
|
|
'server_id' => $this->server->id,
|
|
'vm_id' => $this->server->vm_id,
|
|
]);
|
|
$this->firewallLogs = [];
|
|
return;
|
|
}
|
|
|
|
// Proxmox firewall log endpoint can return either text/plain or JSON format
|
|
// Try to parse as JSON first, then fall back to text/plain
|
|
$body = $response->body();
|
|
$totalBodyLength += strlen($body);
|
|
$batchRawLogs = [];
|
|
|
|
// Check if response is JSON
|
|
$contentType = $response->header('Content-Type');
|
|
$lastContentType = $contentType ?? '';
|
|
$isJson = str_contains($contentType ?? '', 'json');
|
|
$lastIsJson = $isJson;
|
|
|
|
if ($isJson) {
|
|
try {
|
|
$jsonData = $response->json();
|
|
// Proxmox may return logs in different formats:
|
|
// 1. { "data": [ {"n": 1, "t": "log text"}, ... ] }
|
|
// 2. { "data": [ "line1", "line2", ... ] }
|
|
// 3. { "data": "line1\nline2\n..." }
|
|
// 4. [ {"n": 1, "t": "log text"}, ... ]
|
|
|
|
if (isset($jsonData['data'])) {
|
|
if (is_array($jsonData['data'])) {
|
|
// Handle array of log objects or strings
|
|
foreach ($jsonData['data'] as $item) {
|
|
if (is_array($item) && isset($item['t'])) {
|
|
// Proxmox format: {"n": 1, "t": "log text"}
|
|
$batchRawLogs[] = $item['t'];
|
|
} elseif (is_string($item)) {
|
|
// Plain string format
|
|
$batchRawLogs[] = $item;
|
|
} elseif (is_array($item)) {
|
|
// Other array format, try to get text from various fields
|
|
$text = $item['text'] ?? $item['message'] ?? $item['line'] ?? json_encode($item);
|
|
$batchRawLogs[] = $text;
|
|
}
|
|
}
|
|
} elseif (is_string($jsonData['data'])) {
|
|
// Multi-line string format
|
|
$lines = preg_split('/\r\n|\r|\n/', $jsonData['data']);
|
|
$batchRawLogs = array_filter($lines, static function ($line) {
|
|
return ! empty(trim($line));
|
|
});
|
|
}
|
|
} elseif (is_array($jsonData)) {
|
|
// Array at root level
|
|
foreach ($jsonData as $item) {
|
|
if (is_array($item) && isset($item['t'])) {
|
|
$batchRawLogs[] = $item['t'];
|
|
} elseif (is_string($item)) {
|
|
$batchRawLogs[] = $item;
|
|
}
|
|
}
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Fall through to text parsing
|
|
\Log::warning('Failed to parse JSON response, falling back to text', [
|
|
'error' => $e->getMessage(),
|
|
'response_sample' => substr($body, 0, 500)
|
|
]);
|
|
}
|
|
}
|
|
|
|
// If no JSON data or JSON parsing failed, try text/plain format
|
|
if (empty($batchRawLogs) && ! empty($body)) {
|
|
// Split by newlines (handle both \n and \r\n)
|
|
$lines = preg_split('/\r\n|\r|\n/', $body);
|
|
$batchRawLogs = array_filter($lines, static function ($line) {
|
|
return ! empty(trim($line));
|
|
});
|
|
}
|
|
|
|
// Re-index array after filtering
|
|
$batchRawLogs = array_values($batchRawLogs);
|
|
|
|
// Add this batch to all logs
|
|
$allRawLogs = array_merge($allRawLogs, $batchRawLogs);
|
|
$fetchedCount = count($batchRawLogs);
|
|
|
|
// Move to next batch
|
|
$start += $batchSize;
|
|
|
|
// Break if we got fewer logs than requested (meaning we've reached the end)
|
|
// or if we've reached our limit
|
|
if ($fetchedCount < $batchSize || count($allRawLogs) >= $maxLogs) {
|
|
break;
|
|
}
|
|
} while (true);
|
|
|
|
// Limit to maxLogs
|
|
if (count($allRawLogs) > $maxLogs) {
|
|
$allRawLogs = array_slice($allRawLogs, 0, $maxLogs);
|
|
}
|
|
|
|
$rawLogs = $allRawLogs;
|
|
|
|
\Log::info('Firewall logs fetched from Proxmox API', [
|
|
'raw_count' => count($rawLogs),
|
|
'total_body_length' => $totalBodyLength,
|
|
'last_content_type' => $lastContentType,
|
|
'last_is_json' => $lastIsJson,
|
|
'batches_fetched' => ($start / $batchSize),
|
|
'first_log_type' => ! empty($rawLogs) ? gettype($rawLogs[0]) : 'empty',
|
|
'first_log_length' => ! empty($rawLogs) && is_string($rawLogs[0]) ? strlen($rawLogs[0]) : 'N/A',
|
|
'sample' => ! empty($rawLogs) ? substr(is_array($rawLogs[0]) ? json_encode($rawLogs[0]) : $rawLogs[0], 0, 300) : null,
|
|
'first_3_logs_full' => array_slice($rawLogs, 0, 3),
|
|
]);
|
|
|
|
$this->firewallLogs = [];
|
|
|
|
$successfullyParsed = 0;
|
|
$skippedEmpty = 0;
|
|
$skippedInvalid = 0;
|
|
|
|
foreach ($rawLogs as $index => $logEntry) {
|
|
$logText = '';
|
|
|
|
if (is_string($logEntry)) {
|
|
$logText = trim($logEntry);
|
|
} elseif (is_array($logEntry)) {
|
|
// Handle various possible array formats from Proxmox API
|
|
// Note: 't' is the text field, 'n' is the line number - prioritize 't'!
|
|
$logText = trim(
|
|
$logEntry['t'] // Proxmox format: {"n": 1, "t": "log text"}
|
|
?? $logEntry['text'] // Alternative text field
|
|
?? $logEntry['raw'] // Raw log field
|
|
?? $logEntry['message'] // Message field
|
|
?? $logEntry['log'] // Log field
|
|
?? $logEntry['line'] // Line field
|
|
?? json_encode($logEntry) // Last resort: encode the whole thing
|
|
);
|
|
}
|
|
|
|
if ($logText === '' || strlen($logText) < 10) {
|
|
$skippedEmpty++;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$parsed = $this->parseFirewallLog($logText);
|
|
// Check if the log was actually parsed (has at least action or source/destination)
|
|
if ($parsed !== null && ($parsed['action'] !== null || $parsed['source'] !== null || $parsed['destination'] !== null)) {
|
|
$this->firewallLogs[] = $parsed;
|
|
$successfullyParsed++;
|
|
} else {
|
|
$skippedInvalid++;
|
|
if ($index < 5) { // Log first 5 unparseable entries for debugging
|
|
\Log::warning('Firewall log could not be parsed (missing key fields)', [
|
|
'entry' => substr($logText, 0, 300),
|
|
'parsed' => $parsed
|
|
]);
|
|
}
|
|
}
|
|
} catch (\Exception $e) {
|
|
$skippedInvalid++;
|
|
if ($index < 5) { // Log first 5 errors for debugging
|
|
\Log::warning('Failed to parse firewall log entry', [
|
|
'entry' => substr($logText, 0, 300),
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
\Log::info('Firewall log parsing summary', [
|
|
'total_raw_logs' => count($rawLogs),
|
|
'successfully_parsed' => $successfullyParsed,
|
|
'skipped_empty' => $skippedEmpty,
|
|
'skipped_invalid' => $skippedInvalid
|
|
]);
|
|
|
|
\Log::info('Firewall logs final count', [
|
|
'total_logs' => count($this->firewallLogs),
|
|
'first_3_sources' => array_slice(array_column($this->firewallLogs, 'source'), 0, 3),
|
|
]);
|
|
|
|
if (empty($this->firewallLogs) && ! empty($rawLogs)) {
|
|
$this->logError = 'Logs received (' . count($rawLogs) . ') but could not be processed. Successfully parsed: ' . $successfullyParsed . ', Skipped invalid: ' . $skippedInvalid . '. Check Laravel logs for details.';
|
|
} elseif (empty($this->firewallLogs)) {
|
|
$this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).';
|
|
}
|
|
} catch (\Exception $e) {
|
|
$errorMessage = $e->getMessage();
|
|
if (method_exists($e, 'response') && $e->response()) {
|
|
try {
|
|
$errorData = $e->response()->json();
|
|
if (isset($errorData['message'])) {
|
|
$errorMessage = $errorData['message'];
|
|
}
|
|
} catch (\Exception $jsonError) {
|
|
// Response might not be JSON
|
|
}
|
|
}
|
|
$this->logError = 'Error loading logs: ' . $errorMessage;
|
|
\Log::error('Failed to load firewall logs', [
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
'server_id' => $this->server->id,
|
|
'vm_id' => $this->server->vm_id,
|
|
]);
|
|
$this->firewallLogs = [];
|
|
}
|
|
}
|
|
|
|
protected function parseFirewallLog(string $logText): array
|
|
{
|
|
$result = [
|
|
'raw' => $logText,
|
|
'timestamp_display' => null,
|
|
'timestamp_iso' => null,
|
|
'action' => null,
|
|
'action_badge_class' => 'bg-base/20 text-base/70',
|
|
'level_numeric' => null,
|
|
'level_label' => null,
|
|
'level_badge_class' => 'bg-base/20 text-base/70',
|
|
'protocol' => null,
|
|
'source' => null,
|
|
'source_port' => null,
|
|
'destination' => null,
|
|
'destination_port' => null,
|
|
'iface' => null,
|
|
];
|
|
|
|
// Extract level and interface (first tokens)
|
|
if (preg_match('/^\S+\s+(\d+)\s+([^\s]+)/', $logText, $matches)) {
|
|
$levelNumeric = (int) $matches[1];
|
|
$iface = $matches[2];
|
|
$priMap = [
|
|
0 => 'emerg',
|
|
1 => 'alert',
|
|
2 => 'crit',
|
|
3 => 'err',
|
|
4 => 'warning',
|
|
5 => 'notice',
|
|
6 => 'info',
|
|
7 => 'debug',
|
|
];
|
|
$levelLabel = $priMap[$levelNumeric] ?? 'info';
|
|
$levelBadgeMap = [
|
|
'emerg' => 'bg-error text-white',
|
|
'alert' => 'bg-error/80 text-white',
|
|
'crit' => 'bg-error/60 text-white',
|
|
'err' => 'bg-error/20 text-error',
|
|
'warning' => 'bg-warning/20 text-warning',
|
|
'notice' => 'bg-primary/20 text-primary',
|
|
'info' => 'bg-primary/20 text-primary',
|
|
'debug' => 'bg-base/20 text-base/70',
|
|
];
|
|
|
|
$result['level_numeric'] = $levelNumeric;
|
|
$result['level_label'] = strtoupper($levelLabel);
|
|
$result['level_badge_class'] = $levelBadgeMap[$levelLabel] ?? 'bg-base/20 text-base/70';
|
|
$result['iface'] = $iface;
|
|
}
|
|
|
|
// Extract timestamp and timezone
|
|
if (preg_match('/(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2})\s+([+-]\d{4})/', $logText, $matches)) {
|
|
$timestamp = $matches[1];
|
|
$tz = $matches[2];
|
|
$tzNormalized = substr($tz, 0, 3) . ':' . substr($tz, 3);
|
|
try {
|
|
$carbon = Carbon::createFromFormat('d/M/Y:H:i:s P', $timestamp . ' ' . $tzNormalized);
|
|
$result['timestamp_iso'] = $carbon->toIso8601String();
|
|
$result['timestamp_display'] = $carbon->format('Y-m-d H:i:s T');
|
|
} catch (\Exception $e) {
|
|
$result['timestamp_display'] = str_replace('/', '-', $timestamp) . ' ' . $tzNormalized;
|
|
}
|
|
}
|
|
|
|
// Extract action (REJECT, ACCEPT, DROP)
|
|
if (preg_match('/\b(REJECT|ACCEPT|DROP):/i', $logText, $matches)) {
|
|
$action = strtoupper($matches[1]);
|
|
$actionBadgeMap = [
|
|
'ACCEPT' => 'bg-success/20 text-success',
|
|
'DROP' => 'bg-error/20 text-error',
|
|
'REJECT' => 'bg-warning/20 text-warning',
|
|
];
|
|
$result['action'] = $action;
|
|
$result['action_badge_class'] = $actionBadgeMap[$action] ?? 'bg-base/20 text-base/70';
|
|
}
|
|
|
|
// Extract protocol
|
|
if (preg_match('/PROTO=([^\s]+)/', $logText, $matches)) {
|
|
$result['protocol'] = strtoupper($matches[1]);
|
|
}
|
|
|
|
// Extract source/destination IPs and ports
|
|
if (preg_match('/SRC=([^\s]+)/', $logText, $matches)) {
|
|
$result['source'] = $matches[1];
|
|
}
|
|
|
|
if (preg_match('/DST=([^\s]+)/', $logText, $matches)) {
|
|
$result['destination'] = $matches[1];
|
|
}
|
|
|
|
if (preg_match('/SPT=(\d+)/', $logText, $matches)) {
|
|
$result['source_port'] = $matches[1];
|
|
}
|
|
|
|
if (preg_match('/DPT=(\d+)/', $logText, $matches)) {
|
|
$result['destination_port'] = $matches[1];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Fetch IP location data from ip-api.com
|
|
*/
|
|
public function fetchIPLocation(string $ip): array
|
|
{
|
|
try {
|
|
// Validate IP address
|
|
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
|
return [
|
|
'status' => 'fail',
|
|
'message' => 'Invalid or private IP address'
|
|
];
|
|
}
|
|
|
|
// Fetch location data from ip-api.com (HTTP endpoint)
|
|
$response = Http::timeout(5)->get("http://ip-api.com/json/{$ip}");
|
|
|
|
if ($response->successful()) {
|
|
$data = $response->json();
|
|
|
|
// Check if the API returned success
|
|
if (isset($data['status']) && $data['status'] === 'success') {
|
|
return $data;
|
|
} else {
|
|
return [
|
|
'status' => 'fail',
|
|
'message' => $data['message'] ?? 'Unable to fetch location data'
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'status' => 'fail',
|
|
'message' => 'Failed to fetch location data (HTTP ' . $response->status() . ')'
|
|
];
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'status' => 'fail',
|
|
'message' => 'Error: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('proxmox::firewall')->layoutData([
|
|
'server' => $this->server,
|
|
'status' => $this->status,
|
|
]);
|
|
}
|
|
}
|