- 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.
1047 lines
36 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|