fix(engine-b): apply plan CPU/RAM; idempotent backup + monitoring registration
- ConfigureCloudInit sets cores/memory from the plan (not the template default). - RegisterBackup uses a deterministic Proxmox job id (ignore-exists) + firstOrCreate. - RegisterMonitoring is idempotent by URL + firstOrCreate — no duplicate schedules or monitors after a crash/retry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
5cf1964d7d
commit
3c5a18b5d7
|
|
@ -32,6 +32,8 @@ class ConfigureCloudInit extends CustomerStep
|
|||
'ipconfig0' => 'ip=dhcp',
|
||||
'ciuser' => 'clupilot',
|
||||
'nameserver' => '1.1.1.1',
|
||||
'cores' => $instance->cores, // apply the plan's CPU/RAM, not the template's
|
||||
'memory' => $instance->ram_mb,
|
||||
]);
|
||||
// Absolute target (not '+…') so a retry can't grow the disk twice.
|
||||
$pve->resizeDisk($node, $vmid, 'scsi0', $instance->disk_gb.'G');
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@ class RegisterBackup extends CustomerStep
|
|||
->createBackupJob((string) $run->context('node'), (int) $instance->vmid, $schedule);
|
||||
|
||||
$this->recordResource($run, $instance->host, 'backup_job_id', $jobId);
|
||||
$instance->backups()->create([
|
||||
'external_job_id' => $jobId,
|
||||
'schedule' => $schedule,
|
||||
'status' => 'scheduled',
|
||||
]);
|
||||
// firstOrCreate keyed on the deterministic job id → no duplicate row if a
|
||||
// crash re-runs this step before the breadcrumb was recorded.
|
||||
$instance->backups()->firstOrCreate(
|
||||
['external_job_id' => $jobId],
|
||||
['schedule' => $schedule, 'status' => 'scheduled'],
|
||||
);
|
||||
|
||||
return StepResult::advance();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,10 @@ class RegisterMonitoring extends CustomerStep
|
|||
$targetId = $this->monitoring->registerTarget($instance->subdomain, $url);
|
||||
|
||||
$this->recordResource($run, $instance->host, 'monitoring_target_id', $targetId);
|
||||
$instance->monitoringTargets()->create([
|
||||
'external_id' => $targetId,
|
||||
'url' => $url,
|
||||
'status' => 'up',
|
||||
]);
|
||||
$instance->monitoringTargets()->firstOrCreate(
|
||||
['external_id' => $targetId],
|
||||
['url' => $url, 'status' => 'up'],
|
||||
);
|
||||
|
||||
return StepResult::advance();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,15 @@ class FakeMonitoringClient implements MonitoringClient
|
|||
/** @var array<string, string> id => url */
|
||||
public array $targets = [];
|
||||
|
||||
private int $counter = 1;
|
||||
|
||||
public function registerTarget(string $name, string $url): string
|
||||
{
|
||||
$id = 'mon-'.$this->counter++;
|
||||
// Idempotent by URL — a retry after a crash returns the same target id.
|
||||
$existing = array_search($url, $this->targets, true);
|
||||
if ($existing !== false) {
|
||||
return $existing;
|
||||
}
|
||||
|
||||
$id = 'mon-'.substr(sha1($url), 0, 12);
|
||||
$this->targets[$id] = $url;
|
||||
|
||||
return $id;
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@ class FakeProxmoxClient implements ProxmoxClient
|
|||
/** @var array<int, string> */
|
||||
public array $cloudInitCalls = [];
|
||||
|
||||
/** @var array<int, array<string, mixed>> vmid => params */
|
||||
public array $cloudInitParams = [];
|
||||
|
||||
/** @var array<int, string> */
|
||||
public array $resizeCalls = [];
|
||||
|
||||
|
|
@ -96,6 +99,7 @@ class FakeProxmoxClient implements ProxmoxClient
|
|||
public function setCloudInit(string $node, int $vmid, array $params): void
|
||||
{
|
||||
$this->cloudInitCalls[] = $vmid;
|
||||
$this->cloudInitParams[$vmid] = $params;
|
||||
}
|
||||
|
||||
public function resizeDisk(string $node, int $vmid, string $disk, string $size): void
|
||||
|
|
|
|||
|
|
@ -141,14 +141,22 @@ class HttpProxmoxClient implements ProxmoxClient
|
|||
|
||||
public function createBackupJob(string $node, int $vmid, string $schedule): string
|
||||
{
|
||||
$data = $this->http()->asForm()->post('/cluster/backup', [
|
||||
// Deterministic job id → creating it twice (crash/retry) is idempotent.
|
||||
$jobId = 'clupilot-'.$vmid;
|
||||
|
||||
$response = $this->http()->asForm()->post('/cluster/backup', [
|
||||
'id' => $jobId,
|
||||
'vmid' => $vmid,
|
||||
'schedule' => $schedule,
|
||||
'storage' => 'local',
|
||||
'mode' => 'snapshot',
|
||||
'enabled' => 1,
|
||||
])->throw()->json('data');
|
||||
]);
|
||||
|
||||
return is_string($data) ? $data : (string) ($data['id'] ?? 'backup-'.$vmid);
|
||||
if ($response->failed() && ! str_contains($response->body(), 'already')) {
|
||||
$response->throw();
|
||||
}
|
||||
|
||||
return $jobId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,8 @@ it('configures cloud-init and resizes the disk', function () {
|
|||
['run' => $run] = reservedRun();
|
||||
expect(app(ConfigureCloudInit::class)->execute($run)->type)->toBe('advance')
|
||||
->and($s['pve']->cloudInitCalls)->toContain(101)
|
||||
->and($s['pve']->cloudInitParams[101]['cores'])->toBe(2) // start plan
|
||||
->and($s['pve']->cloudInitParams[101]['memory'])->toBe(4096)
|
||||
->and($s['pve']->resizeCalls)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue