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,516 @@
<?php
/**
* Node Class
*
* Manages router/node configurations within ASNs
*/
require_once __DIR__ . '/FlatFileDB.php';
require_once __DIR__ . '/Logger.php';
require_once __DIR__ . '/Validator.php';
class Node {
private FlatFileDB $db;
private Logger $logger;
private Validator $validator;
/**
* Constructor
*
* @param string $dataFile Path to nodes JSON file
* @param Logger $logger Logger instance
*/
public function __construct(string $dataFile, Logger $logger) {
$this->db = new FlatFileDB($dataFile);
$this->logger = $logger;
$this->validator = new Validator();
if (!$this->db->exists('nodes')) {
$this->db->set('nodes', []);
}
}
/**
* Get all nodes
*
* @return array
*/
public function getAll(): array {
return $this->db->get('nodes') ?? [];
}
/**
* Get nodes by ASN
*
* @param int $asn
* @return array
*/
public function getByAsn(int $asn): array {
$nodes = $this->db->get('nodes') ?? [];
return array_filter($nodes, fn($node) => ($node['asn'] ?? 0) === $asn);
}
/**
* Get node by ID
*
* @param string $id
* @return array|null
*/
public function get(string $id): ?array {
$nodes = $this->db->get('nodes') ?? [];
return $nodes[$id] ?? null;
}
/**
* Create new node
*
* @param array $data
* @return array
*/
public function create(array $data): array {
$this->validator->clear();
// Validate required fields
if (empty($data['id'])) {
return [
'success' => false,
'message' => 'Node ID is required',
'errors' => ['id' => 'Node ID is required'],
];
}
if (empty($data['asn'])) {
return [
'success' => false,
'message' => 'ASN is required',
'errors' => ['asn' => 'ASN is required'],
];
}
if (!$this->validator->validateAsn($data['asn'], 'asn')) {
return [
'success' => false,
'message' => 'Invalid ASN',
'errors' => $this->validator->getErrors(),
];
}
$nodes = $this->db->get('nodes') ?? [];
$id = $this->sanitizeId($data['id']);
if (isset($nodes[$id])) {
return [
'success' => false,
'message' => "Node $id already exists",
'errors' => ['id' => 'Node ID already exists'],
];
}
// Validate optional fields
if (!empty($data['router_id']) && !$this->validator->validateRouterId($data['router_id'], 'router_id')) {
return [
'success' => false,
'message' => 'Invalid router ID',
'errors' => $this->validator->getErrors(),
];
}
$nodeData = [
'id' => $id,
'asn' => (int) $data['asn'],
'name' => $data['name'] ?? $id,
'hostname' => $data['hostname'] ?? '',
'router_id' => $data['router_id'] ?? '',
'description' => $data['description'] ?? '',
'location' => $data['location'] ?? '',
'pathvector' => $this->buildPathvectorConfig($data['pathvector'] ?? []),
'host' => $data['host'] ?? '',
'templates' => $data['templates'] ?? [],
'overrides' => $data['overrides'] ?? [],
'metadata' => $data['metadata'] ?? [],
'is_active' => $data['is_active'] ?? true,
'created_at' => date('c'),
'updated_at' => date('c'),
];
$nodes[$id] = $nodeData;
if ($this->db->set('nodes', $nodes)) {
$this->logger->success('node', "Created node: $id for ASN {$nodeData['asn']}");
return [
'success' => true,
'message' => "Node $id created successfully",
'data' => $nodeData,
];
}
return [
'success' => false,
'message' => 'Failed to save node',
'errors' => ['database' => 'Failed to save node'],
];
}
/**
* Update node
*
* @param string $id
* @param array $data
* @return array
*/
public function update(string $id, array $data): array {
$nodes = $this->db->get('nodes') ?? [];
if (!isset($nodes[$id])) {
return [
'success' => false,
'message' => "Node $id not found",
'errors' => ['id' => 'Node not found'],
];
}
$nodeData = $nodes[$id];
// Update allowed fields
$allowedFields = [
'name', 'hostname', 'router_id', 'description', 'location',
'host', 'templates', 'overrides', 'metadata', 'is_active'
];
foreach ($allowedFields as $field) {
if (isset($data[$field])) {
$nodeData[$field] = $data[$field];
}
}
// Update Pathvector config
if (isset($data['pathvector'])) {
$nodeData['pathvector'] = $this->buildPathvectorConfig(
array_merge($nodeData['pathvector'], $data['pathvector'])
);
}
// Validate router_id if changed
if (!empty($nodeData['router_id']) && !$this->validator->validateRouterId($nodeData['router_id'], 'router_id')) {
return [
'success' => false,
'message' => 'Invalid router ID',
'errors' => $this->validator->getErrors(),
];
}
$nodeData['updated_at'] = date('c');
$nodes[$id] = $nodeData;
if ($this->db->set('nodes', $nodes)) {
$this->logger->info('node', "Updated node: $id");
return [
'success' => true,
'message' => "Node $id updated successfully",
'data' => $nodeData,
];
}
return [
'success' => false,
'message' => 'Failed to update node',
'errors' => ['database' => 'Failed to save node'],
];
}
/**
* Delete node
*
* @param string $id
* @return array
*/
public function delete(string $id): array {
$nodes = $this->db->get('nodes') ?? [];
if (!isset($nodes[$id])) {
return [
'success' => false,
'message' => "Node $id not found",
];
}
$nodeName = $nodes[$id]['name'];
$nodeAsn = $nodes[$id]['asn'];
unset($nodes[$id]);
if ($this->db->set('nodes', $nodes)) {
$this->logger->warning('node', "Deleted node: $id ($nodeName) from ASN $nodeAsn");
return [
'success' => true,
'message' => "Node $id deleted successfully",
];
}
return [
'success' => false,
'message' => 'Failed to delete node',
];
}
/**
* Build Pathvector config structure for a node
*
* @param array $data
* @return array
*/
private function buildPathvectorConfig(array $data): array {
return [
'binary_path' => $data['binary_path'] ?? '/usr/local/bin/pathvector',
'config_path' => $data['config_path'] ?? '/etc/pathvector/pathvector.yml',
'output_path' => $data['output_path'] ?? '/etc/bird/bird.conf',
'bird_reload_cmd' => $data['bird_reload_cmd'] ?? 'birdc configure',
'bird_directory' => $data['bird_directory'] ?? '/etc/bird',
'bird_socket' => $data['bird_socket'] ?? '/var/run/bird.ctl',
'cache_directory' => $data['cache_directory'] ?? '/var/cache/pathvector',
// Node-specific overrides
'source4' => $data['source4'] ?? '',
'source6' => $data['source6'] ?? '',
'prefixes' => $data['prefixes'] ?? [],
// RPKI settings
'rpki_enable' => $data['rpki_enable'] ?? true,
'rtr_server' => $data['rtr_server'] ?? '',
// PeeringDB settings
'peeringdb_api_key' => $data['peeringdb_api_key'] ?? '',
'peeringdb_cache' => $data['peeringdb_cache'] ?? true,
// IRR settings
'irr_server' => $data['irr_server'] ?? 'rr.ntt.net',
// Kernel settings
'kernel_table' => $data['kernel_table'] ?? null,
'kernel_learn' => $data['kernel_learn'] ?? false,
'kernel_export' => $data['kernel_export'] ?? [],
// Global config snippet
'global_config' => $data['global_config'] ?? '',
// Web UI
'web_ui_file' => $data['web_ui_file'] ?? '',
// Logging
'log_file' => $data['log_file'] ?? 'syslog',
];
}
/**
* Sanitize node ID
*
* @param string $id
* @return string
*/
private function sanitizeId(string $id): string {
// Convert to lowercase, replace spaces with hyphens, remove invalid characters
$id = strtolower(trim($id));
$id = preg_replace('/\s+/', '-', $id);
$id = preg_replace('/[^a-z0-9-_]/', '', $id);
return $id;
}
/**
* Count nodes
*
* @return int
*/
public function count(): int {
$nodes = $this->db->get('nodes') ?? [];
return count($nodes);
}
/**
* Count nodes by ASN
*
* @param int $asn
* @return int
*/
public function countByAsn(int $asn): int {
return count($this->getByAsn($asn));
}
/**
* Search nodes
*
* @param string $query
* @return array
*/
public function search(string $query): array {
$nodes = $this->db->get('nodes') ?? [];
$query = strtolower($query);
return array_filter($nodes, function($node) use ($query) {
return strpos(strtolower($node['id']), $query) !== false ||
strpos(strtolower($node['name']), $query) !== false ||
strpos(strtolower($node['hostname'] ?? ''), $query) !== false ||
strpos(strtolower($node['description'] ?? ''), $query) !== false ||
strpos(strtolower($node['location'] ?? ''), $query) !== false;
});
}
/**
* Get nodes by host
*
* @param string $hostId
* @return array
*/
public function getByHost(string $hostId): array {
$nodes = $this->db->get('nodes') ?? [];
return array_filter($nodes, fn($node) => ($node['host'] ?? '') === $hostId);
}
/**
* Export node configuration to YAML
*
* @param string $id
* @param ASN $asnManager ASN manager for defaults
* @return string|null
*/
public function exportYaml(string $id, $asnManager = null): ?string {
$node = $this->get($id);
if (!$node) {
return null;
}
$config = [];
$config['asn'] = $node['asn'];
if (!empty($node['router_id'])) {
$config['router-id'] = $node['router_id'];
}
if (!empty($node['hostname'])) {
$config['hostname'] = $node['hostname'];
}
// Pathvector settings
$pv = $node['pathvector'];
if (!empty($pv['source4'])) {
$config['source4'] = $pv['source4'];
}
if (!empty($pv['source6'])) {
$config['source6'] = $pv['source6'];
}
if (!empty($pv['prefixes'])) {
$config['prefixes'] = $pv['prefixes'];
}
if (!empty($pv['bird_directory'])) {
$config['bird-directory'] = $pv['bird_directory'];
}
if (!empty($pv['bird_socket'])) {
$config['bird-socket'] = $pv['bird_socket'];
}
if (!empty($pv['cache_directory'])) {
$config['cache-directory'] = $pv['cache_directory'];
}
if (isset($pv['rpki_enable'])) {
$config['rpki-enable'] = $pv['rpki_enable'];
}
if (!empty($pv['rtr_server'])) {
$config['rtr-server'] = $pv['rtr_server'];
}
if (!empty($pv['irr_server'])) {
$config['irr-server'] = $pv['irr_server'];
}
if (!empty($pv['global_config'])) {
$config['global-config'] = $pv['global_config'];
}
// Apply overrides
foreach ($node['overrides'] as $key => $value) {
if ($value !== '' && $value !== null) {
$config[str_replace('_', '-', $key)] = $value;
}
}
return $this->arrayToYaml($config);
}
/**
* Convert array to YAML string
*
* @param array $data
* @param int $indent
* @return string
*/
private function arrayToYaml(array $data, int $indent = 0): string {
$yaml = '';
$prefix = str_repeat(' ', $indent);
foreach ($data as $key => $value) {
if (is_array($value)) {
if (empty($value)) {
continue;
}
if (array_keys($value) === range(0, count($value) - 1)) {
$yaml .= "$prefix$key:\n";
foreach ($value as $item) {
if (is_array($item)) {
$yaml .= "$prefix -\n" . $this->arrayToYaml($item, $indent + 2);
} else {
$yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n";
}
}
} else {
$yaml .= "$prefix$key:\n" . $this->arrayToYaml($value, $indent + 1);
}
} else {
if ($value === '' || $value === null) {
continue;
}
$yaml .= "$prefix$key: " . $this->formatYamlValue($value) . "\n";
}
}
return $yaml;
}
/**
* Format value for YAML output
*
* @param mixed $value
* @return string
*/
private function formatYamlValue($value): string {
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_numeric($value)) {
return (string) $value;
}
if (is_string($value) && (strpos($value, ':') !== false || strpos($value, '#') !== false || strpos($value, "\n") !== false)) {
return '"' . addslashes($value) . '"';
}
return $value;
}
/**
* Backup nodes data
*
* @param string $backupDir
* @return string|false
*/
public function backup(string $backupDir): string|false {
return $this->db->backup($backupDir);
}
}