Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/Actions/Server/InstallServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public function run(Server $server): void
{
$this->server = $server;

$maxWait = self::MAX_WAIT_SECONDS;
$timeout = (int) (config('server-provider.providers.'.$server->provider.'.provision_timeout') ?? self::MAX_WAIT_SECONDS);
$maxWait = $timeout;
$connected = false;
$lastError = null;

Expand All @@ -51,7 +52,7 @@ public function run(Server $server): void

if (! $connected) {
throw new SSHConnectionError(
'The server did not become reachable within '.self::MAX_WAIT_SECONDS.' seconds.',
'The server did not become reachable within '.$timeout.' seconds.',
previous: $lastError,
);
}
Expand Down
2 changes: 2 additions & 0 deletions app/Actions/ServerProvider/CreateServerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public function create(User $user, array $input, ?int $projectId): ServerProvide

try {
$provider->connect($input);
} catch (ValidationException $e) {
throw $e;
} catch (Exception) {
throw ValidationException::withMessages([
'provider' => [
Expand Down
32 changes: 32 additions & 0 deletions app/DTOs/DynamicField.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function __construct(
private ?array $link = null,
private ?string $className = null,
private ?array $componentProps = null,
private ?string $width = null,
) {}

public static function make(string $name): self
Expand Down Expand Up @@ -81,6 +82,19 @@ public function alert(): self
return $this;
}

/**
* An alert with a button that opens a step-by-step setup guide.
*
* @param array<int, array{title: string, description?: string, code?: string}> $steps
*/
public function guide(array $steps): self
{
$this->type = 'guide';
$this->componentProps = ['steps' => $steps];

return $this;
}

public function tooling(): self
{
$this->type = 'tooling';
Expand Down Expand Up @@ -141,6 +155,23 @@ public function toolingSelector(array $toolClasses, array $labelOverrides = [],
return $this;
}

/**
* Layout hint for forms rendered in columns; fields are full width by default.
*/
public function half(): self
{
$this->width = 'half';

return $this;
}

public function third(): self
{
$this->width = 'third';

return $this;
}

public function name(string $name): self
{
$this->name = $name;
Expand Down Expand Up @@ -227,6 +258,7 @@ public function toArray(): array
'link' => $this->link,
'className' => $this->className,
'componentProps' => $this->componentProps,
'width' => $this->width,
];
}
}
25 changes: 25 additions & 0 deletions app/Plugins/RegisterServerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public function __construct(
private string $handler = '',
private ?DynamicForm $form = null,
private string $defaultUser = '',
private ?DynamicForm $createForm = null,
private ?int $provisionTimeout = null,
) {}

public static function make(string $name): self
Expand Down Expand Up @@ -47,13 +49,34 @@ public function form(DynamicForm $form): self
return $this;
}

/**
* Extra provider-specific fields rendered on the create-server form.
*/
public function createForm(DynamicForm $createForm): self
{
$this->createForm = $createForm;

return $this;
}

public function defaultUser(string $defaultUser): self
{
$this->defaultUser = $defaultUser;

return $this;
}

/**
* How long the install waits for a new server to boot and accept SSH, for
* providers that provision slower than the default.
*/
public function provisionTimeout(int $seconds): self
{
$this->provisionTimeout = $seconds;

return $this;
}

public function register(): void
{
$providers = config('server-provider.providers');
Expand All @@ -63,6 +86,8 @@ public function register(): void
'handler' => $this->handler,
'form' => $this->form ? $this->form->toArray() : [],
'default_user' => $this->defaultUser,
'create_form' => $this->createForm ? $this->createForm->toArray() : [],
'provision_timeout' => $this->provisionTimeout,
];

config(['server-provider.providers' => $providers]);
Expand Down
100 changes: 100 additions & 0 deletions app/Providers/ServerProviderServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

use App\DTOs\DynamicField;
use App\DTOs\DynamicForm;
use App\Enums\OperatingSystem;
use App\Plugins\RegisterServerProvider;
use App\ServerProviders\AWS;
use App\ServerProviders\Custom;
use App\ServerProviders\DigitalOcean;
use App\ServerProviders\Hetzner;
use App\ServerProviders\Linode;
use App\ServerProviders\Proxmox;
use App\ServerProviders\Vultr;
use Illuminate\Support\ServiceProvider;

Expand All @@ -25,6 +27,7 @@ public function boot(): void
$this->digitalOcean();
$this->linode();
$this->vultr();
$this->proxmox();
}

private function custom(): void
Expand Down Expand Up @@ -118,4 +121,101 @@ private function vultr(): void
->defaultUser('root')
->register();
}

