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); } }