config = $config; $this->logger = $logger; $this->asnManager = $asnManager; $this->nodeManager = $nodeManager; $this->peerManager = $peerManager; $this->templateManager = $templateManager; $this->hostManager = $hostManager; } /** * Generate Pathvector YAML configuration for a node * * @param string $nodeId * @return array */ public function generateConfig(string $nodeId): array { $node = $this->nodeManager->get($nodeId); if (!$node) { return [ 'success' => false, 'message' => "Node $nodeId not found", 'config' => '', ]; } $asn = $this->asnManager->get((string) $node['asn']); if (!$asn) { return [ 'success' => false, 'message' => "ASN {$node['asn']} not found", 'config' => '', ]; } $peers = $this->peerManager->getByNode($nodeId); // Build configuration $config = $this->buildGlobalConfig($asn, $node); $config['templates'] = $this->buildTemplates(); $config['peers'] = $this->buildPeers($peers); // Add optional sections $this->addOptionalSections($config, $asn, $node); $yaml = $this->configToYaml($config); $this->logger->info('pathvector', "Generated config for node: $nodeId", [ 'peer_count' => count($peers), ]); return [ 'success' => true, 'message' => 'Configuration generated successfully', 'config' => $yaml, 'peer_count' => count($peers), ]; } /** * Build global config section * * @param array $asn * @param array $node * @return array */ private function buildGlobalConfig(array $asn, array $node): array { $defaults = $asn['pathvector_defaults'] ?? []; $pv = $node['pathvector'] ?? []; $config = [ 'asn' => $asn['asn'], ]; // Router ID (node overrides ASN default) $routerId = $node['router_id'] ?: ($defaults['router_id'] ?? ''); if ($routerId) { $config['router-id'] = $routerId; } // Hostname $hostname = $node['hostname'] ?: ($defaults['hostname'] ?? ''); if ($hostname) { $config['hostname'] = $hostname; } // Source addresses $source4 = $pv['source4'] ?: ($defaults['source4'] ?? ''); $source6 = $pv['source6'] ?: ($defaults['source6'] ?? ''); if ($source4) $config['source4'] = $source4; if ($source6) $config['source6'] = $source6; // Prefixes $prefixes = !empty($pv['prefixes']) ? $pv['prefixes'] : ($defaults['prefixes'] ?? []); if (!empty($prefixes)) { $config['prefixes'] = $prefixes; } // BIRD paths $birdDir = $pv['bird_directory'] ?: ($defaults['bird_directory'] ?? '/etc/bird'); $birdSocket = $pv['bird_socket'] ?: ($defaults['bird_socket'] ?? '/var/run/bird.ctl'); $cacheDir = $pv['cache_directory'] ?: ($defaults['cache_directory'] ?? '/var/cache/pathvector'); $config['bird-directory'] = $birdDir; $config['bird-socket'] = $birdSocket; $config['cache-directory'] = $cacheDir; // IRR settings $irrServer = $pv['irr_server'] ?: ($defaults['irr_server'] ?? 'rr.ntt.net'); $config['irr-server'] = $irrServer; // RPKI settings $rpkiEnable = $pv['rpki_enable'] ?? ($defaults['rpki_enable'] ?? true); $config['rpki-enable'] = $rpkiEnable; $rtrServer = $pv['rtr_server'] ?: ($defaults['rtr_server'] ?? ''); if ($rtrServer) { $config['rtr-server'] = $rtrServer; } // PeeringDB settings if (!empty($defaults['peeringdb_api_key'])) { $config['peeringdb-api-key'] = $defaults['peeringdb_api_key']; } $peeringdbCache = $pv['peeringdb_cache'] ?? ($defaults['peeringdb_cache'] ?? true); $config['peeringdb-cache'] = $peeringdbCache; // Global filtering if ($defaults['keep_filtered'] ?? false) { $config['keep-filtered'] = true; } if ($defaults['merge_paths'] ?? false) { $config['merge-paths'] = true; } if ($defaults['default_route'] ?? false) { $config['default-route'] = true; } if ($defaults['accept_default'] ?? false) { $config['accept-default'] = true; } // Communities if (!empty($defaults['origin_communities'])) { $config['origin-communities'] = $defaults['origin_communities']; } if (!empty($defaults['local_communities'])) { $config['local-communities'] = $defaults['local_communities']; } if (!empty($defaults['add_on_import'])) { $config['add-on-import'] = $defaults['add_on_import']; } if (!empty($defaults['add_on_export'])) { $config['add-on-export'] = $defaults['add_on_export']; } // Blocklist if (!empty($defaults['blocklist'])) { $config['blocklist'] = $defaults['blocklist']; } if (!empty($defaults['blocklist_urls'])) { $config['blocklist-urls'] = $defaults['blocklist_urls']; } // Bogons if (!empty($defaults['bogons4'])) { $config['bogons4'] = $defaults['bogons4']; } if (!empty($defaults['bogons6'])) { $config['bogons6'] = $defaults['bogons6']; } if (!empty($defaults['bogon_asns'])) { $config['bogon-asns'] = $defaults['bogon_asns']; } // Transit ASNs if (!empty($defaults['transit_asns'])) { $config['transit-asns'] = $defaults['transit_asns']; } // Operation modes if ($defaults['no_announce'] ?? false) { $config['no-announce'] = true; } if ($defaults['no_accept'] ?? false) { $config['no-accept'] = true; } if ($defaults['stun'] ?? false) { $config['stun'] = true; } // Global config snippet $globalConfig = $pv['global_config'] ?: ($defaults['global_config'] ?? ''); if ($globalConfig) { $config['global-config'] = $globalConfig; } // Web UI $webUiFile = $pv['web_ui_file'] ?: ($defaults['web_ui_file'] ?? ''); if ($webUiFile) { $config['web-ui-file'] = $webUiFile; } // Logging $logFile = $pv['log_file'] ?: ($defaults['log_file'] ?? 'syslog'); if ($logFile !== 'syslog') { $config['log-file'] = $logFile; } // ASPA if (!empty($defaults['authorized_providers'])) { $config['authorized-providers'] = $defaults['authorized_providers']; } return $config; } /** * Build templates section * * @return array */ private function buildTemplates(): array { $templates = $this->templateManager->getAll(Template::TYPE_PEER); $result = []; foreach ($templates as $id => $template) { $pv = $template['pathvector'] ?? []; $templateConfig = []; // Add all non-empty, non-default values foreach ($pv as $key => $value) { if ($value === null || $value === '' || $value === []) { continue; } $yamlKey = str_replace('_', '-', $key); $templateConfig[$yamlKey] = $value; } if (!empty($templateConfig)) { $result[$id] = $templateConfig; } } return $result; } /** * Build peers section * * @param array $peers * @return array */ private function buildPeers(array $peers): array { $result = []; foreach ($peers as $peer) { if (!($peer['is_active'] ?? true)) { continue; } $pv = $peer['pathvector'] ?? []; $peerConfig = [ 'asn' => $peer['remote_asn'], ]; // Template if (!empty($peer['template'])) { $peerConfig['template'] = $peer['template']; } // Neighbors if (!empty($peer['neighbors'])) { $peerConfig['neighbors'] = $peer['neighbors']; } // Description if (!empty($peer['description'])) { $peerConfig['description'] = $peer['description']; } // Apply template first if (!empty($peer['template'])) { $template = $this->templateManager->get($peer['template']); if ($template) { $templatePv = $template['pathvector'] ?? []; // Template values can be overridden by peer-specific values } } // Add all non-empty, non-default peer-specific values $this->addPeerOptions($peerConfig, $pv); $result[$peer['name']] = $peerConfig; } return $result; } /** * Add peer options to config * * @param array &$config * @param array $pv */ private function addPeerOptions(array &$config, array $pv): void { $optionMappings = [ // Session control 'disabled' => ['default' => false, 'yaml' => 'disabled'], 'import' => ['default' => true, 'yaml' => 'import'], 'export' => ['default' => true, 'yaml' => 'export'], // BGP attributes 'local_asn' => ['default' => null, 'yaml' => 'local-asn'], 'prepends' => ['default' => 0, 'yaml' => 'prepends'], 'prepend_path' => ['default' => [], 'yaml' => 'prepend-path'], 'clear_path' => ['default' => false, 'yaml' => 'clear-path'], 'local_pref' => ['default' => 100, 'yaml' => 'local-pref'], 'local_pref4' => ['default' => null, 'yaml' => 'local-pref4'], 'local_pref6' => ['default' => null, 'yaml' => 'local-pref6'], 'set_local_pref' => ['default' => false, 'yaml' => 'set-local-pref'], 'multihop' => ['default' => null, 'yaml' => 'multihop'], // Listening 'listen4' => ['default' => '', 'yaml' => 'listen4'], 'listen6' => ['default' => '', 'yaml' => 'listen6'], 'local_port' => ['default' => 179, 'yaml' => 'local-port'], 'neighbor_port' => ['default' => 179, 'yaml' => 'neighbor-port'], 'passive' => ['default' => false, 'yaml' => 'passive'], 'direct' => ['default' => false, 'yaml' => 'direct'], // Next-hop 'next_hop_self' => ['default' => false, 'yaml' => 'next-hop-self'], 'next_hop_self_ebgp' => ['default' => false, 'yaml' => 'next-hop-self-ebgp'], 'next_hop_self_ibgp' => ['default' => false, 'yaml' => 'next-hop-self-ibgp'], 'import_next_hop' => ['default' => '', 'yaml' => 'import-next-hop'], 'export_next_hop' => ['default' => '', 'yaml' => 'export-next-hop'], 'enforce_peer_nexthop' => ['default' => true, 'yaml' => 'enforce-peer-nexthop'], 'force_peer_nexthop' => ['default' => false, 'yaml' => 'force-peer-nexthop'], // BFD 'bfd' => ['default' => false, 'yaml' => 'bfd'], // Auth 'password' => ['default' => '', 'yaml' => 'password'], // Route server/reflector 'rs_client' => ['default' => false, 'yaml' => 'rs-client'], 'rr_client' => ['default' => false, 'yaml' => 'rr-client'], // AS path 'remove_private_asns' => ['default' => true, 'yaml' => 'remove-private-asns'], 'allow_local_as' => ['default' => false, 'yaml' => 'allow-local-as'], 'enforce_first_as' => ['default' => true, 'yaml' => 'enforce-first-as'], // Multi-protocol 'mp_unicast_46' => ['default' => false, 'yaml' => 'mp-unicast-46'], // Add-path 'add_path_tx' => ['default' => false, 'yaml' => 'add-path-tx'], 'add_path_rx' => ['default' => false, 'yaml' => 'add-path-rx'], // Confederation 'confederation' => ['default' => null, 'yaml' => 'confederation'], 'confederation_member' => ['default' => false, 'yaml' => 'confederation-member'], // TTL 'ttl_security' => ['default' => false, 'yaml' => 'ttl-security'], // Limits 'receive_limit4' => ['default' => null, 'yaml' => 'receive-limit4'], 'receive_limit6' => ['default' => null, 'yaml' => 'receive-limit6'], 'receive_limit_violation' => ['default' => 'disable', 'yaml' => 'receive-limit-violation'], 'export_limit4' => ['default' => null, 'yaml' => 'export-limit4'], 'export_limit6' => ['default' => null, 'yaml' => 'export-limit6'], 'export_limit_violation' => ['default' => 'disable', 'yaml' => 'export-limit-violation'], // Session options 'interpret_communities' => ['default' => true, 'yaml' => 'interpret-communities'], 'default_local_pref' => ['default' => null, 'yaml' => 'default-local-pref'], 'advertise_hostname' => ['default' => false, 'yaml' => 'advertise-hostname'], 'disable_after_error' => ['default' => false, 'yaml' => 'disable-after-error'], 'prefer_older_routes' => ['default' => false, 'yaml' => 'prefer-older-routes'], 'irr_accept_child_prefixes' => ['default' => false, 'yaml' => 'irr-accept-child-prefixes'], // Communities 'add_on_import' => ['default' => [], 'yaml' => 'add-on-import'], 'add_on_export' => ['default' => [], 'yaml' => 'add-on-export'], 'announce' => ['default' => [], 'yaml' => 'announce'], 'remove_communities' => ['default' => [], 'yaml' => 'remove-communities'], 'remove_all_communities' => ['default' => null, 'yaml' => 'remove-all-communities'], // AS prefs 'as_prefs' => ['default' => [], 'yaml' => 'as-prefs'], 'community_prefs' => ['default' => [], 'yaml' => 'community-prefs'], 'large_community_prefs' => ['default' => [], 'yaml' => 'large-community-prefs'], // AS-SET 'as_set' => ['default' => '', 'yaml' => 'as-set'], // Blackhole 'allow_blackhole_community' => ['default' => false, 'yaml' => 'allow-blackhole-community'], 'blackhole_in' => ['default' => false, 'yaml' => 'blackhole-in'], 'blackhole_out' => ['default' => false, 'yaml' => 'blackhole-out'], // Filtering 'filter_irr' => ['default' => false, 'yaml' => 'filter-irr'], 'filter_rpki' => ['default' => true, 'yaml' => 'filter-rpki'], 'strict_rpki' => ['default' => false, 'yaml' => 'strict-rpki'], 'filter_max_prefix' => ['default' => true, 'yaml' => 'filter-max-prefix'], 'filter_bogon_routes' => ['default' => true, 'yaml' => 'filter-bogon-routes'], 'filter_bogon_asns' => ['default' => true, 'yaml' => 'filter-bogon-asns'], 'filter_transit_asns' => ['default' => false, 'yaml' => 'filter-transit-asns'], 'filter_prefix_length' => ['default' => true, 'yaml' => 'filter-prefix-length'], 'filter_never_via_route_servers' => ['default' => false, 'yaml' => 'filter-never-via-route-servers'], 'filter_as_set' => ['default' => false, 'yaml' => 'filter-as-set'], 'filter_aspa' => ['default' => false, 'yaml' => 'filter-aspa'], 'filter_blocklist' => ['default' => true, 'yaml' => 'filter-blocklist'], // Transit lock 'transit_lock' => ['default' => [], 'yaml' => 'transit-lock'], // Announcement control 'dont_announce' => ['default' => [], 'yaml' => 'dont-announce'], 'only_announce' => ['default' => [], 'yaml' => 'only-announce'], 'prefix_communities' => ['default' => [], 'yaml' => 'prefix-communities'], // Auto-config 'auto_import_limits' => ['default' => false, 'yaml' => 'auto-import-limits'], 'auto_as_set' => ['default' => false, 'yaml' => 'auto-as-set'], 'auto_as_set_members' => ['default' => false, 'yaml' => 'auto-as-set-members'], // Graceful shutdown 'honor_graceful_shutdown' => ['default' => true, 'yaml' => 'honor-graceful-shutdown'], // Prefixes 'prefixes' => ['default' => [], 'yaml' => 'prefixes'], 'as_set_members' => ['default' => [], 'yaml' => 'as-set-members'], // BGP Role 'role' => ['default' => '', 'yaml' => 'role'], 'require_roles' => ['default' => false, 'yaml' => 'require-roles'], // Export options 'announce_default' => ['default' => false, 'yaml' => 'announce-default'], 'announce_originated' => ['default' => true, 'yaml' => 'announce-originated'], 'announce_all' => ['default' => false, 'yaml' => 'announce-all'], // Custom config 'session_global' => ['default' => '', 'yaml' => 'session-global'], 'pre_import_filter' => ['default' => '', 'yaml' => 'pre-import-filter'], 'post_import_filter' => ['default' => '', 'yaml' => 'post-import-filter'], 'pre_import_accept' => ['default' => '', 'yaml' => 'pre-import-accept'], 'pre_export' => ['default' => '', 'yaml' => 'pre-export'], 'pre_export_final' => ['default' => '', 'yaml' => 'pre-export-final'], // Optimizer 'probe_sources' => ['default' => [], 'yaml' => 'probe-sources'], 'optimize_inbound' => ['default' => false, 'yaml' => 'optimize-inbound'], ]; foreach ($optionMappings as $key => $mapping) { $value = $pv[$key] ?? $mapping['default']; if ($value === $mapping['default']) { continue; } if ($value === null || $value === '' || $value === []) { continue; } $config[$mapping['yaml']] = $value; } } /** * Add optional config sections * * @param array &$config * @param array $asn * @param array $node */ private function addOptionalSections(array &$config, array $asn, array $node): void { // Kernel settings $pv = $node['pathvector'] ?? []; if (!empty($pv['kernel_table']) || !empty($pv['kernel_learn']) || !empty($pv['kernel_export'])) { $kernel = []; if (!empty($pv['kernel_table'])) { $kernel['table'] = $pv['kernel_table']; } if ($pv['kernel_learn'] ?? false) { $kernel['learn'] = true; } if (!empty($pv['kernel_export'])) { $kernel['export'] = $pv['kernel_export']; } if (!empty($kernel)) { $config['kernel'] = $kernel; } } } /** * Convert config array to YAML string * * @param array $config * @return string */ private function configToYaml(array $config): string { $yaml = "# Pathvector Configuration\n"; $yaml .= "# Generated by Pathvector Admin Dashboard\n"; $yaml .= "# Generated at: " . date('c') . "\n\n"; // Global config first $globalKeys = array_diff(array_keys($config), ['templates', 'peers', 'kernel', 'vrrp', 'bfd', 'mrt', 'optimizer']); foreach ($globalKeys as $key) { $yaml .= $this->formatYamlEntry($key, $config[$key], 0); } // Templates if (!empty($config['templates'])) { $yaml .= "\ntemplates:\n"; foreach ($config['templates'] as $name => $template) { $yaml .= " $name:\n"; foreach ($template as $key => $value) { $yaml .= $this->formatYamlEntry($key, $value, 2); } } } // Peers if (!empty($config['peers'])) { $yaml .= "\npeers:\n"; foreach ($config['peers'] as $name => $peer) { $yaml .= " $name:\n"; foreach ($peer as $key => $value) { $yaml .= $this->formatYamlEntry($key, $value, 2); } } } // Other sections foreach (['kernel', 'vrrp', 'bfd', 'mrt', 'optimizer'] as $section) { if (!empty($config[$section])) { $yaml .= "\n$section:\n"; foreach ($config[$section] as $key => $value) { $yaml .= $this->formatYamlEntry($key, $value, 1); } } } return $yaml; } /** * Format YAML entry * * @param string $key * @param mixed $value * @param int $indent * @return string */ private function formatYamlEntry(string $key, $value, int $indent): string { $prefix = str_repeat(' ', $indent); if (is_array($value)) { if (empty($value)) { return ''; } if (array_keys($value) === range(0, count($value) - 1)) { // Sequential array $yaml = "$prefix$key:\n"; foreach ($value as $item) { if (is_array($item)) { $yaml .= "$prefix -\n"; foreach ($item as $k => $v) { $yaml .= $this->formatYamlEntry($k, $v, $indent + 2); } } else { $yaml .= "$prefix - " . $this->formatYamlValue($item) . "\n"; } } return $yaml; } else { // Associative array $yaml = "$prefix$key:\n"; foreach ($value as $k => $v) { $yaml .= $this->formatYamlEntry($k, $v, $indent + 1); } return $yaml; } } return "$prefix$key: " . $this->formatYamlValue($value) . "\n"; } /** * Format value for YAML * * @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)) { if (strpos($value, ':') !== false || strpos($value, '#') !== false || strpos($value, '"') !== false || strpos($value, "'") !== false || strpos($value, "\n") !== false) { return '"' . addslashes($value) . '"'; } } return (string) $value; } /** * Validate configuration * * @param string $nodeId * @param string $hostId * @return array */ public function validateConfig(string $nodeId, string $hostId = 'localhost'): array { $result = $this->generateConfig($nodeId); if (!$result['success']) { return $result; } $config = $result['config']; $host = $this->hostManager->get($hostId); if (!$host) { return [ 'success' => false, 'message' => "Host $hostId not found", ]; } // Write config to temp file $tempFile = tempnam(sys_get_temp_dir(), 'pathvector_'); file_put_contents($tempFile, $config); // Run pathvector validate $exec = $host['execution']; $cmd = escapeshellcmd($exec['pathvector_bin']) . ' -c ' . escapeshellarg($tempFile) . ' validate'; $cmdResult = $this->hostManager->executeCommand($hostId, $cmd); // Clean up temp file unlink($tempFile); $this->logger->info('pathvector', "Validated config for node: $nodeId", [ 'success' => $cmdResult['success'], ]); return [ 'success' => $cmdResult['success'], 'message' => $cmdResult['success'] ? 'Configuration is valid' : 'Configuration validation failed', 'output' => $cmdResult['output'], 'config' => $config, ]; } /** * Generate BIRD configuration (dry-run) * * @param string $nodeId * @param string $hostId * @return array */ public function dryRun(string $nodeId, string $hostId = 'localhost'): array { $result = $this->generateConfig($nodeId); if (!$result['success']) { return $result; } $config = $result['config']; $host = $this->hostManager->get($hostId); if (!$host) { return [ 'success' => false, 'message' => "Host $hostId not found", ]; } // Write config to temp file $tempFile = tempnam(sys_get_temp_dir(), 'pathvector_'); file_put_contents($tempFile, $config); // Run pathvector generate (dry-run) $exec = $host['execution']; $outputDir = tempnam(sys_get_temp_dir(), 'bird_'); unlink($outputDir); mkdir($outputDir); $cmd = escapeshellcmd($exec['pathvector_bin']) . ' -c ' . escapeshellarg($tempFile) . ' -o ' . escapeshellarg($outputDir) . ' generate'; $cmdResult = $this->hostManager->executeCommand($hostId, $cmd); // Read generated BIRD config $birdConfig = ''; if ($cmdResult['success'] && file_exists($outputDir . '/bird.conf')) { $birdConfig = file_get_contents($outputDir . '/bird.conf'); } // Clean up unlink($tempFile); $this->recursiveDelete($outputDir); $this->logger->info('pathvector', "Dry-run for node: $nodeId", [ 'success' => $cmdResult['success'], ]); return [ 'success' => $cmdResult['success'], 'message' => $cmdResult['success'] ? 'BIRD configuration generated' : 'Generation failed', 'output' => $cmdResult['output'], 'pathvector_config' => $config, 'bird_config' => $birdConfig, ]; } /** * Apply configuration to node * * @param string $nodeId * @param string $hostId * @return array */ public function applyConfig(string $nodeId, string $hostId = 'localhost'): array { $node = $this->nodeManager->get($nodeId); if (!$node) { return [ 'success' => false, 'message' => "Node $nodeId not found", ]; } $host = $this->hostManager->get($hostId); if (!$host) { return [ 'success' => false, 'message' => "Host $hostId not found", ]; } // Generate config $result = $this->generateConfig($nodeId); if (!$result['success']) { return $result; } $config = $result['config']; $pv = $node['pathvector']; $exec = $host['execution']; // Write config to Pathvector config path $configPath = $pv['config_path'] ?: '/etc/pathvector/pathvector.yml'; // For local execution, write directly // For SSH, use scp or write via SSH if ($exec['method'] === Host::METHOD_LOCAL) { $dir = dirname($configPath); if (!is_dir($dir)) { mkdir($dir, 0755, true); } file_put_contents($configPath, $config); } else { // Write to temp file and copy via SSH $tempFile = tempnam(sys_get_temp_dir(), 'pathvector_'); file_put_contents($tempFile, $config); $scpCmd = 'scp'; if (!empty($exec['ssh_key'])) { $scpCmd .= ' -i ' . escapeshellarg($exec['ssh_key']); } if (!empty($exec['ssh_port']) && $exec['ssh_port'] != 22) { $scpCmd .= ' -P ' . (int) $exec['ssh_port']; } $scpCmd .= ' ' . escapeshellarg($tempFile) . ' ' . escapeshellarg($exec['ssh_user'] . '@' . $exec['ssh_host'] . ':' . $configPath); exec($scpCmd . ' 2>&1', $output, $returnCode); unlink($tempFile); if ($returnCode !== 0) { return [ 'success' => false, 'message' => 'Failed to copy config to remote host', 'output' => implode("\n", $output), ]; } } // Run pathvector generate $cmd = escapeshellcmd($exec['pathvector_bin']) . ' -c ' . escapeshellarg($configPath) . ' generate'; $cmdResult = $this->hostManager->executeCommand($hostId, $cmd); if (!$cmdResult['success']) { return [ 'success' => false, 'message' => 'Failed to generate BIRD config', 'output' => $cmdResult['output'], ]; } // Reload BIRD $reloadCmd = $pv['bird_reload_cmd'] ?: 'birdc configure'; $reloadResult = $this->hostManager->executeCommand($hostId, $reloadCmd); $this->logger->success('pathvector', "Applied config to node: $nodeId on host: $hostId"); return [ 'success' => $reloadResult['success'], 'message' => $reloadResult['success'] ? 'Configuration applied successfully' : 'BIRD reload failed', 'generate_output' => $cmdResult['output'], 'reload_output' => $reloadResult['output'], ]; } /** * Reload BIRD on host * * @param string $hostId * @param string $reloadCmd * @return array */ public function reloadBird(string $hostId = 'localhost', string $reloadCmd = 'birdc configure'): array { $result = $this->hostManager->executeCommand($hostId, $reloadCmd); $this->logger->info('pathvector', "Reloaded BIRD on host: $hostId", [ 'success' => $result['success'], ]); return $result; } /** * Get BIRD status * * @param string $hostId * @return array */ public function getBirdStatus(string $hostId = 'localhost'): array { $host = $this->hostManager->get($hostId); if (!$host) { return [ 'success' => false, 'message' => "Host $hostId not found", ]; } $exec = $host['execution']; $socket = $exec['bird_socket'] ?? '/var/run/bird.ctl'; $cmd = escapeshellcmd($exec['birdc_bin'] ?? 'birdc') . ' -s ' . escapeshellarg($socket) . ' show status'; return $this->hostManager->executeCommand($hostId, $cmd); } /** * Get BGP protocol status * * @param string $hostId * @param string|null $protocol Specific protocol name or null for all * @return array */ public function getProtocolStatus(string $hostId = 'localhost', ?string $protocol = null): array { $host = $this->hostManager->get($hostId); if (!$host) { return [ 'success' => false, 'message' => "Host $hostId not found", ]; } $exec = $host['execution']; $socket = $exec['bird_socket'] ?? '/var/run/bird.ctl'; $birdc = escapeshellcmd($exec['birdc_bin'] ?? 'birdc') . ' -s ' . escapeshellarg($socket); if ($protocol) { $cmd = $birdc . ' show protocols all ' . escapeshellarg($protocol); } else { $cmd = $birdc . ' show protocols'; } return $this->hostManager->executeCommand($hostId, $cmd); } /** * Get route table summary * * @param string $hostId * @param string $table ipv4 or ipv6 * @return array */ public function getRouteCount(string $hostId = 'localhost', string $table = 'master4'): array { $host = $this->hostManager->get($hostId); if (!$host) { return [ 'success' => false, 'message' => "Host $hostId not found", ]; } $exec = $host['execution']; $socket = $exec['bird_socket'] ?? '/var/run/bird.ctl'; $cmd = escapeshellcmd($exec['birdc_bin'] ?? 'birdc') . ' -s ' . escapeshellarg($socket) . ' show route count table ' . escapeshellarg($table); return $this->hostManager->executeCommand($hostId, $cmd); } /** * Get Pathvector version * * @param string $hostId * @return array */ public function getVersion(string $hostId = 'localhost'): array { $host = $this->hostManager->get($hostId); if (!$host) { return [ 'success' => false, 'message' => "Host $hostId not found", ]; } $exec = $host['execution']; $cmd = escapeshellcmd($exec['pathvector_bin']) . ' version'; return $this->hostManager->executeCommand($hostId, $cmd); } /** * Recursively delete directory * * @param string $dir */ private function recursiveDelete(string $dir): void { if (is_dir($dir)) { $objects = scandir($dir); foreach ($objects as $object) { if ($object !== '.' && $object !== '..') { $path = $dir . '/' . $object; if (is_dir($path)) { $this->recursiveDelete($path); } else { unlink($path); } } } rmdir($dir); } } }