diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2852cae1aa..dc9cea3b1a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,11 +7,27 @@ follows [Semantic Versioning](https://semver.org) guidelines.
## Unreleased
+### Added
+
+- A server whose installation failed can now be retried. The "Install failed" screen was a dead end that only said to
+ contact an administrator; it now offers the reinstall form, and the API accepts a reinstall for a server in that
+ state. Every other blocked state, such as suspended, still refuses it.
+- The "Installing", "Restoring Backup" and "Restoring Snapshot" screens now update by themselves. They check the
+ server every few seconds and move on once the work is done, with a spinning icon to show they're live; before, they
+ stayed put until the page was refreshed.
+
### Fixed
- Fixed every error from Proxmox filling the log with three copies of the same stack trace -- about 256 lines per error,
every few seconds while a page polled a server whose VM was gone. An error is now logged once, with one trace, and its
message is Proxmox's own ("Proxmox returned 500: Configuration file ... does not exist").
+- Fixed reinstalling a server whose VM no longer exists failing every time. Reinstall stopped and deleted the old VM
+ first, and Proxmox answers both with "does not exist" when a previous install died before or during the clone. It
+ now skips straight to building when there is no VM.
+- Fixed a failed reinstall of an already-failed server staying on "Installing" forever instead of going back to
+ "Install failed". The failure handler wrote through the copy of the server captured when the reinstall started, and
+ when that copy already read "install_failed" the write was skipped as a no-op.
+
## v4.7.0-rc.2
diff --git a/app/Http/Middleware/Client/Server/AuthenticateServerAccess.php b/app/Http/Middleware/Client/Server/AuthenticateServerAccess.php
index 5a35d92af26..aa4a73f969c 100644
--- a/app/Http/Middleware/Client/Server/AuthenticateServerAccess.php
+++ b/app/Http/Middleware/Client/Server/AuthenticateServerAccess.php
@@ -6,6 +6,7 @@
use Convoy\Models\Server;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
+use Convoy\Enums\Server\Status;
use Convoy\Exceptions\Http\Server\ServerStatusConflictException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -39,6 +40,15 @@ public function handle(Request $request, Closure $next): Response
return $next($request);
}
+ // A failed install can be retried: reinstalling is the retry, and
+ // the form needs the template list. Every other state stays blocked.
+ if (
+ $server->status === Status::INSTALL_FAILED->value
+ && $request->routeIs('client.servers.settings.template-groups', 'client.servers.settings.reinstall')
+ ) {
+ return $next($request);
+ }
+
throw $exception;
}
diff --git a/app/Services/Servers/ServerBuildDispatchService.php b/app/Services/Servers/ServerBuildDispatchService.php
index 42a6357f2a0..e430400383c 100644
--- a/app/Services/Servers/ServerBuildDispatchService.php
+++ b/app/Services/Servers/ServerBuildDispatchService.php
@@ -20,12 +20,16 @@
class ServerBuildDispatchService
{
+ public function __construct(private ServerBuildService $buildService)
+ {
+ }
+
public function build(ServerDeploymentData $deployment): void
{
$jobs = $this->getChainedBuildJobs($deployment);
Bus::chain($jobs)
- ->catch(fn () => $deployment->server->update(['status' => Status::INSTALL_FAILED->value]))
+ ->catch(self::markInstallFailed($deployment->server->id))
->dispatch();
$deployment->server->update(['status' => Status::INSTALLING->value]);
@@ -42,18 +46,38 @@ public function delete(Server $server): void
public function rebuild(ServerDeploymentData $deployment): void
{
+ // A retry after a failed install can find no VM at all -- the build
+ // may have died before or during the clone. Stopping and deleting a VM
+ // that does not exist fails on Proxmox's "does not exist", which would
+ // fail the whole chain, so go straight to building instead. If the VM
+ // does exist after all, the clone refuses its VMID and nothing is lost.
$jobs = [
- ...$this->getChainedDeleteJobs($deployment->server),
+ ...($this->buildService->isVmDeleted($deployment->server)
+ ? []
+ : $this->getChainedDeleteJobs($deployment->server)),
...$this->getChainedBuildJobs($deployment),
];
Bus::chain($jobs)
- ->catch(fn () => $deployment->server->update(['status' => Status::INSTALL_FAILED->value]))
+ ->catch(self::markInstallFailed($deployment->server->id))
->dispatch();
$deployment->server->update(['status' => Status::INSTALLING->value]);
}
+ /**
+ * The chain's failure handler. It writes by ID rather than through the
+ * model captured at dispatch: that copy is serialized with the chain, and
+ * if it already read install_failed -- as it does when retrying a failed
+ * install -- Eloquent sees nothing dirty and skips the write, leaving the
+ * server stuck on "installing".
+ */
+ private static function markInstallFailed(int $serverId): \Closure
+ {
+ return static fn () => Server::whereKey($serverId)
+ ->update(['status' => Status::INSTALL_FAILED->value]);
+ }
+
private function getChainedBuildJobs(ServerDeploymentData $deployment): array
{
// Base jobs: either create a new server or sync an existing one
diff --git a/database/seeders/E2eSeeder.php b/database/seeders/E2eSeeder.php
index df537cc5b75..d091c814f58 100644
--- a/database/seeders/E2eSeeder.php
+++ b/database/seeders/E2eSeeder.php
@@ -3,7 +3,9 @@
namespace Database\Seeders;
use Convoy\Models\Location;
+use Convoy\Enums\Server\Status;
use Convoy\Models\Node;
+use Convoy\Models\Server;
use Convoy\Models\Template;
use Convoy\Models\TemplateGroup;
use Convoy\Models\User;
@@ -23,7 +25,7 @@ class E2eSeeder extends Seeder
{
public function run(): void
{
- User::factory()->create([
+ $admin = User::factory()->create([
'name' => 'E2E Admin',
'email' => 'admin@e2e.test',
'root_admin' => true,
@@ -46,5 +48,24 @@ public function run(): void
'vmid' => 9000,
'hidden' => false,
]);
+
+ // A server whose install failed, for the retry screen. The UUID is
+ // fixed so the specs can open /servers/e2e00000.
+ Server::create([
+ 'uuid' => 'e2e00000-0000-4000-8000-000000000001',
+ 'uuid_short' => 'e2e00000',
+ 'status' => Status::INSTALL_FAILED->value,
+ 'name' => 'E2E Failed Install',
+ 'hostname' => 'e2e-failed',
+ 'user_id' => $admin->id,
+ 'node_id' => $node->id,
+ 'vmid' => 9100,
+ 'cpu' => 1,
+ 'memory' => 512 * 1024 * 1024,
+ 'disk' => 4 * 1024 * 1024 * 1024,
+ 'snapshot_limit' => 0,
+ 'backup_limit' => null,
+ 'bandwidth_limit' => null,
+ ]);
}
}
diff --git a/lang/en_US/server/settings.php b/lang/en_US/server/settings.php
index 1564707efc3..526a3bf2ea6 100644
--- a/lang/en_US/server/settings.php
+++ b/lang/en_US/server/settings.php
@@ -16,6 +16,13 @@
'description' => 'Are you sure you want to reinstall this server? All data will be lost.',
],
],
+ 'retry_install' => [
+ 'failed_title' => 'Install failed',
+ 'failed_message' => 'Your server failed to install. Pick a template to try again, or contact your administrator if it keeps failing.',
+ 'title' => 'Retry Installation',
+ 'description' => 'Installs the server again from the template you pick, with a new OS password.',
+ 'button' => 'Retry Installation',
+ ],
'isos' => [
'title' => 'Mountable ISOs',
'empty' => 'There are no ISOs',
diff --git a/resources/scripts/components/elements/ScreenBlock.tsx b/resources/scripts/components/elements/ScreenBlock.tsx
index 728e4d7a409..cc9e87b3201 100644
--- a/resources/scripts/components/elements/ScreenBlock.tsx
+++ b/resources/scripts/components/elements/ScreenBlock.tsx
@@ -14,6 +14,8 @@ interface BaseProps {
message: string
full?: boolean
center?: boolean
+ /** Spin the icon, for screens that update themselves when the work is done. */
+ spin?: boolean
children?: ReactNode
onRetry?: () => void
onBack?: () => void
@@ -36,6 +38,7 @@ const ScreenBlock = ({
icon: Icon,
message,
center,
+ spin,
onBack,
onRetry,
full,
@@ -54,7 +57,15 @@ const ScreenBlock = ({
}`}
>
{Icon && (
-
+ {t('retry_install.failed_message')} +
+- {t('reinstall.description')} + {description ?? t('reinstall.description')}