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.
This commit is contained in:
559
pathvector-admin/lib/Host.php
Normal file
559
pathvector-admin/lib/Host.php
Normal file
@@ -0,0 +1,559 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user