411 lines
14 KiB
PHP
411 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\User;
|
|
use App\Services\Agent\AgentClient;
|
|
use App\Services\Migration\MigrationDnsSyncService;
|
|
use App\Services\Migration\WhmApiService;
|
|
use App\Services\Migration\WhmMigrationStatusStore;
|
|
use Exception;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Queue\Queueable;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class RunWhmMigrationBatch implements ShouldQueue
|
|
{
|
|
use Queueable;
|
|
|
|
public int $tries = 1;
|
|
|
|
public int $timeout = 7200;
|
|
|
|
private bool $shouldReloadFpm = false;
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $accounts
|
|
* @param array<int, string> $selectedAccounts
|
|
*/
|
|
public function __construct(
|
|
public string $cacheKey,
|
|
public string $hostname,
|
|
public string $whmUsername,
|
|
public string $apiToken,
|
|
public int $port,
|
|
public bool $useSSL,
|
|
public array $accounts,
|
|
public array $selectedAccounts,
|
|
public bool $restoreFiles,
|
|
public bool $restoreDatabases,
|
|
public bool $restoreEmails,
|
|
public bool $restoreSsl,
|
|
public bool $createLinuxUsers,
|
|
) {}
|
|
|
|
public function handle(AgentClient $agent, MigrationDnsSyncService $dnsSyncService): void
|
|
{
|
|
$store = new WhmMigrationStatusStore($this->cacheKey);
|
|
$store->initialize($this->selectedAccounts);
|
|
$reloadDelaySeconds = 15;
|
|
|
|
try {
|
|
$whm = new WhmApiService(
|
|
$this->hostname,
|
|
$this->whmUsername,
|
|
$this->apiToken,
|
|
$this->port,
|
|
$this->useSSL,
|
|
);
|
|
|
|
$accountsByUser = $this->indexAccountsByUser($this->accounts);
|
|
|
|
foreach ($this->selectedAccounts as $cpanelUser) {
|
|
$this->migrateAccount($store, $whm, $agent, $dnsSyncService, $cpanelUser, $accountsByUser[$cpanelUser] ?? []);
|
|
}
|
|
|
|
$store->setMigrating(false);
|
|
|
|
try {
|
|
$agent->send('service.reload', ['service' => 'nginx']);
|
|
} catch (Exception $e) {
|
|
Log::warning('Failed to reload nginx after WHM migration', ['error' => $e->getMessage()]);
|
|
}
|
|
|
|
if ($this->shouldReloadFpm) {
|
|
try {
|
|
$agent->send('php.reload_all_fpm', [
|
|
'background' => true,
|
|
'delay' => $reloadDelaySeconds,
|
|
]);
|
|
} catch (Exception $e) {
|
|
Log::warning('Failed to reload PHP-FPM after WHM migration', ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::error('WHM migration batch failed', ['error' => $e->getMessage()]);
|
|
} finally {
|
|
$state = $store->get();
|
|
if (($state['isMigrating'] ?? false) === true) {
|
|
$store->setMigrating(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $account
|
|
*/
|
|
protected function migrateAccount(WhmMigrationStatusStore $store, WhmApiService $whm, AgentClient $agent, MigrationDnsSyncService $dnsSyncService, string $cpanelUser, array $account): void
|
|
{
|
|
$store->updateAccountStatus($cpanelUser, 'processing', __('Starting migration...'));
|
|
|
|
try {
|
|
$domain = $account['domain'] ?? '';
|
|
$email = $account['email'] ?? ($domain !== '' ? "{$cpanelUser}@{$domain}" : "{$cpanelUser}@example.com");
|
|
|
|
$user = $this->createOrGetUser($agent, $cpanelUser, $email);
|
|
if (! $user) {
|
|
throw new Exception(__('Failed to create user'));
|
|
}
|
|
|
|
$store->addAccountLog($cpanelUser, __('User ready: :username', ['username' => $user->username]), 'success');
|
|
|
|
$store->updateAccountStatus($cpanelUser, 'backup_creating', __('Setting up backup transfer...'));
|
|
|
|
$keyName = $this->getSshKeyName();
|
|
$destPath = $this->getBackupDestPath();
|
|
|
|
if (! is_dir($destPath)) {
|
|
mkdir($destPath, 0755, true);
|
|
}
|
|
|
|
$agent->send('jabali_ssh.ensure_exists', []);
|
|
|
|
$publicKeyResult = $agent->send('jabali_ssh.get_public_key', []);
|
|
if (! ($publicKeyResult['success'] ?? false) || ! ($publicKeyResult['exists'] ?? false)) {
|
|
throw new Exception(__('Failed to get Jabali public key'));
|
|
}
|
|
$publicKey = $publicKeyResult['public_key'] ?? null;
|
|
|
|
$agent->send('jabali_ssh.add_to_authorized_keys', [
|
|
'public_key' => $publicKey,
|
|
'comment' => 'whm-migration-'.$cpanelUser,
|
|
]);
|
|
|
|
$privateKeyResult = $agent->send('jabali_ssh.get_private_key', []);
|
|
if (! ($privateKeyResult['success'] ?? false) || ! ($privateKeyResult['exists'] ?? false)) {
|
|
throw new Exception(__('Failed to read Jabali private key'));
|
|
}
|
|
|
|
$privateKey = $privateKeyResult['private_key'] ?? null;
|
|
if (empty($privateKey)) {
|
|
throw new Exception(__('Private key is empty'));
|
|
}
|
|
|
|
$store->addAccountLog($cpanelUser, __('Importing SSH key to cPanel...'), 'pending');
|
|
|
|
$importResult = $whm->importSshPrivateKey($cpanelUser, $keyName, $privateKey);
|
|
if (! ($importResult['success'] ?? false)) {
|
|
throw new Exception($importResult['message'] ?? __('Failed to import SSH key'));
|
|
}
|
|
|
|
$actualKeyName = $importResult['actual_key_name'] ?? $keyName;
|
|
$store->addAccountLog($cpanelUser, __('SSH key imported'), 'success');
|
|
|
|
$authResult = $whm->authorizeSshKey($cpanelUser, $actualKeyName);
|
|
if (! ($authResult['success'] ?? false)) {
|
|
$store->addAccountLog($cpanelUser, __('SSH key authorization skipped'), 'info');
|
|
} else {
|
|
$store->addAccountLog($cpanelUser, __('SSH key authorized'), 'success');
|
|
}
|
|
|
|
$store->addAccountLog($cpanelUser, __('Initiating backup transfer...'), 'pending');
|
|
|
|
$jabaliIp = $this->getJabaliPublicIp();
|
|
|
|
$backupResult = $whm->createBackupToScpWithKey(
|
|
$cpanelUser,
|
|
$jabaliIp,
|
|
'root',
|
|
$destPath,
|
|
$actualKeyName,
|
|
22
|
|
);
|
|
|
|
if (! ($backupResult['success'] ?? false)) {
|
|
throw new Exception($backupResult['message'] ?? __('Failed to start backup'));
|
|
}
|
|
|
|
$store->addAccountLog($cpanelUser, __('Backup initiated, transferring via SCP...'), 'success');
|
|
|
|
$store->updateAccountStatus($cpanelUser, 'backup_downloading', __('Waiting for backup file...'));
|
|
|
|
$backupPath = $this->waitForBackupFile($agent, $store, $cpanelUser, $destPath);
|
|
if (! $backupPath) {
|
|
throw new Exception(__('Backup file did not arrive'));
|
|
}
|
|
|
|
$store->addAccountLog($cpanelUser, __('Backup received: :size', ['size' => $this->formatBytes(filesize($backupPath))]), 'success');
|
|
|
|
$summary = $whm->getUserMigrationSummary($cpanelUser);
|
|
$discoveredData = $whm->convertApiDataToAgentFormat($summary);
|
|
|
|
$store->updateAccountStatus($cpanelUser, 'restoring', __('Restoring data...'));
|
|
|
|
$result = $agent->send('cpanel.restore_backup', [
|
|
'backup_path' => $backupPath,
|
|
'username' => $user->username,
|
|
'restore_files' => $this->restoreFiles,
|
|
'restore_databases' => $this->restoreDatabases,
|
|
'restore_emails' => $this->restoreEmails,
|
|
'restore_ssl' => $this->restoreSsl,
|
|
'discovered_data' => $discoveredData,
|
|
]);
|
|
|
|
if ($result['success'] ?? false) {
|
|
foreach ($result['log'] ?? [] as $entry) {
|
|
$store->addAccountLog($cpanelUser, $entry['message'], $entry['status'] ?? 'info');
|
|
}
|
|
|
|
$this->syncDnsZones($dnsSyncService, $user, $discoveredData);
|
|
|
|
$store->updateAccountStatus($cpanelUser, 'completed', __('Migration completed'));
|
|
@unlink($backupPath);
|
|
} else {
|
|
throw new Exception($result['error'] ?? __('Restore failed'));
|
|
}
|
|
} catch (Exception $e) {
|
|
Log::error('WHM migration failed for user', ['user' => $cpanelUser, 'error' => $e->getMessage()]);
|
|
$store->updateAccountStatus($cpanelUser, 'error', $e->getMessage(), 'error');
|
|
}
|
|
}
|
|
|
|
protected function createOrGetUser(AgentClient $agent, string $cpanelUser, string $email): ?User
|
|
{
|
|
$existingUser = User::where('username', $cpanelUser)->first();
|
|
if ($existingUser) {
|
|
return $existingUser;
|
|
}
|
|
|
|
if (User::where('email', $email)->exists()) {
|
|
$email = "{$cpanelUser}.".time().'@'.explode('@', $email)[1];
|
|
}
|
|
|
|
$password = bin2hex(random_bytes(12));
|
|
|
|
try {
|
|
if ($this->createLinuxUsers) {
|
|
exec('id '.escapeshellarg($cpanelUser).' 2>/dev/null', $output, $exitCode);
|
|
|
|
if ($exitCode !== 0) {
|
|
$result = $agent->send('user.create', [
|
|
'username' => $cpanelUser,
|
|
'password' => $password,
|
|
]);
|
|
|
|
if (! ($result['success'] ?? false)) {
|
|
throw new Exception($result['error'] ?? __('Failed to create system user'));
|
|
}
|
|
|
|
if (($result['fpm_pool_created'] ?? false) === true) {
|
|
$this->shouldReloadFpm = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return User::create([
|
|
'name' => ucfirst($cpanelUser),
|
|
'username' => $cpanelUser,
|
|
'email' => $email,
|
|
'password' => Hash::make($password),
|
|
'home_directory' => '/home/'.$cpanelUser,
|
|
'disk_quota_mb' => null,
|
|
'is_active' => true,
|
|
'is_admin' => false,
|
|
]);
|
|
} catch (Exception $e) {
|
|
Log::error('Failed to create user', ['username' => $cpanelUser, 'error' => $e->getMessage()]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
protected function waitForBackupFile(AgentClient $agent, WhmMigrationStatusStore $store, string $cpanelUser, string $destPath): ?string
|
|
{
|
|
$maxAttempts = 120;
|
|
$attempt = 0;
|
|
$lastSeenSize = 0;
|
|
$sizeStableCount = 0;
|
|
|
|
while ($attempt < $maxAttempts) {
|
|
$attempt++;
|
|
sleep(5);
|
|
|
|
$pattern = "{$destPath}/backup-*_{$cpanelUser}.tar.gz";
|
|
$files = glob($pattern);
|
|
|
|
if (empty($files)) {
|
|
$pattern = "{$destPath}/cpmove-{$cpanelUser}.tar.gz";
|
|
$files = glob($pattern);
|
|
}
|
|
|
|
if (empty($files)) {
|
|
if ($attempt % 6 === 0) {
|
|
$store->addAccountLog($cpanelUser, __('Waiting for backup file... (:count s)', ['count' => $attempt * 5]), 'pending');
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
usort($files, fn ($a, $b) => filemtime($b) - filemtime($a));
|
|
$backupFile = $files[0];
|
|
$currentSize = filesize($backupFile);
|
|
|
|
if ($currentSize > 0 && $currentSize === $lastSeenSize) {
|
|
$sizeStableCount++;
|
|
} else {
|
|
$sizeStableCount = 0;
|
|
}
|
|
$lastSeenSize = $currentSize;
|
|
|
|
if ($sizeStableCount >= 3 && $currentSize >= 10 * 1024) {
|
|
$agent->send('file.chown', [
|
|
'path' => $backupFile,
|
|
'owner' => 'www-data',
|
|
'group' => 'www-data',
|
|
]);
|
|
|
|
$handle = fopen($backupFile, 'rb');
|
|
$magic = $handle ? fread($handle, 2) : '';
|
|
if ($handle) {
|
|
fclose($handle);
|
|
}
|
|
|
|
if ($magic === "\x1f\x8b") {
|
|
return $backupFile;
|
|
}
|
|
|
|
$store->addAccountLog($cpanelUser, __('Invalid backup file format, waiting...'), 'warning');
|
|
$sizeStableCount = 0;
|
|
}
|
|
|
|
if ($attempt % 6 === 0) {
|
|
$store->addAccountLog($cpanelUser, __('Receiving backup... :size', [
|
|
'size' => $this->formatBytes($currentSize),
|
|
]), 'pending');
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $accounts
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
protected function indexAccountsByUser(array $accounts): array
|
|
{
|
|
$indexed = [];
|
|
|
|
foreach ($accounts as $account) {
|
|
if (! isset($account['user'])) {
|
|
continue;
|
|
}
|
|
|
|
$indexed[$account['user']] = $account;
|
|
}
|
|
|
|
return $indexed;
|
|
}
|
|
|
|
protected function getBackupDestPath(): string
|
|
{
|
|
return '/var/backups/jabali/whm-migrations';
|
|
}
|
|
|
|
protected function getSshKeyName(): string
|
|
{
|
|
return 'jabali-system-key';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $discoveredData
|
|
*/
|
|
protected function syncDnsZones(MigrationDnsSyncService $dnsSyncService, User $user, array $discoveredData): void
|
|
{
|
|
try {
|
|
$domains = $discoveredData['domains'] ?? [];
|
|
$dnsSyncService->syncDomainsForUser($user, $domains);
|
|
} catch (Exception $e) {
|
|
Log::warning('Failed to sync DNS zones after WHM migration', [
|
|
'user' => $user->username,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
protected function getJabaliPublicIp(): string
|
|
{
|
|
$ip = trim(shell_exec('curl -s ifconfig.me 2>/dev/null') ?? '');
|
|
|
|
if (empty($ip)) {
|
|
$ip = gethostbyname(gethostname());
|
|
}
|
|
|
|
return $ip;
|
|
}
|
|
|
|
protected 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];
|
|
}
|
|
}
|