Private
Public Access
1
0

Add "Apply to All Servers" functionality in Firewall component and view

This commit is contained in:
2025-12-08 14:13:14 -05:00
parent 78d3d17c36
commit b01593eac2
2 changed files with 464 additions and 4 deletions

View File

@@ -62,6 +62,10 @@ class Firewall extends Component
public ?FirewallConfiguration $applyingConfiguration = null;
public array $applyCompatibilityWarnings = [];
// Apply to All Servers
public bool $showApplyToAllModal = false;
public array $applyToAllResults = [];
// Available Proxmox Macros
public array $availableMacros = [
'' => 'None',
@@ -2288,6 +2292,329 @@ class Firewall extends Component
}
}
// ==================== APPLY TO ALL SERVERS ====================
/**
* Get the current configuration as a normalized array for comparison.
*/
protected function getCurrentConfigurationData(): array
{
return [
'rules' => $this->rules,
'policyIn' => $this->policyIn,
'policyOut' => $this->policyOut,
];
}
/**
* Check if the current configuration matches a saved configuration.
*/
public function currentConfigMatchesSaved(): bool
{
$currentConfig = $this->getCurrentConfigurationData();
foreach ($this->savedConfigurations as $saved) {
if ($this->configurationsMatch($currentConfig, [
'rules' => $saved['firewall_rules'] ?? [],
'policyIn' => $saved['incoming_policy'] ?? 'DROP',
'policyOut' => $saved['outgoing_policy'] ?? 'ACCEPT',
])) {
return true;
}
}
return false;
}
/**
* Get the saved configuration that matches the current configuration, if any.
*/
public function getMatchingSavedConfiguration(): ?array
{
$currentConfig = $this->getCurrentConfigurationData();
foreach ($this->savedConfigurations as $saved) {
if ($this->configurationsMatch($currentConfig, [
'rules' => $saved['firewall_rules'] ?? [],
'policyIn' => $saved['incoming_policy'] ?? 'DROP',
'policyOut' => $saved['outgoing_policy'] ?? 'ACCEPT',
])) {
return $saved;
}
}
return null;
}
/**
* Compare two configurations to see if they match.
*/
protected function configurationsMatch(array $config1, array $config2): bool
{
// Compare policies
if ($config1['policyIn'] !== $config2['policyIn'] || $config1['policyOut'] !== $config2['policyOut']) {
return false;
}
// Compare rules count
$rules1 = $config1['rules'] ?? [];
$rules2 = $config2['rules'] ?? [];
if (count($rules1) !== count($rules2)) {
return false;
}
// Compare each rule (ignoring position)
$normalizedRules1 = $this->normalizeRulesForComparison($rules1);
$normalizedRules2 = $this->normalizeRulesForComparison($rules2);
return $normalizedRules1 === $normalizedRules2;
}
/**
* Normalize rules for comparison by removing position-specific data.
*/
protected function normalizeRulesForComparison(array $rules): string
{
$normalized = [];
foreach ($rules as $rule) {
$normalizedRule = [
'type' => $rule['type'] ?? '',
'action' => $rule['action'] ?? '',
'enable' => isset($rule['enable']) ? (int)$rule['enable'] : 1,
'source' => $rule['source'] ?? '',
'dest' => $rule['dest'] ?? '',
'proto' => $rule['proto'] ?? '',
'dport' => $rule['dport'] ?? '',
'sport' => $rule['sport'] ?? '',
'macro' => $rule['macro'] ?? '',
'iface' => $rule['iface'] ?? '',
'log' => $rule['log'] ?? 'nolog',
'comment' => $rule['comment'] ?? '',
];
ksort($normalizedRule);
$normalized[] = $normalizedRule;
}
// Sort rules by their content to ensure order doesn't matter
usort($normalized, function($a, $b) {
return json_encode($a) <=> json_encode($b);
});
return json_encode($normalized);
}
/**
* Get all other servers owned by the current user.
*/
public function getUserOtherServers(): array
{
$userId = Auth::id();
if (!$userId) {
return [];
}
return Server::where('user_id', $userId)
->where('id', '!=', $this->server->id)
->with(['node', 'primaryIpv4', 'primaryIpv6'])
->get()
->toArray();
}
/**
* Check if Apply to All should be enabled.
* Returns true if current config is saved AND user has other servers with different configs.
*/
public function canApplyToAll(): bool
{
// Must have a saved configuration that matches current
if (!$this->currentConfigMatchesSaved()) {
return false;
}
// Must have other servers
$otherServers = $this->getUserOtherServers();
if (count($otherServers) === 0) {
return false;
}
return true;
}
/**
* Open the Apply to All modal.
*/
public function openApplyToAllModal(): void
{
$this->applyToAllResults = [];
$this->showApplyToAllModal = true;
}
/**
* Close the Apply to All modal.
*/
public function closeApplyToAllModal(): void
{
$this->applyToAllResults = [];
$this->showApplyToAllModal = false;
}
/**
* Apply the current configuration to all other servers owned by the user.
*/
public function applyToAllServers(): void
{
$userId = Auth::id();
if (!$userId) {
$this->error = 'You must be logged in to apply configurations';
$this->showErrorModal = true;
return;
}
// Verify we have a saved configuration
$matchingConfig = $this->getMatchingSavedConfiguration();
if (!$matchingConfig) {
$this->error = 'Current configuration must be saved before applying to all servers';
$this->showErrorModal = true;
return;
}
$otherServers = $this->getUserOtherServers();
if (count($otherServers) === 0) {
$this->error = 'No other servers found to apply configuration to';
$this->showErrorModal = true;
return;
}
$this->loading = true;
$results = [];
try {
foreach ($otherServers as $serverData) {
$server = Server::with('node')->find($serverData['id']);
if (!$server || !$server->node) {
$results[] = [
'server_id' => $serverData['id'],
'hostname' => $serverData['hostname'] ?? 'Unknown',
'success' => false,
'message' => 'Server or node not found',
];
continue;
}
try {
// Apply configuration to this server
$this->applyConfigurationToServer($server, $matchingConfig);
$results[] = [
'server_id' => $server->id,
'hostname' => $server->hostname ?? 'VPS ' . $server->vm_id,
'success' => true,
'message' => 'Configuration applied successfully',
];
} catch (\Exception $e) {
Log::error("Failed to apply firewall config to server {$server->id}: " . $e->getMessage());
$results[] = [
'server_id' => $server->id,
'hostname' => $server->hostname ?? 'VPS ' . $server->vm_id,
'success' => false,
'message' => 'Failed: ' . $e->getMessage(),
];
}
}
$this->applyToAllResults = $results;
$successCount = collect($results)->where('success', true)->count();
$totalCount = count($results);
if ($successCount === $totalCount) {
$this->notify("Configuration applied to all {$successCount} servers successfully", 'success');
} elseif ($successCount > 0) {
$this->notify("Configuration applied to {$successCount} of {$totalCount} servers", 'warning');
} else {
$this->error = 'Failed to apply configuration to any servers';
$this->showErrorModal = true;
}
} catch (\Exception $e) {
Log::error('Failed to apply configuration to all servers: ' . $e->getMessage());
$this->error = 'Failed to apply configuration: ' . $e->getMessage();
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
/**
* Apply a saved configuration to a specific server.
*/
protected function applyConfigurationToServer(Server $server, array $config): void
{
$savedRules = $config['firewall_rules'] ?? [];
$incomingPolicy = $config['incoming_policy'] ?? 'DROP';
$outgoingPolicy = $config['outgoing_policy'] ?? 'ACCEPT';
// Step 1: Update policies
$response = $this->firewallRequest(
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/firewall/options',
'put',
[
'policy_in' => $incomingPolicy,
'policy_out' => $outgoingPolicy,
'dhcp' => 0,
'ndp' => 1,
]
);
if (!$response->successful()) {
throw new \Exception('Failed to update firewall policies');
}
// Step 2: Get existing rules and delete them
$existingRulesResponse = $this->firewallRequest(
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/firewall/rules'
);
if ($existingRulesResponse->successful()) {
$existingRules = $existingRulesResponse->json('data') ?? [];
foreach (array_reverse($existingRules) as $rule) {
if (isset($rule['pos'])) {
$this->firewallRequest(
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/firewall/rules/' . $rule['pos'],
'delete'
);
}
}
}
// Step 3: Create new rules from saved configuration
if (is_array($savedRules)) {
foreach ($savedRules as $savedRule) {
$rulePayload = [
'type' => $savedRule['type'] ?? 'in',
'action' => $savedRule['action'] ?? 'ACCEPT',
'enable' => isset($savedRule['enable']) ? (int)$savedRule['enable'] : 1,
];
if (!empty($savedRule['source'])) $rulePayload['source'] = $savedRule['source'];
if (!empty($savedRule['dest'])) $rulePayload['dest'] = $savedRule['dest'];
if (!empty($savedRule['proto'])) $rulePayload['proto'] = $savedRule['proto'];
if (!empty($savedRule['dport'])) $rulePayload['dport'] = $savedRule['dport'];
if (!empty($savedRule['sport'])) $rulePayload['sport'] = $savedRule['sport'];
if (!empty($savedRule['macro'])) $rulePayload['macro'] = $savedRule['macro'];
if (!empty($savedRule['comment'])) $rulePayload['comment'] = $savedRule['comment'];
if (!empty($savedRule['log']) && $savedRule['log'] !== 'nolog') $rulePayload['log'] = $savedRule['log'];
// Skip iface as it may not exist on other servers
$this->firewallRequest(
'/nodes/' . $server->node->name . '/qemu/' . $server->vm_id . '/firewall/rules',
'post',
$rulePayload
);
}
}
}
public function render()
{
return view('proxmox::firewall')->layoutData([