package cloud import ( "context" "errors" "fmt" "time" "github.com/ovh/go-ovh/ovh" ) // OVHProvider implements the Provider interface for OVH Cloud type OVHProvider struct { client *ovh.Client projectID string region string configured bool } // NewOVHProvider creates a new OVH provider func NewOVHProvider() Provider { return &OVHProvider{} } func init() { RegisterProvider("ovh", NewOVHProvider) } // Initialize sets up the OVH provider with credentials and configuration func (p *OVHProvider) Initialize(config map[string]string) error { // Check required configuration requiredKeys := []string{"application_key", "application_secret", "consumer_key", "project_id"} for _, key := range requiredKeys { if _, ok := config[key]; !ok { return fmt.Errorf("missing required configuration key: %s", key) } } // Create OVH client client, err := ovh.NewClient( "ovh-eu", // Endpoint (can be configurable) config["application_key"], config["application_secret"], config["consumer_key"], ) if err != nil { return fmt.Errorf("failed to create OVH client: %w", err) } p.client = client p.projectID = config["project_id"] // Set default region if provided if region, ok := config["region"]; ok { p.region = region } p.configured = true return nil } // Validate checks if the OVH provider credentials are valid func (p *OVHProvider) Validate(ctx context.Context) (bool, error) { if !p.configured { return false, errors.New("provider not configured") } // Try to get project info to verify credentials path := fmt.Sprintf("/cloud/project/%s", p.projectID) var project map[string]interface{} err := p.client.Get(path, &project) if err != nil { return false, fmt.Errorf("validation failed: %w", err) } return true, nil } // ListRegions lists all available OVH regions func (p *OVHProvider) ListRegions(ctx context.Context) ([]Region, error) { if !p.configured { return nil, errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/region", p.projectID) var regionIDs []string err := p.client.Get(path, ®ionIDs) if err != nil { return nil, fmt.Errorf("failed to list regions: %w", err) } regions := make([]Region, 0, len(regionIDs)) for _, id := range regionIDs { var regionDetails struct { Name string `json:"name"` ContinentCode string `json:"continentCode"` Status string `json:"status"` } regionPath := fmt.Sprintf("/cloud/project/%s/region/%s", p.projectID, id) err := p.client.Get(regionPath, ®ionDetails) if err != nil { continue // Skip this region if we can't get details } regions = append(regions, Region{ ID: id, Name: regionDetails.Name, Zone: regionDetails.ContinentCode, }) } return regions, nil } // ListInstanceSizes lists available VM sizes (flavors) in OVH func (p *OVHProvider) ListInstanceSizes(ctx context.Context, region string) ([]InstanceSize, error) { if !p.configured { return nil, errors.New("provider not configured") } if region == "" { region = p.region } if region == "" { return nil, errors.New("region must be specified") } path := fmt.Sprintf("/cloud/project/%s/flavor?region=%s", p.projectID, region) var flavors []struct { ID string `json:"id"` Name string `json:"name"` Vcpus int `json:"vcpus"` RAM int `json:"ram"` // in MB Disk int `json:"disk"` // in GB Type string `json:"type"` HourlyPrice float64 `json:"hourlyPrice"` } err := p.client.Get(path, &flavors) if err != nil { return nil, fmt.Errorf("failed to list flavors: %w", err) } sizes := make([]InstanceSize, 0, len(flavors)) for _, flavor := range flavors { sizes = append(sizes, InstanceSize{ ID: flavor.ID, Name: flavor.Name, CPUCores: flavor.Vcpus, MemoryGB: flavor.RAM / 1024, // Convert MB to GB DiskGB: flavor.Disk, Price: flavor.HourlyPrice, }) } return sizes, nil } // ListInstances lists all instances in OVH func (p *OVHProvider) ListInstances(ctx context.Context) ([]Instance, error) { if !p.configured { return nil, errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/instance", p.projectID) var ovhInstances []struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` Created time.Time `json:"created"` Region string `json:"region"` FlavorID string `json:"flavorId"` ImageID string `json:"imageId"` IPAddresses []struct { IP string `json:"ip"` Type string `json:"type"` // public or private Version int `json:"version"` // 4 or 6 } `json:"ipAddresses"` } err := p.client.Get(path, &ovhInstances) if err != nil { return nil, fmt.Errorf("failed to list instances: %w", err) } instances := make([]Instance, 0, len(ovhInstances)) for _, ovhInstance := range ovhInstances { instance := Instance{ ID: ovhInstance.ID, Name: ovhInstance.Name, Region: ovhInstance.Region, Size: ovhInstance.FlavorID, ImageID: ovhInstance.ImageID, Status: mapOVHStatus(ovhInstance.Status), CreatedAt: ovhInstance.Created, } // Extract IP addresses for _, ip := range ovhInstance.IPAddresses { if ip.Version == 4 { // Only use IPv4 for now if ip.Type == "public" { instance.IPAddress = ip.IP } else { instance.PrivateIP = ip.IP } } } instances = append(instances, instance) } return instances, nil } // GetInstance gets a specific instance by ID func (p *OVHProvider) GetInstance(ctx context.Context, id string) (*Instance, error) { if !p.configured { return nil, errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/instance/%s", p.projectID, id) var ovhInstance struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` Created time.Time `json:"created"` Region string `json:"region"` FlavorID string `json:"flavorId"` ImageID string `json:"imageId"` IPAddresses []struct { IP string `json:"ip"` Type string `json:"type"` // public or private Version int `json:"version"` // 4 or 6 } `json:"ipAddresses"` } err := p.client.Get(path, &ovhInstance) if err != nil { return nil, fmt.Errorf("failed to get instance: %w", err) } instance := &Instance{ ID: ovhInstance.ID, Name: ovhInstance.Name, Region: ovhInstance.Region, Size: ovhInstance.FlavorID, ImageID: ovhInstance.ImageID, Status: mapOVHStatus(ovhInstance.Status), CreatedAt: ovhInstance.Created, } // Extract IP addresses for _, ip := range ovhInstance.IPAddresses { if ip.Version == 4 { // Only use IPv4 for now if ip.Type == "public" { instance.IPAddress = ip.IP } else { instance.PrivateIP = ip.IP } } } return instance, nil } // CreateInstance creates a new instance in OVH func (p *OVHProvider) CreateInstance(ctx context.Context, opts InstanceCreateOpts) (*Instance, error) { if !p.configured { return nil, errors.New("provider not configured") } // Prepare create request path := fmt.Sprintf("/cloud/project/%s/instance", p.projectID) request := struct { Name string `json:"name"` Region string `json:"region"` FlavorID string `json:"flavorId"` ImageID string `json:"imageId"` SSHKeyID []string `json:"sshKeyId,omitempty"` UserData string `json:"userData,omitempty"` Networks []string `json:"networks,omitempty"` }{ Name: opts.Name, Region: opts.Region, FlavorID: opts.Size, ImageID: opts.ImageID, SSHKeyID: opts.SSHKeyIDs, UserData: opts.UserData, } var result struct { ID string `json:"id"` Status string `json:"status"` Created string `json:"created"` } err := p.client.Post(path, request, &result) if err != nil { return nil, fmt.Errorf("failed to create instance: %w", err) } // Fetch the full instance details createdInstance, err := p.GetInstance(ctx, result.ID) if err != nil { return nil, fmt.Errorf("instance created but failed to retrieve details: %w", err) } return createdInstance, nil } // DeleteInstance deletes an instance in OVH func (p *OVHProvider) DeleteInstance(ctx context.Context, id string) error { if !p.configured { return errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/instance/%s", p.projectID, id) err := p.client.Delete(path, nil) if err != nil { return fmt.Errorf("failed to delete instance: %w", err) } return nil } // StartInstance starts an instance in OVH func (p *OVHProvider) StartInstance(ctx context.Context, id string) error { if !p.configured { return errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/instance/%s/start", p.projectID, id) err := p.client.Post(path, nil, nil) if err != nil { return fmt.Errorf("failed to start instance: %w", err) } return nil } // StopInstance stops an instance in OVH func (p *OVHProvider) StopInstance(ctx context.Context, id string) error { if !p.configured { return errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/instance/%s/stop", p.projectID, id) err := p.client.Post(path, nil, nil) if err != nil { return fmt.Errorf("failed to stop instance: %w", err) } return nil } // RestartInstance restarts an instance in OVH func (p *OVHProvider) RestartInstance(ctx context.Context, id string) error { if !p.configured { return errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/instance/%s/reboot", p.projectID, id) err := p.client.Post(path, nil, nil) if err != nil { return fmt.Errorf("failed to restart instance: %w", err) } return nil } // ListImages lists available OS images in OVH func (p *OVHProvider) ListImages(ctx context.Context) ([]Image, error) { if !p.configured { return nil, errors.New("provider not configured") } // Get all images path := fmt.Sprintf("/cloud/project/%s/image", p.projectID) var ovhImages []struct { ID string `json:"id"` Name string `json:"name"` Region string `json:"region"` Visibility string `json:"visibility"` Type string `json:"type"` Status string `json:"status"` CreationDate time.Time `json:"creationDate"` MinDisk int `json:"minDisk"` Size int `json:"size"` } err := p.client.Get(path, &ovhImages) if err != nil { return nil, fmt.Errorf("failed to list images: %w", err) } images := make([]Image, 0, len(ovhImages)) for _, ovhImage := range ovhImages { images = append(images, Image{ ID: ovhImage.ID, Name: ovhImage.Name, Description: ovhImage.Type, Type: ovhImage.Type, Status: ovhImage.Status, CreatedAt: ovhImage.CreationDate, MinDiskGB: ovhImage.MinDisk, SizeGB: ovhImage.Size / (1024 * 1024 * 1024), // Convert bytes to GB }) } return images, nil } // ListSSHKeys lists SSH keys in OVH func (p *OVHProvider) ListSSHKeys(ctx context.Context) ([]SSHKey, error) { if !p.configured { return nil, errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/sshkey", p.projectID) var ovhKeys []struct { ID string `json:"id"` Name string `json:"name"` PublicKey string `json:"publicKey"` Fingerprint string `json:"fingerprint"` CreatedAt time.Time `json:"creationDate"` } err := p.client.Get(path, &ovhKeys) if err != nil { return nil, fmt.Errorf("failed to list SSH keys: %w", err) } keys := make([]SSHKey, 0, len(ovhKeys)) for _, ovhKey := range ovhKeys { keys = append(keys, SSHKey{ ID: ovhKey.ID, Name: ovhKey.Name, PublicKey: ovhKey.PublicKey, Fingerprint: ovhKey.Fingerprint, CreatedAt: ovhKey.CreatedAt, }) } return keys, nil } // CreateSSHKey creates a new SSH key in OVH func (p *OVHProvider) CreateSSHKey(ctx context.Context, name, publicKey string) (*SSHKey, error) { if !p.configured { return nil, errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/sshkey", p.projectID) request := struct { Name string `json:"name"` PublicKey string `json:"publicKey"` Region string `json:"region,omitempty"` }{ Name: name, PublicKey: publicKey, Region: p.region, // Optional region } var result struct { ID string `json:"id"` Name string `json:"name"` PublicKey string `json:"publicKey"` Fingerprint string `json:"fingerprint"` CreatedAt time.Time `json:"creationDate"` } err := p.client.Post(path, request, &result) if err != nil { return nil, fmt.Errorf("failed to create SSH key: %w", err) } return &SSHKey{ ID: result.ID, Name: result.Name, PublicKey: result.PublicKey, Fingerprint: result.Fingerprint, CreatedAt: result.CreatedAt, }, nil } // DeleteSSHKey deletes an SSH key in OVH func (p *OVHProvider) DeleteSSHKey(ctx context.Context, id string) error { if !p.configured { return errors.New("provider not configured") } path := fmt.Sprintf("/cloud/project/%s/sshkey/%s", p.projectID, id) err := p.client.Delete(path, nil) if err != nil { return fmt.Errorf("failed to delete SSH key: %w", err) } return nil } // GetInstanceStatus gets the current status of an instance in OVH func (p *OVHProvider) GetInstanceStatus(ctx context.Context, id string) (string, error) { instance, err := p.GetInstance(ctx, id) if err != nil { return "", err } return instance.Status, nil } // WaitForInstanceStatus waits for an instance to reach a specific status func (p *OVHProvider) WaitForInstanceStatus(ctx context.Context, id, status string, timeout time.Duration) error { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { currentStatus, err := p.GetInstanceStatus(ctx, id) if err != nil { return err } if currentStatus == status { return nil } select { case <-ctx.Done(): return ctx.Err() case <-time.After(5 * time.Second): // Wait 5 seconds before next check } } return fmt.Errorf("timeout waiting for instance %s to reach status %s", id, status) } // mapOVHStatus maps OVH instance status to standardized status func mapOVHStatus(ovhStatus string) string { switch ovhStatus { case "ACTIVE": return "Running" case "BUILD": return "Creating" case "BUILDING": return "Creating" case "SHUTOFF": return "Stopped" case "DELETED": return "Terminated" case "SOFT_DELETED": return "Terminated" case "HARD_REBOOT": return "Restarting" case "REBOOT": return "Restarting" case "RESCUE": return "Running" case "ERROR": return "Error" case "PAUSED": return "Stopped" case "SUSPENDED": return "Stopped" case "STOPPING": return "Stopping" default: return ovhStatus } }