3281 lines
128 KiB
PHP
Executable File
3281 lines
128 KiB
PHP
Executable File
#!/usr/bin/env php
|
||
<?php
|
||
|
||
declare(strict_types=1);
|
||
/**
|
||
* Jabali CLI - Command Line Interface for Jabali Panel
|
||
*/
|
||
|
||
define('JABALI_ROOT', dirname(__DIR__));
|
||
define('AGENT_SOCKET', '/var/run/jabali/agent.sock');
|
||
|
||
// Read version from VERSION file
|
||
$versionFile = JABALI_ROOT . '/VERSION';
|
||
$version = '1.0.1';
|
||
if (file_exists($versionFile)) {
|
||
$content = file_get_contents($versionFile);
|
||
if (preg_match('/VERSION=(.+)/', $content, $m)) {
|
||
$version = trim($m[1]);
|
||
}
|
||
}
|
||
define('VERSION', $version);
|
||
|
||
// Colors
|
||
define('C_RESET', "\033[0m");
|
||
define('C_RED', "\033[31m");
|
||
define('C_GREEN', "\033[32m");
|
||
define('C_YELLOW', "\033[33m");
|
||
define('C_BLUE', "\033[34m");
|
||
define('C_MAGENTA', "\033[35m");
|
||
define('C_CYAN', "\033[36m");
|
||
define('C_WHITE', "\033[37m");
|
||
define('C_BOLD', "\033[1m");
|
||
define('C_DIM', "\033[2m");
|
||
|
||
chdir(JABALI_ROOT);
|
||
|
||
// Bootstrap Laravel for database access
|
||
require 'vendor/autoload.php';
|
||
$app = require 'bootstrap/app.php';
|
||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||
$kernel->bootstrap();
|
||
|
||
// Parse arguments
|
||
$args = $_SERVER['argv'];
|
||
array_shift($args); // Remove script name
|
||
|
||
$command = $args[0] ?? '';
|
||
$subcommand = $args[1] ?? '';
|
||
$options = parseOptions(array_slice($args, 2));
|
||
|
||
// Route commands
|
||
switch ($command) {
|
||
case '':
|
||
case 'list':
|
||
case '--help':
|
||
case '-h':
|
||
showHelp();
|
||
break;
|
||
case '--help-full':
|
||
case 'help-full':
|
||
showHelpFull();
|
||
break;
|
||
case 'user':
|
||
handleUser($subcommand, $options);
|
||
break;
|
||
case 'domain':
|
||
handleDomain($subcommand, $options);
|
||
break;
|
||
case 'service':
|
||
handleService($subcommand, $options);
|
||
break;
|
||
case 'wp':
|
||
case 'wordpress':
|
||
handleWordPress($subcommand, $options);
|
||
break;
|
||
case 'db':
|
||
case 'database':
|
||
handleDatabase($subcommand, $options);
|
||
break;
|
||
case 'mail':
|
||
case 'email':
|
||
handleEmail($subcommand, $options);
|
||
break;
|
||
case 'backup':
|
||
handleBackup($subcommand, $options);
|
||
break;
|
||
case 'cpanel':
|
||
handleCpanel($subcommand, $options);
|
||
break;
|
||
case 'system':
|
||
handleSystem($subcommand, $options);
|
||
break;
|
||
case 'agent':
|
||
handleAgent($subcommand, $options);
|
||
break;
|
||
case 'php':
|
||
handlePhp($subcommand, $options);
|
||
break;
|
||
case 'firewall':
|
||
case 'fw':
|
||
handleFirewall($subcommand, $options);
|
||
break;
|
||
case 'ssl':
|
||
handleSsl($subcommand, $options);
|
||
break;
|
||
case '--version':
|
||
case '-v':
|
||
echo "Jabali CLI v" . VERSION . "\n";
|
||
break;
|
||
default:
|
||
error("Unknown command: $command");
|
||
echo "Run 'jabali --help' for usage information.\n";
|
||
exit(1);
|
||
}
|
||
|
||
exit(0);
|
||
|
||
// ============ HELPER FUNCTIONS ============
|
||
|
||
function parseOptions(array $args): array
|
||
{
|
||
$options = ['_args' => []];
|
||
foreach ($args as $arg) {
|
||
if (strpos($arg, '--') === 0) {
|
||
$arg = substr($arg, 2);
|
||
if (strpos($arg, '=') !== false) {
|
||
[$key, $value] = explode('=', $arg, 2);
|
||
$options[$key] = $value;
|
||
} else {
|
||
$options[$arg] = true;
|
||
}
|
||
} elseif (strpos($arg, '-') === 0) {
|
||
$options[substr($arg, 1)] = true;
|
||
} else {
|
||
$options['_args'][] = $arg;
|
||
}
|
||
}
|
||
return $options;
|
||
}
|
||
|
||
function showHelp(): void
|
||
{
|
||
echo "\n" . C_YELLOW . "░░░░░██╗░█████╗░██████╗░░█████╗░██╗░░░░░██╗" . C_RESET . "\n";
|
||
echo C_YELLOW . "░░░░░██║██╔══██╗██╔══██╗██╔══██╗██║░░░░░██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "░░░░░██║███████║██████╦╝███████║██║░░░░░██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "██╗░░██║██╔══██║██╔══██╗██╔══██║██║░░░░░██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "╚█████╔╝██║░░██║██████╦╝██║░░██║███████╗██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "░╚════╝░╚═╝░░╚═╝╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝" . C_RESET . "\n\n";
|
||
echo " " . C_GREEN . C_BOLD . "Jabali CLI" . C_RESET . " v" . VERSION . " - " . C_CYAN . "Modern Web Hosting Control Panel" . C_RESET . "\n\n";
|
||
echo C_YELLOW . "Usage:" . C_RESET . " jabali <command> <subcommand> [options]\n\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "User Management:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "user list" . C_RESET . " List all users\n";
|
||
echo " " . C_GREEN . "user create <username>" . C_RESET . " Create a new user\n";
|
||
echo " " . C_GREEN . "user show <username>" . C_RESET . " Show user details\n";
|
||
echo " " . C_GREEN . "user delete <username>" . C_RESET . " Delete a user\n";
|
||
echo " " . C_GREEN . "user password <username>" . C_RESET . " Change user password\n";
|
||
echo " " . C_GREEN . "user suspend <username>" . C_RESET . " Suspend a user\n";
|
||
echo " " . C_GREEN . "user unsuspend <username>" . C_RESET . " Unsuspend a user\n\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "Domain Management:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "domain list [--user=<user>]" . C_RESET . " List domains\n";
|
||
echo " " . C_GREEN . "domain create <domain>" . C_RESET . " Create a domain\n";
|
||
echo " " . C_GREEN . "domain show <domain>" . C_RESET . " Show domain details\n";
|
||
echo " " . C_GREEN . "domain delete <domain>" . C_RESET . " Delete a domain\n";
|
||
echo " " . C_GREEN . "domain enable <domain>" . C_RESET . " Enable a domain\n";
|
||
echo " " . C_GREEN . "domain disable <domain>" . C_RESET . " Disable a domain\n\n";
|
||
|
||
echo C_DIM . "More commands: service, wp, db, mail, backup, cpanel, system, agent, php, firewall, ssl\n" . C_RESET;
|
||
echo C_DIM . "Run " . C_RESET . C_CYAN . "jabali --help-full" . C_RESET . C_DIM . " for the full command list.\n\n" . C_RESET;
|
||
|
||
echo C_YELLOW . C_BOLD . "Options:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "-h, --help" . C_RESET . " Show this help\n";
|
||
echo " " . C_GREEN . "--help-full" . C_RESET . " Show all commands\n";
|
||
echo " " . C_GREEN . "-v, --version" . C_RESET . " Show version\n";
|
||
echo " " . C_GREEN . "-y, --yes" . C_RESET . " Auto-confirm prompts\n";
|
||
echo " " . C_GREEN . "-q, --quiet" . C_RESET . " Quiet mode\n\n";
|
||
}
|
||
|
||
function showHelpFull(): void
|
||
{
|
||
echo "\n" . C_YELLOW . "░░░░░██╗░█████╗░██████╗░░█████╗░██╗░░░░░██╗" . C_RESET . "\n";
|
||
echo C_YELLOW . "░░░░░██║██╔══██╗██╔══██╗██╔══██╗██║░░░░░██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "░░░░░██║███████║██████╦╝███████║██║░░░░░██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "██╗░░██║██╔══██║██╔══██╗██╔══██║██║░░░░░██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "╚█████╔╝██║░░██║██████╦╝██║░░██║███████╗██║" . C_RESET . "\n";
|
||
echo C_YELLOW . "░╚════╝░╚═╝░░╚═╝╚═════╝░╚═╝░░╚═╝╚══════╝╚═╝" . C_RESET . "\n\n";
|
||
echo " " . C_GREEN . C_BOLD . "Jabali CLI" . C_RESET . " v" . VERSION . " - " . C_CYAN . "Full Command Reference" . C_RESET . "\n\n";
|
||
echo C_YELLOW . "Usage:" . C_RESET . " jabali <command> <subcommand> [options]\n\n";
|
||
|
||
// Two-column layout helper
|
||
$col1Width = 40;
|
||
$printRow = function($left, $right) use ($col1Width) {
|
||
$leftClean = preg_replace('/\e\[[0-9;]*m/', '', $left);
|
||
$padding = max(1, $col1Width - strlen($leftClean));
|
||
echo " " . $left . str_repeat(' ', $padding) . $right . "\n";
|
||
};
|
||
|
||
echo C_YELLOW . C_BOLD . "User Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Domain Management" . C_RESET . "\n";
|
||
$printRow(C_GREEN . "user list" . C_RESET . " - List users", C_GREEN . "domain list" . C_RESET . " - List domains");
|
||
$printRow(C_GREEN . "user create <user>" . C_RESET . " - Create user", C_GREEN . "domain create <d>" . C_RESET . " - Create domain");
|
||
$printRow(C_GREEN . "user show <user>" . C_RESET . " - Show details", C_GREEN . "domain show <d>" . C_RESET . " - Show details");
|
||
$printRow(C_GREEN . "user delete <user>" . C_RESET . " - Delete user", C_GREEN . "domain delete <d>" . C_RESET . " - Delete domain");
|
||
$printRow(C_GREEN . "user password <user>" . C_RESET . " - Set password", C_GREEN . "domain enable <d>" . C_RESET . " - Enable domain");
|
||
$printRow(C_GREEN . "user suspend <user>" . C_RESET . " - Suspend", C_GREEN . "domain disable <d>" . C_RESET . " - Disable domain");
|
||
$printRow(C_GREEN . "user unsuspend <user>" . C_RESET . " - Unsuspend", "");
|
||
echo "\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "Database Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Email Management" . C_RESET . "\n";
|
||
$printRow(C_GREEN . "db list" . C_RESET . " - List databases", C_GREEN . "mail list" . C_RESET . " - List mailboxes");
|
||
$printRow(C_GREEN . "db create <name>" . C_RESET . " - Create database", C_GREEN . "mail create <email>" . C_RESET . " - Create mailbox");
|
||
$printRow(C_GREEN . "db delete <name>" . C_RESET . " - Delete database", C_GREEN . "mail delete <email>" . C_RESET . " - Delete mailbox");
|
||
$printRow(C_GREEN . "db users" . C_RESET . " - List db users", C_GREEN . "mail password <email>" . C_RESET . " - Set password");
|
||
$printRow(C_GREEN . "db user-create <name>" . C_RESET . " - Create db user", C_GREEN . "mail quota <email> <MB>" . C_RESET . " - Set quota");
|
||
$printRow(C_GREEN . "db user-delete <name>" . C_RESET . " - Delete db user", C_GREEN . "mail domains" . C_RESET . " - List mail domains");
|
||
echo "\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "Service Management" . C_RESET . " " . C_YELLOW . C_BOLD . "WordPress Management" . C_RESET . "\n";
|
||
$printRow(C_GREEN . "service list" . C_RESET . " - List services", C_GREEN . "wp list <user>" . C_RESET . " - List WP sites");
|
||
$printRow(C_GREEN . "service status <name>" . C_RESET . " - Show status", C_GREEN . "wp install <user> <d>" . C_RESET . " - Install WP");
|
||
$printRow(C_GREEN . "service start <name>" . C_RESET . " - Start service", C_GREEN . "wp scan <user>" . C_RESET . " - Scan for WP sites");
|
||
$printRow(C_GREEN . "service stop <name>" . C_RESET . " - Stop service", C_GREEN . "wp import <user> <path>" . C_RESET . " - Import WP");
|
||
$printRow(C_GREEN . "service restart <name>" . C_RESET . " - Restart", C_GREEN . "wp delete <user> <id>" . C_RESET . " - Delete WP site");
|
||
$printRow(C_GREEN . "service enable <name>" . C_RESET . " - Enable on boot", C_GREEN . "wp update <user> <id>" . C_RESET . " - Update WP");
|
||
$printRow(C_GREEN . "service disable <name>" . C_RESET . " - Disable on boot", "");
|
||
echo "\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "System Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Agent Management" . C_RESET . "\n";
|
||
$printRow(C_GREEN . "system info" . C_RESET . " - System information", C_GREEN . "agent status" . C_RESET . " - Agent status");
|
||
$printRow(C_GREEN . "system status" . C_RESET . " - Services status", C_GREEN . "agent start" . C_RESET . " - Start agent");
|
||
$printRow(C_GREEN . "system hostname [name]" . C_RESET . " - Get/set hostname", C_GREEN . "agent stop" . C_RESET . " - Stop agent");
|
||
$printRow(C_GREEN . "system disk" . C_RESET . " - Disk usage", C_GREEN . "agent restart" . C_RESET . " - Restart agent");
|
||
$printRow(C_GREEN . "system memory" . C_RESET . " - Memory usage", C_GREEN . "agent ping" . C_RESET . " - Ping agent");
|
||
$printRow("", C_GREEN . "agent log [--lines=N]" . C_RESET . " - View logs");
|
||
echo "\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "PHP Management" . C_RESET . " " . C_YELLOW . C_BOLD . "Firewall Management" . C_RESET . "\n";
|
||
$printRow(C_GREEN . "php list" . C_RESET . " - List PHP versions", C_GREEN . "firewall status" . C_RESET . " - Show status");
|
||
$printRow(C_GREEN . "php install <ver>" . C_RESET . " - Install PHP", C_GREEN . "firewall enable" . C_RESET . " - Enable firewall");
|
||
$printRow(C_GREEN . "php uninstall <ver>" . C_RESET . " - Uninstall PHP", C_GREEN . "firewall disable" . C_RESET . " - Disable firewall");
|
||
$printRow(C_GREEN . "php default [ver]" . C_RESET . " - Get/set default", C_GREEN . "firewall rules" . C_RESET . " - List rules");
|
||
$printRow(C_GREEN . "php status" . C_RESET . " - PHP-FPM status", C_GREEN . "firewall allow <port>" . C_RESET . " - Allow port");
|
||
$printRow("", C_GREEN . "firewall deny <port>" . C_RESET . " - Deny port");
|
||
$printRow("", C_GREEN . "firewall delete <rule>" . C_RESET . " - Delete rule");
|
||
echo "\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "Backup Management" . C_RESET . " " . C_YELLOW . C_BOLD . "SSL Management" . C_RESET . "\n";
|
||
$printRow(C_GREEN . "backup list [--user=<u>]" . C_RESET . " - List backups", C_GREEN . "ssl check" . C_RESET . " - Check/issue/renew certs");
|
||
$printRow(C_GREEN . "backup create <user>" . C_RESET . " - Create user backup", C_GREEN . "ssl issue <domain>" . C_RESET . " - Issue certificate");
|
||
$printRow(C_GREEN . "backup restore <path>" . C_RESET . " - Restore backup", C_GREEN . "ssl renew <domain>" . C_RESET . " - Renew certificate");
|
||
$printRow(C_GREEN . "backup info <path>" . C_RESET . " - Show backup info", C_GREEN . "ssl status <domain>" . C_RESET . " - Show cert status");
|
||
$printRow(C_GREEN . "backup verify <path>" . C_RESET . " - Verify backup", C_GREEN . "ssl list" . C_RESET . " - List all certificates");
|
||
$printRow(C_GREEN . "backup server" . C_RESET . " - Create server backup", "");
|
||
$printRow(C_GREEN . "backup history" . C_RESET . " - Show backup history", "");
|
||
$printRow(C_GREEN . "backup schedules" . C_RESET . " - List schedules", "");
|
||
$printRow(C_GREEN . "backup destinations" . C_RESET . " - List destinations", "");
|
||
$printRow(C_GREEN . "backup help" . C_RESET . " - Show all backup cmds", "");
|
||
echo "\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "Migration & Restore" . C_RESET . "\n";
|
||
$printRow(C_GREEN . "cpanel analyze <file>" . C_RESET . " - Analyze backup", C_GREEN . "cpanel restore <file> <user>" . C_RESET . " - Restore backup");
|
||
$printRow(C_GREEN . "cpanel fix-permissions <file>" . C_RESET . " - Fix backup perms", "");
|
||
echo "\n";
|
||
|
||
echo C_YELLOW . C_BOLD . "Options" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "-h, --help" . C_RESET . " Show basic help " . C_GREEN . "-v, --version" . C_RESET . " Show version\n";
|
||
echo " " . C_GREEN . "--help-full" . C_RESET . " Show full help " . C_GREEN . "-y, --yes" . C_RESET . " Auto-confirm\n";
|
||
echo " " . C_GREEN . "-q, --quiet" . C_RESET . " Quiet mode\n";
|
||
echo "\n";
|
||
}
|
||
|
||
function success(string $message): void
|
||
{
|
||
echo C_GREEN . "✓ " . C_RESET . $message . "\n";
|
||
}
|
||
|
||
function error(string $message): void
|
||
{
|
||
echo C_RED . "✗ " . C_RESET . $message . "\n";
|
||
}
|
||
|
||
function info(string $message): void
|
||
{
|
||
echo C_CYAN . "ℹ " . C_RESET . $message . "\n";
|
||
}
|
||
|
||
function warning(string $message): void
|
||
{
|
||
echo C_YELLOW . "⚠ " . C_RESET . $message . "\n";
|
||
}
|
||
|
||
function confirm(string $message, array $options): bool
|
||
{
|
||
if (isset($options['y']) || isset($options['yes'])) {
|
||
return true;
|
||
}
|
||
echo $message . " [y/N]: ";
|
||
$handle = fopen("php://stdin", "r");
|
||
$line = fgets($handle);
|
||
fclose($handle);
|
||
return strtolower(trim($line)) === 'y';
|
||
}
|
||
|
||
function prompt(string $message, bool $hidden = false): string
|
||
{
|
||
echo $message . ": ";
|
||
if ($hidden) {
|
||
system('stty -echo');
|
||
}
|
||
$handle = fopen("php://stdin", "r");
|
||
$line = trim(fgets($handle));
|
||
fclose($handle);
|
||
if ($hidden) {
|
||
system('stty echo');
|
||
echo "\n";
|
||
}
|
||
return $line;
|
||
}
|
||
|
||
function generateSecurePassword(int $length = 16): string
|
||
{
|
||
$lower = 'abcdefghijklmnopqrstuvwxyz';
|
||
$upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||
$numbers = '0123456789';
|
||
$special = '!@#$%^&*';
|
||
$password = $lower[random_int(0, 25)] . $upper[random_int(0, 25)] . $numbers[random_int(0, 9)] . $special[random_int(0, 7)];
|
||
$all = $lower . $upper . $numbers . $special;
|
||
for ($i = 4; $i < $length; $i++) $password .= $all[random_int(0, strlen($all) - 1)];
|
||
return str_shuffle($password);
|
||
}
|
||
|
||
function validatePassword(string $password): ?string
|
||
{
|
||
if (strlen($password) < 8) return 'Password must be at least 8 characters';
|
||
if (!preg_match('/[a-z]/', $password)) return 'Password must contain a lowercase letter';
|
||
if (!preg_match('/[A-Z]/', $password)) return 'Password must contain an uppercase letter';
|
||
if (!preg_match('/[0-9]/', $password)) return 'Password must contain a number';
|
||
return null;
|
||
}
|
||
|
||
function promptPassword(string $message = "Password", bool $allowAutoGenerate = false): string
|
||
{
|
||
$hint = $allowAutoGenerate ? " (enter to auto-generate)" : "";
|
||
while (true) {
|
||
$password = prompt($message . $hint, true);
|
||
if ($allowAutoGenerate && $password === '') {
|
||
$password = generateSecurePassword();
|
||
echo C_GREEN . "Generated password: " . C_RESET . $password . "\n";
|
||
return $password;
|
||
}
|
||
if ($error = validatePassword($password)) {
|
||
error($error);
|
||
continue;
|
||
}
|
||
return $password;
|
||
}
|
||
}
|
||
|
||
function agentSend(string $action, array $params = []): array
|
||
{
|
||
return agentSendWithTimeout($action, $params, 60);
|
||
}
|
||
|
||
function agentSendWithTimeout(string $action, array $params = [], int $timeoutSeconds = 60): array
|
||
{
|
||
if (!file_exists(AGENT_SOCKET)) {
|
||
return ['success' => false, 'error' => 'Agent socket not found. Is the agent running?'];
|
||
}
|
||
|
||
$socket = @socket_create(AF_UNIX, SOCK_STREAM, 0);
|
||
if (!$socket) {
|
||
return ['success' => false, 'error' => 'Failed to create socket'];
|
||
}
|
||
|
||
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => $timeoutSeconds, 'usec' => 0]);
|
||
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => $timeoutSeconds, 'usec' => 0]);
|
||
|
||
if (!@socket_connect($socket, AGENT_SOCKET)) {
|
||
socket_close($socket);
|
||
return ['success' => false, 'error' => 'Failed to connect to agent'];
|
||
}
|
||
|
||
$request = json_encode(['action' => $action, 'params' => $params]);
|
||
socket_write($socket, $request, strlen($request));
|
||
|
||
$response = '';
|
||
while ($buf = socket_read($socket, 8192)) {
|
||
$response .= $buf;
|
||
}
|
||
socket_close($socket);
|
||
|
||
return json_decode($response, true) ?: ['success' => false, 'error' => 'Invalid response from agent'];
|
||
}
|
||
|
||
function table(array $headers, array $rows): void
|
||
{
|
||
if (empty($rows)) {
|
||
echo C_DIM . "No data to display." . C_RESET . "\n";
|
||
return;
|
||
}
|
||
|
||
// Calculate column widths
|
||
$widths = [];
|
||
foreach ($headers as $i => $header) {
|
||
$widths[$i] = strlen($header);
|
||
}
|
||
foreach ($rows as $row) {
|
||
foreach ($row as $i => $cell) {
|
||
$widths[$i] = max($widths[$i] ?? 0, strlen((string)$cell));
|
||
}
|
||
}
|
||
|
||
// Print header
|
||
echo C_BOLD;
|
||
foreach ($headers as $i => $header) {
|
||
echo str_pad($header, $widths[$i] + 2);
|
||
}
|
||
echo C_RESET . "\n";
|
||
|
||
// Print separator
|
||
foreach ($widths as $width) {
|
||
echo str_repeat('─', $width + 2);
|
||
}
|
||
echo "\n";
|
||
|
||
// Print rows
|
||
foreach ($rows as $row) {
|
||
foreach ($row as $i => $cell) {
|
||
echo str_pad((string)$cell, $widths[$i] + 2);
|
||
}
|
||
echo "\n";
|
||
}
|
||
}
|
||
|
||
// ============ USER COMMANDS ============
|
||
|
||
function handleUser(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
$users = App\Models\User::all();
|
||
$rows = [];
|
||
foreach ($users as $user) {
|
||
$rows[] = [
|
||
$user->id,
|
||
$user->username ?? $user->name,
|
||
$user->email,
|
||
$user->is_admin ? 'Admin' : 'User',
|
||
$user->suspended ? C_RED . 'Suspended' . C_RESET : C_GREEN . 'Active' . C_RESET,
|
||
$user->created_at->format('Y-m-d'),
|
||
];
|
||
}
|
||
table(['ID', 'Username', 'Email', 'Role', 'Status', 'Created'], $rows);
|
||
break;
|
||
|
||
case 'create':
|
||
$username = $options['_args'][0] ?? prompt("Username");
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$email = $options['email'] ?? prompt("Email");
|
||
if (isset($options['password'])) {
|
||
$password = $options['password'];
|
||
if ($err = validatePassword($password)) { error($err); exit(1); }
|
||
} else {
|
||
$password = promptPassword("Password");
|
||
}
|
||
|
||
// Create system user via agent
|
||
$result = agentSend('user.create', ['username' => $username, 'password' => $password]);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to create system user: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
|
||
// Create database user
|
||
$user = App\Models\User::create([
|
||
'name' => $username,
|
||
'username' => $username,
|
||
'email' => $email,
|
||
'password' => bcrypt($password),
|
||
]);
|
||
|
||
success("User '$username' created successfully (ID: {$user->id})");
|
||
break;
|
||
|
||
case 'show':
|
||
$username = $options['_args'][0] ?? null;
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$user = App\Models\User::where('username', $username)->orWhere('name', $username)->first();
|
||
if (!$user) {
|
||
error("User not found: $username");
|
||
exit(1);
|
||
}
|
||
echo "\n" . C_BOLD . "User Details" . C_RESET . "\n";
|
||
echo str_repeat('─', 40) . "\n";
|
||
echo C_CYAN . "ID:" . C_RESET . " {$user->id}\n";
|
||
echo C_CYAN . "Username:" . C_RESET . " " . ($user->username ?? $user->name) . "\n";
|
||
echo C_CYAN . "Email:" . C_RESET . " {$user->email}\n";
|
||
echo C_CYAN . "Role:" . C_RESET . " " . ($user->is_admin ? 'Admin' : 'User') . "\n";
|
||
echo C_CYAN . "Status:" . C_RESET . " " . ($user->suspended ? 'Suspended' : 'Active') . "\n";
|
||
echo C_CYAN . "Created:" . C_RESET . " {$user->created_at}\n";
|
||
$domainCount = App\Models\Domain::where('user_id', $user->id)->count();
|
||
echo C_CYAN . "Domains:" . C_RESET . " $domainCount\n";
|
||
break;
|
||
|
||
case 'delete':
|
||
$username = $options['_args'][0] ?? null;
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$user = App\Models\User::where('username', $username)->orWhere('name', $username)->first();
|
||
if (!$user) {
|
||
error("User not found: $username");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete user '$username'? This cannot be undone.", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
// Delete system user
|
||
$result = agentSend('user.delete', ['username' => $username]);
|
||
if (!($result['success'] ?? false)) {
|
||
warning("Could not delete system user: " . ($result['error'] ?? 'Unknown error'));
|
||
}
|
||
|
||
$user->delete();
|
||
success("User '$username' deleted");
|
||
break;
|
||
|
||
case 'password':
|
||
$username = $options['_args'][0] ?? null;
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$user = App\Models\User::where('username', $username)->orWhere('name', $username)->first();
|
||
if (!$user) {
|
||
error("User not found: $username");
|
||
exit(1);
|
||
}
|
||
if (isset($options['password'])) {
|
||
$password = $options['password'];
|
||
if ($err = validatePassword($password)) { error($err); exit(1); }
|
||
} else {
|
||
$password = promptPassword("New password", true);
|
||
}
|
||
$user->password = bcrypt($password);
|
||
$user->save();
|
||
|
||
// Update system user password
|
||
agentSend('user.password', ['username' => $username, 'password' => $password]);
|
||
|
||
success("Password updated for '$username'");
|
||
break;
|
||
|
||
case 'suspend':
|
||
$username = $options['_args'][0] ?? null;
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$user = App\Models\User::where('username', $username)->orWhere('name', $username)->first();
|
||
if (!$user) {
|
||
error("User not found: $username");
|
||
exit(1);
|
||
}
|
||
$user->suspended = true;
|
||
$user->save();
|
||
success("User '$username' suspended");
|
||
break;
|
||
|
||
case 'unsuspend':
|
||
$username = $options['_args'][0] ?? null;
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$user = App\Models\User::where('username', $username)->orWhere('name', $username)->first();
|
||
if (!$user) {
|
||
error("User not found: $username");
|
||
exit(1);
|
||
}
|
||
$user->suspended = false;
|
||
$user->save();
|
||
success("User '$username' unsuspended");
|
||
break;
|
||
|
||
default:
|
||
error("Unknown user command: $subcommand");
|
||
echo "Run 'jabali user --help' for available commands.\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ DOMAIN COMMANDS ============
|
||
|
||
function handleDomain(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
$query = App\Models\Domain::with('user');
|
||
if (isset($options['user'])) {
|
||
$user = App\Models\User::where('username', $options['user'])->orWhere('name', $options['user'])->first();
|
||
if ($user) {
|
||
$query->where('user_id', $user->id);
|
||
}
|
||
}
|
||
$domains = $query->get();
|
||
$rows = [];
|
||
foreach ($domains as $domain) {
|
||
$rows[] = [
|
||
$domain->id,
|
||
$domain->domain,
|
||
$domain->user->username ?? $domain->user->name ?? 'N/A',
|
||
$domain->is_active ? C_GREEN . 'Active' . C_RESET : C_RED . 'Inactive' . C_RESET,
|
||
$domain->ssl_enabled ? C_GREEN . 'SSL' . C_RESET : C_DIM . 'No SSL' . C_RESET,
|
||
$domain->created_at->format('Y-m-d'),
|
||
];
|
||
}
|
||
table(['ID', 'Domain', 'User', 'Status', 'SSL', 'Created'], $rows);
|
||
break;
|
||
|
||
case 'create':
|
||
$domain = $options['_args'][0] ?? prompt("Domain name");
|
||
if (!$domain) {
|
||
error("Domain name is required");
|
||
exit(1);
|
||
}
|
||
$username = $options['user'] ?? prompt("Username");
|
||
$user = App\Models\User::where('username', $username)->orWhere('name', $username)->first();
|
||
if (!$user) {
|
||
error("User not found: $username");
|
||
exit(1);
|
||
}
|
||
|
||
$result = agentSend('domain.create', [
|
||
'username' => $username,
|
||
'domain' => $domain,
|
||
]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
$domainModel = App\Models\Domain::create([
|
||
'user_id' => $user->id,
|
||
'domain' => $domain,
|
||
'is_active' => true,
|
||
]);
|
||
success("Domain '$domain' created (ID: {$domainModel->id})");
|
||
} else {
|
||
error("Failed to create domain: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'show':
|
||
$domain = $options['_args'][0] ?? null;
|
||
if (!$domain) {
|
||
error("Domain name is required");
|
||
exit(1);
|
||
}
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->with('user')->first();
|
||
if (!$domainModel) {
|
||
error("Domain not found: $domain");
|
||
exit(1);
|
||
}
|
||
echo "\n" . C_BOLD . "Domain Details" . C_RESET . "\n";
|
||
echo str_repeat('─', 40) . "\n";
|
||
echo C_CYAN . "ID:" . C_RESET . " {$domainModel->id}\n";
|
||
echo C_CYAN . "Domain:" . C_RESET . " {$domainModel->domain}\n";
|
||
echo C_CYAN . "User:" . C_RESET . " " . ($domainModel->user->username ?? $domainModel->user->name) . "\n";
|
||
echo C_CYAN . "Status:" . C_RESET . " " . ($domainModel->is_active ? 'Active' : 'Inactive') . "\n";
|
||
echo C_CYAN . "SSL:" . C_RESET . " " . ($domainModel->ssl_enabled ? 'Enabled' : 'Disabled') . "\n";
|
||
echo C_CYAN . "Created:" . C_RESET . " {$domainModel->created_at}\n";
|
||
break;
|
||
|
||
case 'delete':
|
||
$domain = $options['_args'][0] ?? null;
|
||
if (!$domain) {
|
||
error("Domain name is required");
|
||
exit(1);
|
||
}
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->with('user')->first();
|
||
if (!$domainModel) {
|
||
error("Domain not found: $domain");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete domain '$domain'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
$username = $domainModel->user->username ?? $domainModel->user->name;
|
||
$result = agentSend('domain.delete', ['username' => $username, 'domain' => $domain]);
|
||
|
||
$domainModel->delete();
|
||
success("Domain '$domain' deleted");
|
||
break;
|
||
|
||
case 'enable':
|
||
$domain = $options['_args'][0] ?? null;
|
||
if (!$domain) {
|
||
error("Domain name is required");
|
||
exit(1);
|
||
}
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->first();
|
||
if (!$domainModel) {
|
||
error("Domain not found: $domain");
|
||
exit(1);
|
||
}
|
||
$domainModel->is_active = true;
|
||
$domainModel->save();
|
||
success("Domain '$domain' enabled");
|
||
break;
|
||
|
||
case 'disable':
|
||
$domain = $options['_args'][0] ?? null;
|
||
if (!$domain) {
|
||
error("Domain name is required");
|
||
exit(1);
|
||
}
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->first();
|
||
if (!$domainModel) {
|
||
error("Domain not found: $domain");
|
||
exit(1);
|
||
}
|
||
$domainModel->is_active = false;
|
||
$domainModel->save();
|
||
success("Domain '$domain' disabled");
|
||
break;
|
||
|
||
default:
|
||
error("Unknown domain command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ SERVICE COMMANDS ============
|
||
|
||
function handleService(string $subcommand, array $options): void
|
||
{
|
||
$services = [
|
||
'nginx', 'mariadb', 'redis-server', 'postfix', 'dovecot',
|
||
'rspamd', 'clamav-daemon', 'named', 'opendkim', 'fail2ban',
|
||
'ssh', 'cron'
|
||
];
|
||
|
||
// Add PHP-FPM versions
|
||
exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $phpServices);
|
||
foreach ($phpServices as $svc) {
|
||
if (preg_match('/php[\d.]+-fpm/', basename($svc, '.service'), $m)) {
|
||
$services[] = $m[0];
|
||
}
|
||
}
|
||
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
$result = agentSend('service.list', ['services' => $services]);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to get service list: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
$rows = [];
|
||
foreach ($result['services'] ?? [] as $name => $status) {
|
||
$rows[] = [
|
||
$name,
|
||
$status['is_active'] ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET,
|
||
$status['is_enabled'] ? C_GREEN . 'Enabled' . C_RESET : C_DIM . 'Disabled' . C_RESET,
|
||
];
|
||
}
|
||
table(['Service', 'Status', 'Boot'], $rows);
|
||
break;
|
||
|
||
case 'status':
|
||
$service = $options['_args'][0] ?? null;
|
||
if (!$service) {
|
||
error("Service name is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('service.list', ['services' => [$service]]);
|
||
if ($result['success'] ?? false) {
|
||
$status = $result['services'][$service] ?? null;
|
||
if ($status) {
|
||
echo C_BOLD . $service . C_RESET . ": ";
|
||
echo ($status['is_active'] ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET);
|
||
echo " (Boot: " . ($status['is_enabled'] ? 'Enabled' : 'Disabled') . ")\n";
|
||
} else {
|
||
error("Service not found: $service");
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 'start':
|
||
$service = $options['_args'][0] ?? null;
|
||
if (!$service) {
|
||
error("Service name is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('service.start', ['service' => $service]);
|
||
if ($result['success'] ?? false) {
|
||
success("Service '$service' started");
|
||
} else {
|
||
error("Failed to start '$service': " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'stop':
|
||
$service = $options['_args'][0] ?? null;
|
||
if (!$service) {
|
||
error("Service name is required");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to stop '$service'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
$result = agentSend('service.stop', ['service' => $service]);
|
||
if ($result['success'] ?? false) {
|
||
success("Service '$service' stopped");
|
||
} else {
|
||
error("Failed to stop '$service': " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'restart':
|
||
$service = $options['_args'][0] ?? null;
|
||
if (!$service) {
|
||
error("Service name is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('service.restart', ['service' => $service]);
|
||
if ($result['success'] ?? false) {
|
||
success("Service '$service' restarted");
|
||
} else {
|
||
error("Failed to restart '$service': " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'enable':
|
||
$service = $options['_args'][0] ?? null;
|
||
if (!$service) {
|
||
error("Service name is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('service.enable', ['service' => $service]);
|
||
if ($result['success'] ?? false) {
|
||
success("Service '$service' enabled on boot");
|
||
} else {
|
||
error("Failed to enable '$service': " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'disable':
|
||
$service = $options['_args'][0] ?? null;
|
||
if (!$service) {
|
||
error("Service name is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('service.disable', ['service' => $service]);
|
||
if ($result['success'] ?? false) {
|
||
success("Service '$service' disabled on boot");
|
||
} else {
|
||
error("Failed to disable '$service': " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
error("Unknown service command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ WORDPRESS COMMANDS ============
|
||
|
||
function handleWordPress(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
$username = $options['_args'][0] ?? null;
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('wp.list', ['username' => $username]);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to list WordPress sites: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
$rows = [];
|
||
foreach ($result['sites'] ?? [] as $site) {
|
||
$rows[] = [
|
||
$site['id'],
|
||
$site['domain'] ?? 'N/A',
|
||
$site['version'] ?? 'N/A',
|
||
$site['url'] ?? 'N/A',
|
||
];
|
||
}
|
||
table(['ID', 'Domain', 'Version', 'URL'], $rows);
|
||
break;
|
||
|
||
case 'install':
|
||
$username = $options['_args'][0] ?? null;
|
||
$domain = $options['_args'][1] ?? null;
|
||
if (!$username || !$domain) {
|
||
error("Usage: jabali wp install <username> <domain>");
|
||
exit(1);
|
||
}
|
||
$title = $options['title'] ?? 'My WordPress Site';
|
||
$adminUser = $options['admin'] ?? 'admin';
|
||
$adminEmail = $options['email'] ?? prompt("Admin email");
|
||
$adminPass = $options['password'] ?? null;
|
||
|
||
info("Installing WordPress for $username on $domain...");
|
||
$result = agentSend('wp.install', [
|
||
'username' => $username,
|
||
'domain' => $domain,
|
||
'title' => $title,
|
||
'admin_user' => $adminUser,
|
||
'admin_email' => $adminEmail,
|
||
'admin_password' => $adminPass,
|
||
]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
success("WordPress installed successfully!");
|
||
echo C_CYAN . "URL:" . C_RESET . " https://$domain\n";
|
||
echo C_CYAN . "Admin:" . C_RESET . " https://$domain/wp-admin/\n";
|
||
echo C_CYAN . "Username:" . C_RESET . " $adminUser\n";
|
||
if (isset($result['admin_password'])) {
|
||
echo C_CYAN . "Password:" . C_RESET . " {$result['admin_password']}\n";
|
||
}
|
||
} else {
|
||
error("Failed to install WordPress: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'scan':
|
||
$username = $options['_args'][0] ?? null;
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
info("Scanning for WordPress installations...");
|
||
$result = agentSend('wp.scan', ['username' => $username]);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to scan: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
$found = $result['found'] ?? [];
|
||
if (empty($found)) {
|
||
info("No untracked WordPress installations found.");
|
||
} else {
|
||
success("Found " . count($found) . " WordPress installation(s):");
|
||
$rows = [];
|
||
foreach ($found as $site) {
|
||
$rows[] = [
|
||
$site['path'],
|
||
$site['version'] ?? 'N/A',
|
||
$site['site_url'] ?? 'N/A',
|
||
];
|
||
}
|
||
table(['Path', 'Version', 'URL'], $rows);
|
||
}
|
||
break;
|
||
|
||
case 'import':
|
||
$username = $options['_args'][0] ?? null;
|
||
$path = $options['_args'][1] ?? null;
|
||
if (!$username || !$path) {
|
||
error("Usage: jabali wp import <username> <path>");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('wp.import', ['username' => $username, 'path' => $path]);
|
||
if ($result['success'] ?? false) {
|
||
success("WordPress site imported: " . ($result['site_id'] ?? ''));
|
||
} else {
|
||
error("Failed to import: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'delete':
|
||
$username = $options['_args'][0] ?? null;
|
||
$siteId = $options['_args'][1] ?? null;
|
||
if (!$username || !$siteId) {
|
||
error("Usage: jabali wp delete <username> <site_id>");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete this WordPress site?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
$result = agentSend('wp.delete', [
|
||
'username' => $username,
|
||
'site_id' => $siteId,
|
||
'delete_files' => isset($options['files']),
|
||
'delete_database' => isset($options['database']),
|
||
]);
|
||
if ($result['success'] ?? false) {
|
||
success("WordPress site deleted");
|
||
} else {
|
||
error("Failed to delete: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'update':
|
||
$username = $options['_args'][0] ?? null;
|
||
$siteId = $options['_args'][1] ?? null;
|
||
if (!$username || !$siteId) {
|
||
error("Usage: jabali wp update <username> <site_id>");
|
||
exit(1);
|
||
}
|
||
info("Updating WordPress...");
|
||
$result = agentSend('wp.update', ['username' => $username, 'site_id' => $siteId]);
|
||
if ($result['success'] ?? false) {
|
||
success("WordPress updated to " . ($result['new_version'] ?? 'latest'));
|
||
} else {
|
||
error("Failed to update: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
error("Unknown WordPress command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ DATABASE COMMANDS ============
|
||
|
||
function handleDatabase(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
$username = $options['user'] ?? null;
|
||
$result = agentSend('mysql.list_databases', ['username' => $username ?? 'admin']);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to list databases: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
$rows = [];
|
||
foreach ($result['databases'] ?? [] as $db) {
|
||
$rows[] = [
|
||
is_array($db) ? ($db['name'] ?? $db) : $db,
|
||
];
|
||
}
|
||
table(['Database'], $rows);
|
||
break;
|
||
|
||
case 'create':
|
||
$dbName = $options['_args'][0] ?? prompt("Database name");
|
||
if (!$dbName) {
|
||
error("Database name is required");
|
||
exit(1);
|
||
}
|
||
$username = $options['user'] ?? 'admin';
|
||
$result = agentSend('mysql.create_database', [
|
||
'username' => $username,
|
||
'database' => $dbName,
|
||
]);
|
||
if ($result['success'] ?? false) {
|
||
success("Database '$dbName' created");
|
||
} else {
|
||
error("Failed to create database: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'delete':
|
||
$dbName = $options['_args'][0] ?? null;
|
||
if (!$dbName) {
|
||
error("Database name is required");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete database '$dbName'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
$result = agentSend('mysql.delete_database', ['database' => $dbName]);
|
||
if ($result['success'] ?? false) {
|
||
success("Database '$dbName' deleted");
|
||
} else {
|
||
error("Failed to delete database: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'users':
|
||
$username = $options['user'] ?? 'admin';
|
||
$result = agentSend('mysql.list_users', ['username' => $username]);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to list users: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
$rows = [];
|
||
foreach ($result['users'] ?? [] as $user) {
|
||
$rows[] = [
|
||
is_array($user) ? ($user['user'] ?? $user['name'] ?? $user) : $user,
|
||
is_array($user) ? ($user['host'] ?? 'localhost') : 'localhost',
|
||
];
|
||
}
|
||
table(['User', 'Host'], $rows);
|
||
break;
|
||
|
||
case 'user-create':
|
||
$dbUser = $options['_args'][0] ?? prompt("Username");
|
||
if (isset($options['password'])) {
|
||
$password = $options['password'];
|
||
if ($err = validatePassword($password)) { error($err); exit(1); }
|
||
} else {
|
||
$password = promptPassword("Password");
|
||
}
|
||
$host = $options['host'] ?? 'localhost';
|
||
$result = agentSend('mysql.create_user', [
|
||
'username' => $dbUser,
|
||
'password' => $password,
|
||
'host' => $host,
|
||
]);
|
||
if ($result['success'] ?? false) {
|
||
success("Database user '$dbUser' created");
|
||
} else {
|
||
error("Failed to create user: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'user-delete':
|
||
$dbUser = $options['_args'][0] ?? null;
|
||
if (!$dbUser) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$host = $options['host'] ?? 'localhost';
|
||
if (!confirm("Are you sure you want to delete database user '$dbUser'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
$result = agentSend('mysql.delete_user', ['username' => $dbUser, 'host' => $host]);
|
||
if ($result['success'] ?? false) {
|
||
success("Database user '$dbUser' deleted");
|
||
} else {
|
||
error("Failed to delete user: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
error("Unknown database command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ EMAIL COMMANDS ============
|
||
|
||
function handleEmail(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
$query = App\Models\Mailbox::query();
|
||
if (isset($options['domain'])) {
|
||
$query->where('domain', $options['domain']);
|
||
}
|
||
$mailboxes = $query->get();
|
||
$rows = [];
|
||
foreach ($mailboxes as $mb) {
|
||
$rows[] = [
|
||
$mb->id,
|
||
$mb->local_part . '@' . $mb->domain,
|
||
$mb->quota_mb . ' MB',
|
||
$mb->is_active ? C_GREEN . 'Active' . C_RESET : C_RED . 'Inactive' . C_RESET,
|
||
];
|
||
}
|
||
table(['ID', 'Email', 'Quota', 'Status'], $rows);
|
||
break;
|
||
|
||
case 'create':
|
||
$email = $options['_args'][0] ?? prompt("Email address");
|
||
if (!$email || !strpos($email, '@')) {
|
||
error("Valid email address is required");
|
||
exit(1);
|
||
}
|
||
[$localPart, $domain] = explode('@', $email);
|
||
if (isset($options['password'])) {
|
||
$password = $options['password'];
|
||
if ($err = validatePassword($password)) { error($err); exit(1); }
|
||
} else {
|
||
$password = promptPassword("Password");
|
||
}
|
||
$quota = $options['quota'] ?? 1024;
|
||
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->first();
|
||
if (!$domainModel) {
|
||
error("Domain not found: $domain");
|
||
exit(1);
|
||
}
|
||
|
||
$result = agentSend('email.mailbox_create', [
|
||
'username' => $domainModel->user->username ?? $domainModel->user->name,
|
||
'domain' => $domain,
|
||
'local_part' => $localPart,
|
||
'password' => $password,
|
||
'quota_mb' => (int)$quota,
|
||
]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
App\Models\Mailbox::create([
|
||
'domain_id' => $domainModel->id,
|
||
'local_part' => $localPart,
|
||
'domain' => $domain,
|
||
'password_hash' => $result['password_hash'] ?? '',
|
||
'quota_mb' => (int)$quota,
|
||
'is_active' => true,
|
||
]);
|
||
success("Mailbox '$email' created");
|
||
} else {
|
||
error("Failed to create mailbox: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'delete':
|
||
$email = $options['_args'][0] ?? null;
|
||
if (!$email) {
|
||
error("Email address is required");
|
||
exit(1);
|
||
}
|
||
[$localPart, $domain] = explode('@', $email);
|
||
$mailbox = App\Models\Mailbox::where('local_part', $localPart)->where('domain', $domain)->first();
|
||
if (!$mailbox) {
|
||
error("Mailbox not found: $email");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete mailbox '$email'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->first();
|
||
$username = $domainModel ? ($domainModel->user->username ?? $domainModel->user->name) : 'admin';
|
||
|
||
agentSend('email.mailbox_delete', [
|
||
'username' => $username,
|
||
'domain' => $domain,
|
||
'local_part' => $localPart,
|
||
]);
|
||
|
||
$mailbox->delete();
|
||
success("Mailbox '$email' deleted");
|
||
break;
|
||
|
||
case 'password':
|
||
$email = $options['_args'][0] ?? null;
|
||
if (!$email) {
|
||
error("Email address is required");
|
||
exit(1);
|
||
}
|
||
[$localPart, $domain] = explode('@', $email);
|
||
$mailbox = App\Models\Mailbox::where('local_part', $localPart)->where('domain', $domain)->first();
|
||
if (!$mailbox) {
|
||
error("Mailbox not found: $email");
|
||
exit(1);
|
||
}
|
||
if (isset($options['password'])) {
|
||
$password = $options['password'];
|
||
if ($err = validatePassword($password)) { error($err); exit(1); }
|
||
} else {
|
||
$password = promptPassword("New password", true);
|
||
}
|
||
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->first();
|
||
$username = $domainModel ? ($domainModel->user->username ?? $domainModel->user->name) : 'admin';
|
||
|
||
$result = agentSend('email.mailbox_change_password', [
|
||
'username' => $username,
|
||
'domain' => $domain,
|
||
'local_part' => $localPart,
|
||
'password' => $password,
|
||
]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
if (isset($result['password_hash'])) {
|
||
$mailbox->password_hash = $result['password_hash'];
|
||
$mailbox->save();
|
||
}
|
||
success("Password updated for '$email'");
|
||
} else {
|
||
error("Failed to update password: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'quota':
|
||
$email = $options['_args'][0] ?? null;
|
||
$quota = $options['_args'][1] ?? null;
|
||
if (!$email || !$quota) {
|
||
error("Usage: jabali mail quota <email> <size_mb>");
|
||
exit(1);
|
||
}
|
||
[$localPart, $domain] = explode('@', $email);
|
||
$mailbox = App\Models\Mailbox::where('local_part', $localPart)->where('domain', $domain)->first();
|
||
if (!$mailbox) {
|
||
error("Mailbox not found: $email");
|
||
exit(1);
|
||
}
|
||
|
||
$domainModel = App\Models\Domain::where('domain', $domain)->first();
|
||
$username = $domainModel ? ($domainModel->user->username ?? $domainModel->user->name) : 'admin';
|
||
|
||
$result = agentSend('email.mailbox_set_quota', [
|
||
'username' => $username,
|
||
'domain' => $domain,
|
||
'local_part' => $localPart,
|
||
'quota_mb' => (int)$quota,
|
||
]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
$mailbox->quota_mb = (int)$quota;
|
||
$mailbox->save();
|
||
success("Quota set to {$quota}MB for '$email'");
|
||
} else {
|
||
error("Failed to set quota: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'domains':
|
||
$domains = App\Models\Domain::where('mail_enabled', true)->get();
|
||
$rows = [];
|
||
foreach ($domains as $d) {
|
||
$mailboxCount = App\Models\Mailbox::where('domain', $d->domain)->count();
|
||
$rows[] = [
|
||
$d->domain,
|
||
$mailboxCount,
|
||
$d->dkim_enabled ? C_GREEN . 'Yes' . C_RESET : C_DIM . 'No' . C_RESET,
|
||
];
|
||
}
|
||
table(['Domain', 'Mailboxes', 'DKIM'], $rows);
|
||
break;
|
||
|
||
default:
|
||
error("Unknown email command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ BACKUP COMMANDS ============
|
||
|
||
function handleBackup(string $subcommand, array $options): void
|
||
{
|
||
$backupDir = '/var/backups/jabali';
|
||
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
case 'user-list':
|
||
$username = $options['user'] ?? $options['username'] ?? $options['_args'][0] ?? null;
|
||
|
||
if ($username !== null && !is_string($username)) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
if (is_string($username)) {
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
$username = null;
|
||
}
|
||
}
|
||
if ($subcommand === 'user-list' && $username === null) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
|
||
if ($subcommand === 'user-list' || $username !== null) {
|
||
if (!$username) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
|
||
$path = $options['path'] ?? '';
|
||
$result = agentSend('backup.list', [
|
||
'username' => $username,
|
||
'path' => $path,
|
||
]);
|
||
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to list backups: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
|
||
$backups = $result['backups'] ?? [];
|
||
if (empty($backups)) {
|
||
info("No backups found for $username.");
|
||
exit(0);
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($backups as $backup) {
|
||
$manifest = $backup['manifest'] ?? [];
|
||
$createdAt = $backup['created_at'] ?? null;
|
||
|
||
$rows[] = [
|
||
$backup['name'] ?? '-',
|
||
$backup['type'] ?? '-',
|
||
formatBytes((int) ($backup['size'] ?? 0)),
|
||
is_array($manifest['domains'] ?? null) ? count($manifest['domains']) : '-',
|
||
is_array($manifest['databases'] ?? null) ? count($manifest['databases']) : '-',
|
||
is_array($manifest['mailboxes'] ?? null) ? count($manifest['mailboxes']) : '-',
|
||
$createdAt ? date('Y-m-d H:i', strtotime($createdAt)) : '-',
|
||
];
|
||
}
|
||
|
||
table(['Name', 'Type', 'Size', 'Domains', 'DBs', 'Mailboxes', 'Created'], $rows);
|
||
break;
|
||
}
|
||
|
||
if (!is_dir($backupDir)) {
|
||
info("No local backups found.");
|
||
exit(0);
|
||
}
|
||
|
||
$files = array_merge(
|
||
glob("$backupDir/*.tar.gz") ?: [],
|
||
glob("$backupDir/*", GLOB_ONLYDIR) ?: []
|
||
);
|
||
$files = array_filter($files, fn($f) => basename($f) !== '.' && basename($f) !== '..');
|
||
if (empty($files)) {
|
||
info("No local backups found.");
|
||
exit(0);
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($files as $file) {
|
||
if (is_dir($file)) {
|
||
$size = trim(shell_exec("du -sh " . escapeshellarg($file) . " 2>/dev/null | cut -f1") ?: '0');
|
||
$rows[] = [
|
||
basename($file) . '/',
|
||
$size,
|
||
date('Y-m-d H:i', filemtime($file)),
|
||
'server',
|
||
];
|
||
} else {
|
||
$rows[] = [
|
||
basename($file),
|
||
formatBytes((int) filesize($file)),
|
||
date('Y-m-d H:i', filemtime($file)),
|
||
'user',
|
||
];
|
||
}
|
||
}
|
||
table(['Filename', 'Size', 'Created', 'Type'], $rows);
|
||
break;
|
||
|
||
case 'create':
|
||
$username = $options['_args'][0] ?? $options['user'] ?? $options['username'] ?? null;
|
||
if (!is_string($username)) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$exists = agentSend('user.exists', ['username' => $username]);
|
||
if (!($exists['success'] ?? false) || !($exists['exists'] ?? false)) {
|
||
error("User not found: $username");
|
||
exit(1);
|
||
}
|
||
|
||
$backupType = $options['type'] ?? 'full';
|
||
if (!in_array($backupType, ['full', 'incremental'], true)) {
|
||
error("Invalid backup type: $backupType (use full or incremental)");
|
||
exit(1);
|
||
}
|
||
|
||
$timestamp = date('Y-m-d_His');
|
||
$outputPath = $options['output'] ?? $options['path'] ?? null;
|
||
if ($outputPath !== null && !is_string($outputPath)) {
|
||
error("Output path is required");
|
||
exit(1);
|
||
}
|
||
if (is_string($outputPath)) {
|
||
$outputPath = trim($outputPath);
|
||
if ($outputPath === '') {
|
||
$outputPath = null;
|
||
}
|
||
}
|
||
if ($outputPath === null) {
|
||
$outputPath = $backupType === 'incremental'
|
||
? "/home/$username/backups/{$username}_{$timestamp}"
|
||
: "/home/$username/backups/{$username}_{$timestamp}.tar.gz";
|
||
}
|
||
|
||
$params = [
|
||
'username' => $username,
|
||
'output_path' => $outputPath,
|
||
'backup_type' => $backupType,
|
||
'include_files' => !isset($options['no-files']),
|
||
'include_databases' => !isset($options['no-databases']),
|
||
'include_mailboxes' => !isset($options['no-mailboxes']),
|
||
'include_dns' => !isset($options['no-dns']),
|
||
'include_ssl' => !isset($options['no-ssl']),
|
||
'domains' => parseListOption($options['domains'] ?? null),
|
||
'databases' => parseListOption($options['databases'] ?? null),
|
||
'mailboxes' => parseListOption($options['mailboxes'] ?? null),
|
||
];
|
||
|
||
if (!empty($options['incremental-base'])) {
|
||
$params['incremental_base'] = $options['incremental-base'];
|
||
}
|
||
|
||
info("Creating backup for $username...");
|
||
|
||
$timeout = (int) ($options['timeout'] ?? 7200);
|
||
$result = agentSendWithTimeout('backup.create', $params, $timeout);
|
||
|
||
if ($result['success'] ?? false) {
|
||
$size = formatBytes((int) ($result['size'] ?? 0));
|
||
success("Backup created: {$result['path']} ($size)");
|
||
if (!empty($result['checksum'])) {
|
||
info("Checksum: {$result['checksum']}");
|
||
}
|
||
if (isset($result['domains'])) {
|
||
info("Domains: " . count($result['domains']));
|
||
}
|
||
if (isset($result['databases'])) {
|
||
info("Databases: " . count($result['databases']));
|
||
}
|
||
if (isset($result['mailboxes'])) {
|
||
info("Mailboxes: " . count($result['mailboxes']));
|
||
}
|
||
} else {
|
||
error("Backup failed: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'restore':
|
||
$backupPath = $options['_args'][0] ?? $options['file'] ?? null;
|
||
$username = $options['_args'][1] ?? $options['user'] ?? $options['username'] ?? null;
|
||
|
||
if (!is_string($backupPath)) {
|
||
error("Backup path is required");
|
||
exit(1);
|
||
}
|
||
$backupPath = trim($backupPath);
|
||
if ($backupPath === '') {
|
||
error("Backup path is required");
|
||
exit(1);
|
||
}
|
||
|
||
if ($username !== null && !is_string($username)) {
|
||
$username = null;
|
||
}
|
||
if (is_string($username)) {
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
$username = null;
|
||
}
|
||
}
|
||
|
||
$backupPath = resolveBackupPath($backupPath, $backupDir);
|
||
if (!file_exists($backupPath) && $username !== null) {
|
||
$backupPath = resolveBackupPath($backupPath, "/home/$username/backups");
|
||
}
|
||
|
||
if (!file_exists($backupPath)) {
|
||
error("Backup not found: $backupPath");
|
||
exit(1);
|
||
}
|
||
|
||
if ($username === null) {
|
||
$infoResult = agentSend('backup.get_info', ['backup_path' => $backupPath]);
|
||
if ($infoResult['success'] ?? false) {
|
||
$manifest = $infoResult['manifest'] ?? [];
|
||
$username = $manifest['username'] ?? null;
|
||
}
|
||
}
|
||
|
||
if ($username === null) {
|
||
error("Username is required for restore. Use --user=<username>.");
|
||
exit(1);
|
||
}
|
||
|
||
if (!confirm("Restore backup '$backupPath' for user '$username'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
$params = [
|
||
'username' => $username,
|
||
'backup_path' => $backupPath,
|
||
'restore_files' => !isset($options['no-files']),
|
||
'restore_databases' => !isset($options['no-databases']),
|
||
'restore_mailboxes' => !isset($options['no-mailboxes']),
|
||
'restore_dns' => !isset($options['no-dns']),
|
||
'restore_ssl' => !isset($options['no-ssl']),
|
||
'selected_domains' => parseListOption($options['domains'] ?? null),
|
||
'selected_databases' => parseListOption($options['databases'] ?? null),
|
||
'selected_mailboxes' => parseListOption($options['mailboxes'] ?? null),
|
||
];
|
||
|
||
info("Restoring backup...");
|
||
|
||
$timeout = (int) ($options['timeout'] ?? 7200);
|
||
$result = agentSendWithTimeout('backup.restore', $params, $timeout);
|
||
|
||
if ($result['success'] ?? false) {
|
||
success("Backup restored successfully");
|
||
} else {
|
||
error("Restore failed: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'info':
|
||
$backupPath = $options['_args'][0] ?? $options['file'] ?? null;
|
||
$username = $options['user'] ?? $options['username'] ?? null;
|
||
|
||
if (!is_string($backupPath)) {
|
||
error("Backup path is required");
|
||
exit(1);
|
||
}
|
||
$backupPath = trim($backupPath);
|
||
if ($backupPath === '') {
|
||
error("Backup path is required");
|
||
exit(1);
|
||
}
|
||
|
||
if ($username !== null && !is_string($username)) {
|
||
$username = null;
|
||
}
|
||
if (is_string($username)) {
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
$username = null;
|
||
}
|
||
}
|
||
|
||
$backupPath = resolveBackupPath($backupPath, $backupDir);
|
||
if (!file_exists($backupPath) && $username !== null) {
|
||
$backupPath = resolveBackupPath($backupPath, "/home/$username/backups");
|
||
}
|
||
|
||
if (!file_exists($backupPath)) {
|
||
error("Backup not found: $backupPath");
|
||
exit(1);
|
||
}
|
||
|
||
$result = agentSend('backup.get_info', ['backup_path' => $backupPath]);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Failed to read backup info: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
|
||
$manifest = $result['manifest'] ?? null;
|
||
|
||
echo "\n" . C_BOLD . "Backup Info" . C_RESET . "\n";
|
||
echo str_repeat('─', 60) . "\n";
|
||
echo C_CYAN . "Path:" . C_RESET . " $backupPath\n";
|
||
echo C_CYAN . "Type:" . C_RESET . " " . ($result['type'] ?? '-') . "\n";
|
||
echo C_CYAN . "Size:" . C_RESET . " " . formatBytes((int) ($result['size'] ?? 0)) . "\n";
|
||
echo C_CYAN . "Modified:" . C_RESET . " " . ($result['modified_at'] ?? '-') . "\n";
|
||
|
||
if (is_array($manifest)) {
|
||
if (!empty($manifest['username'])) {
|
||
echo C_CYAN . "User:" . C_RESET . " {$manifest['username']}\n";
|
||
}
|
||
if (!empty($manifest['backup_type'])) {
|
||
echo C_CYAN . "Backup Type:" . C_RESET . " {$manifest['backup_type']}\n";
|
||
}
|
||
if (!empty($manifest['includes']) && is_array($manifest['includes'])) {
|
||
$includes = array_keys(array_filter($manifest['includes'], fn($value) => $value));
|
||
echo C_CYAN . "Includes:" . C_RESET . " " . (empty($includes) ? '-' : implode(', ', $includes)) . "\n";
|
||
}
|
||
if (isset($manifest['domains'])) {
|
||
echo C_CYAN . "Domains:" . C_RESET . " " . count($manifest['domains']) . "\n";
|
||
}
|
||
if (isset($manifest['databases'])) {
|
||
echo C_CYAN . "Databases:" . C_RESET . " " . count($manifest['databases']) . "\n";
|
||
}
|
||
if (isset($manifest['mailboxes'])) {
|
||
echo C_CYAN . "Mailboxes:" . C_RESET . " " . count($manifest['mailboxes']) . "\n";
|
||
}
|
||
}
|
||
|
||
echo "\n";
|
||
break;
|
||
|
||
case 'verify':
|
||
$backupPath = $options['_args'][0] ?? $options['file'] ?? null;
|
||
$username = $options['user'] ?? $options['username'] ?? null;
|
||
|
||
if (!is_string($backupPath)) {
|
||
error("Backup path is required");
|
||
exit(1);
|
||
}
|
||
$backupPath = trim($backupPath);
|
||
if ($backupPath === '') {
|
||
error("Backup path is required");
|
||
exit(1);
|
||
}
|
||
|
||
if ($username !== null && !is_string($username)) {
|
||
$username = null;
|
||
}
|
||
if (is_string($username)) {
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
$username = null;
|
||
}
|
||
}
|
||
|
||
$backupPath = resolveBackupPath($backupPath, $backupDir);
|
||
if (!file_exists($backupPath) && $username !== null) {
|
||
$backupPath = resolveBackupPath($backupPath, "/home/$username/backups");
|
||
}
|
||
|
||
if (!file_exists($backupPath)) {
|
||
error("Backup not found: $backupPath");
|
||
exit(1);
|
||
}
|
||
|
||
$result = agentSend('backup.verify', ['backup_path' => $backupPath]);
|
||
if ($result['success'] ?? false) {
|
||
success("Backup verified successfully");
|
||
if (!empty($result['checksum'])) {
|
||
info("Checksum: {$result['checksum']}");
|
||
}
|
||
} else {
|
||
error("Backup verification failed");
|
||
$issues = $result['issues'] ?? [];
|
||
foreach ($issues as $issue) {
|
||
echo " - $issue\n";
|
||
}
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'delete':
|
||
$file = $options['_args'][0] ?? null;
|
||
$username = $options['user'] ?? $options['username'] ?? null;
|
||
|
||
if ($file === null) {
|
||
error("Backup file or ID is required");
|
||
exit(1);
|
||
}
|
||
if (is_string($file)) {
|
||
$file = trim($file);
|
||
}
|
||
if ($file === '') {
|
||
error("Backup file or ID is required");
|
||
exit(1);
|
||
}
|
||
if (!is_string($file) && !is_numeric($file)) {
|
||
error("Backup file or ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
if ($username !== null && !is_string($username)) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
if (is_string($username)) {
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
$username = null;
|
||
}
|
||
}
|
||
|
||
if ($username !== null) {
|
||
$backupPath = resolveBackupPath($file, "/home/$username/backups");
|
||
if (!file_exists($backupPath)) {
|
||
error("Backup file not found: $backupPath");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete '$backupPath'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
$result = agentSend('backup.delete', [
|
||
'username' => $username,
|
||
'backup_path' => $backupPath,
|
||
]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
success("Backup deleted: $backupPath");
|
||
} else {
|
||
error("Delete failed: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
}
|
||
|
||
if (is_numeric($file)) {
|
||
$backup = App\Models\Backup::find($file);
|
||
if (!$backup) {
|
||
error("Backup ID not found: $file");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Delete backup '{$backup->name}'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
if ($backup->local_path && file_exists($backup->local_path)) {
|
||
if (is_dir($backup->local_path)) {
|
||
exec("rm -rf " . escapeshellarg($backup->local_path));
|
||
} else {
|
||
unlink($backup->local_path);
|
||
}
|
||
}
|
||
|
||
if ($backup->remote_path && $backup->destination) {
|
||
info("Deleting remote backup...");
|
||
try {
|
||
$config = array_merge($backup->destination->config ?? [], ['type' => $backup->destination->type]);
|
||
agentSend('backup.delete_remote', [
|
||
'remote_path' => $backup->remote_path,
|
||
'destination' => $config,
|
||
]);
|
||
} catch (Exception $e) {
|
||
warning("Failed to delete remote: " . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
$backup->delete();
|
||
success("Backup deleted: {$backup->name}");
|
||
} else {
|
||
$file = resolveBackupPath($file, $backupDir);
|
||
if (!file_exists($file)) {
|
||
error("Backup file not found: $file");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete '$file'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
if (is_dir($file)) {
|
||
exec("rm -rf " . escapeshellarg($file));
|
||
} else {
|
||
unlink($file);
|
||
}
|
||
success("Backup deleted: $file");
|
||
}
|
||
break;
|
||
|
||
// ========== SERVER BACKUPS ==========
|
||
case 'server':
|
||
case 'server-backup':
|
||
$backupType = $options['type'] ?? 'full';
|
||
$users = parseListOption($options['users'] ?? null);
|
||
$destId = $options['destination'] ?? $options['dest'] ?? null;
|
||
$includeFiles = !isset($options['no-files']);
|
||
$includeDatabases = !isset($options['no-databases']);
|
||
$includeMailboxes = !isset($options['no-mailboxes']);
|
||
$includeDns = !isset($options['no-dns']);
|
||
|
||
info("Creating server backup ($backupType)...");
|
||
|
||
$timestamp = date('Y-m-d_His');
|
||
$outputPath = "$backupDir/$timestamp";
|
||
|
||
// Get destination if specified
|
||
$destination = null;
|
||
if ($destId) {
|
||
$destination = App\Models\BackupDestination::find($destId);
|
||
if (!$destination) {
|
||
error("Destination not found: $destId");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// Create backup record
|
||
$backup = App\Models\Backup::create([
|
||
'name' => "CLI Server Backup - " . now()->format('M j, Y H:i'),
|
||
'filename' => $timestamp,
|
||
'type' => 'server',
|
||
'status' => 'running',
|
||
'local_path' => $outputPath,
|
||
'destination_id' => $destination?->id,
|
||
'include_files' => $includeFiles,
|
||
'include_databases' => $includeDatabases,
|
||
'include_mailboxes' => $includeMailboxes,
|
||
'include_dns' => $includeDns,
|
||
'users' => $users,
|
||
'started_at' => now(),
|
||
'metadata' => ['backup_type' => $backupType],
|
||
]);
|
||
|
||
// Dispatch the job
|
||
App\Jobs\RunServerBackup::dispatch($backup->id);
|
||
|
||
success("Server backup queued (ID: {$backup->id})");
|
||
info("Monitor progress: jabali backup history");
|
||
break;
|
||
|
||
case 'server-list':
|
||
// List server backups from database
|
||
$backups = App\Models\Backup::where('type', 'server')
|
||
->orderByDesc('created_at')
|
||
->limit(20)
|
||
->get();
|
||
|
||
if ($backups->isEmpty()) {
|
||
info("No server backups found.");
|
||
exit(0);
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($backups as $backup) {
|
||
$rows[] = [
|
||
$backup->id,
|
||
$backup->name,
|
||
formatBytes((int) ($backup->size_bytes ?? 0)),
|
||
$backup->status,
|
||
$backup->created_at->format('Y-m-d H:i'),
|
||
];
|
||
}
|
||
table(['ID', 'Name', 'Size', 'Status', 'Created'], $rows);
|
||
break;
|
||
|
||
// ========== BACKUP HISTORY (DATABASE) ==========
|
||
case 'history':
|
||
$limit = $options['limit'] ?? 20;
|
||
$status = $options['status'] ?? null;
|
||
$type = $options['type'] ?? null;
|
||
|
||
$query = App\Models\Backup::orderByDesc('created_at')->limit($limit);
|
||
if ($status) {
|
||
$query->where('status', $status);
|
||
}
|
||
if ($type) {
|
||
$query->where('type', $type);
|
||
}
|
||
|
||
$backups = $query->get();
|
||
|
||
if ($backups->isEmpty()) {
|
||
info("No backups found.");
|
||
exit(0);
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($backups as $backup) {
|
||
$location = [];
|
||
if ($backup->local_path) {
|
||
$location[] = 'local';
|
||
}
|
||
if ($backup->remote_path) {
|
||
$location[] = 'remote';
|
||
}
|
||
|
||
$rows[] = [
|
||
$backup->id,
|
||
strlen($backup->name) > 30 ? substr($backup->name, 0, 27) . '...' : $backup->name,
|
||
$backup->type,
|
||
formatBytes((int) ($backup->size_bytes ?? 0)),
|
||
$backup->status,
|
||
implode('+', $location) ?: '-',
|
||
$backup->created_at->format('Y-m-d H:i'),
|
||
];
|
||
}
|
||
table(['ID', 'Name', 'Type', 'Size', 'Status', 'Location', 'Created'], $rows);
|
||
break;
|
||
|
||
case 'show':
|
||
$id = $options['_args'][0] ?? null;
|
||
if (!$id) {
|
||
error("Backup ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
$backup = App\Models\Backup::with(['destination', 'schedule'])->find($id);
|
||
if (!$backup) {
|
||
error("Backup not found: $id");
|
||
exit(1);
|
||
}
|
||
|
||
echo "\n" . C_BOLD . "Backup Details" . C_RESET . "\n";
|
||
echo str_repeat('─', 50) . "\n";
|
||
echo C_CYAN . "ID:" . C_RESET . " $backup->id\n";
|
||
echo C_CYAN . "Name:" . C_RESET . " $backup->name\n";
|
||
echo C_CYAN . "Type:" . C_RESET . " $backup->type\n";
|
||
echo C_CYAN . "Status:" . C_RESET . " " . statusColor((string) ($backup->status ?? '')) . "\n";
|
||
echo C_CYAN . "Size:" . C_RESET . " " . formatBytes((int) ($backup->size_bytes ?? 0)) . "\n";
|
||
if ($backup->local_path) {
|
||
echo C_CYAN . "Local Path:" . C_RESET . " $backup->local_path\n";
|
||
}
|
||
if ($backup->remote_path) {
|
||
echo C_CYAN . "Remote Path:" . C_RESET . " $backup->remote_path\n";
|
||
}
|
||
if ($backup->destination) {
|
||
echo C_CYAN . "Destination:" . C_RESET . " {$backup->destination->name} ({$backup->destination->type})\n";
|
||
}
|
||
if ($backup->schedule) {
|
||
echo C_CYAN . "Schedule:" . C_RESET . " {$backup->schedule->name}\n";
|
||
}
|
||
echo C_CYAN . "Created:" . C_RESET . " " . $backup->created_at->format('Y-m-d H:i:s') . "\n";
|
||
if ($backup->started_at) {
|
||
echo C_CYAN . "Started:" . C_RESET . " " . $backup->started_at->format('Y-m-d H:i:s') . "\n";
|
||
}
|
||
if ($backup->completed_at) {
|
||
echo C_CYAN . "Completed:" . C_RESET . " " . $backup->completed_at->format('Y-m-d H:i:s') . "\n";
|
||
}
|
||
if ($backup->error_message) {
|
||
echo C_CYAN . "Error:" . C_RESET . " " . C_RED . $backup->error_message . C_RESET . "\n";
|
||
}
|
||
echo "\n";
|
||
break;
|
||
|
||
// ========== SCHEDULES ==========
|
||
case 'schedules':
|
||
case 'schedule-list':
|
||
$schedules = App\Models\BackupSchedule::with('destination')
|
||
->orderBy('name')
|
||
->get();
|
||
|
||
if ($schedules->isEmpty()) {
|
||
info("No backup schedules found.");
|
||
exit(0);
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($schedules as $schedule) {
|
||
$rows[] = [
|
||
$schedule->id,
|
||
$schedule->name,
|
||
$schedule->frequency,
|
||
$schedule->is_active ? C_GREEN . 'active' . C_RESET : C_DIM . 'inactive' . C_RESET,
|
||
$schedule->retention_count,
|
||
$schedule->destination?->name ?? 'local',
|
||
$schedule->next_run_at?->format('Y-m-d H:i') ?? '-',
|
||
];
|
||
}
|
||
table(['ID', 'Name', 'Frequency', 'Status', 'Retention', 'Destination', 'Next Run'], $rows);
|
||
break;
|
||
|
||
case 'schedule-create':
|
||
$name = $options['name'] ?? $options['_args'][0] ?? null;
|
||
if (!$name) {
|
||
error("Schedule name is required (--name=<name>)");
|
||
exit(1);
|
||
}
|
||
|
||
$schedule = App\Models\BackupSchedule::create([
|
||
'name' => $name,
|
||
'frequency' => $options['frequency'] ?? 'daily',
|
||
'time' => $options['time'] ?? '02:00',
|
||
'day_of_week' => $options['day'] ?? null,
|
||
'day_of_month' => $options['date'] ?? null,
|
||
'is_active' => true,
|
||
'is_server_backup' => ($options['type'] ?? 'server') === 'server',
|
||
'retention_count' => (int)($options['retention'] ?? 7),
|
||
'destination_id' => $options['destination'] ?? $options['dest'] ?? null,
|
||
'include_files' => !isset($options['no-files']),
|
||
'include_databases' => !isset($options['no-databases']),
|
||
'include_mailboxes' => !isset($options['no-mailboxes']),
|
||
'include_dns' => !isset($options['no-dns']),
|
||
'metadata' => ['backup_type' => $options['backup-type'] ?? 'full'],
|
||
]);
|
||
|
||
$schedule->calculateNextRun();
|
||
$schedule->save();
|
||
|
||
success("Schedule created: {$schedule->name} (ID: {$schedule->id})");
|
||
info("Next run: " . $schedule->next_run_at?->format('Y-m-d H:i'));
|
||
break;
|
||
|
||
case 'schedule-run':
|
||
$id = $options['_args'][0] ?? null;
|
||
if (!$id) {
|
||
error("Schedule ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
$schedule = App\Models\BackupSchedule::find($id);
|
||
if (!$schedule) {
|
||
error("Schedule not found: $id");
|
||
exit(1);
|
||
}
|
||
|
||
info("Running schedule: {$schedule->name}...");
|
||
|
||
// Create backup record and dispatch job
|
||
$timestamp = now()->format('Y-m-d_His');
|
||
$backupType = $schedule->metadata['backup_type'] ?? 'full';
|
||
|
||
$backup = App\Models\Backup::create([
|
||
'user_id' => $schedule->user_id,
|
||
'destination_id' => $schedule->destination_id,
|
||
'schedule_id' => $schedule->id,
|
||
'name' => "{$schedule->name} - " . now()->format('M j, Y H:i'),
|
||
'filename' => $timestamp,
|
||
'type' => $schedule->is_server_backup ? 'server' : 'partial',
|
||
'include_files' => $schedule->include_files,
|
||
'include_databases' => $schedule->include_databases,
|
||
'include_mailboxes' => $schedule->include_mailboxes,
|
||
'include_dns' => $schedule->include_dns,
|
||
'users' => $schedule->users,
|
||
'status' => 'pending',
|
||
'local_path' => "/var/backups/jabali/$timestamp",
|
||
'metadata' => ['backup_type' => $backupType, 'schedule_id' => $schedule->id],
|
||
]);
|
||
|
||
App\Jobs\RunServerBackup::dispatch($backup->id);
|
||
|
||
success("Backup queued (ID: {$backup->id})");
|
||
break;
|
||
|
||
case 'schedule-enable':
|
||
$id = $options['_args'][0] ?? null;
|
||
if (!$id) {
|
||
error("Schedule ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
$schedule = App\Models\BackupSchedule::find($id);
|
||
if (!$schedule) {
|
||
error("Schedule not found: $id");
|
||
exit(1);
|
||
}
|
||
|
||
$schedule->update(['is_active' => true]);
|
||
$schedule->calculateNextRun();
|
||
$schedule->save();
|
||
|
||
success("Schedule enabled: {$schedule->name}");
|
||
break;
|
||
|
||
case 'schedule-disable':
|
||
$id = $options['_args'][0] ?? null;
|
||
if (!$id) {
|
||
error("Schedule ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
$schedule = App\Models\BackupSchedule::find($id);
|
||
if (!$schedule) {
|
||
error("Schedule not found: $id");
|
||
exit(1);
|
||
}
|
||
|
||
$schedule->update(['is_active' => false, 'next_run_at' => null]);
|
||
|
||
success("Schedule disabled: {$schedule->name}");
|
||
break;
|
||
|
||
case 'schedule-delete':
|
||
$id = $options['_args'][0] ?? null;
|
||
if (!$id) {
|
||
error("Schedule ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
$schedule = App\Models\BackupSchedule::find($id);
|
||
if (!$schedule) {
|
||
error("Schedule not found: $id");
|
||
exit(1);
|
||
}
|
||
|
||
if (!confirm("Delete schedule '{$schedule->name}'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
$schedule->delete();
|
||
success("Schedule deleted");
|
||
break;
|
||
|
||
// ========== DESTINATIONS ==========
|
||
case 'destinations':
|
||
case 'dest-list':
|
||
$destinations = App\Models\BackupDestination::orderBy('name')->get();
|
||
|
||
if ($destinations->isEmpty()) {
|
||
info("No backup destinations configured.");
|
||
info("Add one with: jabali backup dest-add --type=sftp --name=...");
|
||
exit(0);
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($destinations as $dest) {
|
||
$rows[] = [
|
||
$dest->id,
|
||
$dest->name,
|
||
$dest->type,
|
||
$dest->config['host'] ?? $dest->config['path'] ?? '-',
|
||
$dest->is_active ? C_GREEN . 'active' . C_RESET : C_DIM . 'inactive' . C_RESET,
|
||
];
|
||
}
|
||
table(['ID', 'Name', 'Type', 'Host/Path', 'Status'], $rows);
|
||
break;
|
||
|
||
case 'dest-add':
|
||
$type = $options['type'] ?? null;
|
||
$name = $options['name'] ?? null;
|
||
|
||
if (!$type || !$name) {
|
||
error("Required: --type=<sftp|nfs|s3> --name=<name>");
|
||
exit(1);
|
||
}
|
||
|
||
$config = [];
|
||
switch ($type) {
|
||
case 'sftp':
|
||
$config = [
|
||
'host' => $options['host'] ?? null,
|
||
'port' => (int)($options['port'] ?? 22),
|
||
'username' => $options['user'] ?? $options['username'] ?? null,
|
||
'password' => $options['password'] ?? null,
|
||
'path' => $options['path'] ?? '/backups',
|
||
];
|
||
if (!$config['host'] || !$config['username']) {
|
||
error("SFTP requires: --host=<host> --user=<user> [--password=<pass>] [--port=22] [--path=/backups]");
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'nfs':
|
||
$config = [
|
||
'host' => $options['host'] ?? null,
|
||
'path' => $options['path'] ?? null,
|
||
'mount_point' => $options['mount'] ?? '/mnt/backup',
|
||
];
|
||
if (!$config['host'] || !$config['path']) {
|
||
error("NFS requires: --host=<host> --path=<remote-path> [--mount=/mnt/backup]");
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 's3':
|
||
$config = [
|
||
'bucket' => $options['bucket'] ?? null,
|
||
'region' => $options['region'] ?? 'us-east-1',
|
||
'access_key' => $options['key'] ?? $options['access-key'] ?? null,
|
||
'secret_key' => $options['secret'] ?? $options['secret-key'] ?? null,
|
||
'path' => $options['path'] ?? '',
|
||
];
|
||
if (!$config['bucket'] || !$config['access_key'] || !$config['secret_key']) {
|
||
error("S3 requires: --bucket=<name> --key=<access-key> --secret=<secret-key> [--region=us-east-1] [--path=]");
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
error("Unknown destination type: $type (use: sftp, nfs, s3)");
|
||
exit(1);
|
||
}
|
||
|
||
$destination = App\Models\BackupDestination::create([
|
||
'name' => $name,
|
||
'type' => $type,
|
||
'config' => $config,
|
||
'is_active' => true,
|
||
]);
|
||
|
||
success("Destination created: {$destination->name} (ID: {$destination->id})");
|
||
info("Test connection: jabali backup dest-test {$destination->id}");
|
||
break;
|
||
|
||
case 'dest-test':
|
||
$id = $options['_args'][0] ?? null;
|
||
if (!$id) {
|
||
error("Destination ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
$destination = App\Models\BackupDestination::find($id);
|
||
if (!$destination) {
|
||
error("Destination not found: $id");
|
||
exit(1);
|
||
}
|
||
|
||
info("Testing connection to {$destination->name}...");
|
||
|
||
$config = array_merge($destination->config ?? [], ['type' => $destination->type]);
|
||
$result = agentSend('backup.test_destination', ['destination' => $config]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
success("Connection successful!");
|
||
if (!empty($result['message'])) {
|
||
info($result['message']);
|
||
}
|
||
} else {
|
||
error("Connection failed: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'dest-delete':
|
||
$id = $options['_args'][0] ?? null;
|
||
if (!$id) {
|
||
error("Destination ID is required");
|
||
exit(1);
|
||
}
|
||
|
||
$destination = App\Models\BackupDestination::find($id);
|
||
if (!$destination) {
|
||
error("Destination not found: $id");
|
||
exit(1);
|
||
}
|
||
|
||
// Check if any schedules use this destination
|
||
$scheduleCount = App\Models\BackupSchedule::where('destination_id', $id)->count();
|
||
if ($scheduleCount > 0) {
|
||
error("Cannot delete: $scheduleCount schedule(s) use this destination");
|
||
exit(1);
|
||
}
|
||
|
||
if (!confirm("Delete destination '{$destination->name}'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
$destination->delete();
|
||
success("Destination deleted");
|
||
break;
|
||
|
||
// ========== HELP ==========
|
||
case 'help':
|
||
case '':
|
||
echo "\n" . C_BOLD . "Backup Commands" . C_RESET . "\n";
|
||
echo str_repeat('─', 60) . "\n\n";
|
||
|
||
echo C_YELLOW . "Local Backups:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "backup list [--user=<user>]" . C_RESET . " List backups\n";
|
||
echo " " . C_GREEN . "backup user-list <user>" . C_RESET . " List user backups\n";
|
||
echo " " . C_GREEN . "backup create <user>" . C_RESET . " Create user backup\n";
|
||
echo " --type=full|incremental Backup type (default: full)\n";
|
||
echo " --output=<path> Output file/dir\n";
|
||
echo " --incremental-base=<path> Base backup for incremental\n";
|
||
echo " --domains=a,b Include domains\n";
|
||
echo " --databases=a,b Include databases\n";
|
||
echo " --mailboxes=a,b Include mailboxes\n";
|
||
echo " --no-files --no-databases --no-mailboxes --no-dns --no-ssl\n";
|
||
echo " " . C_GREEN . "backup restore <path> [<user>]" . C_RESET . " Restore backup\n";
|
||
echo " --user=<user> User for server backups\n";
|
||
echo " --domains=a,b Restore domains\n";
|
||
echo " --databases=a,b Restore databases\n";
|
||
echo " --mailboxes=a,b Restore mailboxes\n";
|
||
echo " --no-files --no-databases --no-mailboxes --no-dns --no-ssl\n";
|
||
echo " " . C_GREEN . "backup info <path>" . C_RESET . " Show backup info\n";
|
||
echo " " . C_GREEN . "backup verify <path>" . C_RESET . " Verify backup\n";
|
||
echo " " . C_GREEN . "backup delete <file|id>" . C_RESET . " Delete backup\n";
|
||
echo " --user=<user> Delete user backup\n\n";
|
||
|
||
echo C_YELLOW . "Server Backups:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "backup server" . C_RESET . " Create server backup\n";
|
||
echo " --type=full|incremental Backup type (default: full)\n";
|
||
echo " --users=user1,user2 Specific users only\n";
|
||
echo " --dest=<id> Upload to destination\n";
|
||
echo " " . C_GREEN . "backup server-list" . C_RESET . " List server backups\n\n";
|
||
|
||
echo C_YELLOW . "Backup History:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "backup history" . C_RESET . " Show all backups from database\n";
|
||
echo " --limit=20 Number of records\n";
|
||
echo " --status=completed|failed Filter by status\n";
|
||
echo " --type=server|user Filter by type\n";
|
||
echo " " . C_GREEN . "backup show <id>" . C_RESET . " Show backup details\n\n";
|
||
|
||
echo C_YELLOW . "Schedules:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "backup schedules" . C_RESET . " List backup schedules\n";
|
||
echo " " . C_GREEN . "backup schedule-create" . C_RESET . " Create a schedule\n";
|
||
echo " --name=<name> Schedule name (required)\n";
|
||
echo " --frequency=daily|weekly Run frequency\n";
|
||
echo " --time=02:00 Run time (24h)\n";
|
||
echo " --retention=7 Keep N backups\n";
|
||
echo " --dest=<id> Destination ID\n";
|
||
echo " --backup-type=full|incremental Backup type\n";
|
||
echo " " . C_GREEN . "backup schedule-run <id>" . C_RESET . " Run schedule now\n";
|
||
echo " " . C_GREEN . "backup schedule-enable <id>" . C_RESET . " Enable schedule\n";
|
||
echo " " . C_GREEN . "backup schedule-disable <id>" . C_RESET . " Disable schedule\n";
|
||
echo " " . C_GREEN . "backup schedule-delete <id>" . C_RESET . " Delete schedule\n\n";
|
||
|
||
echo C_YELLOW . "Destinations:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "backup destinations" . C_RESET . " List destinations\n";
|
||
echo " " . C_GREEN . "backup dest-add" . C_RESET . " Add destination\n";
|
||
echo " --type=sftp --host=<host> --user=<user>\n";
|
||
echo " --type=nfs --host=<host> --path=<path>\n";
|
||
echo " --type=s3 --bucket=<name> --key=<key> --secret=<secret>\n";
|
||
echo " " . C_GREEN . "backup dest-test <id>" . C_RESET . " Test connection\n";
|
||
echo " " . C_GREEN . "backup dest-delete <id>" . C_RESET . " Delete destination\n\n";
|
||
break;
|
||
|
||
default:
|
||
error("Unknown backup command: $subcommand");
|
||
echo "Run 'jabali backup help' for usage.\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ CPANEL COMMANDS ============
|
||
|
||
function handleCpanel(string $subcommand, array $options): void
|
||
{
|
||
$defaultDir = '/var/backups/jabali/cpanel-migrations';
|
||
|
||
switch ($subcommand) {
|
||
case 'analyze':
|
||
$backupPath = $options['_args'][0] ?? $options['file'] ?? null;
|
||
if (!is_string($backupPath)) {
|
||
error("Backup file is required");
|
||
exit(1);
|
||
}
|
||
$backupPath = trim($backupPath);
|
||
if ($backupPath === '') {
|
||
error("Backup file is required");
|
||
exit(1);
|
||
}
|
||
|
||
$backupPath = resolveBackupPath($backupPath, $defaultDir);
|
||
if (!file_exists($backupPath)) {
|
||
error("Backup file not found: $backupPath");
|
||
exit(1);
|
||
}
|
||
|
||
info("Analyzing cPanel backup...");
|
||
$timeout = (int) ($options['timeout'] ?? 600);
|
||
$result = agentSendWithTimeout('cpanel.analyze_backup', ['backup_path' => $backupPath], $timeout);
|
||
|
||
if (!($result['success'] ?? false)) {
|
||
error("Analysis failed: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
|
||
$data = $result['data'] ?? [];
|
||
printCpanelAnalysis($backupPath, $data);
|
||
break;
|
||
|
||
case 'restore':
|
||
$backupPath = $options['_args'][0] ?? $options['file'] ?? null;
|
||
$username = $options['_args'][1] ?? $options['user'] ?? $options['username'] ?? null;
|
||
|
||
if (!is_string($backupPath)) {
|
||
error("Backup file is required");
|
||
exit(1);
|
||
}
|
||
$backupPath = trim($backupPath);
|
||
if ($backupPath === '') {
|
||
error("Backup file is required");
|
||
exit(1);
|
||
}
|
||
if (!is_string($username)) {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
error("Username is required");
|
||
exit(1);
|
||
}
|
||
|
||
$backupPath = resolveBackupPath($backupPath, $defaultDir);
|
||
if (!file_exists($backupPath)) {
|
||
error("Backup file not found: $backupPath");
|
||
exit(1);
|
||
}
|
||
|
||
$panelUser = App\Models\User::where('username', $username)->orWhere('name', $username)->first();
|
||
$systemCheck = agentSend('user.exists', ['username' => $username]);
|
||
$systemExists = (bool) ($systemCheck['exists'] ?? false);
|
||
|
||
if (!$panelUser || !$systemExists) {
|
||
$missing = [];
|
||
if (!$systemExists) {
|
||
$missing[] = 'system user';
|
||
}
|
||
if (!$panelUser) {
|
||
$missing[] = 'panel user';
|
||
}
|
||
error("Missing " . implode(' and ', $missing) . " for '$username'. Create with: jabali user create $username");
|
||
exit(1);
|
||
}
|
||
|
||
$restoreOptions = [
|
||
'backup_path' => $backupPath,
|
||
'username' => $username,
|
||
'restore_files' => !isset($options['no-files']),
|
||
'restore_databases' => !isset($options['no-databases']),
|
||
'restore_emails' => !isset($options['no-emails']),
|
||
'restore_ssl' => !isset($options['no-ssl']),
|
||
];
|
||
|
||
if (!empty($options['log'])) {
|
||
$restoreOptions['log_path'] = $options['log'];
|
||
}
|
||
|
||
if (isset($options['analyze'])) {
|
||
info("Analyzing backup...");
|
||
$analysisTimeout = (int) ($options['analysis-timeout'] ?? $options['timeout'] ?? 600);
|
||
$analysisResult = agentSendWithTimeout('cpanel.analyze_backup', ['backup_path' => $backupPath], $analysisTimeout);
|
||
if (!($analysisResult['success'] ?? false)) {
|
||
error("Analysis failed: " . ($analysisResult['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
$analysisData = $analysisResult['data'] ?? [];
|
||
printCpanelAnalysis($backupPath, $analysisData);
|
||
$restoreOptions['discovered_data'] = $analysisData;
|
||
}
|
||
|
||
if (!confirm("Restore backup '$backupPath' into '$username'?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
|
||
info("Restoring cPanel backup...");
|
||
$timeout = (int) ($options['timeout'] ?? 7200);
|
||
$result = agentSendWithTimeout('cpanel.restore_backup', $restoreOptions, $timeout);
|
||
|
||
if (!($result['success'] ?? false)) {
|
||
error("Restore failed: " . ($result['error'] ?? 'Unknown error'));
|
||
$logEntries = $result['log'] ?? [];
|
||
if (is_array($logEntries) && !empty($logEntries)) {
|
||
printCpanelLog($logEntries);
|
||
}
|
||
exit(1);
|
||
}
|
||
|
||
$logEntries = $result['log'] ?? [];
|
||
if (is_array($logEntries) && !empty($logEntries)) {
|
||
printCpanelLog($logEntries);
|
||
}
|
||
|
||
success("cPanel restore completed");
|
||
break;
|
||
|
||
case 'fix-permissions':
|
||
$backupPath = $options['_args'][0] ?? $options['file'] ?? null;
|
||
if (!is_string($backupPath)) {
|
||
error("Backup file is required");
|
||
exit(1);
|
||
}
|
||
$backupPath = trim($backupPath);
|
||
if ($backupPath === '') {
|
||
error("Backup file is required");
|
||
exit(1);
|
||
}
|
||
|
||
$backupPath = resolveBackupPath($backupPath, $defaultDir);
|
||
$result = agentSend('cpanel.fix_backup_permissions', ['backup_path' => $backupPath]);
|
||
if (!($result['success'] ?? false)) {
|
||
error("Fix permissions failed: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
success("Permissions updated for $backupPath");
|
||
break;
|
||
|
||
case 'help':
|
||
case '':
|
||
echo "\n" . C_BOLD . "cPanel Migration Commands" . C_RESET . "\n";
|
||
echo str_repeat('─', 60) . "\n\n";
|
||
|
||
echo C_YELLOW . "Analyze:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "cpanel analyze <file>" . C_RESET . " Analyze backup contents\n";
|
||
echo " --timeout=600 Analysis timeout\n\n";
|
||
|
||
echo C_YELLOW . "Restore:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "cpanel restore <file> <user>" . C_RESET . " Restore backup\n";
|
||
echo " --no-files Skip website files\n";
|
||
echo " --no-databases Skip databases\n";
|
||
echo " --no-emails Skip mailboxes/forwarders\n";
|
||
echo " --no-ssl Skip SSL certificates\n";
|
||
echo " --log=/path/to/log.jsonl Append log entries to file\n";
|
||
echo " --analyze Run analysis and reuse results\n";
|
||
echo " --timeout=7200 Restore timeout\n\n";
|
||
|
||
echo C_YELLOW . "Maintenance:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "cpanel fix-permissions <file>" . C_RESET . " Fix backup file permissions\n\n";
|
||
break;
|
||
|
||
default:
|
||
error("Unknown cPanel command: $subcommand");
|
||
echo "Run 'jabali cpanel help' for usage.\n";
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
function formatCpanelStatus(string $status): string
|
||
{
|
||
return match ($status) {
|
||
'success' => C_GREEN . '✓' . C_RESET,
|
||
'error' => C_RED . '✗' . C_RESET,
|
||
'warning' => C_YELLOW . '⚠' . C_RESET,
|
||
'pending' => C_CYAN . '○' . C_RESET,
|
||
'info' => C_CYAN . 'ℹ' . C_RESET,
|
||
default => $status !== '' ? $status : '-',
|
||
};
|
||
}
|
||
|
||
function printCpanelLog(array $entries): void
|
||
{
|
||
foreach ($entries as $entry) {
|
||
$status = formatCpanelStatus((string) ($entry['status'] ?? ''));
|
||
$time = $entry['time'] ?? '';
|
||
$message = $entry['message'] ?? '';
|
||
$timeLabel = $time !== '' ? "[$time] " : '';
|
||
echo $status . " " . $timeLabel . $message . "\n";
|
||
}
|
||
}
|
||
|
||
function printCpanelAnalysis(string $backupPath, array $data): void
|
||
{
|
||
$domains = $data['domains'] ?? [];
|
||
$databases = $data['databases'] ?? [];
|
||
$mailboxes = $data['mailboxes'] ?? [];
|
||
$forwarders = $data['forwarders'] ?? [];
|
||
$ssl = $data['ssl_certificates'] ?? [];
|
||
|
||
echo "\n" . C_BOLD . "cPanel Backup Analysis" . C_RESET . "\n";
|
||
echo str_repeat('─', 60) . "\n";
|
||
echo C_CYAN . "File:" . C_RESET . " $backupPath\n";
|
||
if (!empty($data['total_size'])) {
|
||
echo C_CYAN . "Size:" . C_RESET . " " . formatBytes((int) $data['total_size']) . "\n";
|
||
}
|
||
if (!empty($data['cpanel_username'])) {
|
||
echo C_CYAN . "cPanel User:" . C_RESET . " {$data['cpanel_username']}\n";
|
||
}
|
||
echo C_CYAN . "Domains:" . C_RESET . " " . count($domains) . "\n";
|
||
echo C_CYAN . "Databases:" . C_RESET . " " . count($databases) . "\n";
|
||
echo C_CYAN . "Mailboxes:" . C_RESET . " " . count($mailboxes) . "\n";
|
||
echo C_CYAN . "Forwarders:" . C_RESET . " " . count($forwarders) . "\n";
|
||
echo C_CYAN . "SSL Certificates:" . C_RESET . " " . count($ssl) . "\n\n";
|
||
|
||
if (!empty($domains)) {
|
||
$rows = [];
|
||
foreach ($domains as $domain) {
|
||
$rows[] = [
|
||
$domain['name'] ?? (string) $domain,
|
||
$domain['type'] ?? '-',
|
||
];
|
||
}
|
||
table(['Domain', 'Type'], $rows);
|
||
echo "\n";
|
||
}
|
||
|
||
if (!empty($databases)) {
|
||
$rows = [];
|
||
foreach ($databases as $database) {
|
||
$rows[] = [
|
||
$database['name'] ?? (string) $database,
|
||
];
|
||
}
|
||
table(['Database'], $rows);
|
||
echo "\n";
|
||
}
|
||
|
||
if (!empty($mailboxes)) {
|
||
$rows = [];
|
||
foreach ($mailboxes as $mailbox) {
|
||
$rows[] = [
|
||
$mailbox['email'] ?? (string) $mailbox,
|
||
];
|
||
}
|
||
table(['Mailbox'], $rows);
|
||
echo "\n";
|
||
}
|
||
|
||
if (!empty($forwarders)) {
|
||
$rows = [];
|
||
foreach ($forwarders as $forwarder) {
|
||
$rows[] = [
|
||
$forwarder['email'] ?? '-',
|
||
$forwarder['destinations'] ?? ($forwarder['format'] ?? '-'),
|
||
];
|
||
}
|
||
table(['Forwarder', 'Destinations'], $rows);
|
||
echo "\n";
|
||
}
|
||
|
||
if (!empty($ssl)) {
|
||
$rows = [];
|
||
foreach ($ssl as $cert) {
|
||
$rows[] = [
|
||
$cert['domain'] ?? '-',
|
||
$cert['keyid'] ?? '-',
|
||
];
|
||
}
|
||
table(['Domain', 'Key ID'], $rows);
|
||
echo "\n";
|
||
}
|
||
}
|
||
|
||
function formatBytes(int $bytes, int $precision = 2): string
|
||
{
|
||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
$bytes = max($bytes, 0);
|
||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||
$pow = min($pow, count($units) - 1);
|
||
$bytes /= pow(1024, $pow);
|
||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||
}
|
||
|
||
function parseListOption(string|bool|null $value): ?array
|
||
{
|
||
if (!is_string($value) || $value === '') {
|
||
return null;
|
||
}
|
||
|
||
$items = array_map('trim', explode(',', $value));
|
||
$items = array_values(array_filter($items, fn($item) => $item !== ''));
|
||
|
||
return empty($items) ? null : $items;
|
||
}
|
||
|
||
function resolveBackupPath(string $path, string $defaultDir): string
|
||
{
|
||
if (file_exists($path)) {
|
||
return $path;
|
||
}
|
||
|
||
$candidate = rtrim($defaultDir, '/') . '/' . ltrim($path, '/');
|
||
if (file_exists($candidate)) {
|
||
return $candidate;
|
||
}
|
||
|
||
return $path;
|
||
}
|
||
|
||
function statusColor(string $status): string
|
||
{
|
||
return match ($status) {
|
||
'completed' => C_GREEN . $status . C_RESET,
|
||
'running', 'uploading', 'pending' => C_YELLOW . $status . C_RESET,
|
||
'failed' => C_RED . $status . C_RESET,
|
||
default => $status,
|
||
};
|
||
}
|
||
|
||
// ============ SYSTEM COMMANDS ============
|
||
|
||
function handleSystem(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'info':
|
||
$result = agentSend('server.info', []);
|
||
echo "\n" . C_BOLD . "System Information" . C_RESET . "\n";
|
||
echo str_repeat('─', 50) . "\n";
|
||
|
||
// Hostname
|
||
echo C_CYAN . "Hostname:" . C_RESET . " " . trim(shell_exec('hostname')) . "\n";
|
||
|
||
// OS
|
||
if (file_exists('/etc/os-release')) {
|
||
$osRelease = parse_ini_file('/etc/os-release');
|
||
echo C_CYAN . "OS:" . C_RESET . " " . ($osRelease['PRETTY_NAME'] ?? 'Unknown') . "\n";
|
||
}
|
||
|
||
// Kernel
|
||
echo C_CYAN . "Kernel:" . C_RESET . " " . trim(shell_exec('uname -r')) . "\n";
|
||
|
||
// Uptime
|
||
$uptime = trim(shell_exec('uptime -p'));
|
||
echo C_CYAN . "Uptime:" . C_RESET . " $uptime\n";
|
||
|
||
// CPU
|
||
$cpuInfo = shell_exec("grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2");
|
||
$cpuCores = trim(shell_exec("nproc"));
|
||
echo C_CYAN . "CPU:" . C_RESET . " " . trim($cpuInfo) . " ($cpuCores cores)\n";
|
||
|
||
// Load
|
||
$load = sys_getloadavg();
|
||
echo C_CYAN . "Load:" . C_RESET . " " . implode(', ', array_map(fn($l) => number_format($l, 2), $load)) . "\n";
|
||
|
||
// Memory
|
||
$memInfo = shell_exec('free -m | grep Mem');
|
||
if (preg_match('/Mem:\s+(\d+)\s+(\d+)/', $memInfo, $m)) {
|
||
$total = $m[1];
|
||
$used = $m[2];
|
||
$pct = round($used / $total * 100);
|
||
echo C_CYAN . "Memory:" . C_RESET . " {$used}MB / {$total}MB ({$pct}%)\n";
|
||
}
|
||
|
||
// Disk
|
||
$diskTotal = disk_total_space('/');
|
||
$diskFree = disk_free_space('/');
|
||
$diskUsed = $diskTotal - $diskFree;
|
||
$diskPct = round($diskUsed / $diskTotal * 100);
|
||
echo C_CYAN . "Disk:" . C_RESET . " " . round($diskUsed / 1024 / 1024 / 1024, 1) . "GB / " . round($diskTotal / 1024 / 1024 / 1024, 1) . "GB ({$diskPct}%)\n";
|
||
|
||
// PHP
|
||
echo C_CYAN . "PHP:" . C_RESET . " " . PHP_VERSION . "\n";
|
||
|
||
// Laravel
|
||
echo C_CYAN . "Laravel:" . C_RESET . " " . app()->version() . "\n";
|
||
break;
|
||
|
||
case 'status':
|
||
handleService('list', $options);
|
||
break;
|
||
|
||
case 'hostname':
|
||
$newHostname = $options['_args'][0] ?? null;
|
||
if ($newHostname) {
|
||
$result = agentSend('server.set_hostname', ['hostname' => $newHostname]);
|
||
if ($result['success'] ?? false) {
|
||
success("Hostname set to '$newHostname'");
|
||
} else {
|
||
error("Failed to set hostname: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
} else {
|
||
echo trim(shell_exec('hostname')) . "\n";
|
||
}
|
||
break;
|
||
|
||
case 'disk':
|
||
echo shell_exec('df -h');
|
||
break;
|
||
|
||
case 'memory':
|
||
echo shell_exec('free -h');
|
||
break;
|
||
|
||
default:
|
||
error("Unknown system command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ AGENT COMMANDS ============
|
||
|
||
function handleAgent(string $subcommand, array $options): void
|
||
{
|
||
$agentScript = JABALI_ROOT . '/bin/jabali-agent';
|
||
$pidFile = '/var/run/jabali/agent.pid';
|
||
|
||
switch ($subcommand) {
|
||
case 'status':
|
||
if (file_exists(AGENT_SOCKET)) {
|
||
$result = agentSend('ping', []);
|
||
if ($result['success'] ?? false) {
|
||
success("Agent is running (version: " . ($result['version'] ?? 'unknown') . ")");
|
||
} else {
|
||
warning("Agent socket exists but not responding");
|
||
}
|
||
} else {
|
||
error("Agent is not running");
|
||
}
|
||
break;
|
||
|
||
case 'start':
|
||
if (file_exists(AGENT_SOCKET)) {
|
||
$result = agentSend('ping', []);
|
||
if ($result['success'] ?? false) {
|
||
info("Agent is already running");
|
||
exit(0);
|
||
}
|
||
}
|
||
info("Starting agent...");
|
||
exec("nohup /usr/bin/php $agentScript > /dev/null 2>&1 &");
|
||
sleep(2);
|
||
if (file_exists(AGENT_SOCKET)) {
|
||
success("Agent started");
|
||
} else {
|
||
error("Failed to start agent");
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'stop':
|
||
if (file_exists($pidFile)) {
|
||
$pid = trim(file_get_contents($pidFile));
|
||
if ($pid && posix_kill((int)$pid, 15)) {
|
||
sleep(1);
|
||
success("Agent stopped");
|
||
} else {
|
||
exec("pkill -f jabali-agent");
|
||
success("Agent stopped");
|
||
}
|
||
} else {
|
||
exec("pkill -f jabali-agent");
|
||
info("Agent stopped");
|
||
}
|
||
break;
|
||
|
||
case 'restart':
|
||
handleAgent('stop', $options);
|
||
sleep(1);
|
||
handleAgent('start', $options);
|
||
break;
|
||
|
||
case 'ping':
|
||
$result = agentSend('ping', []);
|
||
if ($result['success'] ?? false) {
|
||
success("Pong! Agent version: " . ($result['version'] ?? 'unknown'));
|
||
} else {
|
||
error("Agent not responding: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'log':
|
||
$lines = $options['lines'] ?? 50;
|
||
$logFile = '/var/log/jabali/agent.log';
|
||
if (file_exists($logFile)) {
|
||
passthru("tail -n $lines " . escapeshellarg($logFile));
|
||
} else {
|
||
error("Log file not found: $logFile");
|
||
}
|
||
break;
|
||
|
||
default:
|
||
error("Unknown agent command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ PHP COMMANDS ============
|
||
|
||
function handlePhp(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'list':
|
||
$result = agentSend('php.list_versions', []);
|
||
if (!($result['success'] ?? false)) {
|
||
// Fallback to local detection
|
||
exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $services);
|
||
$rows = [];
|
||
foreach ($services as $svc) {
|
||
if (preg_match('/php([\d.]+)-fpm/', $svc, $m)) {
|
||
$version = $m[1];
|
||
exec("systemctl is-active php{$version}-fpm 2>/dev/null", $active);
|
||
$isActive = (trim($active[0] ?? '') === 'active');
|
||
$rows[] = [
|
||
$version,
|
||
$isActive ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET,
|
||
];
|
||
}
|
||
}
|
||
table(['Version', 'Status'], $rows);
|
||
} else {
|
||
$rows = [];
|
||
foreach ($result['versions'] ?? [] as $v) {
|
||
$rows[] = [
|
||
is_array($v) ? $v['version'] : $v,
|
||
is_array($v) ? ($v['active'] ? C_GREEN . 'Running' . C_RESET : C_RED . 'Stopped' . C_RESET) : '',
|
||
];
|
||
}
|
||
table(['Version', 'Status'], $rows);
|
||
}
|
||
break;
|
||
|
||
case 'install':
|
||
$version = $options['_args'][0] ?? null;
|
||
if (!$version) {
|
||
error("PHP version is required (e.g., 8.2)");
|
||
exit(1);
|
||
}
|
||
info("Installing PHP $version...");
|
||
$result = agentSend('php.install', ['version' => $version]);
|
||
if ($result['success'] ?? false) {
|
||
success("PHP $version installed");
|
||
} else {
|
||
error("Failed to install PHP $version: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'uninstall':
|
||
$version = $options['_args'][0] ?? null;
|
||
if (!$version) {
|
||
error("PHP version is required");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to uninstall PHP $version?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
$result = agentSend('php.uninstall', ['version' => $version]);
|
||
if ($result['success'] ?? false) {
|
||
success("PHP $version uninstalled");
|
||
} else {
|
||
error("Failed to uninstall: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'default':
|
||
$version = $options['_args'][0] ?? null;
|
||
if ($version) {
|
||
$result = agentSend('php.set_default', ['version' => $version]);
|
||
if ($result['success'] ?? false) {
|
||
success("Default PHP version set to $version");
|
||
} else {
|
||
error("Failed to set default: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
} else {
|
||
$current = trim(shell_exec('php -v | head -1 | cut -d" " -f2 | cut -d"." -f1,2'));
|
||
echo "Current default: PHP $current\n";
|
||
}
|
||
break;
|
||
|
||
case 'status':
|
||
exec('ls /lib/systemd/system/php*-fpm.service 2>/dev/null', $services);
|
||
foreach ($services as $svc) {
|
||
if (preg_match('/php([\d.]+)-fpm/', $svc, $m)) {
|
||
$service = "php{$m[1]}-fpm";
|
||
passthru("systemctl status $service --no-pager -l | head -15");
|
||
echo "\n";
|
||
}
|
||
}
|
||
break;
|
||
|
||
default:
|
||
error("Unknown PHP command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
// ============ FIREWALL COMMANDS ============
|
||
|
||
function handleFirewall(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'status':
|
||
$result = agentSend('ufw.status', []);
|
||
if ($result['success'] ?? false) {
|
||
echo C_BOLD . "Firewall Status: " . C_RESET;
|
||
echo ($result['enabled'] ?? false) ? C_GREEN . "Active" . C_RESET : C_RED . "Inactive" . C_RESET;
|
||
echo "\n";
|
||
} else {
|
||
passthru('ufw status');
|
||
}
|
||
break;
|
||
|
||
case 'enable':
|
||
$result = agentSend('ufw.enable', []);
|
||
if ($result['success'] ?? false) {
|
||
success("Firewall enabled");
|
||
} else {
|
||
error("Failed to enable firewall: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'disable':
|
||
if (!confirm("Are you sure you want to disable the firewall?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
$result = agentSend('ufw.disable', []);
|
||
if ($result['success'] ?? false) {
|
||
success("Firewall disabled");
|
||
} else {
|
||
error("Failed to disable firewall: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'rules':
|
||
$result = agentSend('ufw.list_rules', []);
|
||
if ($result['success'] ?? false) {
|
||
$rows = [];
|
||
foreach ($result['rules'] ?? [] as $rule) {
|
||
$rows[] = [
|
||
$rule['number'] ?? '',
|
||
$rule['to'] ?? '',
|
||
$rule['action'] ?? '',
|
||
$rule['from'] ?? '',
|
||
];
|
||
}
|
||
table(['#', 'To', 'Action', 'From'], $rows);
|
||
} else {
|
||
passthru('ufw status numbered');
|
||
}
|
||
break;
|
||
|
||
case 'allow':
|
||
$port = $options['_args'][0] ?? null;
|
||
if (!$port) {
|
||
error("Port is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('ufw.allow_port', ['port' => $port]);
|
||
if ($result['success'] ?? false) {
|
||
success("Allowed port $port");
|
||
} else {
|
||
error("Failed to allow port: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'deny':
|
||
$port = $options['_args'][0] ?? null;
|
||
if (!$port) {
|
||
error("Port is required");
|
||
exit(1);
|
||
}
|
||
$result = agentSend('ufw.deny_port', ['port' => $port]);
|
||
if ($result['success'] ?? false) {
|
||
success("Denied port $port");
|
||
} else {
|
||
error("Failed to deny port: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'delete':
|
||
$rule = $options['_args'][0] ?? null;
|
||
if (!$rule) {
|
||
error("Rule number is required");
|
||
exit(1);
|
||
}
|
||
if (!confirm("Are you sure you want to delete rule #$rule?", $options)) {
|
||
info("Operation cancelled");
|
||
exit(0);
|
||
}
|
||
$result = agentSend('ufw.delete_rule', ['rule' => $rule]);
|
||
if ($result['success'] ?? false) {
|
||
success("Rule deleted");
|
||
} else {
|
||
error("Failed to delete rule: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
error("Unknown firewall command: $subcommand");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
function handleSsl(string $subcommand, array $options): void
|
||
{
|
||
switch ($subcommand) {
|
||
case 'check':
|
||
$domain = $options['_args'][0] ?? null;
|
||
info("Checking SSL certificates...");
|
||
|
||
$cmd = 'php /var/www/jabali/artisan jabali:ssl-check';
|
||
if ($domain) {
|
||
$cmd .= ' --domain=' . escapeshellarg($domain);
|
||
}
|
||
if ($options['issue-only'] ?? false) {
|
||
$cmd .= ' --issue-only';
|
||
}
|
||
if ($options['renew-only'] ?? false) {
|
||
$cmd .= ' --renew-only';
|
||
}
|
||
|
||
passthru($cmd, $exitCode);
|
||
exit($exitCode);
|
||
|
||
case 'issue':
|
||
$domain = $options['_args'][0] ?? null;
|
||
if (!$domain) {
|
||
error("Domain is required");
|
||
echo "Usage: jabali ssl issue <domain>\n";
|
||
exit(1);
|
||
}
|
||
|
||
info("Issuing SSL certificate for $domain...");
|
||
$result = agentSend('ssl.issue', [
|
||
'domain' => $domain,
|
||
'force' => $options['force'] ?? false,
|
||
]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
success("SSL certificate issued for $domain");
|
||
if (!empty($result['valid_to'])) {
|
||
echo " Expires: " . $result['valid_to'] . "\n";
|
||
}
|
||
} else {
|
||
error("Failed to issue SSL: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'renew':
|
||
$domain = $options['_args'][0] ?? null;
|
||
if (!$domain) {
|
||
error("Domain is required");
|
||
echo "Usage: jabali ssl renew <domain>\n";
|
||
exit(1);
|
||
}
|
||
|
||
info("Renewing SSL certificate for $domain...");
|
||
$result = agentSend('ssl.renew', ['domain' => $domain]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
success("SSL certificate renewed for $domain");
|
||
if (!empty($result['valid_to'])) {
|
||
echo " New expiry: " . $result['valid_to'] . "\n";
|
||
}
|
||
} else {
|
||
error("Failed to renew SSL: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'status':
|
||
$domain = $options['_args'][0] ?? null;
|
||
if (!$domain) {
|
||
error("Domain is required");
|
||
echo "Usage: jabali ssl status <domain>\n";
|
||
exit(1);
|
||
}
|
||
|
||
$result = agentSend('ssl.info', ['domain' => $domain]);
|
||
|
||
if ($result['success'] ?? false) {
|
||
echo C_BOLD . "SSL Certificate Status for $domain" . C_RESET . "\n\n";
|
||
echo " Status: " . (($result['valid'] ?? false) ? C_GREEN . "Valid" : C_RED . "Invalid") . C_RESET . "\n";
|
||
echo " Issuer: " . ($result['issuer'] ?? 'Unknown') . "\n";
|
||
echo " From: " . ($result['valid_from'] ?? 'Unknown') . "\n";
|
||
echo " To: " . ($result['valid_to'] ?? 'Unknown') . "\n";
|
||
|
||
if (!empty($result['days_remaining'])) {
|
||
$days = $result['days_remaining'];
|
||
$color = $days > 30 ? C_GREEN : ($days > 7 ? C_YELLOW : C_RED);
|
||
echo " Days: " . $color . $days . " days remaining" . C_RESET . "\n";
|
||
}
|
||
} else {
|
||
error("Failed to get SSL status: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case 'list':
|
||
info("Listing SSL certificates...");
|
||
$result = agentSend('ssl.list', []);
|
||
|
||
if ($result['success'] ?? false) {
|
||
$rows = [];
|
||
foreach ($result['certificates'] ?? [] as $cert) {
|
||
$status = ($cert['valid'] ?? false) ? C_GREEN . 'Valid' . C_RESET : C_RED . 'Invalid' . C_RESET;
|
||
$days = $cert['days_remaining'] ?? 0;
|
||
$daysColor = $days > 30 ? C_GREEN : ($days > 7 ? C_YELLOW : C_RED);
|
||
|
||
$rows[] = [
|
||
$cert['domain'] ?? '',
|
||
$status,
|
||
$cert['issuer'] ?? '',
|
||
$cert['valid_to'] ?? '',
|
||
$daysColor . $days . C_RESET,
|
||
];
|
||
}
|
||
table(['Domain', 'Status', 'Issuer', 'Expires', 'Days'], $rows);
|
||
} else {
|
||
error("Failed to list SSL certificates: " . ($result['error'] ?? 'Unknown error'));
|
||
exit(1);
|
||
}
|
||
break;
|
||
|
||
case '':
|
||
case 'help':
|
||
echo C_BOLD . "SSL Certificate Management" . C_RESET . "\n\n";
|
||
echo "Usage: jabali ssl <command> [options]\n\n";
|
||
echo C_YELLOW . "Commands:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "check" . C_RESET . " Check all domains and issue/renew as needed\n";
|
||
echo " " . C_GREEN . "check <domain>" . C_RESET . " Check specific domain\n";
|
||
echo " " . C_GREEN . "issue <domain>" . C_RESET . " Issue SSL certificate for domain\n";
|
||
echo " " . C_GREEN . "renew <domain>" . C_RESET . " Renew SSL certificate for domain\n";
|
||
echo " " . C_GREEN . "status <domain>" . C_RESET . " Show SSL status for domain\n";
|
||
echo " " . C_GREEN . "list" . C_RESET . " List all SSL certificates\n\n";
|
||
echo C_YELLOW . "Options:" . C_RESET . "\n";
|
||
echo " " . C_GREEN . "--issue-only" . C_RESET . " Only issue new certificates (with check)\n";
|
||
echo " " . C_GREEN . "--renew-only" . C_RESET . " Only renew expiring certificates (with check)\n";
|
||
echo " " . C_GREEN . "--force" . C_RESET . " Force issue even if certificate exists\n";
|
||
break;
|
||
|
||
default:
|
||
error("Unknown ssl command: $subcommand");
|
||
echo "Run 'jabali ssl help' for usage information.\n";
|
||
exit(1);
|
||
}
|
||
}
|