disktiers.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. package azure
  2. import (
  3. "fmt"
  4. "regexp"
  5. "strconv"
  6. "strings"
  7. "github.com/opencost/opencost/core/pkg/clustercache"
  8. "github.com/opencost/opencost/core/pkg/util/timeutil"
  9. )
  10. // Azure managed disk size tiers (GiB), ordered ascending.
  11. // Source: https://learn.microsoft.com/en-us/azure/virtual-machines/disks-types
  12. var (
  13. premiumSSDTiers = []diskTier{
  14. {Name: "P1", SizeGiB: 4},
  15. {Name: "P2", SizeGiB: 8},
  16. {Name: "P3", SizeGiB: 16},
  17. {Name: "P4", SizeGiB: 32},
  18. {Name: "P6", SizeGiB: 64},
  19. {Name: "P10", SizeGiB: 128},
  20. {Name: "P15", SizeGiB: 256},
  21. {Name: "P20", SizeGiB: 512},
  22. {Name: "P30", SizeGiB: 1024},
  23. {Name: "P40", SizeGiB: 2048},
  24. {Name: "P50", SizeGiB: 4096},
  25. {Name: "P60", SizeGiB: 8192},
  26. {Name: "P70", SizeGiB: 16384},
  27. {Name: "P80", SizeGiB: 32767},
  28. }
  29. standardSSDTiers = []diskTier{
  30. {Name: "E1", SizeGiB: 4},
  31. {Name: "E2", SizeGiB: 8},
  32. {Name: "E3", SizeGiB: 16},
  33. {Name: "E4", SizeGiB: 32},
  34. {Name: "E6", SizeGiB: 64},
  35. {Name: "E10", SizeGiB: 128},
  36. {Name: "E15", SizeGiB: 256},
  37. {Name: "E20", SizeGiB: 512},
  38. {Name: "E30", SizeGiB: 1024},
  39. {Name: "E40", SizeGiB: 2048},
  40. {Name: "E50", SizeGiB: 4096},
  41. {Name: "E60", SizeGiB: 8192},
  42. {Name: "E70", SizeGiB: 16384},
  43. {Name: "E80", SizeGiB: 32767},
  44. }
  45. standardHDDTiers = []diskTier{
  46. {Name: "S4", SizeGiB: 32},
  47. {Name: "S6", SizeGiB: 64},
  48. {Name: "S10", SizeGiB: 128},
  49. {Name: "S15", SizeGiB: 256},
  50. {Name: "S20", SizeGiB: 512},
  51. {Name: "S30", SizeGiB: 1024},
  52. {Name: "S40", SizeGiB: 2048},
  53. {Name: "S50", SizeGiB: 4096},
  54. {Name: "S60", SizeGiB: 8192},
  55. {Name: "S70", SizeGiB: 16384},
  56. {Name: "S80", SizeGiB: 32767},
  57. }
  58. )
  59. type diskTier struct {
  60. Name string
  61. SizeGiB int
  62. }
  63. // managedDiskMeterRE matches Rate Card / Price Sheet managed disk capacity meters,
  64. // e.g. "P4 LRS Disk", "E10 ZRS Disk". Disk Mount meters are excluded.
  65. var managedDiskMeterRE = regexp.MustCompile(`^(P|E|S)(\d+)\s+(LRS|ZRS)\s+Disk$`)
  66. var managedDiskTierNameRE = regexp.MustCompile(`^(P|E|S)(\d+)$`)
  67. const (
  68. azureDiskRedundancyLRS = "LRS"
  69. azureDiskRedundancyZRS = "ZRS"
  70. )
  71. type managedDiskSKU struct {
  72. StorageClass string // premium_ssd / standard_ssd / standard_hdd
  73. Redundancy string // LRS / ZRS
  74. }
  75. // parseManagedDiskMeter parses a managed disk capacity meter name into storage class,
  76. // redundancy, and tier (e.g. P4). Returns ok=false for non-matching names.
  77. func parseManagedDiskMeter(meterName string) (storageClass, redundancy, tier string, ok bool) {
  78. matches := managedDiskMeterRE.FindStringSubmatch(strings.TrimSpace(meterName))
  79. if len(matches) != 4 {
  80. return "", "", "", false
  81. }
  82. prefix := matches[1]
  83. tier = prefix + matches[2]
  84. redundancy = matches[3]
  85. switch prefix {
  86. case "P":
  87. storageClass = AzureDiskPremiumSSDStorageClass
  88. case "E":
  89. storageClass = AzureDiskStandardSSDStorageClass
  90. case "S":
  91. storageClass = AzureDiskStandardStorageClass
  92. default:
  93. return "", "", "", false
  94. }
  95. return storageClass, redundancy, tier, true
  96. }
  97. // diskTierKey builds the Pricing map key for a managed disk tier monthly price.
  98. // Format: region,storageClass,redundancy,tier (e.g. "centralus,premium_ssd,LRS,P4").
  99. func diskTierKey(region, storageClass, redundancy, tier string) string {
  100. return fmt.Sprintf("%s,%s,%s,%s", region, storageClass, redundancy, tier)
  101. }
  102. // diskClassKey builds the legacy class-level Pricing key used for Azure Files and
  103. // as a size-unknown fallback (linearized $/GiB-hour).
  104. func diskClassKey(region, storageClass string) string {
  105. return region + "," + storageClass
  106. }
  107. // resolveDiskSKU maps StorageClass / Azure disk SKU parameters to an OpenCost
  108. // storage class and redundancy. Returns ok=false for Azure Files SKUs handled separately.
  109. func resolveDiskSKU(sku string) (managedDiskSKU, bool) {
  110. switch strings.ToLower(strings.TrimSpace(sku)) {
  111. case "premium_lrs":
  112. return managedDiskSKU{StorageClass: AzureDiskPremiumSSDStorageClass, Redundancy: azureDiskRedundancyLRS}, true
  113. case "premium_zrs":
  114. return managedDiskSKU{StorageClass: AzureDiskPremiumSSDStorageClass, Redundancy: azureDiskRedundancyZRS}, true
  115. case "standardssd_lrs":
  116. return managedDiskSKU{StorageClass: AzureDiskStandardSSDStorageClass, Redundancy: azureDiskRedundancyLRS}, true
  117. case "standardssd_zrs":
  118. return managedDiskSKU{StorageClass: AzureDiskStandardSSDStorageClass, Redundancy: azureDiskRedundancyZRS}, true
  119. case "standard_lrs":
  120. return managedDiskSKU{StorageClass: AzureDiskStandardStorageClass, Redundancy: azureDiskRedundancyLRS}, true
  121. case "standard_zrs":
  122. return managedDiskSKU{StorageClass: AzureDiskStandardStorageClass, Redundancy: azureDiskRedundancyZRS}, true
  123. default:
  124. return managedDiskSKU{}, false
  125. }
  126. }
  127. func tiersForStorageClass(storageClass string) []diskTier {
  128. switch storageClass {
  129. case AzureDiskPremiumSSDStorageClass:
  130. return premiumSSDTiers
  131. case AzureDiskStandardSSDStorageClass:
  132. return standardSSDTiers
  133. case AzureDiskStandardStorageClass:
  134. return standardHDDTiers
  135. default:
  136. return nil
  137. }
  138. }
  139. // selectDiskTier returns the smallest Azure disk tier that can hold sizeGiB.
  140. // If sizeGiB is larger than the biggest tier, the largest tier is returned.
  141. func selectDiskTier(storageClass string, sizeGiB float64) (diskTier, bool) {
  142. tiers := tiersForStorageClass(storageClass)
  143. if len(tiers) == 0 || sizeGiB <= 0 {
  144. return diskTier{}, false
  145. }
  146. for _, tier := range tiers {
  147. if float64(tier.SizeGiB) >= sizeGiB {
  148. return tier, true
  149. }
  150. }
  151. return tiers[len(tiers)-1], true
  152. }
  153. // smallestDiskTier returns the smallest tier for a storage class (used for
  154. // linearized class-level fallback rates).
  155. func smallestDiskTier(storageClass string) (diskTier, bool) {
  156. tiers := tiersForStorageClass(storageClass)
  157. if len(tiers) == 0 {
  158. return diskTier{}, false
  159. }
  160. return tiers[0], true
  161. }
  162. // tierHourlyFromMonthly converts a fixed monthly tier price to a whole-disk
  163. // hourly cost. Stored in models.PV.Cost for tier keys so AllNodePricing stays
  164. // in hourly units (class keys store $/GiB-hour).
  165. func tierHourlyFromMonthly(monthlyTierPrice float64) float64 {
  166. if monthlyTierPrice <= 0 {
  167. return 0
  168. }
  169. return monthlyTierPrice / timeutil.HoursPerMonth
  170. }
  171. // effectiveGiBHourRate converts a fixed monthly tier price into the $/GiB-hour
  172. // rate that, when multiplied by reportedSizeGiB, recovers the tier hourly cost.
  173. func effectiveGiBHourRate(monthlyTierPrice, reportedSizeGiB float64) float64 {
  174. return effectiveGiBHourRateFromHourly(tierHourlyFromMonthly(monthlyTierPrice), reportedSizeGiB)
  175. }
  176. // effectiveGiBHourRateFromHourly converts a whole-disk hourly tier price into
  177. // the $/GiB-hour rate used by allocation (rate × GiB × hours).
  178. func effectiveGiBHourRateFromHourly(tierHourlyPrice, reportedSizeGiB float64) float64 {
  179. if tierHourlyPrice <= 0 || reportedSizeGiB <= 0 {
  180. return 0
  181. }
  182. return tierHourlyPrice / reportedSizeGiB
  183. }
  184. // nearestDiskTierIndex returns the index of preferred in the class tier table,
  185. // or -1 if not found.
  186. func diskTierIndex(storageClass, tierName string) int {
  187. tiers := tiersForStorageClass(storageClass)
  188. for i, tier := range tiers {
  189. if tier.Name == tierName {
  190. return i
  191. }
  192. }
  193. return -1
  194. }
  195. // pickSizedOrLargerAvailableTier chooses the first available priced tier at the
  196. // preferred tier index or larger. It never selects a smaller tier to avoid
  197. // underpricing fixed-tier Azure disks.
  198. func pickSizedOrLargerAvailableTier(storageClass, preferredTier string, hasPrice func(tierName string) bool) (diskTier, bool) {
  199. tiers := tiersForStorageClass(storageClass)
  200. idx := diskTierIndex(storageClass, preferredTier)
  201. if idx < 0 {
  202. return diskTier{}, false
  203. }
  204. for i := idx; i < len(tiers); i++ {
  205. if hasPrice(tiers[i].Name) {
  206. return tiers[i], true
  207. }
  208. }
  209. return diskTier{}, false
  210. }
  211. func formatPrice(price float64) string {
  212. return fmt.Sprintf("%f", price)
  213. }
  214. func parsePrice(s string) (float64, error) {
  215. return strconv.ParseFloat(s, 64)
  216. }
  217. // isManagedDiskTierKey returns true if the key is a valid managed disk tier pricing key.
  218. func isManagedDiskTierKey(key string) bool {
  219. parts := strings.Split(key, ",")
  220. if len(parts) != 4 {
  221. return false
  222. }
  223. storageClass := parts[1]
  224. redundancy := parts[2]
  225. tierName := parts[3]
  226. if redundancy != azureDiskRedundancyLRS && redundancy != azureDiskRedundancyZRS {
  227. return false
  228. }
  229. tierParts := managedDiskTierNameRE.FindStringSubmatch(strings.TrimSpace(tierName))
  230. if len(tierParts) != 3 {
  231. return false
  232. }
  233. prefix := tierParts[1]
  234. switch storageClass {
  235. case AzureDiskPremiumSSDStorageClass:
  236. return prefix == "P"
  237. case AzureDiskStandardSSDStorageClass:
  238. return prefix == "E"
  239. case AzureDiskStandardStorageClass:
  240. return prefix == "S"
  241. default:
  242. return false
  243. }
  244. }
  245. // pvSizeGiB returns the PersistentVolume capacity in GiB, or 0 if unavailable.
  246. func pvSizeGiB(pv *clustercache.PersistentVolume) float64 {
  247. if pv == nil {
  248. return 0
  249. }
  250. qty := pv.Spec.Capacity.Storage()
  251. if qty == nil || qty.IsZero() {
  252. return 0
  253. }
  254. return float64(qty.Value()) / (1024 * 1024 * 1024)
  255. }