ovh.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. package cloud
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "time"
  7. "github.com/ovh/go-ovh/ovh"
  8. )
  9. // OVHProvider implements the Provider interface for OVH Cloud
  10. type OVHProvider struct {
  11. client *ovh.Client
  12. projectID string
  13. region string
  14. configured bool
  15. }
  16. // NewOVHProvider creates a new OVH provider
  17. func NewOVHProvider() Provider {
  18. return &OVHProvider{}
  19. }
  20. func init() {
  21. RegisterProvider("ovh", NewOVHProvider)
  22. }
  23. // Initialize sets up the OVH provider with credentials and configuration
  24. func (p *OVHProvider) Initialize(config map[string]string) error {
  25. // Check required configuration
  26. requiredKeys := []string{"application_key", "application_secret", "consumer_key", "project_id"}
  27. for _, key := range requiredKeys {
  28. if _, ok := config[key]; !ok {
  29. return fmt.Errorf("missing required configuration key: %s", key)
  30. }
  31. }
  32. // Create OVH client
  33. client, err := ovh.NewClient(
  34. "ovh-eu", // Endpoint (can be configurable)
  35. config["application_key"],
  36. config["application_secret"],
  37. config["consumer_key"],
  38. )
  39. if err != nil {
  40. return fmt.Errorf("failed to create OVH client: %w", err)
  41. }
  42. p.client = client
  43. p.projectID = config["project_id"]
  44. // Set default region if provided
  45. if region, ok := config["region"]; ok {
  46. p.region = region
  47. }
  48. p.configured = true
  49. return nil
  50. }
  51. // Validate checks if the OVH provider credentials are valid
  52. func (p *OVHProvider) Validate(ctx context.Context) (bool, error) {
  53. if !p.configured {
  54. return false, errors.New("provider not configured")
  55. }
  56. // Try to get project info to verify credentials
  57. path := fmt.Sprintf("/cloud/project/%s", p.projectID)
  58. var project map[string]interface{}
  59. err := p.client.Get(path, &project)
  60. if err != nil {
  61. return false, fmt.Errorf("validation failed: %w", err)
  62. }
  63. return true, nil
  64. }
  65. // ListRegions lists all available OVH regions
  66. func (p *OVHProvider) ListRegions(ctx context.Context) ([]Region, error) {
  67. if !p.configured {
  68. return nil, errors.New("provider not configured")
  69. }
  70. path := fmt.Sprintf("/cloud/project/%s/region", p.projectID)
  71. var regionIDs []string
  72. err := p.client.Get(path, &regionIDs)
  73. if err != nil {
  74. return nil, fmt.Errorf("failed to list regions: %w", err)
  75. }
  76. regions := make([]Region, 0, len(regionIDs))
  77. for _, id := range regionIDs {
  78. var regionDetails struct {
  79. Name string `json:"name"`
  80. ContinentCode string `json:"continentCode"`
  81. Status string `json:"status"`
  82. }
  83. regionPath := fmt.Sprintf("/cloud/project/%s/region/%s", p.projectID, id)
  84. err := p.client.Get(regionPath, &regionDetails)
  85. if err != nil {
  86. continue // Skip this region if we can't get details
  87. }
  88. regions = append(regions, Region{
  89. ID: id,
  90. Name: regionDetails.Name,
  91. Zone: regionDetails.ContinentCode,
  92. })
  93. }
  94. return regions, nil
  95. }
  96. // ListInstanceSizes lists available VM sizes (flavors) in OVH
  97. func (p *OVHProvider) ListInstanceSizes(ctx context.Context, region string) ([]InstanceSize, error) {
  98. if !p.configured {
  99. return nil, errors.New("provider not configured")
  100. }
  101. if region == "" {
  102. region = p.region
  103. }
  104. if region == "" {
  105. return nil, errors.New("region must be specified")
  106. }
  107. path := fmt.Sprintf("/cloud/project/%s/flavor?region=%s", p.projectID, region)
  108. var flavors []struct {
  109. ID string `json:"id"`
  110. Name string `json:"name"`
  111. Vcpus int `json:"vcpus"`
  112. RAM int `json:"ram"` // in MB
  113. Disk int `json:"disk"` // in GB
  114. Type string `json:"type"`
  115. HourlyPrice float64 `json:"hourlyPrice"`
  116. }
  117. err := p.client.Get(path, &flavors)
  118. if err != nil {
  119. return nil, fmt.Errorf("failed to list flavors: %w", err)
  120. }
  121. sizes := make([]InstanceSize, 0, len(flavors))
  122. for _, flavor := range flavors {
  123. sizes = append(sizes, InstanceSize{
  124. ID: flavor.ID,
  125. Name: flavor.Name,
  126. CPUCores: flavor.Vcpus,
  127. MemoryGB: flavor.RAM / 1024, // Convert MB to GB
  128. DiskGB: flavor.Disk,
  129. Price: flavor.HourlyPrice,
  130. })
  131. }
  132. return sizes, nil
  133. }
  134. // ListInstances lists all instances in OVH
  135. func (p *OVHProvider) ListInstances(ctx context.Context) ([]Instance, error) {
  136. if !p.configured {
  137. return nil, errors.New("provider not configured")
  138. }
  139. path := fmt.Sprintf("/cloud/project/%s/instance", p.projectID)
  140. var ovhInstances []struct {
  141. ID string `json:"id"`
  142. Name string `json:"name"`
  143. Status string `json:"status"`
  144. Created time.Time `json:"created"`
  145. Region string `json:"region"`
  146. FlavorID string `json:"flavorId"`
  147. ImageID string `json:"imageId"`
  148. IPAddresses []struct {
  149. IP string `json:"ip"`
  150. Type string `json:"type"` // public or private
  151. Version int `json:"version"` // 4 or 6
  152. } `json:"ipAddresses"`
  153. }
  154. err := p.client.Get(path, &ovhInstances)
  155. if err != nil {
  156. return nil, fmt.Errorf("failed to list instances: %w", err)
  157. }
  158. instances := make([]Instance, 0, len(ovhInstances))
  159. for _, ovhInstance := range ovhInstances {
  160. instance := Instance{
  161. ID: ovhInstance.ID,
  162. Name: ovhInstance.Name,
  163. Region: ovhInstance.Region,
  164. Size: ovhInstance.FlavorID,
  165. ImageID: ovhInstance.ImageID,
  166. Status: mapOVHStatus(ovhInstance.Status),
  167. CreatedAt: ovhInstance.Created,
  168. }
  169. // Extract IP addresses
  170. for _, ip := range ovhInstance.IPAddresses {
  171. if ip.Version == 4 { // Only use IPv4 for now
  172. if ip.Type == "public" {
  173. instance.IPAddress = ip.IP
  174. } else {
  175. instance.PrivateIP = ip.IP
  176. }
  177. }
  178. }
  179. instances = append(instances, instance)
  180. }
  181. return instances, nil
  182. }
  183. // GetInstance gets a specific instance by ID
  184. func (p *OVHProvider) GetInstance(ctx context.Context, id string) (*Instance, error) {
  185. if !p.configured {
  186. return nil, errors.New("provider not configured")
  187. }
  188. path := fmt.Sprintf("/cloud/project/%s/instance/%s", p.projectID, id)
  189. var ovhInstance struct {
  190. ID string `json:"id"`
  191. Name string `json:"name"`
  192. Status string `json:"status"`
  193. Created time.Time `json:"created"`
  194. Region string `json:"region"`
  195. FlavorID string `json:"flavorId"`
  196. ImageID string `json:"imageId"`
  197. IPAddresses []struct {
  198. IP string `json:"ip"`
  199. Type string `json:"type"` // public or private
  200. Version int `json:"version"` // 4 or 6
  201. } `json:"ipAddresses"`
  202. }
  203. err := p.client.Get(path, &ovhInstance)
  204. if err != nil {
  205. return nil, fmt.Errorf("failed to get instance: %w", err)
  206. }
  207. instance := &Instance{
  208. ID: ovhInstance.ID,
  209. Name: ovhInstance.Name,
  210. Region: ovhInstance.Region,
  211. Size: ovhInstance.FlavorID,
  212. ImageID: ovhInstance.ImageID,
  213. Status: mapOVHStatus(ovhInstance.Status),
  214. CreatedAt: ovhInstance.Created,
  215. }
  216. // Extract IP addresses
  217. for _, ip := range ovhInstance.IPAddresses {
  218. if ip.Version == 4 { // Only use IPv4 for now
  219. if ip.Type == "public" {
  220. instance.IPAddress = ip.IP
  221. } else {
  222. instance.PrivateIP = ip.IP
  223. }
  224. }
  225. }
  226. return instance, nil
  227. }
  228. // CreateInstance creates a new instance in OVH
  229. func (p *OVHProvider) CreateInstance(ctx context.Context, opts InstanceCreateOpts) (*Instance, error) {
  230. if !p.configured {
  231. return nil, errors.New("provider not configured")
  232. }
  233. // Prepare create request
  234. path := fmt.Sprintf("/cloud/project/%s/instance", p.projectID)
  235. request := struct {
  236. Name string `json:"name"`
  237. Region string `json:"region"`
  238. FlavorID string `json:"flavorId"`
  239. ImageID string `json:"imageId"`
  240. SSHKeyID []string `json:"sshKeyId,omitempty"`
  241. UserData string `json:"userData,omitempty"`
  242. Networks []string `json:"networks,omitempty"`
  243. }{
  244. Name: opts.Name,
  245. Region: opts.Region,
  246. FlavorID: opts.Size,
  247. ImageID: opts.ImageID,
  248. SSHKeyID: opts.SSHKeyIDs,
  249. UserData: opts.UserData,
  250. }
  251. var result struct {
  252. ID string `json:"id"`
  253. Status string `json:"status"`
  254. Created string `json:"created"`
  255. }
  256. err := p.client.Post(path, request, &result)
  257. if err != nil {
  258. return nil, fmt.Errorf("failed to create instance: %w", err)
  259. }
  260. // Fetch the full instance details
  261. createdInstance, err := p.GetInstance(ctx, result.ID)
  262. if err != nil {
  263. return nil, fmt.Errorf("instance created but failed to retrieve details: %w", err)
  264. }
  265. return createdInstance, nil
  266. }
  267. // DeleteInstance deletes an instance in OVH
  268. func (p *OVHProvider) DeleteInstance(ctx context.Context, id string) error {
  269. if !p.configured {
  270. return errors.New("provider not configured")
  271. }
  272. path := fmt.Sprintf("/cloud/project/%s/instance/%s", p.projectID, id)
  273. err := p.client.Delete(path, nil)
  274. if err != nil {
  275. return fmt.Errorf("failed to delete instance: %w", err)
  276. }
  277. return nil
  278. }
  279. // StartInstance starts an instance in OVH
  280. func (p *OVHProvider) StartInstance(ctx context.Context, id string) error {
  281. if !p.configured {
  282. return errors.New("provider not configured")
  283. }
  284. path := fmt.Sprintf("/cloud/project/%s/instance/%s/start", p.projectID, id)
  285. err := p.client.Post(path, nil, nil)
  286. if err != nil {
  287. return fmt.Errorf("failed to start instance: %w", err)
  288. }
  289. return nil
  290. }
  291. // StopInstance stops an instance in OVH
  292. func (p *OVHProvider) StopInstance(ctx context.Context, id string) error {
  293. if !p.configured {
  294. return errors.New("provider not configured")
  295. }
  296. path := fmt.Sprintf("/cloud/project/%s/instance/%s/stop", p.projectID, id)
  297. err := p.client.Post(path, nil, nil)
  298. if err != nil {
  299. return fmt.Errorf("failed to stop instance: %w", err)
  300. }
  301. return nil
  302. }
  303. // RestartInstance restarts an instance in OVH
  304. func (p *OVHProvider) RestartInstance(ctx context.Context, id string) error {
  305. if !p.configured {
  306. return errors.New("provider not configured")
  307. }
  308. path := fmt.Sprintf("/cloud/project/%s/instance/%s/reboot", p.projectID, id)
  309. err := p.client.Post(path, nil, nil)
  310. if err != nil {
  311. return fmt.Errorf("failed to restart instance: %w", err)
  312. }
  313. return nil
  314. }
  315. // ListImages lists available OS images in OVH
  316. func (p *OVHProvider) ListImages(ctx context.Context) ([]Image, error) {
  317. if !p.configured {
  318. return nil, errors.New("provider not configured")
  319. }
  320. // Get all images
  321. path := fmt.Sprintf("/cloud/project/%s/image", p.projectID)
  322. var ovhImages []struct {
  323. ID string `json:"id"`
  324. Name string `json:"name"`
  325. Region string `json:"region"`
  326. Visibility string `json:"visibility"`
  327. Type string `json:"type"`
  328. Status string `json:"status"`
  329. CreationDate time.Time `json:"creationDate"`
  330. MinDisk int `json:"minDisk"`
  331. Size int `json:"size"`
  332. }
  333. err := p.client.Get(path, &ovhImages)
  334. if err != nil {
  335. return nil, fmt.Errorf("failed to list images: %w", err)
  336. }
  337. images := make([]Image, 0, len(ovhImages))
  338. for _, ovhImage := range ovhImages {
  339. images = append(images, Image{
  340. ID: ovhImage.ID,
  341. Name: ovhImage.Name,
  342. Description: ovhImage.Type,
  343. Type: ovhImage.Type,
  344. Status: ovhImage.Status,
  345. CreatedAt: ovhImage.CreationDate,
  346. MinDiskGB: ovhImage.MinDisk,
  347. SizeGB: ovhImage.Size / (1024 * 1024 * 1024), // Convert bytes to GB
  348. })
  349. }
  350. return images, nil
  351. }
  352. // ListSSHKeys lists SSH keys in OVH
  353. func (p *OVHProvider) ListSSHKeys(ctx context.Context) ([]SSHKey, error) {
  354. if !p.configured {
  355. return nil, errors.New("provider not configured")
  356. }
  357. path := fmt.Sprintf("/cloud/project/%s/sshkey", p.projectID)
  358. var ovhKeys []struct {
  359. ID string `json:"id"`
  360. Name string `json:"name"`
  361. PublicKey string `json:"publicKey"`
  362. Fingerprint string `json:"fingerprint"`
  363. CreatedAt time.Time `json:"creationDate"`
  364. }
  365. err := p.client.Get(path, &ovhKeys)
  366. if err != nil {
  367. return nil, fmt.Errorf("failed to list SSH keys: %w", err)
  368. }
  369. keys := make([]SSHKey, 0, len(ovhKeys))
  370. for _, ovhKey := range ovhKeys {
  371. keys = append(keys, SSHKey{
  372. ID: ovhKey.ID,
  373. Name: ovhKey.Name,
  374. PublicKey: ovhKey.PublicKey,
  375. Fingerprint: ovhKey.Fingerprint,
  376. CreatedAt: ovhKey.CreatedAt,
  377. })
  378. }
  379. return keys, nil
  380. }
  381. // CreateSSHKey creates a new SSH key in OVH
  382. func (p *OVHProvider) CreateSSHKey(ctx context.Context, name, publicKey string) (*SSHKey, error) {
  383. if !p.configured {
  384. return nil, errors.New("provider not configured")
  385. }
  386. path := fmt.Sprintf("/cloud/project/%s/sshkey", p.projectID)
  387. request := struct {
  388. Name string `json:"name"`
  389. PublicKey string `json:"publicKey"`
  390. Region string `json:"region,omitempty"`
  391. }{
  392. Name: name,
  393. PublicKey: publicKey,
  394. Region: p.region, // Optional region
  395. }
  396. var result struct {
  397. ID string `json:"id"`
  398. Name string `json:"name"`
  399. PublicKey string `json:"publicKey"`
  400. Fingerprint string `json:"fingerprint"`
  401. CreatedAt time.Time `json:"creationDate"`
  402. }
  403. err := p.client.Post(path, request, &result)
  404. if err != nil {
  405. return nil, fmt.Errorf("failed to create SSH key: %w", err)
  406. }
  407. return &SSHKey{
  408. ID: result.ID,
  409. Name: result.Name,
  410. PublicKey: result.PublicKey,
  411. Fingerprint: result.Fingerprint,
  412. CreatedAt: result.CreatedAt,
  413. }, nil
  414. }
  415. // DeleteSSHKey deletes an SSH key in OVH
  416. func (p *OVHProvider) DeleteSSHKey(ctx context.Context, id string) error {
  417. if !p.configured {
  418. return errors.New("provider not configured")
  419. }
  420. path := fmt.Sprintf("/cloud/project/%s/sshkey/%s", p.projectID, id)
  421. err := p.client.Delete(path, nil)
  422. if err != nil {
  423. return fmt.Errorf("failed to delete SSH key: %w", err)
  424. }
  425. return nil
  426. }
  427. // GetInstanceStatus gets the current status of an instance in OVH
  428. func (p *OVHProvider) GetInstanceStatus(ctx context.Context, id string) (string, error) {
  429. instance, err := p.GetInstance(ctx, id)
  430. if err != nil {
  431. return "", err
  432. }
  433. return instance.Status, nil
  434. }
  435. // WaitForInstanceStatus waits for an instance to reach a specific status
  436. func (p *OVHProvider) WaitForInstanceStatus(ctx context.Context, id, status string, timeout time.Duration) error {
  437. deadline := time.Now().Add(timeout)
  438. for time.Now().Before(deadline) {
  439. currentStatus, err := p.GetInstanceStatus(ctx, id)
  440. if err != nil {
  441. return err
  442. }
  443. if currentStatus == status {
  444. return nil
  445. }
  446. select {
  447. case <-ctx.Done():
  448. return ctx.Err()
  449. case <-time.After(5 * time.Second):
  450. // Wait 5 seconds before next check
  451. }
  452. }
  453. return fmt.Errorf("timeout waiting for instance %s to reach status %s", id, status)
  454. }
  455. // mapOVHStatus maps OVH instance status to standardized status
  456. func mapOVHStatus(ovhStatus string) string {
  457. switch ovhStatus {
  458. case "ACTIVE":
  459. return "Running"
  460. case "BUILD":
  461. return "Creating"
  462. case "BUILDING":
  463. return "Creating"
  464. case "SHUTOFF":
  465. return "Stopped"
  466. case "DELETED":
  467. return "Terminated"
  468. case "SOFT_DELETED":
  469. return "Terminated"
  470. case "HARD_REBOOT":
  471. return "Restarting"
  472. case "REBOOT":
  473. return "Restarting"
  474. case "RESCUE":
  475. return "Running"
  476. case "ERROR":
  477. return "Error"
  478. case "PAUSED":
  479. return "Stopped"
  480. case "SUSPENDED":
  481. return "Stopped"
  482. case "STOPPING":
  483. return "Stopping"
  484. default:
  485. return ovhStatus
  486. }
  487. }