Add nodes management and initial setup pages

- Implemented nodes management functionality in `nodes.php` including create, update, and delete actions.
- Added form validation and error handling for node operations.
- Created a new setup page in `setup.php` for initial administrator account creation.
- Included user feedback messages for successful operations and errors.
- Designed user interface for both nodes management and setup processes.
This commit is contained in:
2025-12-14 01:33:12 -05:00
parent cf0ec74888
commit d8b76233c0
19 changed files with 8800 additions and 0 deletions

View File

@@ -0,0 +1,457 @@
<?php
/**
* BirdConfig Class
*
* Helper class for BIRD 3 configuration parsing and generation
*/
class BirdConfig {
/**
* Parse BIRD protocol status output
*
* @param string $output
* @return array
*/
public static function parseProtocolStatus(string $output): array {
$protocols = [];
$lines = explode("\n", trim($output));
// Skip header lines
$dataStarted = false;
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) {
continue;
}
// Skip BIRD header
if (strpos($line, 'BIRD') === 0 || strpos($line, 'Name') === 0) {
$dataStarted = true;
continue;
}
if (!$dataStarted) {
continue;
}
// Parse protocol line
// Format: Name Proto Table State Since Info
$parts = preg_split('/\s+/', $line, 6);
if (count($parts) >= 5) {
$protocols[] = [
'name' => $parts[0],
'proto' => $parts[1],
'table' => $parts[2],
'state' => $parts[3],
'since' => $parts[4],
'info' => $parts[5] ?? '',
];
}
}
return $protocols;
}
/**
* Parse BIRD route count output
*
* @param string $output
* @return array
*/
public static function parseRouteCount(string $output): array {
$counts = [
'total' => 0,
'primary' => 0,
'filtered' => 0,
];
// Format: X of Y routes for Z networks
if (preg_match('/(\d+)\s+of\s+(\d+)\s+routes\s+for\s+(\d+)\s+networks/', $output, $matches)) {
$counts['primary'] = (int) $matches[1];
$counts['total'] = (int) $matches[2];
$counts['networks'] = (int) $matches[3];
}
// Filtered routes
if (preg_match('/(\d+)\s+routes\s+\((\d+)\s+filtered\)/', $output, $matches)) {
$counts['total'] = (int) $matches[1];
$counts['filtered'] = (int) $matches[2];
}
return $counts;
}
/**
* Parse BIRD status output
*
* @param string $output
* @return array
*/
public static function parseStatus(string $output): array {
$status = [
'version' => '',
'router_id' => '',
'hostname' => '',
'server_time' => '',
'last_reboot' => '',
'last_reconfiguration' => '',
];
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
$line = trim($line);
if (preg_match('/^BIRD\s+(.+)$/', $line, $matches)) {
$status['version'] = trim($matches[1]);
} elseif (preg_match('/^Router ID is\s+(.+)$/', $line, $matches)) {
$status['router_id'] = trim($matches[1]);
} elseif (preg_match('/^Hostname is\s+(.+)$/', $line, $matches)) {
$status['hostname'] = trim($matches[1]);
} elseif (preg_match('/^Current server time is\s+(.+)$/', $line, $matches)) {
$status['server_time'] = trim($matches[1]);
} elseif (preg_match('/^Last reboot on\s+(.+)$/', $line, $matches)) {
$status['last_reboot'] = trim($matches[1]);
} elseif (preg_match('/^Last reconfiguration on\s+(.+)$/', $line, $matches)) {
$status['last_reconfiguration'] = trim($matches[1]);
}
}
return $status;
}
/**
* Parse detailed protocol info
*
* @param string $output
* @return array
*/
public static function parseProtocolDetail(string $output): array {
$info = [
'name' => '',
'description' => '',
'state' => '',
'table' => '',
'neighbor_address' => '',
'neighbor_as' => '',
'local_as' => '',
'neighbor_id' => '',
'routes' => [
'imported' => 0,
'exported' => 0,
'preferred' => 0,
'filtered' => 0,
],
'uptime' => '',
'hold_timer' => '',
'keepalive_timer' => '',
'last_error' => '',
'channel' => [],
];
$lines = explode("\n", trim($output));
$currentChannel = null;
foreach ($lines as $line) {
$line = trim($line);
// Protocol name
if (preg_match('/^(\S+)\s+BGP\s+/', $line, $matches)) {
$info['name'] = $matches[1];
}
// Description
if (preg_match('/Description:\s+(.+)$/', $line, $matches)) {
$info['description'] = trim($matches[1]);
}
// State
if (preg_match('/BGP state:\s+(\S+)/', $line, $matches)) {
$info['state'] = $matches[1];
}
// Neighbor address and AS
if (preg_match('/Neighbor address:\s+(\S+)/', $line, $matches)) {
$info['neighbor_address'] = $matches[1];
}
if (preg_match('/Neighbor AS:\s+(\d+)/', $line, $matches)) {
$info['neighbor_as'] = (int) $matches[1];
}
if (preg_match('/Local AS:\s+(\d+)/', $line, $matches)) {
$info['local_as'] = (int) $matches[1];
}
if (preg_match('/Neighbor ID:\s+(\S+)/', $line, $matches)) {
$info['neighbor_id'] = $matches[1];
}
// Routes
if (preg_match('/Routes:\s+(\d+)\s+imported,\s+(\d+)\s+exported,\s+(\d+)\s+preferred/', $line, $matches)) {
$info['routes']['imported'] = (int) $matches[1];
$info['routes']['exported'] = (int) $matches[2];
$info['routes']['preferred'] = (int) $matches[3];
}
if (preg_match('/(\d+)\s+filtered/', $line, $matches)) {
$info['routes']['filtered'] = (int) $matches[1];
}
// Hold timer
if (preg_match('/Hold timer:\s+(\S+)/', $line, $matches)) {
$info['hold_timer'] = $matches[1];
}
// Keepalive timer
if (preg_match('/Keepalive timer:\s+(\S+)/', $line, $matches)) {
$info['keepalive_timer'] = $matches[1];
}
// Last error
if (preg_match('/Last error:\s+(.+)$/', $line, $matches)) {
$info['last_error'] = trim($matches[1]);
}
// Channel info
if (preg_match('/Channel (ipv[46])/', $line, $matches)) {
$currentChannel = $matches[1];
$info['channel'][$currentChannel] = [];
}
if ($currentChannel && preg_match('/State:\s+(\S+)/', $line, $matches)) {
$info['channel'][$currentChannel]['state'] = $matches[1];
}
if ($currentChannel && preg_match('/Table:\s+(\S+)/', $line, $matches)) {
$info['channel'][$currentChannel]['table'] = $matches[1];
}
}
return $info;
}
/**
* Get state color class for UI
*
* @param string $state
* @return string
*/
public static function getStateColorClass(string $state): string {
return match (strtolower($state)) {
'established' => 'color-fg-success',
'up' => 'color-fg-success',
'start', 'connect', 'active', 'opensent', 'openconfirm' => 'color-fg-attention',
'idle' => 'color-fg-muted',
'down' => 'color-fg-danger',
default => 'color-fg-default',
};
}
/**
* Get state label for UI
*
* @param string $state
* @return string
*/
public static function getStateLabel(string $state): string {
return match (strtolower($state)) {
'established' => 'Label--success',
'up' => 'Label--success',
'start', 'connect', 'active', 'opensent', 'openconfirm' => 'Label--attention',
'idle' => 'Label--secondary',
'down' => 'Label--danger',
default => 'Label--primary',
};
}
/**
* Generate basic BIRD 3 configuration
*
* @param array $config
* @return string
*/
public static function generateBasicConfig(array $config): string {
$bird = "# BIRD 3 Configuration\n";
$bird .= "# Generated by Pathvector Admin\n\n";
// Router ID
if (!empty($config['router_id'])) {
$bird .= "router id {$config['router_id']};\n\n";
}
// Log
$bird .= "log syslog all;\n\n";
// Debug
$bird .= "debug protocols all;\n\n";
// Timeformat
$bird .= "timeformat protocol iso long;\n\n";
// Protocol device
$bird .= "protocol device {\n";
$bird .= " scan time 10;\n";
$bird .= "}\n\n";
// Protocol direct
$bird .= "protocol direct {\n";
$bird .= " ipv4;\n";
$bird .= " ipv6;\n";
$bird .= "}\n\n";
// Protocol kernel
$bird .= "protocol kernel {\n";
$bird .= " ipv4 {\n";
$bird .= " export all;\n";
$bird .= " import all;\n";
$bird .= " };\n";
$bird .= "}\n\n";
$bird .= "protocol kernel {\n";
$bird .= " ipv6 {\n";
$bird .= " export all;\n";
$bird .= " import all;\n";
$bird .= " };\n";
$bird .= "}\n\n";
return $bird;
}
/**
* Validate BIRD config syntax (basic)
*
* @param string $config
* @return array
*/
public static function validateSyntax(string $config): array {
$errors = [];
$warnings = [];
// Check for common issues
$lines = explode("\n", $config);
$braceCount = 0;
$inString = false;
foreach ($lines as $lineNum => $line) {
$lineNum++; // 1-indexed
// Skip comments
if (preg_match('/^\s*#/', $line)) {
continue;
}
// Count braces
for ($i = 0; $i < strlen($line); $i++) {
$char = $line[$i];
if ($char === '"' && ($i === 0 || $line[$i - 1] !== '\\')) {
$inString = !$inString;
}
if (!$inString) {
if ($char === '{') {
$braceCount++;
} elseif ($char === '}') {
$braceCount--;
}
}
}
// Check for missing semicolons (basic check)
$trimmed = trim($line);
if (!empty($trimmed) &&
!preg_match('/[{};#]$/', $trimmed) &&
!preg_match('/^\s*(protocol|filter|function|table|if|else|for|case|switch)/', $trimmed)) {
$warnings[] = "Line $lineNum: Possible missing semicolon";
}
}
if ($braceCount !== 0) {
$errors[] = "Unbalanced braces: " . ($braceCount > 0 ? "missing $braceCount closing braces" : "extra " . abs($braceCount) . " closing braces");
}
return [
'valid' => empty($errors),
'errors' => $errors,
'warnings' => $warnings,
];
}
/**
* Format BIRD configuration
*
* @param string $config
* @return string
*/
public static function formatConfig(string $config): string {
$lines = explode("\n", $config);
$output = [];
$indent = 0;
foreach ($lines as $line) {
$trimmed = trim($line);
// Decrease indent for closing braces
if (preg_match('/^}/', $trimmed)) {
$indent = max(0, $indent - 1);
}
// Add line with proper indentation
if (!empty($trimmed)) {
$output[] = str_repeat("\t", $indent) . $trimmed;
} else {
$output[] = '';
}
// Increase indent for opening braces
if (preg_match('/{\s*$/', $trimmed)) {
$indent++;
}
}
return implode("\n", $output);
}
/**
* Get list of BIRD 3 protocol types
*
* @return array
*/
public static function getProtocolTypes(): array {
return [
'bgp' => 'Border Gateway Protocol',
'ospf' => 'Open Shortest Path First',
'rip' => 'Routing Information Protocol',
'babel' => 'Babel Routing Protocol',
'static' => 'Static Routes',
'kernel' => 'Kernel Route Table',
'device' => 'Device Protocol',
'direct' => 'Direct Interfaces',
'pipe' => 'Table Pipe',
'bfd' => 'Bidirectional Forwarding Detection',
'rpki' => 'Resource Public Key Infrastructure',
'mrt' => 'MRT Table Dump',
];
}
/**
* Get BGP session states
*
* @return array
*/
public static function getBgpStates(): array {
return [
'Idle' => 'Not running',
'Connect' => 'Connecting to peer',
'Active' => 'Trying to connect',
'OpenSent' => 'Open message sent',
'OpenConfirm' => 'Waiting for keepalive',
'Established' => 'Session established',
];
}
}