Files
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

560 lines
16 KiB
PHP

<?php
/**
* Host Class
*
* Manages execution hosts for Pathvector operations
*/
require_once __DIR__ . '/FlatFileDB.php';
require_once __DIR__ . '/Logger.php';
require_once __DIR__ . '/Validator.php';
class Host {
private FlatFileDB $db;
private Logger $logger;
private Validator $validator;
// Execution methods
public const METHOD_LOCAL = 'local';
public const METHOD_SSH = 'ssh';
/**
* Constructor
*
* @param string $dataFile Path to hosts JSON file
* @param Logger $logger Logger instance
*/
public function __construct(string $dataFile, Logger $logger) {
$this->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);
}
}