From 20944421de5db8d47f88216ad0dcf73df5cbcf26 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Date: Sun, 17 Nov 2024 12:09:41 +0100 Subject: [PATCH] load Vultr regions and plans dynamically (#369) --- app/ServerProviders/AWS.php | 9 +- app/ServerProviders/Custom.php | 3 +- app/ServerProviders/DigitalOcean.php | 33 +++++- app/ServerProviders/Vultr.php | 155 +++++++++++++++++++-------- 4 files changed, 144 insertions(+), 56 deletions(-) diff --git a/app/ServerProviders/AWS.php b/app/ServerProviders/AWS.php index 48c2f4f..f40c5e6 100755 --- a/app/ServerProviders/AWS.php +++ b/app/ServerProviders/AWS.php @@ -6,7 +6,6 @@ use App\Facades\Notifier; use App\Notifications\FailedToDeleteServerFromProvider; use Aws\Ec2\Ec2Client; -use Aws\EC2InstanceConnect\EC2InstanceConnectClient; use Exception; use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Facades\Storage; @@ -16,8 +15,6 @@ class AWS extends AbstractProvider { protected Ec2Client $ec2Client; - protected EC2InstanceConnectClient $ec2InstanceConnectClient; - public function createRules(array $input): array { return [ @@ -272,14 +269,12 @@ private function runInstance(): void /** * @throws Exception */ - public function getImageId(string $os): string + private function getImageId(string $os): string { $this->connectToEc2Client(); $version = config('core.operating_system_versions.'.$os); - ds($version); - $result = $this->ec2Client->describeImages([ 'Filters' => [ [ @@ -301,8 +296,6 @@ public function getImageId(string $os): string // Extract and display image information $images = $result->get('Images'); - ds($images); - if (! empty($images)) { // Sort images by creation date to get the latest one usort($images, function ($a, $b) { diff --git a/app/ServerProviders/Custom.php b/app/ServerProviders/Custom.php index 5bf308a..30ac62e 100755 --- a/app/ServerProviders/Custom.php +++ b/app/ServerProviders/Custom.php @@ -3,6 +3,7 @@ namespace App\ServerProviders; use App\ValidationRules\RestrictedIPAddressesRule; +use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use Illuminate\Validation\Rule; @@ -58,7 +59,7 @@ public function regions(): array public function create(): void { - /** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */ + /** @var FilesystemAdapter $storageDisk */ $storageDisk = Storage::disk(config('core.key_pairs_disk')); File::copy( storage_path(config('core.ssh_private_key_name')), diff --git a/app/ServerProviders/DigitalOcean.php b/app/ServerProviders/DigitalOcean.php index 631fdf8..85a23da 100644 --- a/app/ServerProviders/DigitalOcean.php +++ b/app/ServerProviders/DigitalOcean.php @@ -69,8 +69,6 @@ public function plans(?string $region): array ->get($this->apiUrl.'/sizes', ['per_page' => 200]) ->json(); - ds($region); - return collect($plans['sizes'])->filter(function ($size) use ($region) { return in_array($region, $size['regions']); }) @@ -133,7 +131,7 @@ public function create(): void 'name' => str($this->server->name)->slug(), 'region' => $this->server->provider_data['region'], 'size' => $this->server->provider_data['plan'], - 'image' => config('serverproviders.digitalocean.images')[$this->server->os], + 'image' => $this->getImageId($this->server->os, $this->server->provider_data['region']), 'backups' => false, 'ipv6' => false, 'monitoring' => false, @@ -195,4 +193,33 @@ public function delete(): void } } } + + /** + * @throws Exception + */ + private function getImageId(string $os, string $region): int + { + $version = config('core.operating_system_versions.'.$os); + + try { + $result = Http::withToken($this->serverProvider->credentials['token']) + ->get($this->apiUrl.'/images', [ + 'per_page' => 200, + 'type' => 'distribution', + ]) + ->json(); + + $image = collect($result['images']) + ->filter(function ($image) use ($region, $version) { + return in_array($region, $image['regions']) && str_contains($image['name'], $version); + }) + ->where('distribution', 'Ubuntu') + ->where('status', 'available') + ->first(); + + return $image['id']; + } catch (Exception) { + throw new Exception('Could not find image ID'); + } + } } diff --git a/app/ServerProviders/Vultr.php b/app/ServerProviders/Vultr.php index 733857b..6fffb51 100644 --- a/app/ServerProviders/Vultr.php +++ b/app/ServerProviders/Vultr.php @@ -7,6 +7,7 @@ use App\Facades\Notifier; use App\Notifications\FailedToDeleteServerFromProvider; use Exception; +use Illuminate\Filesystem\FilesystemAdapter; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; @@ -15,23 +16,12 @@ class Vultr extends AbstractProvider { protected string $apiUrl = 'https://api.vultr.com/v2'; - public function createRules($input): array + public function createRules(array $input): array { - $rules = []; - // plans - $plans = []; - foreach (config('serverproviders.vultr.plans') as $plan) { - $plans[] = $plan['value']; - } - $rules['plan'] = 'required|in:'.implode(',', $plans); - // regions - $regions = []; - foreach (config('serverproviders.vultr.regions') as $region) { - $regions[] = $region['value']; - } - $rules['region'] = 'required|in:'.implode(',', $regions); - - return $rules; + return [ + 'plan' => 'required', + 'region' => 'required', + ]; } public function credentialValidationRules($input): array @@ -61,7 +51,12 @@ public function data(array $input): array */ public function connect(?array $credentials = null): bool { - $connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account'); + try { + $connect = Http::withToken($credentials['token'])->get($this->apiUrl.'/account'); + } catch (Exception) { + throw new CouldNotConnectToProvider('Vultr'); + } + if (! $connect->ok()) { throw new CouldNotConnectToProvider('Vultr'); } @@ -71,16 +66,43 @@ public function connect(?array $credentials = null): bool public function plans(?string $region): array { - return collect(config('serverproviders.vultr.plans')) - ->mapWithKeys(fn ($value) => [$value['value'] => $value['title']]) - ->toArray(); + try { + $plans = Http::withToken($this->serverProvider->credentials['token']) + ->get($this->apiUrl.'/plans', ['per_page' => 500]) + ->json(); + + return collect($plans['plans'])->filter(function ($plan) use ($region) { + return in_array($region, $plan['locations']); + }) + ->mapWithKeys(function ($value) { + return [ + $value['id'] => __('server_providers.plan', [ + 'name' => $value['type'], + 'cpu' => $value['vcpu_count'], + 'memory' => $value['ram'], + 'disk' => $value['disk'], + ]), + ]; + }) + ->toArray(); + } catch (Exception) { + return []; + } } public function regions(): array { - return collect(config('serverproviders.vultr.regions')) - ->mapWithKeys(fn ($value) => [$value['value'] => $value['title']]) - ->toArray(); + try { + $regions = Http::withToken($this->serverProvider->credentials['token']) + ->get($this->apiUrl.'/regions', ['per_page' => 500]) + ->json(); + + return collect($regions['regions']) + ->mapWithKeys(fn ($value) => [$value['id'] => $value['country'].' - '.$value['city']]) + ->toArray(); + } catch (Exception) { + return []; + } } /** @@ -89,32 +111,41 @@ public function regions(): array public function create(): void { // generate key pair - /** @var \Illuminate\Filesystem\FilesystemAdapter $storageDisk */ + /** @var FilesystemAdapter $storageDisk */ $storageDisk = Storage::disk(config('core.key_pairs_disk')); generate_key_pair($storageDisk->path((string) $this->server->id)); - $createSshKey = Http::withToken($this->server->serverProvider->credentials['token']) - ->post($this->apiUrl.'/ssh-keys', [ - 'ssh_key' => $this->server->sshKey()['public_key'], - 'name' => $this->server->name.'_'.$this->server->id, - ]); + try { + $createSshKey = Http::withToken($this->server->serverProvider->credentials['token']) + ->post($this->apiUrl.'/ssh-keys', [ + 'ssh_key' => $this->server->sshKey()['public_key'], + 'name' => $this->server->name.'_'.$this->server->id, + ]); + } catch (Exception) { + throw new ServerProviderError('Error creating SSH Key on Vultr'); + } + if ($createSshKey->status() != 201) { throw new ServerProviderError('Error creating SSH Key on Vultr'); } - $create = Http::withToken($this->server->serverProvider->credentials['token']) - ->post($this->apiUrl.'/instances', [ - 'label' => $this->server->name, - 'region' => $this->server->provider_data['region'], - 'plan' => $this->server->provider_data['plan'], - 'os_id' => config('serverproviders.vultr.images')[$this->server->os], - 'enable_ipv6' => false, - 'sshkey_id' => [$createSshKey->json()['ssh_key']['id']], - ]); + try { + $create = Http::withToken($this->server->serverProvider->credentials['token']) + ->post($this->apiUrl.'/instances', [ + 'label' => $this->server->name, + 'region' => $this->server->provider_data['region'], + 'plan' => $this->server->provider_data['plan'], + 'os_id' => $this->getImageId($this->server->os), + 'enable_ipv6' => false, + 'sshkey_id' => [$createSshKey->json()['ssh_key']['id']], + ]); + } catch (Exception) { + throw new ServerProviderError('Failed to create server on Vultr'); + } + if ($create->status() != 202) { - $msg = __('Failed to create server on Vultr'); Log::error('Failed to create server on Vultr', $create->json()); - throw new ServerProviderError($msg); + throw new ServerProviderError('Failed: '.$create->body()); } $providerData = $this->server->provider_data; $providerData['instance_id'] = $create->json()['instance']['id']; @@ -124,8 +155,12 @@ public function create(): void public function isRunning(): bool { - $status = Http::withToken($this->server->serverProvider->credentials['token']) - ->get($this->apiUrl.'/instances/'.$this->server->provider_data['instance_id']); + try { + $status = Http::withToken($this->server->serverProvider->credentials['token']) + ->get($this->apiUrl.'/instances/'.$this->server->provider_data['instance_id']); + } catch (Exception) { + return false; + } if (! $status->ok()) { return false; @@ -145,12 +180,44 @@ public function isRunning(): bool public function delete(): void { if (isset($this->server->provider_data['instance_id'])) { - $delete = Http::withToken($this->server->serverProvider->credentials['token']) - ->delete($this->apiUrl.'/instances/'.$this->server->provider_data['instance_id']); + try { + $delete = Http::withToken($this->server->serverProvider->credentials['token']) + ->delete($this->apiUrl.'/instances/'.$this->server->provider_data['instance_id']); + } catch (Exception) { + Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server)); + + return; + } if (! $delete->ok()) { Notifier::send($this->server, new FailedToDeleteServerFromProvider($this->server)); } } } + + /** + * @throws Exception + */ + private function getImageId(string $os): int + { + $version = config('core.operating_system_versions.'.$os); + + try { + $result = Http::withToken($this->serverProvider->credentials['token']) + ->get($this->apiUrl.'/os', ['per_page' => 500]) + ->json(); + + $image = collect($result['os']) + ->filter(function ($os) use ($version) { + return str_contains($os['name'], $version); + }) + ->where('family', 'ubuntu') + ->where('arch', 'x64') + ->first(); + + return $image['id']; + } catch (Exception) { + throw new Exception('Could not find image ID'); + } + } }