Files
Pathvector.app/pathvector-admin/lib/Pathvector.php
Joseph.Rawlings d8b76233c0 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.
2025-12-14 01:33:12 -05:00

1047 lines
36 KiB
PHP

<?php
/**
* Pathvector Class
*
* Main integration class for interacting with the Pathvector executable
* Handles configuration generation, validation, and BIRD 3 management
*/
require_once __DIR__ . '/FlatFileDB.php';
require_once __DIR__ . '/Logger.php';
require_once __DIR__ . '/Validator.php';
require_once __DIR__ . '/ASN.php';
require_once __DIR__ . '/Node.php';
require_once __DIR__ . '/Peer.php';
require_once __DIR__ . '/Template.php';
require_once __DIR__ . '/Host.php';
class Pathvector {
private Logger $logger;
private array $config;
private ASN $asnManager;
private Node $nodeManager;
private Peer $peerManager;
private Template $templateManager;
private Host $hostManager;
/**
* Constructor
*
* @param array $config Application config
* @param Logger $logger Logger instance
* @param ASN $asnManager
* @param Node $nodeManager
* @param Peer $peerManager
* @param Template $templateManager
* @param Host $hostManager
*/
public function __construct(
array $config,
Logger $logger,
ASN $asnManager,
Node $nodeManager,
Peer $peerManager,
Template $templateManager,
Host $hostManager
) {
$this->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);
}
}
}