private function proxmox(): void
{
RegisterServerProvider::make(Proxmox::id())
->label('Proxmox VE')
->handler(Proxmox::class)
->form(
DynamicForm::make([
DynamicField::make('proxmox_guide')
->guide($this->proxmoxGuide())
->label('Proxmox setup guide')
->description('Requires Proxmox VE 8.1+, an API token and a cloud-init template for each Ubuntu version.'),
DynamicField::make('api_url')
->text()
->half()
->label('API URL')
->placeholder('https://proxmox.example.com:8006'),
DynamicField::make('storage')
->text()
->half()
->label('Disk Storage')
->placeholder('Same as template'),
DynamicField::make('token_id')
->text()
->half()
->label('API Token ID')
->placeholder('vito@pve!vito'),
DynamicField::make('token_secret')
->password()
->half()
->label('API Token Secret'),
...array_map(
fn (string $os): DynamicField => DynamicField::make(Proxmox::templateField($os))
->text()
->third()
->label('Ubuntu '.OperatingSystem::from($os)->getVersion().' Template')
->placeholder('VMID'),
config('core.operating_systems'),
),
DynamicField::make('verify_ssl')
->checkbox()
->label('Verify SSL certificate')
->default(true),
])
)
->createForm(
DynamicForm::make([
DynamicField::make('static_ip')
->text()
->half()
->label('Static IP (CIDR)')
->placeholder('192.168.1.50/24')
->description('Leave empty to use DHCP. DHCP needs qemu-guest-agent in the template so Vito can find the IP.'),
DynamicField::make('gateway')
->text()
->half()
->label('Gateway')
->placeholder('192.168.1.1'),
])
)
->defaultUser('root')
->provisionTimeout(600)
->register();
}

/**
* @return array<int, array{title: string, description?: string, code?: string}>
*/
private function proxmoxGuide(): array
{
return [
[
'title' => 'Create an API token',
'description' => 'Run this in the Proxmox host shell and use vito@pve!vito as the Token ID and the printed value as the secret. If you create the token in the web UI instead, untick Privilege Separation or grant these roles to the token itself.',
'code' => "pveum user add vito@pve\npveum acl modify / --users vito@pve --roles PVEVMAdmin,PVEDatastoreUser,PVESDNUser,PVEAuditor\npveum user token add vito@pve vito --privsep 0",
],
[
'title' => 'Download the Ubuntu cloud image',
'description' => 'Use an official cloud image, not an ISO install. Replace noble with jammy for Ubuntu 22.04.',
'code' => "cd /root\nwget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img",
],
[
'title' => 'Add the QEMU guest agent',
'description' => 'Vito reads a DHCP-assigned IP from the guest agent. Skip this step if you always give servers a static IP.',
'code' => "apt install -y libguestfs-tools\nvirt-customize -a noble-server-cloudimg-amd64.img --install qemu-guest-agent",
],
[
'title' => 'Create the template',
'description' => 'Change vmbr0 and local-lvm if your bridge or storage is named differently. Vito grows the disk to the plan size when it creates a server.',
'code' => "qm create 9000 --name ubuntu-2404-cloud --memory 2048 --cores 2 --ostype l26 --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci\nqm set 9000 --scsi0 local-lvm:0,import-from=/root/noble-server-cloudimg-amd64.img\nqm set 9000 --ide2 local-lvm:cloudinit --boot order=scsi0 --serial0 socket --vga serial0 --agent 1\nqm template 9000",
],
[
'title' => 'Connect Vito',
'description' => 'Use https://your-host:8006 as the API URL, turn off Verify SSL for a self-signed certificate, and enter 9000 as the Ubuntu 24.04 template VMID. Vito must be able to reach the new servers over SSH.',
],
];
}
}
Loading
Loading