db = new FlatFileDB($dataFile); $this->logger = $logger; $this->validator = new Validator(); if (!$this->db->exists('hosts')) { $this->db->set('hosts', []); $this->initializeLocalHost(); } } /** * Initialize localhost entry */ private function initializeLocalHost(): void { $hosts = [ 'localhost' => [ 'id' => 'localhost', 'name' => 'Local Host', 'hostname' => 'localhost', 'description' => 'Local machine', 'execution' => [ 'method' => self::METHOD_LOCAL, 'pathvector_bin' => '/usr/local/bin/pathvector', 'bird_bin' => '/usr/sbin/bird', 'birdc_bin' => '/usr/sbin/birdc', 'bird_socket' => '/var/run/bird.ctl', 'config_dir' => '/etc/pathvector', 'bird_dir' => '/etc/bird', 'sudo' => true, ], 'is_active' => true, 'is_default' => true, 'created_at' => date('c'), 'updated_at' => date('c'), ], ]; $this->db->set('hosts', $hosts); } /** * Get all hosts * * @return array */ public function getAll(): array { return $this->db->get('hosts') ?? []; } /** * Get host by ID * * @param string $id * @return array|null */ public function get(string $id): ?array { $hosts = $this->db->get('hosts') ?? []; return $hosts[$id] ?? null; } /** * Get default host * * @return array|null */ public function getDefault(): ?array { $hosts = $this->db->get('hosts') ?? []; foreach ($hosts as $host) { if ($host['is_default'] ?? false) { return $host; } } // Return first active host if no default foreach ($hosts as $host) { if ($host['is_active'] ?? false) { return $host; } } return null; } /** * Create new host * * @param array $data * @return array */ public function create(array $data): array { if (empty($data['id'])) { return [ 'success' => false, 'message' => 'Host ID is required', 'errors' => ['id' => 'Host ID is required'], ]; } $hosts = $this->db->get('hosts') ?? []; $id = $this->sanitizeId($data['id']); if (isset($hosts[$id])) { return [ 'success' => false, 'message' => "Host $id already exists", 'errors' => ['id' => 'Host ID already exists'], ]; } // Validate SSH settings if SSH method $method = $data['execution']['method'] ?? self::METHOD_LOCAL; if ($method === self::METHOD_SSH) { if (empty($data['execution']['ssh_host'])) { return [ 'success' => false, 'message' => 'SSH host is required for SSH execution method', 'errors' => ['ssh_host' => 'SSH host is required'], ]; } } $hostData = [ 'id' => $id, 'name' => $data['name'] ?? $id, 'hostname' => $data['hostname'] ?? '', 'description' => $data['description'] ?? '', 'execution' => $this->buildExecutionConfig($data['execution'] ?? []), 'metadata' => $data['metadata'] ?? [], 'is_active' => $data['is_active'] ?? true, 'is_default' => false, // New hosts are not default 'created_at' => date('c'), 'updated_at' => date('c'), ]; $hosts[$id] = $hostData; if ($this->db->set('hosts', $hosts)) { $this->logger->success('host', "Created host: $id"); return [ 'success' => true, 'message' => "Host $id created successfully", 'data' => $hostData, ]; } return [ 'success' => false, 'message' => 'Failed to save host', 'errors' => ['database' => 'Failed to save host'], ]; } /** * Update host * * @param string $id * @param array $data * @return array */ public function update(string $id, array $data): array { $hosts = $this->db->get('hosts') ?? []; if (!isset($hosts[$id])) { return [ 'success' => false, 'message' => "Host $id not found", 'errors' => ['id' => 'Host not found'], ]; } $hostData = $hosts[$id]; $allowedFields = ['name', 'hostname', 'description', 'metadata', 'is_active']; foreach ($allowedFields as $field) { if (isset($data[$field])) { $hostData[$field] = $data[$field]; } } if (isset($data['execution'])) { $hostData['execution'] = $this->buildExecutionConfig( array_merge($hostData['execution'], $data['execution']) ); } $hostData['updated_at'] = date('c'); $hosts[$id] = $hostData; if ($this->db->set('hosts', $hosts)) { $this->logger->info('host', "Updated host: $id"); return [ 'success' => true, 'message' => "Host $id updated successfully", 'data' => $hostData, ]; } return [ 'success' => false, 'message' => 'Failed to update host', 'errors' => ['database' => 'Failed to save host'], ]; } /** * Delete host * * @param string $id * @return array */ public function delete(string $id): array { $hosts = $this->db->get('hosts') ?? []; if (!isset($hosts[$id])) { return [ 'success' => false, 'message' => "Host $id not found", ]; } if ($id === 'localhost') { return [ 'success' => false, 'message' => 'Cannot delete localhost', ]; } if ($hosts[$id]['is_default'] ?? false) { return [ 'success' => false, 'message' => 'Cannot delete default host. Set another host as default first.', ]; } $hostName = $hosts[$id]['name']; unset($hosts[$id]); if ($this->db->set('hosts', $hosts)) { $this->logger->warning('host', "Deleted host: $id ($hostName)"); return [ 'success' => true, 'message' => "Host $id deleted successfully", ]; } return [ 'success' => false, 'message' => 'Failed to delete host', ]; } /** * Set default host * * @param string $id * @return array */ public function setDefault(string $id): array { $hosts = $this->db->get('hosts') ?? []; if (!isset($hosts[$id])) { return [ 'success' => false, 'message' => "Host $id not found", ]; } // Unset previous default foreach ($hosts as $hostId => $host) { $hosts[$hostId]['is_default'] = ($hostId === $id); } if ($this->db->set('hosts', $hosts)) { $this->logger->info('host', "Set default host: $id"); return [ 'success' => true, 'message' => "Host $id set as default", ]; } return [ 'success' => false, 'message' => 'Failed to set default host', ]; } /** * Build execution config structure * * @param array $data * @return array */ private function buildExecutionConfig(array $data): array { return [ 'method' => $data['method'] ?? self::METHOD_LOCAL, // Binary paths 'pathvector_bin' => $data['pathvector_bin'] ?? '/usr/local/bin/pathvector', 'bird_bin' => $data['bird_bin'] ?? '/usr/sbin/bird', 'birdc_bin' => $data['birdc_bin'] ?? '/usr/sbin/birdc', // Paths 'bird_socket' => $data['bird_socket'] ?? '/var/run/bird.ctl', 'config_dir' => $data['config_dir'] ?? '/etc/pathvector', 'bird_dir' => $data['bird_dir'] ?? '/etc/bird', 'cache_dir' => $data['cache_dir'] ?? '/var/cache/pathvector', // Permissions 'sudo' => $data['sudo'] ?? true, // SSH settings (for SSH method) 'ssh_host' => $data['ssh_host'] ?? '', 'ssh_port' => $data['ssh_port'] ?? 22, 'ssh_user' => $data['ssh_user'] ?? 'root', 'ssh_key' => $data['ssh_key'] ?? '', 'ssh_options' => $data['ssh_options'] ?? '-o StrictHostKeyChecking=no', // Timeouts 'timeout' => $data['timeout'] ?? 30, 'connect_timeout' => $data['connect_timeout'] ?? 10, ]; } /** * Test host connectivity * * @param string $id * @return array */ public function testConnection(string $id): array { $host = $this->get($id); if (!$host) { return [ 'success' => false, 'message' => "Host $id not found", ]; } $exec = $host['execution']; if ($exec['method'] === self::METHOD_LOCAL) { // Test local pathvector binary $output = []; $returnCode = 0; $cmd = escapeshellcmd($exec['pathvector_bin']) . ' version'; if ($exec['sudo']) { $cmd = 'sudo ' . $cmd; } exec($cmd . ' 2>&1', $output, $returnCode); if ($returnCode === 0) { return [ 'success' => true, 'message' => 'Local host connection successful', 'output' => implode("\n", $output), ]; } else { return [ 'success' => false, 'message' => 'Failed to execute pathvector', 'output' => implode("\n", $output), ]; } } elseif ($exec['method'] === self::METHOD_SSH) { // Test SSH connection $sshCmd = $this->buildSshCommand($exec, 'echo "Connection successful" && ' . escapeshellcmd($exec['pathvector_bin']) . ' version'); $output = []; $returnCode = 0; exec($sshCmd . ' 2>&1', $output, $returnCode); if ($returnCode === 0) { return [ 'success' => true, 'message' => 'SSH connection successful', 'output' => implode("\n", $output), ]; } else { return [ 'success' => false, 'message' => 'SSH connection failed', 'output' => implode("\n", $output), ]; } } return [ 'success' => false, 'message' => 'Unknown execution method', ]; } /** * Build SSH command * * @param array $exec Execution config * @param string $remoteCmd Command to run on remote host * @return string */ public function buildSshCommand(array $exec, string $remoteCmd): string { $ssh = 'ssh'; if (!empty($exec['ssh_key'])) { $ssh .= ' -i ' . escapeshellarg($exec['ssh_key']); } if (!empty($exec['ssh_port']) && $exec['ssh_port'] != 22) { $ssh .= ' -p ' . (int) $exec['ssh_port']; } if (!empty($exec['ssh_options'])) { $ssh .= ' ' . $exec['ssh_options']; } $ssh .= ' -o ConnectTimeout=' . (int) ($exec['connect_timeout'] ?? 10); $target = escapeshellarg($exec['ssh_user'] . '@' . $exec['ssh_host']); if ($exec['sudo'] ?? false) { $remoteCmd = 'sudo ' . $remoteCmd; } return $ssh . ' ' . $target . ' ' . escapeshellarg($remoteCmd); } /** * Execute command on host * * @param string $id Host ID * @param string $command Command to execute * @return array */ public function executeCommand(string $id, string $command): array { $host = $this->get($id); if (!$host) { return [ 'success' => false, 'message' => "Host $id not found", 'output' => '', ]; } if (!($host['is_active'] ?? false)) { return [ 'success' => false, 'message' => "Host $id is not active", 'output' => '', ]; } $exec = $host['execution']; $output = []; $returnCode = 0; if ($exec['method'] === self::METHOD_LOCAL) { $cmd = $command; if ($exec['sudo'] ?? false) { $cmd = 'sudo ' . $cmd; } exec($cmd . ' 2>&1', $output, $returnCode); } elseif ($exec['method'] === self::METHOD_SSH) { $sshCmd = $this->buildSshCommand($exec, $command); exec($sshCmd . ' 2>&1', $output, $returnCode); } else { return [ 'success' => false, 'message' => 'Unknown execution method', 'output' => '', ]; } $this->logger->info('host', "Executed command on $id: $command", ['return_code' => $returnCode]); return [ 'success' => $returnCode === 0, 'message' => $returnCode === 0 ? 'Command executed successfully' : 'Command failed', 'output' => implode("\n", $output), 'return_code' => $returnCode, ]; } /** * Sanitize host ID * * @param string $id * @return string */ private function sanitizeId(string $id): string { $id = strtolower(trim($id)); $id = preg_replace('/\s+/', '-', $id); $id = preg_replace('/[^a-z0-9-_]/', '', $id); return $id; } /** * Count hosts * * @return int */ public function count(): int { $hosts = $this->db->get('hosts') ?? []; return count($hosts); } /** * Get active hosts * * @return array */ public function getActive(): array { $hosts = $this->db->get('hosts') ?? []; return array_filter($hosts, fn($h) => $h['is_active'] ?? false); } /** * Backup hosts data * * @param string $backupDir * @return string|false */ public function backup(string $backupDir): string|false { return $this->db->backup($backupDir); } }