package providers import ( "context" "fmt" "net" "net/http" "time" "github.com/cloudscale-ch/cloudscale-go-sdk/v6" ) // CloudscaleProvider implements CloudProvider for Cloudscale.ch type CloudscaleProvider struct { client *cloudscale.Client } // NewCloudscaleProvider creates a new Cloudscale provider func NewCloudscaleProvider(token string) (*CloudscaleProvider, error) { if token == "" { return nil, fmt.Errorf("cloudscale API token required") } client := cloudscale.NewClient(http.DefaultClient) client.AuthToken = token return &CloudscaleProvider{client: client}, nil } func (p *CloudscaleProvider) Name() string { return "cloudscale" } func (p *CloudscaleProvider) CreateVM(ctx context.Context, opts VMOptions) (*VM, error) { zone := opts.Zone if zone == "" { zone = "lpg1" // Default: Lupfig } image := opts.Image if image == "" { image = "ubuntu-24.04" } flavor := opts.Flavor if flavor == "" { flavor = "flex-4-2" // 4 vCPU, 2GB RAM } req := &cloudscale.ServerRequest{ Name: opts.Name, Flavor: flavor, Image: image, Zone: zone, SSHKeys: []string{opts.SSHPublicKey}, VolumeSizeGB: int(opts.DiskSizeGB), } if opts.UserData != "" { req.UserData = opts.UserData } server, err := p.client.Servers.Create(ctx, req) if err != nil { return nil, fmt.Errorf("failed to create server: %w", err) } // Wait for server to be running for i := 0; i < 60; i++ { server, err = p.client.Servers.Get(ctx, server.UUID) if err != nil { return nil, fmt.Errorf("failed to get server status: %w", err) } if server.Status == "running" { break } time.Sleep(5 * time.Second) } // Get public IP var publicIP string for _, iface := range server.Interfaces { if iface.Type == "public" { for _, addr := range iface.Addresses { if addr.Version == 4 { publicIP = addr.Address break } } } } return &VM{ ID: server.UUID, Name: server.Name, PublicIP: publicIP, Status: server.Status, Provider: "cloudscale", Zone: server.Zone.Slug, CreatedAt: server.CreatedAt, }, nil } func (p *CloudscaleProvider) DeleteVM(ctx context.Context, vmID string) error { return p.client.Servers.Delete(ctx, vmID) } func (p *CloudscaleProvider) GetVM(ctx context.Context, vmID string) (*VM, error) { server, err := p.client.Servers.Get(ctx, vmID) if err != nil { return nil, err } var publicIP string for _, iface := range server.Interfaces { if iface.Type == "public" { for _, addr := range iface.Addresses { if addr.Version == 4 { publicIP = addr.Address break } } } } return &VM{ ID: server.UUID, Name: server.Name, PublicIP: publicIP, Status: server.Status, Provider: "cloudscale", Zone: server.Zone.Slug, CreatedAt: server.CreatedAt, }, nil } func (p *CloudscaleProvider) WaitForSSH(ctx context.Context, ip string, port int, timeout time.Duration) error { if port == 0 { port = 22 } deadline := time.Now().Add(timeout) address := fmt.Sprintf("%s:%d", ip, port) for time.Now().Before(deadline) { conn, err := net.DialTimeout("tcp", address, 5*time.Second) if err == nil { conn.Close() return nil } select { case <-ctx.Done(): return ctx.Err() case <-time.After(5 * time.Second): } } return fmt.Errorf("SSH not available at %s after %v", address, timeout) } func (p *CloudscaleProvider) ListFlavors(ctx context.Context) ([]Flavor, error) { // Make raw API request to /v1/flavors req, err := p.client.NewRequest(ctx, "GET", "v1/flavors", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } var flavors []cloudscale.Flavor if err := p.client.Do(ctx, req, &flavors); err != nil { return nil, fmt.Errorf("failed to list flavors: %w", err) } var result []Flavor for _, f := range flavors { result = append(result, Flavor{ ID: f.Slug, Name: f.Name, CPUs: f.VCPUCount, Memory: f.MemoryGB * 1024, // Convert to MB }) } return result, nil } func (p *CloudscaleProvider) ListImages(ctx context.Context, filter string) ([]Image, error) { // Make raw API request to /v1/images req, err := p.client.NewRequest(ctx, "GET", "v1/images", nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } var images []cloudscale.Image if err := p.client.Do(ctx, req, &images); err != nil { return nil, fmt.Errorf("failed to list images: %w", err) } var result []Image for _, img := range images { if filter == "" || contains(img.Slug, filter) || contains(img.Name, filter) { result = append(result, Image{ ID: img.Slug, Name: img.Name, }) } } return result, nil } func (p *CloudscaleProvider) ListZones(ctx context.Context) ([]string, error) { regions, err := p.client.Regions.List(ctx) if err != nil { return nil, err } var zones []string for _, r := range regions { for _, z := range r.Zones { zones = append(zones, z.Slug) } } return zones, nil }