- 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.
517 lines
15 KiB
PHP
517 lines
15 KiB
PHP
<?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);
|
|
}
|
|
}
|