/** * Vultr VPS Provider Implementation * API Docs: https://www.vultr.com/api/ */ import type { VPSProviderConfig, CreateServerRequest, CreateServerResponse } from '../types'; import { VPSProviderBase, OS_IMAGE_MAP } from './vps-provider'; import { TIMEOUTS } from '../config'; interface VultrInstance { id: string; os: string; ram: number; disk: number; main_ip: string; vcpu_count: number; region: string; plan: string; date_created: string; status: string; power_status: string; server_status: string; v6_main_ip: string; v6_network: string; hostname: string; label: string; tag: string; } interface VultrCreateResponse { instance: VultrInstance; } interface VultrError { error: string; status: number; } export class VultrProvider extends VPSProviderBase { static readonly DEFAULT_BASE_URL = 'https://api.vultr.com/v2'; constructor(apiKey: string, baseUrl?: string, timeout: number = TIMEOUTS.VPS_PROVIDER_API_MS) { super({ apiKey, baseUrl: baseUrl || VultrProvider.DEFAULT_BASE_URL, timeout, }); } async createServer(request: CreateServerRequest): Promise { const url = `${this.config.baseUrl}/instances`; const body = { plan: request.plan, region: request.region, os_id: parseInt(request.osImage, 10), label: request.label || `server-${Date.now()}`, hostname: request.label || `server-${Date.now()}`, tag: request.tags?.[0] || '', sshkey_id: request.sshKeys || [], enable_ipv6: true, backups: 'disabled', ddos_protection: false, }; try { const response = await this.fetchWithRetry(url, { method: 'POST', headers: { Authorization: `Bearer ${this.config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { const error = (await response.json()) as VultrError; return { success: false, error: { code: `VULTR_${response.status}`, message: error.error || 'Unknown error', }, }; } const data = (await response.json()) as VultrCreateResponse; const instance = data.instance; return { success: true, instanceId: instance.id, ipv4: instance.main_ip !== '0.0.0.0' ? instance.main_ip : undefined, ipv6: instance.v6_main_ip || undefined, status: instance.status, }; } catch (error) { return { success: false, error: { code: 'VULTR_NETWORK_ERROR', message: error instanceof Error ? error.message : 'Network error', }, }; } } async deleteServer(instanceId: string): Promise<{ success: boolean; error?: string }> { const url = `${this.config.baseUrl}/instances/${instanceId}`; try { const response = await this.fetchWithRetry(url, { method: 'DELETE', headers: { Authorization: `Bearer ${this.config.apiKey}`, }, }); // Vultr returns 204 No Content on success if (response.status === 204 || response.ok) { return { success: true }; } const error = (await response.json()) as VultrError; return { success: false, error: error.error || 'Failed to delete instance', }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } async startServer(instanceId: string): Promise<{ success: boolean; error?: string }> { const url = `${this.config.baseUrl}/instances/${instanceId}/start`; try { const response = await this.fetchWithRetry(url, { method: 'POST', headers: { Authorization: `Bearer ${this.config.apiKey}`, }, }); // Vultr returns 204 No Content on success if (response.status === 204 || response.ok) { return { success: true }; } const error = (await response.json()) as VultrError; return { success: false, error: error.error || 'Failed to start instance', }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } async stopServer(instanceId: string): Promise<{ success: boolean; error?: string }> { const url = `${this.config.baseUrl}/instances/${instanceId}/halt`; try { const response = await this.fetchWithRetry(url, { method: 'POST', headers: { Authorization: `Bearer ${this.config.apiKey}`, }, }); // Vultr returns 204 No Content on success if (response.status === 204 || response.ok) { return { success: true }; } const error = (await response.json()) as VultrError; return { success: false, error: error.error || 'Failed to stop instance', }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } async rebootServer(instanceId: string): Promise<{ success: boolean; error?: string }> { const url = `${this.config.baseUrl}/instances/${instanceId}/reboot`; try { const response = await this.fetchWithRetry(url, { method: 'POST', headers: { Authorization: `Bearer ${this.config.apiKey}`, }, }); // Vultr returns 204 No Content on success if (response.status === 204 || response.ok) { return { success: true }; } const error = (await response.json()) as VultrError; return { success: false, error: error.error || 'Failed to reboot instance', }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Network error', }; } } async getServerStatus( instanceId: string ): Promise<{ status: string; power_status?: string; ipv4?: string; ipv6?: string }> { const url = `${this.config.baseUrl}/instances/${instanceId}`; try { const response = await this.fetchWithRetry(url, { method: 'GET', headers: { Authorization: `Bearer ${this.config.apiKey}`, }, }); if (!response.ok) { return { status: 'unknown' }; } const data = (await response.json()) as { instance: VultrInstance }; return { status: data.instance.status, power_status: data.instance.power_status, ipv4: data.instance.main_ip !== '0.0.0.0' ? data.instance.main_ip : undefined, ipv6: data.instance.v6_main_ip || undefined, }; } catch { return { status: 'unknown' }; } } getOsImageId(osImage: string): string { return OS_IMAGE_MAP.vultr[osImage as keyof typeof OS_IMAGE_MAP.vultr] || '2284'; // Default Ubuntu 22.04 } /** * Wait for server to be ready (IP assigned) * Vultr servers may take a minute to get IP addresses */ async waitForReady( instanceId: string, maxWaitMs: number = 120000 ): Promise<{ ready: boolean; ipv4?: string; ipv6?: string }> { const startTime = Date.now(); const pollInterval = 5000; while (Date.now() - startTime < maxWaitMs) { const status = await this.getServerStatus(instanceId); if (status.status === 'active' && status.ipv4) { return { ready: true, ipv4: status.ipv4, ipv6: status.ipv6 }; } await this.sleep(pollInterval); } return { ready: false }; } }