123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- 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
- }
- }
|