diff --git a/lang/English/modules/user_games.php b/lang/English/modules/user_games.php index c103c41e..1a7fb1a2 100644 --- a/lang/English/modules/user_games.php +++ b/lang/English/modules/user_games.php @@ -220,4 +220,24 @@ define('OGP_LANG_ftp_account_username_too_long', "FTP username is too long. Try define('OGP_LANG_ftp_account_password_too_long', "FTP password is too long. Try a shorter password no longer than 20 characters."); define('OGP_LANG_other_servers_exist_with_path_please_change', "Other homes exist with the same path. It is recommended (but not required) that you change this path to something unique. You may have problems if you do NOT."); define('OGP_LANG_change_access_rights_for_selected_servers', "Change access rights for selected servers"); + +// Migrate feature (replaces Clone) +define('OGP_LANG_migrate', "Migrate"); +define('OGP_LANG_migrate_server', "Migrate Server: %s"); +define('OGP_LANG_migrate_info', "This tool copies all files from the source server to a destination server of the same game type. The source is never deleted or modified."); +define('OGP_LANG_migrate_bullet_no_delete', "The source server is NOT deleted."); +define('OGP_LANG_migrate_bullet_same_game', "Destination must be the same game type."); +define('OGP_LANG_migrate_bullet_overwrite', "All files in the destination will be overwritten."); +define('OGP_LANG_migrate_bullet_no_billing', "Billing records are not changed automatically."); +define('OGP_LANG_migrate_source', "Source"); +define('OGP_LANG_migrate_destination', "Destination"); +define('OGP_LANG_migrate_confirm_overwrite', "Confirm overwrite"); +define('OGP_LANG_migrate_confirm_overwrite_info', "Check this box to confirm you want to overwrite the destination server files."); +define('OGP_LANG_migrate_start', "Start Migration"); +define('OGP_LANG_migrate_no_compatible_destinations', "No compatible destination servers found (same game type, different server)."); +define('OGP_LANG_migrate_different_game_type', "Source and destination must be the same game type."); +define('OGP_LANG_migrate_confirm_required', "You must tick the confirmation checkbox before starting a migration."); +define('OGP_LANG_migrate_running_background', "Migration started and is running in the background."); +define('OGP_LANG_migrate_complete', "Migration completed successfully."); +define('OGP_LANG_migrate_failed_code', "Migration failed with return code %d."); ?> \ No newline at end of file diff --git a/modules/billing/create_servers.php b/modules/billing/create_servers.php index 5b0b322e..cc66a614 100644 --- a/modules/billing/create_servers.php +++ b/modules/billing/create_servers.php @@ -20,6 +20,10 @@ function exec_ogp_module() { global $db,$view,$settings; + // $now is used in multiple branches below — define it once here so it is + // always a string that date() / strtotime() can handle safely (PHP 8 fix). + $now = date('Y-m-d H:i:s'); + $override = isset($GLOBALS['BILLING_PROVISION_OVERRIDE']) ? $GLOBALS['BILLING_PROVISION_OVERRIDE'] : null; $user_id = isset($override['user_id']) ? intval($override['user_id']) : (isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0); $isAdmin = isset($override['is_admin']) ? (bool)$override['is_admin'] : $db->isAdmin($user_id); @@ -80,7 +84,7 @@ function exec_ogp_module() $ip = $order['ip']; $max_players = $order['max_players']; $user_id = $order['user_id']; - $extended = $order['extended'] == "1" ? TRUE : FALSE; + $extended = isset($order['extended']) && $order['extended'] == "1" ? TRUE : FALSE; //Query service info $service = $db->resultQuery( "SELECT * FROM OGP_DB_PREFIXbilling_services @@ -413,8 +417,6 @@ function exec_ogp_module() $db->query( "UPDATE OGP_DB_PREFIXgame_mods SET max_players= ".$order['max_players']." WHERE home_id=".$db->realEscapeSingle($home_id)); - } - // Show results and redirect if ($provisioned_count > 0) { echo "
<< " . get_lang('back_to_game_servers') . "
"; + } else { + print_failure(get_lang_f('migrate_failed_code', (int)$result)); + } + return; + } + + // ------------------------------------------------------------------ // + // Build the list of eligible destination servers (same game type, // + // exclude source, admin-visible only). // + // ------------------------------------------------------------------ // + $all_homes = $db->getGameHomes_limit(1, 9999, false, false); + $candidates = array(); + if (!empty($all_homes)) { + foreach ((array)$all_homes as $h) { + if (intval($h['home_id']) === $home_id) continue; + if (intval($h['home_cfg_id']) !== intval($source['home_cfg_id'])) continue; + $candidates[] = $h; + } + } + + if (empty($candidates)) { + print_failure(get_lang('migrate_no_compatible_destinations')); + echo "<< " . get_lang('back_to_game_servers') . "
"; + return; + } + + // ------------------------------------------------------------------ // + // Show migration form // + // ------------------------------------------------------------------ // + echo "" . get_lang('migrate_info') . "
"; + echo "" . get_lang('note_ips_and_ports_are_not_cloned') . "
"; + foreach ((array)$assigned as $r) { + echo "" . $r['ip'] . ":" . $r['port'] . "
\n"; + } + } +} + +// ------------------------------------------------------------------ // +// Core migration function // +// ------------------------------------------------------------------ // + +/** + * Copy all files from $source game server to $dest using rsync (Linux) or + * robocopy (Windows fallback) via the remote agent's exec() call. + * + * Both servers must live on the SAME remote agent. Cross-node migration is + * noted below as a limitation. + * + * @param OGPDatabase $db Panel DB + * @param array $source Row from getGameHomeWithoutMods() for source + * @param array $dest Row from getGameHomeWithoutMods() for dest + * @param array $settings Panel settings array + * + * @return true|int TRUE or -1 on async success, 0 on failure, other int on error + */ +function migrate_game_server($db, $source, $dest, $settings) +{ + require_once('includes/lib_remote.php'); + + $src_path = rtrim($source['home_path'], '/\\'); + $dst_path = rtrim($dest['home_path'], '/\\'); + + // Validate paths + if (empty($src_path) || empty($dst_path)) { + return 0; + } + if ($src_path === $dst_path) { + return 0; + } + + // ------------------------------------------------------------------ // + // Cross-node migration guard // + // rsync between two *different* agents would require SSH access // + // between them which is not guaranteed. For now we only support // + // same-agent migration; the UI should already have filtered this, but // + // we double-check here. // + // ------------------------------------------------------------------ // + $same_node = ($source['remote_server_id'] == $dest['remote_server_id']); + + // Build remote connection to the source agent (used for same-node ops) + $remote_src = new OGPRemoteLibrary( + $source['agent_ip'], + $source['agent_port'], + $source['encryption_key'], + $source['timeout'] + ); + + if (!$same_node) { + // Cross-node: attempt rsync pull from the DESTINATION agent using + // SSH to pull files from the source agent. This requires that the + // destination agent's OS user can reach the source via SSH without + // a passphrase. We attempt it but return 0 on obvious failure. + $remote_dst = new OGPRemoteLibrary( + $dest['agent_ip'], + $dest['agent_port'], + $dest['encryption_key'], + $dest['timeout'] + ); + + // Detect destination OS + $dst_os = $remote_dst->what_os(); + if (stripos($dst_os, 'win') !== false) { + // Windows cross-node not supported via this UI + return 0; + } + + $src_user = $source['ogp_user'] ?? 'gameserver'; + $rsync_pull = sprintf( + 'rsync -avz --delete -e "ssh -o StrictHostKeyChecking=no" %s@%s:%s/ %s/', + escapeshellarg($src_user), + escapeshellarg($source['agent_ip']), + escapeshellarg($src_path), + escapeshellarg($dst_path) + ); + + $out = $remote_dst->exec($rsync_pull); + if ($out === NULL) { + return -1; // running async + } + // Fix ownership on destination + $dst_user = $dest['ogp_user'] ?? 'gameserver'; + $chown_cmd = sprintf( + 'chown -R %s:%s %s/', + escapeshellarg($dst_user), + escapeshellarg($dst_user), + escapeshellarg($dst_path) + ); + $remote_dst->exec($chown_cmd); + $db->logger("Migrated (cross-node) home {$source['home_id']} -> {$dest['home_id']}"); + return TRUE; + } + + // ------------------------------------------------------------------ // + // Same-node migration via clone_home() RPC (rsync under the hood) // + // ------------------------------------------------------------------ // + + // Detect OS so we can choose the right tool + $os = $remote_src->what_os(); + + if (stripos($os, 'win') !== false) { + // Windows: try rsync (Cygwin/MSYS) first, fall back to robocopy + $rsync_cmd = sprintf('rsync -avz --delete %s/ %s/', + escapeshellarg($src_path), escapeshellarg($dst_path)); + $robocopy_cmd = sprintf('robocopy %s %s /MIR /R:1 /W:1', + escapeshellarg($src_path), escapeshellarg($dst_path)); + + $out = $remote_src->exec($rsync_cmd); + if ($out === NULL) { + // rsync not available — fall back to robocopy + $remote_src->exec($robocopy_cmd); + } + + $db->logger("Migrated (Windows same-node) home {$source['home_id']} -> {$dest['home_id']}"); + return TRUE; + } + + // Linux — prefer the agent's built-in clone_home (rsync -a) because it + // runs in the background and returns -1 (async) with progress support. + // We need to pass the owner for chown; fall back to source ogp_user. + $owner = $dest['ogp_user'] ?? $source['ogp_user'] ?? 'gameserver'; + $rc = $remote_src->clone_home($src_path, $dst_path, $owner); + + if ($rc === 1 || $rc === -1) { + // Also fix ownership explicitly (clone_home may already do this) + $chown_cmd = sprintf('chown -R %s:%s %s/', + escapeshellarg($owner), escapeshellarg($owner), escapeshellarg($dst_path)); + $remote_src->exec($chown_cmd); + $db->logger("Migrated home {$source['home_id']} -> {$dest['home_id']}"); + } + + return $rc; +} diff --git a/modules/user_games/navigation.xml b/modules/user_games/navigation.xml index 3986356d..278866ca 100644 --- a/modules/user_games/navigation.xml +++ b/modules/user_games/navigation.xml @@ -4,7 +4,7 @@