provider.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. package ovh
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "strconv"
  10. "strings"
  11. "sync"
  12. "time"
  13. coreenv "github.com/opencost/opencost/core/pkg/env"
  14. "github.com/opencost/opencost/core/pkg/log"
  15. "github.com/opencost/opencost/core/pkg/opencost"
  16. "github.com/opencost/opencost/core/pkg/util"
  17. coreJSON "github.com/opencost/opencost/core/pkg/util/json"
  18. "github.com/opencost/opencost/core/pkg/clustercache"
  19. "github.com/opencost/opencost/pkg/cloud/models"
  20. "github.com/opencost/opencost/pkg/cloud/utils"
  21. "github.com/opencost/opencost/pkg/env"
  22. )
  23. const (
  24. OVHCatalogPricing = "OVH Catalog Pricing"
  25. BillingLabel = "ovh.opencost.io/billing"
  26. NodepoolLabel = "nodepool"
  27. microcentsPerUnit = 100_000_000.0
  28. hoursPerMonth = 730.0
  29. )
  30. // GPU instance prefixes on OVH
  31. var gpuPrefixes = []string{"t2-", "l4-", "l40s-", "a10-", "a100-"}
  32. // OVH MKS regions — update periodically or use REGION_OVERRIDE_LIST env var.
  33. // Source: https://us.ovhcloud.com/public-cloud/regions-availability/
  34. var ovhRegions = []string{
  35. "BHS5", "DE1", "GRA5", "GRA7", "GRA9", "GRA11",
  36. "OR1", "SBG5", "SGP1", "SYD1", "UK1", "VA1", "WAW1",
  37. }
  38. // Storage class to OVH volume type mapping
  39. var storageClassToVolumeType = map[string]string{
  40. "csi-cinder-high-speed-gen2": "high-speed-gen2",
  41. "csi-cinder-high-speed": "high-speed",
  42. "csi-cinder-classic": "classic",
  43. }
  44. // OVH implements the models.Provider interface for OVHcloud.
  45. type OVH struct {
  46. Clientset clustercache.ClusterCache
  47. Config models.ProviderConfig
  48. Pricing map[string]*OVHFlavorPricing
  49. VolumePricing map[string]float64
  50. ClusterRegion string
  51. ClusterAccountID string
  52. DownloadLock sync.RWMutex
  53. catalogURL string
  54. monthlyNodepools []string
  55. }
  56. // OVHFlavorPricing holds pricing and specs for an OVH instance flavor.
  57. type OVHFlavorPricing struct {
  58. HourlyPrice float64
  59. MonthlyPrice float64 // monthly price converted to hourly (/730)
  60. PlanCode string
  61. VCPU int
  62. RAM int // GB
  63. Disk int // GB
  64. GPU int
  65. GPUName string
  66. }
  67. // Catalog JSON types
  68. type ovhCatalog struct {
  69. Plans []ovhPlan `json:"plans"`
  70. Addons []ovhAddon `json:"addons"`
  71. }
  72. type ovhPlan struct {
  73. PlanCode string `json:"planCode"`
  74. AddonFamilies []ovhAddonFamily `json:"addonFamilies"`
  75. }
  76. type ovhAddonFamily struct {
  77. Name string `json:"name"`
  78. Addons []string `json:"addons"`
  79. }
  80. type ovhAddon struct {
  81. PlanCode string `json:"planCode"`
  82. Product string `json:"product"`
  83. Pricings []ovhPricing `json:"pricings"`
  84. Blobs *ovhBlobs `json:"blobs"`
  85. }
  86. type ovhPricing struct {
  87. Price int64 `json:"price"`
  88. Type string `json:"type"`
  89. }
  90. type ovhBlobs struct {
  91. Technical *ovhTechnical `json:"technical"`
  92. Commercial *ovhCommercial `json:"commercial"`
  93. }
  94. type ovhTechnical struct {
  95. CPU *ovhCPU `json:"cpu"`
  96. Memory *ovhMemory `json:"memory"`
  97. Storage *ovhStorage `json:"storage"`
  98. GPU *ovhGPU `json:"gpu"`
  99. Name string `json:"name"`
  100. }
  101. type ovhCommercial struct {
  102. BrickSubtype string `json:"brickSubtype"`
  103. }
  104. type ovhCPU struct {
  105. Cores float64 `json:"cores"`
  106. }
  107. type ovhMemory struct {
  108. Size float64 `json:"size"`
  109. }
  110. type ovhStorage struct {
  111. Disks []ovhDisk `json:"disks"`
  112. }
  113. type ovhDisk struct {
  114. Capacity float64 `json:"capacity"`
  115. }
  116. type ovhGPU struct {
  117. Number int `json:"number"`
  118. Model string `json:"model"`
  119. }
  120. // parseCatalog extracts instance and volume pricing from the OVH public cloud catalog.
  121. func parseCatalog(data []byte) (map[string]*OVHFlavorPricing, map[string]float64, error) {
  122. var catalog ovhCatalog
  123. if err := json.Unmarshal(data, &catalog); err != nil {
  124. return nil, nil, fmt.Errorf("failed to unmarshal OVH catalog: %w", err)
  125. }
  126. // Find the project.2018 plan and collect addon planCodes
  127. instanceAddons := make(map[string]bool)
  128. volumeAddons := make(map[string]bool)
  129. var projectPlan *ovhPlan
  130. for i := range catalog.Plans {
  131. if catalog.Plans[i].PlanCode == "project.2018" {
  132. projectPlan = &catalog.Plans[i]
  133. break
  134. }
  135. }
  136. if projectPlan == nil {
  137. return nil, nil, fmt.Errorf("project.2018 plan not found in OVH catalog")
  138. }
  139. for _, family := range projectPlan.AddonFamilies {
  140. switch family.Name {
  141. case "instance":
  142. for _, a := range family.Addons {
  143. instanceAddons[a] = true
  144. }
  145. case "volume":
  146. for _, a := range family.Addons {
  147. volumeAddons[a] = true
  148. }
  149. }
  150. }
  151. pricing := make(map[string]*OVHFlavorPricing)
  152. volumePricing := make(map[string]float64)
  153. for _, addon := range catalog.Addons {
  154. if instanceAddons[addon.PlanCode] {
  155. parseInstanceAddon(addon, pricing)
  156. } else if volumeAddons[addon.PlanCode] {
  157. parseVolumeAddon(addon, volumePricing)
  158. }
  159. }
  160. return pricing, volumePricing, nil
  161. }
  162. // parseInstanceAddon extracts flavor pricing from an instance addon entry.
  163. func parseInstanceAddon(addon ovhAddon, pricing map[string]*OVHFlavorPricing) {
  164. planCode := addon.PlanCode
  165. isMonthly := strings.Contains(planCode, ".monthly.")
  166. // Extract flavor name: strip .consumption or .monthly.postpaid suffix
  167. flavorName := planCode
  168. if idx := strings.Index(planCode, ".consumption"); idx > 0 {
  169. flavorName = planCode[:idx]
  170. } else if idx := strings.Index(planCode, ".monthly."); idx > 0 {
  171. flavorName = planCode[:idx]
  172. }
  173. if len(addon.Pricings) == 0 {
  174. return
  175. }
  176. // Select pricing entry by type: "consumption" for hourly, "monthly.postpaid" for monthly
  177. targetType := "consumption"
  178. if isMonthly {
  179. targetType = "monthly.postpaid"
  180. }
  181. var rawPrice float64
  182. matched := false
  183. for _, p := range addon.Pricings {
  184. if p.Type == targetType {
  185. rawPrice = float64(p.Price) / microcentsPerUnit
  186. matched = true
  187. break
  188. }
  189. }
  190. if !matched {
  191. rawPrice = float64(addon.Pricings[0].Price) / microcentsPerUnit
  192. }
  193. entry, exists := pricing[flavorName]
  194. if !exists {
  195. entry = &OVHFlavorPricing{PlanCode: planCode}
  196. pricing[flavorName] = entry
  197. }
  198. if isMonthly {
  199. entry.MonthlyPrice = rawPrice / hoursPerMonth
  200. } else {
  201. entry.HourlyPrice = rawPrice
  202. // Extract specs from blobs
  203. if addon.Blobs != nil && addon.Blobs.Technical != nil {
  204. tech := addon.Blobs.Technical
  205. if tech.CPU != nil {
  206. entry.VCPU = int(tech.CPU.Cores)
  207. }
  208. if tech.Memory != nil {
  209. entry.RAM = int(tech.Memory.Size)
  210. }
  211. if tech.Storage != nil && len(tech.Storage.Disks) > 0 {
  212. entry.Disk = int(tech.Storage.Disks[0].Capacity)
  213. }
  214. if tech.GPU != nil {
  215. entry.GPU = tech.GPU.Number
  216. entry.GPUName = tech.GPU.Model
  217. }
  218. }
  219. }
  220. }
  221. // parseVolumeAddon extracts volume pricing from a volume addon entry.
  222. func parseVolumeAddon(addon ovhAddon, volumePricing map[string]float64) {
  223. planCode := addon.PlanCode
  224. // Extract volume type: volume.high-speed-gen2.consumption -> high-speed-gen2
  225. parts := strings.SplitN(planCode, ".", 3)
  226. if len(parts) < 3 {
  227. return
  228. }
  229. volumeType := parts[1]
  230. if len(addon.Pricings) == 0 {
  231. return
  232. }
  233. // Only use consumption (hourly) pricing
  234. for _, p := range addon.Pricings {
  235. if p.Type == "consumption" {
  236. volumePricing[volumeType] = float64(p.Price) / microcentsPerUnit
  237. return
  238. }
  239. }
  240. volumePricing[volumeType] = float64(addon.Pricings[0].Price) / microcentsPerUnit
  241. }
  242. // ovhKey implements models.Key for OVH nodes.
  243. type ovhKey struct {
  244. Labels map[string]string
  245. }
  246. func (k *ovhKey) Features() string {
  247. region, _ := util.GetRegion(k.Labels)
  248. instanceType, _ := util.GetInstanceType(k.Labels)
  249. return region + "," + instanceType
  250. }
  251. func (k *ovhKey) GPUType() string {
  252. instanceType, _ := util.GetInstanceType(k.Labels)
  253. for _, prefix := range gpuPrefixes {
  254. if strings.HasPrefix(instanceType, prefix) {
  255. return instanceType
  256. }
  257. }
  258. return ""
  259. }
  260. // GPUCount returns 0 as GPU count is derived from the flavor lookup in NodePricing,
  261. // not from node labels. This is consistent with other providers.
  262. func (k *ovhKey) GPUCount() int {
  263. return 0
  264. }
  265. func (k *ovhKey) ID() string {
  266. return ""
  267. }
  268. // ovhPVKey implements models.PVKey for OVH persistent volumes.
  269. type ovhPVKey struct {
  270. StorageClassName string
  271. StorageClassParameters map[string]string
  272. Zone string
  273. }
  274. func (k *ovhPVKey) Features() string {
  275. // First try the StorageClass name mapping
  276. volumeType := storageClassToVolumeType[k.StorageClassName]
  277. // Fallback to the "type" parameter from StorageClass (e.g. "high-speed-gen2")
  278. if volumeType == "" && k.StorageClassParameters != nil {
  279. volumeType = k.StorageClassParameters["type"]
  280. }
  281. return k.Zone + "," + volumeType
  282. }
  283. func (k *ovhPVKey) GetStorageClass() string {
  284. return k.StorageClassName
  285. }
  286. func (k *ovhPVKey) ID() string {
  287. return ""
  288. }
  289. // isMonthlyBilling determines whether a node uses monthly billing.
  290. func isMonthlyBilling(labels map[string]string, monthlyPools []string) bool {
  291. if v, ok := labels[BillingLabel]; ok {
  292. if v == "monthly" {
  293. return true
  294. }
  295. if v == "hourly" {
  296. return false
  297. }
  298. }
  299. if pool, ok := labels[NodepoolLabel]; ok {
  300. for _, mp := range monthlyPools {
  301. if pool == mp {
  302. return true
  303. }
  304. }
  305. }
  306. return false
  307. }
  308. func (c *OVH) getCatalogURL() string {
  309. if c.catalogURL != "" {
  310. return c.catalogURL
  311. }
  312. u, _ := url.Parse("https://eu.api.ovh.com/v1/order/catalog/public/cloud")
  313. q := u.Query()
  314. q.Set("ovhSubsidiary", env.GetOVHSubsidiary())
  315. u.RawQuery = q.Encode()
  316. return u.String()
  317. }
  318. // DownloadPricingData fetches the OVH public cloud catalog and parses pricing.
  319. func (c *OVH) DownloadPricingData() error {
  320. c.DownloadLock.Lock()
  321. defer c.DownloadLock.Unlock()
  322. c.monthlyNodepools = env.GetOVHMonthlyNodepools()
  323. if c.Pricing != nil {
  324. return nil
  325. }
  326. catalogURL := c.getCatalogURL()
  327. log.Infof("Downloading OVH pricing data from %s", catalogURL)
  328. client := &http.Client{Timeout: 30 * time.Second}
  329. resp, err := client.Get(catalogURL)
  330. if err != nil {
  331. return fmt.Errorf("failed to fetch OVH catalog: %w", err)
  332. }
  333. defer resp.Body.Close()
  334. if resp.StatusCode != http.StatusOK {
  335. return fmt.Errorf("OVH catalog returned status %d", resp.StatusCode)
  336. }
  337. body, err := io.ReadAll(resp.Body)
  338. if err != nil {
  339. return fmt.Errorf("failed to read OVH catalog response: %w", err)
  340. }
  341. pricing, volumePricing, err := parseCatalog(body)
  342. if err != nil {
  343. return err
  344. }
  345. c.Pricing = pricing
  346. c.VolumePricing = volumePricing
  347. log.Infof("Loaded OVH pricing: %d flavors, %d volume types", len(pricing), len(volumePricing))
  348. return nil
  349. }
  350. // NodePricing returns pricing for a specific node based on its key.
  351. func (c *OVH) NodePricing(key models.Key) (*models.Node, models.PricingMetadata, error) {
  352. c.DownloadLock.RLock()
  353. defer c.DownloadLock.RUnlock()
  354. meta := models.PricingMetadata{Source: "ovh"}
  355. features := strings.Split(key.Features(), ",")
  356. if len(features) < 2 {
  357. return nil, meta, fmt.Errorf("invalid key features: %s", key.Features())
  358. }
  359. region := features[0]
  360. instanceType := features[1]
  361. flavor, ok := c.Pricing[instanceType]
  362. if !ok {
  363. return nil, meta, fmt.Errorf("flavor not found in OVH pricing: %s", instanceType)
  364. }
  365. // Determine billing mode
  366. var labels map[string]string
  367. if k, ok := key.(*ovhKey); ok {
  368. labels = k.Labels
  369. }
  370. price := flavor.HourlyPrice
  371. if isMonthlyBilling(labels, c.monthlyNodepools) && flavor.MonthlyPrice > 0 {
  372. price = flavor.MonthlyPrice
  373. }
  374. return &models.Node{
  375. Cost: fmt.Sprintf("%f", price),
  376. VCPU: fmt.Sprintf("%d", flavor.VCPU),
  377. RAM: fmt.Sprintf("%d", flavor.RAM),
  378. Storage: fmt.Sprintf("%d", flavor.Disk),
  379. GPU: fmt.Sprintf("%d", flavor.GPU),
  380. GPUName: flavor.GPUName,
  381. InstanceType: instanceType,
  382. Region: region,
  383. // DefaultPrices for both hourly and monthly; monthly prices are pre-amortized to hourly (/730).
  384. PricingType: models.DefaultPrices,
  385. }, meta, nil
  386. }
  387. // PVPricing returns pricing for a persistent volume.
  388. func (c *OVH) PVPricing(pvk models.PVKey) (*models.PV, error) {
  389. c.DownloadLock.RLock()
  390. defer c.DownloadLock.RUnlock()
  391. features := strings.Split(pvk.Features(), ",")
  392. volumeType := ""
  393. if len(features) > 1 {
  394. volumeType = features[1]
  395. }
  396. cost, ok := c.VolumePricing[volumeType]
  397. if !ok {
  398. log.Debugf("Volume pricing not found for storage class %s (type: %s)", pvk.GetStorageClass(), volumeType)
  399. return &models.PV{}, nil
  400. }
  401. return &models.PV{
  402. Cost: fmt.Sprintf("%f", cost),
  403. Class: pvk.GetStorageClass(),
  404. }, nil
  405. }
  406. // NetworkPricing returns static network pricing for OVH.
  407. func (c *OVH) NetworkPricing() (*models.Network, error) {
  408. return &models.Network{
  409. ZoneNetworkEgressCost: 0,
  410. RegionNetworkEgressCost: 0,
  411. InternetNetworkEgressCost: 0.01,
  412. NatGatewayEgressCost: 0,
  413. NatGatewayIngressCost: 0,
  414. }, nil
  415. }
  416. // LoadBalancerPricing returns static load balancer pricing for OVH.
  417. func (c *OVH) LoadBalancerPricing() (*models.LoadBalancer, error) {
  418. return &models.LoadBalancer{
  419. Cost: 0.012,
  420. }, nil
  421. }
  422. // GpuPricing returns GPU-specific pricing (not used for OVH).
  423. func (c *OVH) GpuPricing(nodeLabels map[string]string) (string, error) {
  424. return "", nil
  425. }
  426. // ClusterInfo returns metadata about the cluster.
  427. func (c *OVH) ClusterInfo() (map[string]string, error) {
  428. remoteEnabled := env.IsRemoteEnabled()
  429. m := make(map[string]string)
  430. m["name"] = "OVH Cluster #1"
  431. conf, err := c.GetConfig()
  432. if err != nil {
  433. return nil, err
  434. }
  435. if conf.ClusterName != "" {
  436. m["name"] = conf.ClusterName
  437. }
  438. m["provider"] = opencost.OVHProvider
  439. m["region"] = c.ClusterRegion
  440. m["account"] = c.ClusterAccountID
  441. m["remoteReadEnabled"] = strconv.FormatBool(remoteEnabled)
  442. m["id"] = coreenv.GetClusterID()
  443. return m, nil
  444. }
  445. // GetManagementPlatform detects the management platform from node labels.
  446. func (c *OVH) GetManagementPlatform() (string, error) {
  447. nodes := c.Clientset.GetAllNodes()
  448. if len(nodes) > 0 {
  449. n := nodes[0]
  450. if _, ok := n.Labels[NodepoolLabel]; ok {
  451. return "mks", nil
  452. }
  453. }
  454. return "", nil
  455. }
  456. // GetKey returns a Key for matching node pricing.
  457. func (c *OVH) GetKey(labels map[string]string, n *clustercache.Node) models.Key {
  458. return &ovhKey{Labels: labels}
  459. }
  460. // GetPVKey returns a PVKey for matching persistent volume pricing.
  461. func (c *OVH) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
  462. zone := ""
  463. if pv.Spec.CSI != nil {
  464. parts := strings.Split(pv.Spec.CSI.VolumeHandle, "/")
  465. if len(parts) > 0 {
  466. zone = parts[0]
  467. }
  468. }
  469. return &ovhPVKey{
  470. StorageClassName: pv.Spec.StorageClassName,
  471. StorageClassParameters: parameters,
  472. Zone: zone,
  473. }
  474. }
  475. // GetAddresses is not implemented for OVH.
  476. func (c *OVH) GetAddresses() ([]byte, error) {
  477. return nil, nil
  478. }
  479. // GetDisks is not implemented for OVH.
  480. func (c *OVH) GetDisks() ([]byte, error) {
  481. return nil, nil
  482. }
  483. // GetOrphanedResources is not implemented for OVH.
  484. func (c *OVH) GetOrphanedResources() ([]models.OrphanedResource, error) {
  485. return nil, errors.New("not implemented")
  486. }
  487. // AllNodePricing returns all cached node pricing data.
  488. func (c *OVH) AllNodePricing() (interface{}, error) {
  489. c.DownloadLock.RLock()
  490. defer c.DownloadLock.RUnlock()
  491. return c.Pricing, nil
  492. }
  493. // UpdateConfigFromConfigMap updates config from a ConfigMap.
  494. func (c *OVH) UpdateConfigFromConfigMap(a map[string]string) (*models.CustomPricing, error) {
  495. return c.Config.UpdateFromMap(a)
  496. }
  497. // UpdateConfig updates custom pricing from a JSON reader.
  498. func (c *OVH) UpdateConfig(r io.Reader, updateType string) (*models.CustomPricing, error) {
  499. defer c.DownloadPricingData()
  500. return c.Config.Update(func(cp *models.CustomPricing) error {
  501. a := make(map[string]interface{})
  502. err := coreJSON.NewDecoder(r).Decode(&a)
  503. if err != nil {
  504. return err
  505. }
  506. for k, v := range a {
  507. kUpper := utils.ToTitle.String(k)
  508. vstr, ok := v.(string)
  509. if ok {
  510. err := models.SetCustomPricingField(cp, kUpper, vstr)
  511. if err != nil {
  512. return fmt.Errorf("error setting custom pricing field: %w", err)
  513. }
  514. } else {
  515. return fmt.Errorf("type error while updating config for %s", kUpper)
  516. }
  517. }
  518. if env.IsRemoteEnabled() {
  519. err := utils.UpdateClusterMeta(coreenv.GetClusterID(), cp.ClusterName)
  520. if err != nil {
  521. return err
  522. }
  523. }
  524. return nil
  525. })
  526. }
  527. // GetConfig returns the custom pricing configuration with OVH defaults.
  528. func (c *OVH) GetConfig() (*models.CustomPricing, error) {
  529. cp, err := c.Config.GetCustomPricingData()
  530. if err != nil {
  531. return nil, err
  532. }
  533. if cp.Discount == "" {
  534. cp.Discount = "0%"
  535. }
  536. if cp.NegotiatedDiscount == "" {
  537. cp.NegotiatedDiscount = "0%"
  538. }
  539. if cp.CurrencyCode == "" {
  540. cp.CurrencyCode = "EUR"
  541. }
  542. return cp, nil
  543. }
  544. // ClusterManagementPricing returns the management cost for the cluster.
  545. func (c *OVH) ClusterManagementPricing() (string, float64, error) {
  546. return "", 0.0, nil
  547. }
  548. // CombinedDiscountForNode calculates the combined discount for a node.
  549. func (c *OVH) CombinedDiscountForNode(instanceType string, isPreemptible bool, defaultDiscount, negotiatedDiscount float64) float64 {
  550. return 1.0 - ((1.0 - defaultDiscount) * (1.0 - negotiatedDiscount))
  551. }
  552. // Regions returns the list of supported OVH regions.
  553. func (c *OVH) Regions() []string {
  554. regionOverrides := env.GetRegionOverrideList()
  555. if len(regionOverrides) > 0 {
  556. log.Debugf("Overriding OVH regions with configured region list: %+v", regionOverrides)
  557. return regionOverrides
  558. }
  559. return ovhRegions
  560. }
  561. // ApplyReservedInstancePricing is a no-op for OVH.
  562. func (c *OVH) ApplyReservedInstancePricing(nodes map[string]*models.Node) {}
  563. // ServiceAccountStatus returns the service account status.
  564. func (c *OVH) ServiceAccountStatus() *models.ServiceAccountStatus {
  565. return &models.ServiceAccountStatus{
  566. Checks: []*models.ServiceAccountCheck{},
  567. }
  568. }
  569. // PricingSourceStatus returns the status of the pricing data source.
  570. func (c *OVH) PricingSourceStatus() map[string]*models.PricingSource {
  571. return map[string]*models.PricingSource{
  572. OVHCatalogPricing: {
  573. Name: OVHCatalogPricing,
  574. Enabled: true,
  575. Available: true,
  576. },
  577. }
  578. }
  579. // PricingSourceSummary returns the parsed pricing data.
  580. func (c *OVH) PricingSourceSummary() interface{} {
  581. return c.Pricing
  582. }