Răsfoiți Sursa

fix: Azure managed disk tier pricing (#3920)

Signed-off-by: Sawyer Ward <104373596+sawyerward@users.noreply.github.com>
Sawyer Ward 1 lună în urmă
părinte
comite
e2717013d2

+ 284 - 0
pkg/cloud/azure/disktiers.go

@@ -0,0 +1,284 @@
+package azure
+
+import (
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/opencost/opencost/core/pkg/clustercache"
+	"github.com/opencost/opencost/core/pkg/util/timeutil"
+)
+
+// Azure managed disk size tiers (GiB), ordered ascending.
+// Source: https://learn.microsoft.com/en-us/azure/virtual-machines/disks-types
+var (
+	premiumSSDTiers = []diskTier{
+		{Name: "P1", SizeGiB: 4},
+		{Name: "P2", SizeGiB: 8},
+		{Name: "P3", SizeGiB: 16},
+		{Name: "P4", SizeGiB: 32},
+		{Name: "P6", SizeGiB: 64},
+		{Name: "P10", SizeGiB: 128},
+		{Name: "P15", SizeGiB: 256},
+		{Name: "P20", SizeGiB: 512},
+		{Name: "P30", SizeGiB: 1024},
+		{Name: "P40", SizeGiB: 2048},
+		{Name: "P50", SizeGiB: 4096},
+		{Name: "P60", SizeGiB: 8192},
+		{Name: "P70", SizeGiB: 16384},
+		{Name: "P80", SizeGiB: 32767},
+	}
+
+	standardSSDTiers = []diskTier{
+		{Name: "E1", SizeGiB: 4},
+		{Name: "E2", SizeGiB: 8},
+		{Name: "E3", SizeGiB: 16},
+		{Name: "E4", SizeGiB: 32},
+		{Name: "E6", SizeGiB: 64},
+		{Name: "E10", SizeGiB: 128},
+		{Name: "E15", SizeGiB: 256},
+		{Name: "E20", SizeGiB: 512},
+		{Name: "E30", SizeGiB: 1024},
+		{Name: "E40", SizeGiB: 2048},
+		{Name: "E50", SizeGiB: 4096},
+		{Name: "E60", SizeGiB: 8192},
+		{Name: "E70", SizeGiB: 16384},
+		{Name: "E80", SizeGiB: 32767},
+	}
+
+	standardHDDTiers = []diskTier{
+		{Name: "S4", SizeGiB: 32},
+		{Name: "S6", SizeGiB: 64},
+		{Name: "S10", SizeGiB: 128},
+		{Name: "S15", SizeGiB: 256},
+		{Name: "S20", SizeGiB: 512},
+		{Name: "S30", SizeGiB: 1024},
+		{Name: "S40", SizeGiB: 2048},
+		{Name: "S50", SizeGiB: 4096},
+		{Name: "S60", SizeGiB: 8192},
+		{Name: "S70", SizeGiB: 16384},
+		{Name: "S80", SizeGiB: 32767},
+	}
+)
+
+type diskTier struct {
+	Name    string
+	SizeGiB int
+}
+
+// managedDiskMeterRE matches Rate Card / Price Sheet managed disk capacity meters,
+// e.g. "P4 LRS Disk", "E10 ZRS Disk". Disk Mount meters are excluded.
+var managedDiskMeterRE = regexp.MustCompile(`^(P|E|S)(\d+)\s+(LRS|ZRS)\s+Disk$`)
+var managedDiskTierNameRE = regexp.MustCompile(`^(P|E|S)(\d+)$`)
+
+const (
+	azureDiskRedundancyLRS = "LRS"
+	azureDiskRedundancyZRS = "ZRS"
+)
+
+type managedDiskSKU struct {
+	StorageClass string // premium_ssd / standard_ssd / standard_hdd
+	Redundancy   string // LRS / ZRS
+}
+
+// parseManagedDiskMeter parses a managed disk capacity meter name into storage class,
+// redundancy, and tier (e.g. P4). Returns ok=false for non-matching names.
+func parseManagedDiskMeter(meterName string) (storageClass, redundancy, tier string, ok bool) {
+	matches := managedDiskMeterRE.FindStringSubmatch(strings.TrimSpace(meterName))
+	if len(matches) != 4 {
+		return "", "", "", false
+	}
+	prefix := matches[1]
+	tier = prefix + matches[2]
+	redundancy = matches[3]
+	switch prefix {
+	case "P":
+		storageClass = AzureDiskPremiumSSDStorageClass
+	case "E":
+		storageClass = AzureDiskStandardSSDStorageClass
+	case "S":
+		storageClass = AzureDiskStandardStorageClass
+	default:
+		return "", "", "", false
+	}
+	return storageClass, redundancy, tier, true
+}
+
+// diskTierKey builds the Pricing map key for a managed disk tier monthly price.
+// Format: region,storageClass,redundancy,tier (e.g. "centralus,premium_ssd,LRS,P4").
+func diskTierKey(region, storageClass, redundancy, tier string) string {
+	return fmt.Sprintf("%s,%s,%s,%s", region, storageClass, redundancy, tier)
+}
+
+// diskClassKey builds the legacy class-level Pricing key used for Azure Files and
+// as a size-unknown fallback (linearized $/GiB-hour).
+func diskClassKey(region, storageClass string) string {
+	return region + "," + storageClass
+}
+
+// resolveDiskSKU maps StorageClass / Azure disk SKU parameters to an OpenCost
+// storage class and redundancy. Returns ok=false for Azure Files SKUs handled separately.
+func resolveDiskSKU(sku string) (managedDiskSKU, bool) {
+	switch strings.ToLower(strings.TrimSpace(sku)) {
+	case "premium_lrs":
+		return managedDiskSKU{StorageClass: AzureDiskPremiumSSDStorageClass, Redundancy: azureDiskRedundancyLRS}, true
+	case "premium_zrs":
+		return managedDiskSKU{StorageClass: AzureDiskPremiumSSDStorageClass, Redundancy: azureDiskRedundancyZRS}, true
+	case "standardssd_lrs":
+		return managedDiskSKU{StorageClass: AzureDiskStandardSSDStorageClass, Redundancy: azureDiskRedundancyLRS}, true
+	case "standardssd_zrs":
+		return managedDiskSKU{StorageClass: AzureDiskStandardSSDStorageClass, Redundancy: azureDiskRedundancyZRS}, true
+	case "standard_lrs":
+		return managedDiskSKU{StorageClass: AzureDiskStandardStorageClass, Redundancy: azureDiskRedundancyLRS}, true
+	case "standard_zrs":
+		return managedDiskSKU{StorageClass: AzureDiskStandardStorageClass, Redundancy: azureDiskRedundancyZRS}, true
+	default:
+		return managedDiskSKU{}, false
+	}
+}
+
+func tiersForStorageClass(storageClass string) []diskTier {
+	switch storageClass {
+	case AzureDiskPremiumSSDStorageClass:
+		return premiumSSDTiers
+	case AzureDiskStandardSSDStorageClass:
+		return standardSSDTiers
+	case AzureDiskStandardStorageClass:
+		return standardHDDTiers
+	default:
+		return nil
+	}
+}
+
+// selectDiskTier returns the smallest Azure disk tier that can hold sizeGiB.
+// If sizeGiB is larger than the biggest tier, the largest tier is returned.
+func selectDiskTier(storageClass string, sizeGiB float64) (diskTier, bool) {
+	tiers := tiersForStorageClass(storageClass)
+	if len(tiers) == 0 || sizeGiB <= 0 {
+		return diskTier{}, false
+	}
+	for _, tier := range tiers {
+		if float64(tier.SizeGiB) >= sizeGiB {
+			return tier, true
+		}
+	}
+	return tiers[len(tiers)-1], true
+}
+
+// smallestDiskTier returns the smallest tier for a storage class (used for
+// linearized class-level fallback rates).
+func smallestDiskTier(storageClass string) (diskTier, bool) {
+	tiers := tiersForStorageClass(storageClass)
+	if len(tiers) == 0 {
+		return diskTier{}, false
+	}
+	return tiers[0], true
+}
+
+// tierHourlyFromMonthly converts a fixed monthly tier price to a whole-disk
+// hourly cost. Stored in models.PV.Cost for tier keys so AllNodePricing stays
+// in hourly units (class keys store $/GiB-hour).
+func tierHourlyFromMonthly(monthlyTierPrice float64) float64 {
+	if monthlyTierPrice <= 0 {
+		return 0
+	}
+	return monthlyTierPrice / timeutil.HoursPerMonth
+}
+
+// effectiveGiBHourRate converts a fixed monthly tier price into the $/GiB-hour
+// rate that, when multiplied by reportedSizeGiB, recovers the tier hourly cost.
+func effectiveGiBHourRate(monthlyTierPrice, reportedSizeGiB float64) float64 {
+	return effectiveGiBHourRateFromHourly(tierHourlyFromMonthly(monthlyTierPrice), reportedSizeGiB)
+}
+
+// effectiveGiBHourRateFromHourly converts a whole-disk hourly tier price into
+// the $/GiB-hour rate used by allocation (rate × GiB × hours).
+func effectiveGiBHourRateFromHourly(tierHourlyPrice, reportedSizeGiB float64) float64 {
+	if tierHourlyPrice <= 0 || reportedSizeGiB <= 0 {
+		return 0
+	}
+	return tierHourlyPrice / reportedSizeGiB
+}
+
+// nearestDiskTierIndex returns the index of preferred in the class tier table,
+// or -1 if not found.
+func diskTierIndex(storageClass, tierName string) int {
+	tiers := tiersForStorageClass(storageClass)
+	for i, tier := range tiers {
+		if tier.Name == tierName {
+			return i
+		}
+	}
+	return -1
+}
+
+// pickSizedOrLargerAvailableTier chooses the first available priced tier at the
+// preferred tier index or larger. It never selects a smaller tier to avoid
+// underpricing fixed-tier Azure disks.
+func pickSizedOrLargerAvailableTier(storageClass, preferredTier string, hasPrice func(tierName string) bool) (diskTier, bool) {
+	tiers := tiersForStorageClass(storageClass)
+	idx := diskTierIndex(storageClass, preferredTier)
+	if idx < 0 {
+		return diskTier{}, false
+	}
+	for i := idx; i < len(tiers); i++ {
+		if hasPrice(tiers[i].Name) {
+			return tiers[i], true
+		}
+	}
+	return diskTier{}, false
+}
+
+func formatPrice(price float64) string {
+	return fmt.Sprintf("%f", price)
+}
+
+func parsePrice(s string) (float64, error) {
+	return strconv.ParseFloat(s, 64)
+}
+
+// isManagedDiskTierKey returns true if the key is a valid managed disk tier pricing key.
+func isManagedDiskTierKey(key string) bool {
+	parts := strings.Split(key, ",")
+	if len(parts) != 4 {
+		return false
+	}
+
+	storageClass := parts[1]
+	redundancy := parts[2]
+	tierName := parts[3]
+
+	if redundancy != azureDiskRedundancyLRS && redundancy != azureDiskRedundancyZRS {
+		return false
+	}
+
+	tierParts := managedDiskTierNameRE.FindStringSubmatch(strings.TrimSpace(tierName))
+	if len(tierParts) != 3 {
+		return false
+	}
+	prefix := tierParts[1]
+
+	switch storageClass {
+	case AzureDiskPremiumSSDStorageClass:
+		return prefix == "P"
+	case AzureDiskStandardSSDStorageClass:
+		return prefix == "E"
+	case AzureDiskStandardStorageClass:
+		return prefix == "S"
+	default:
+		return false
+	}
+}
+
+// pvSizeGiB returns the PersistentVolume capacity in GiB, or 0 if unavailable.
+func pvSizeGiB(pv *clustercache.PersistentVolume) float64 {
+	if pv == nil {
+		return 0
+	}
+	qty := pv.Spec.Capacity.Storage()
+	if qty == nil || qty.IsZero() {
+		return 0
+	}
+	return float64(qty.Value()) / (1024 * 1024 * 1024)
+}

+ 89 - 0
pkg/cloud/azure/disktiers_test.go

@@ -0,0 +1,89 @@
+package azure
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestParseManagedDiskMeter(t *testing.T) {
+	cases := []struct {
+		name       string
+		meter      string
+		wantClass  string
+		wantRedund string
+		wantTier   string
+		wantOK     bool
+	}{
+		{name: "p4 lrs", meter: "P4 LRS Disk", wantClass: AzureDiskPremiumSSDStorageClass, wantRedund: "LRS", wantTier: "P4", wantOK: true},
+		{name: "p10 zrs", meter: "P10 ZRS Disk", wantClass: AzureDiskPremiumSSDStorageClass, wantRedund: "ZRS", wantTier: "P10", wantOK: true},
+		{name: "e20 lrs", meter: "E20 LRS Disk", wantClass: AzureDiskStandardSSDStorageClass, wantRedund: "LRS", wantTier: "E20", wantOK: true},
+		{name: "s4 lrs", meter: "S4 LRS Disk", wantClass: AzureDiskStandardStorageClass, wantRedund: "LRS", wantTier: "S4", wantOK: true},
+		{name: "disk mount", meter: "P4 LRS Disk Mount", wantOK: false},
+		{name: "files", meter: "LRS Provisioned", wantOK: false},
+		{name: "garbage", meter: "P4 are good", wantOK: false},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			class, redund, tier, ok := parseManagedDiskMeter(tc.meter)
+			require.Equal(t, tc.wantOK, ok)
+			if tc.wantOK {
+				require.Equal(t, tc.wantClass, class)
+				require.Equal(t, tc.wantRedund, redund)
+				require.Equal(t, tc.wantTier, tier)
+			}
+		})
+	}
+}
+
+func TestResolveDiskSKU(t *testing.T) {
+	sku, ok := resolveDiskSKU("Premium_LRS")
+	require.True(t, ok)
+	require.Equal(t, AzureDiskPremiumSSDStorageClass, sku.StorageClass)
+	require.Equal(t, azureDiskRedundancyLRS, sku.Redundancy)
+
+	sku, ok = resolveDiskSKU("Premium_ZRS")
+	require.True(t, ok)
+	require.Equal(t, azureDiskRedundancyZRS, sku.Redundancy)
+
+	_, ok = resolveDiskSKU("not-a-sku")
+	require.False(t, ok)
+}
+
+func TestPickSizedOrLargerAvailableTier(t *testing.T) {
+	available := map[string]bool{"P4": true, "P15": true}
+	has := func(name string) bool { return available[name] }
+
+	tier, ok := pickSizedOrLargerAvailableTier(AzureDiskPremiumSSDStorageClass, "P10", has)
+	require.True(t, ok)
+	require.Equal(t, "P15", tier.Name)
+
+	tier, ok = pickSizedOrLargerAvailableTier(AzureDiskPremiumSSDStorageClass, "P4", has)
+	require.True(t, ok)
+	require.Equal(t, "P4", tier.Name)
+
+	_, ok = pickSizedOrLargerAvailableTier(AzureDiskPremiumSSDStorageClass, "P10", func(string) bool { return false })
+	require.False(t, ok)
+
+	onlySmaller := map[string]bool{"P4": true}
+	_, ok = pickSizedOrLargerAvailableTier(AzureDiskPremiumSSDStorageClass, "P10", func(name string) bool { return onlySmaller[name] })
+	require.False(t, ok)
+}
+
+func TestDiskRateConversions(t *testing.T) {
+	monthly := 5.2795
+	require.InDelta(t, monthly/730.0, tierHourlyFromMonthly(monthly), 1e-12)
+	require.InDelta(t, monthly/730.0/10.0, effectiveGiBHourRate(monthly, 10), 1e-12)
+	require.Equal(t, 0.0, effectiveGiBHourRate(monthly, 0))
+}
+
+func TestIsManagedDiskTierKey(t *testing.T) {
+	require.True(t, isManagedDiskTierKey("centralus,premium_ssd,LRS,P4"))
+	require.True(t, isManagedDiskTierKey("centralus,standard_ssd,ZRS,E20"))
+	require.True(t, isManagedDiskTierKey("centralus,premium_ssd,LRS,P123"))
+	require.False(t, isManagedDiskTierKey("centralus,Standard_D2s_v3,ondemand,windows"))
+	require.False(t, isManagedDiskTierKey("centralus,premium_ssd,LRS,E20"))
+	require.False(t, isManagedDiskTierKey("centralus,standard_hdd,LRS,Sx"))
+	require.False(t, isManagedDiskTierKey("centralus,premium_ssd,LRS,P4 Mount"))
+	require.False(t, isManagedDiskTierKey("centralus,premium_ssd,LRS"))
+}

+ 368 - 60
pkg/cloud/azure/provider.go

@@ -451,6 +451,7 @@ type AzurePricing struct {
 
 type Azure struct {
 	Pricing                 map[string]*AzurePricing
+	managedDiskTierHourly   map[string]float64
 	DownloadPricingDataLock sync.RWMutex
 	Clientset               clustercache.ClusterCache
 	Config                  models.ProviderConfig
@@ -988,8 +989,12 @@ func (az *Azure) DownloadPricingData() error {
 		}
 	}
 	addAzureFilePricing(allPrices, regions)
+	ensureDiskClassFallbacks(allPrices)
+	tierHourly := collectManagedDiskTierHourly(allPrices)
+	removeManagedDiskTierEntries(allPrices)
 
 	az.Pricing = allPrices
+	az.managedDiskTierHourly = tierHourly
 	az.pricingSource = rateCardPricingSource
 	az.rateCardPricingError = nil
 
@@ -1019,7 +1024,11 @@ func (az *Azure) DownloadPricingData() error {
 				return
 			}
 			addAzureFilePricing(allPrices, regions)
+			ensureDiskClassFallbacks(allPrices)
+			tierHourly := collectManagedDiskTierHourly(allPrices)
+			removeManagedDiskTierEntries(allPrices)
 			az.Pricing = allPrices
+			az.managedDiskTierHourly = tierHourly
 			az.pricingSource = priceSheetPricingSource
 			az.priceSheetPricingError = nil
 		}()
@@ -1049,31 +1058,64 @@ func convertMeterToPricings(info commerce.MeterInfo, regions map[string]string,
 
 	if strings.Contains(meterCategory, "Storage") {
 		if strings.Contains(meterSubCategory, "HDD") || strings.Contains(meterSubCategory, "SSD") || strings.Contains(meterSubCategory, "Premium Files") {
-			var storageClass string = ""
-			if strings.Contains(meterName, "P4 ") {
-				storageClass = AzureDiskPremiumSSDStorageClass
-			} else if strings.Contains(meterName, "E4 ") {
-				storageClass = AzureDiskStandardSSDStorageClass
-			} else if strings.Contains(meterName, "S4 ") {
-				storageClass = AzureDiskStandardStorageClass
-			} else if strings.Contains(meterName, "LRS Provisioned") {
-				storageClass = AzureFilePremiumStorageClass
+			if len(info.MeterRates) < 1 {
+				return nil, fmt.Errorf("missing rate info %+v", map[string]interface{}{"MeterSubCategory": *info.MeterSubCategory, "region": region})
+			}
+			var priceInUsd float64
+			for _, rate := range info.MeterRates {
+				priceInUsd += *rate
 			}
 
-			if storageClass != "" {
-				var priceInUsd float64
+			// Shared-disk mount fees are not modeled; skip so they cannot overwrite capacity prices.
+			if strings.Contains(meterName, "Disk Mount") {
+				log.Debugf("Azure shared disk mount pricing is not supported; skipping meter %q in region %s", meterName, region)
+				return nil, nil
+			}
 
-				if len(info.MeterRates) < 1 {
-					return nil, fmt.Errorf("missing rate info %+v", map[string]interface{}{"MeterSubCategory": *info.MeterSubCategory, "region": region})
+			results := make(map[string]*AzurePricing)
+
+			if storageClass, redundancy, tier, ok := parseManagedDiskMeter(meterName); ok {
+				tierKey := diskTierKey(region, storageClass, redundancy, tier)
+				// Store whole-disk hourly cost so AllNodePricing stays in hourly units.
+				// PVPricing converts to effective $/GiB-hour using the PV's reported size.
+				hourly := tierHourlyFromMonthly(priceInUsd)
+				priceStr := formatPrice(hourly)
+				log.Debugf("Adding PV tier key: %s, HourlyCost: %s (monthly %g)", tierKey, priceStr, priceInUsd)
+				results[tierKey] = &AzurePricing{
+					PV: &models.PV{
+						Cost:   priceStr,
+						Class:  storageClass,
+						Region: region,
+						Size:   tier,
+					},
 				}
-				for _, rate := range info.MeterRates {
-					priceInUsd += *rate
+
+				// Maintain a class-level linearized $/GiB-hour fallback from the
+				// smallest catalog LRS tier so size-unknown PVs still resolve.
+				if redundancy == azureDiskRedundancyLRS {
+					if smallest, ok := smallestDiskTier(storageClass); ok && smallest.Name == tier {
+						rate := effectiveGiBHourRateFromHourly(hourly, float64(smallest.SizeGiB))
+						classKey := diskClassKey(region, storageClass)
+						rateStr := formatPrice(rate)
+						log.Debugf("Adding PV class fallback key: %s, Cost: %s", classKey, rateStr)
+						results[classKey] = &AzurePricing{
+							PV: &models.PV{
+								Cost:   rateStr,
+								Class:  storageClass,
+								Region: region,
+							},
+						}
+					}
 				}
-				// rate is in disk per month, resolve price per hour, then GB per hour
-				pricePerHour := priceInUsd / 730.0 / 32.0
-				priceStr := fmt.Sprintf("%f", pricePerHour)
+				return results, nil
+			}
 
-				key := region + "," + storageClass
+			if strings.Contains(meterName, "LRS Provisioned") {
+				// rate is in disk per month; Premium Files uses provisioned capacity.
+				// Keep historical linearization against 32 GiB for Azure Files Premium.
+				pricePerHour := priceInUsd / timeutil.HoursPerMonth / 32.0
+				priceStr := formatPrice(pricePerHour)
+				key := diskClassKey(region, AzureFilePremiumStorageClass)
 				log.Debugf("Adding PV.Key: %s, Cost: %s", key, priceStr)
 				return map[string]*AzurePricing{
 					key: {
@@ -1158,6 +1200,97 @@ func addAzureFilePricing(prices map[string]*AzurePricing, regions map[string]str
 	}
 }
 
+// ensureDiskClassFallbacks fills missing region,storageClass linearized $/GiB-hour
+// keys from the smallest available LRS tier meter for that class. Needed when the
+// Rate Card omits the absolute smallest catalog tier (e.g. P1) but includes P4.
+// Tier keys store whole-disk hourly cost; class keys store $/GiB-hour.
+func ensureDiskClassFallbacks(prices map[string]*AzurePricing) {
+	type candidate struct {
+		sizeGiB int
+		hourly  float64
+		region  string
+		class   string
+	}
+	best := map[string]candidate{} // key: region,class
+
+	for key, pricing := range prices {
+		if pricing == nil || pricing.PV == nil {
+			continue
+		}
+		parts := strings.Split(key, ",")
+		if len(parts) != 4 {
+			continue
+		}
+		region, class, redundancy, tierName := parts[0], parts[1], parts[2], parts[3]
+		if redundancy != azureDiskRedundancyLRS {
+			continue
+		}
+		tiers := tiersForStorageClass(class)
+		var sizeGiB int
+		found := false
+		for _, tier := range tiers {
+			if tier.Name == tierName {
+				sizeGiB = tier.SizeGiB
+				found = true
+				break
+			}
+		}
+		if !found {
+			continue
+		}
+		hourly, err := parsePrice(pricing.PV.Cost)
+		if err != nil {
+			continue
+		}
+		ck := diskClassKey(region, class)
+		if existing, ok := best[ck]; ok && existing.sizeGiB <= sizeGiB {
+			continue
+		}
+		best[ck] = candidate{sizeGiB: sizeGiB, hourly: hourly, region: region, class: class}
+	}
+
+	for ck, c := range best {
+		if existing, ok := prices[ck]; ok && existing != nil && existing.PV != nil && existing.PV.Cost != "" {
+			continue
+		}
+		rate := effectiveGiBHourRateFromHourly(c.hourly, float64(c.sizeGiB))
+		rateStr := formatPrice(rate)
+		log.Debugf("Adding PV class fallback key from available tiers: %s, Cost: %s", ck, rateStr)
+		prices[ck] = &AzurePricing{
+			PV: &models.PV{
+				Cost:   rateStr,
+				Class:  c.class,
+				Region: c.region,
+			},
+		}
+	}
+}
+
+// collectManagedDiskTierHourly collects the hourly cost of managed disk tiers from the provided pricing data.
+func collectManagedDiskTierHourly(prices map[string]*AzurePricing) map[string]float64 {
+	tierHourly := map[string]float64{}
+	for key, pricing := range prices {
+		if !isManagedDiskTierKey(key) || pricing == nil || pricing.PV == nil || pricing.PV.Cost == "" {
+			continue
+		}
+		hourly, err := parsePrice(pricing.PV.Cost)
+		if err != nil {
+			continue
+		}
+		tierHourly[key] = hourly
+	}
+	return tierHourly
+}
+
+// removeManagedDiskTierEntries removes managed disk tier entries from the provided pricing data.
+func removeManagedDiskTierEntries(prices map[string]*AzurePricing) {
+	for key := range prices {
+		if isManagedDiskTierKey(key) {
+			delete(prices, key)
+		}
+	}
+}
+
 // determineCloudByRegion uses region name to pick the correct Cloud Environment for the azure provider to use
 func determineCloudByRegion(region string) azure.Environment {
 	lcRegion := strings.ToLower(region)
@@ -1371,6 +1504,10 @@ type azurePvKey struct {
 	StorageClassParameters map[string]string
 	DefaultRegion          string
 	ProviderId             string
+	SizeGiB                float64
+	DiskStorageClass       string // premium_ssd / standard_ssd / standard_hdd when managed disk
+	DiskRedundancy         string // LRS / ZRS when managed disk
+	IsAzureFiles           bool
 }
 
 func (az *Azure) GetPVKey(pv *clustercache.PersistentVolume, parameters map[string]string, defaultRegion string) models.PVKey {
@@ -1378,13 +1515,16 @@ func (az *Azure) GetPVKey(pv *clustercache.PersistentVolume, parameters map[stri
 	if pv.Spec.AzureDisk != nil {
 		providerID = pv.Spec.AzureDisk.DiskName
 	}
-	return &azurePvKey{
+	key := &azurePvKey{
 		Labels:                 pv.Labels,
 		StorageClass:           pv.Spec.StorageClassName,
 		StorageClassParameters: parameters,
 		DefaultRegion:          defaultRegion,
 		ProviderId:             providerID,
+		SizeGiB:                pvSizeGiB(pv),
 	}
+	key.resolveSKU()
+	return key
 }
 
 func (key *azurePvKey) ID() string {
@@ -1395,33 +1535,70 @@ func (key *azurePvKey) GetStorageClass() string {
 	return key.StorageClass
 }
 
-func (key *azurePvKey) Features() string {
-	storageClass := key.StorageClassParameters["storageaccounttype"]
-	diskSKU := key.StorageClassParameters["skuname"]
+// resolveSKU populates DiskStorageClass / DiskRedundancy / IsAzureFiles from
+// StorageClass parameters (CSI skuname, legacy storageaccounttype, or file skuName).
+func (key *azurePvKey) resolveSKU() {
+	if key.StorageClassParameters == nil {
+		return
+	}
+	diskParam := key.StorageClassParameters["storageaccounttype"]
+	if diskParam == "" {
+		diskParam = key.StorageClassParameters["skuname"]
+	}
+	if diskParam != "" {
+		if sku, ok := resolveDiskSKU(diskParam); ok {
+			key.DiskStorageClass = sku.StorageClass
+			key.DiskRedundancy = sku.Redundancy
+			return
+		}
+	}
 	fileSKU := key.StorageClassParameters["skuName"]
+	if strings.EqualFold(fileSKU, "Premium_LRS") {
+		key.DiskStorageClass = AzureFilePremiumStorageClass
+		key.IsAzureFiles = true
+	} else if strings.EqualFold(fileSKU, "Standard_LRS") {
+		key.DiskStorageClass = AzureFileStandardStorageClass
+		key.IsAzureFiles = true
+	}
+}
+
+func (key *azurePvKey) region() string {
+	if region, ok := util.GetRegion(key.Labels); ok {
+		return region
+	}
+	return key.DefaultRegion
+}
+
+func (key *azurePvKey) legacyStorageClass() string {
+	if key.StorageClassParameters == nil {
+		return ""
+	}
+	storageClass := key.StorageClassParameters["storageaccounttype"]
 	if storageClass == "" {
-		storageClass = diskSKU
+		storageClass = key.StorageClassParameters["skuname"]
 	}
 	if storageClass != "" {
-		if strings.EqualFold(storageClass, "Premium_LRS") {
-			storageClass = AzureDiskPremiumSSDStorageClass
-		} else if strings.EqualFold(storageClass, "StandardSSD_LRS") {
-			storageClass = AzureDiskStandardSSDStorageClass
-		} else if strings.EqualFold(storageClass, "Standard_LRS") {
-			storageClass = AzureDiskStandardStorageClass
-		}
-	} else {
-		if strings.EqualFold(fileSKU, "Premium_LRS") {
-			storageClass = AzureFilePremiumStorageClass
-		} else if strings.EqualFold(fileSKU, "Standard_LRS") {
-			storageClass = AzureFileStandardStorageClass
-		}
+		return storageClass
 	}
-	if region, ok := util.GetRegion(key.Labels); ok {
-		return region + "," + storageClass
+	fileSKU := key.StorageClassParameters["skuName"]
+	if strings.EqualFold(fileSKU, "Premium_LRS") {
+		return AzureFilePremiumStorageClass
+	}
+	if strings.EqualFold(fileSKU, "Standard_LRS") {
+		return AzureFileStandardStorageClass
 	}
+	return ""
+}
 
-	return key.DefaultRegion + "," + storageClass
+func (key *azurePvKey) Features() string {
+	if key.DiskStorageClass == "" {
+		key.resolveSKU()
+	}
+	storageClass := key.DiskStorageClass
+	if storageClass == "" {
+		storageClass = key.legacyStorageClass()
+	}
+	return diskClassKey(key.region(), storageClass)
 }
 
 func (*Azure) GetAddresses() ([]byte, error) {
@@ -1562,6 +1739,9 @@ func (az *Azure) GetOrphanedResources() ([]models.OrphanedResource, error) {
 }
 
 func (az *Azure) findCostForDisk(d *compute.Disk) (float64, error) {
+	az.DownloadPricingDataLock.RLock()
+	defer az.DownloadPricingDataLock.RUnlock()
+
 	if d == nil {
 		return 0.0, fmt.Errorf("disk is empty")
 	}
@@ -1570,40 +1750,73 @@ func (az *Azure) findCostForDisk(d *compute.Disk) (float64, error) {
 		return 0.0, fmt.Errorf("disk sku is nil")
 	}
 
-	storageClass := string(d.Sku.Name)
-	if strings.EqualFold(storageClass, "Premium_LRS") {
-		storageClass = AzureDiskPremiumSSDStorageClass
-	} else if strings.EqualFold(storageClass, "StandardSSD_LRS") {
-		storageClass = AzureDiskStandardSSDStorageClass
-	} else if strings.EqualFold(storageClass, "Standard_LRS") {
-		storageClass = AzureDiskStandardStorageClass
-	}
-
 	loc := ""
 	if d.Location != nil {
 		loc = *d.Location
 	}
-	key := loc + "," + storageClass
 
+	if d.DiskProperties == nil {
+		return 0.0, fmt.Errorf("disk properties are nil")
+	}
+	if d.DiskSizeGB == nil {
+		return 0.0, fmt.Errorf("disk size is nil")
+	}
+	sizeGiB := float64(*d.DiskSizeGB)
+
+	if sku, ok := resolveDiskSKU(string(d.Sku.Name)); ok {
+		tier, ok := selectDiskTier(sku.StorageClass, sizeGiB)
+		if !ok {
+			return 0.0, fmt.Errorf("failed to select disk tier for sku %s size %g", d.Sku.Name, sizeGiB)
+		}
+		hasPrice := func(tierName string) bool {
+			_, ok := az.managedDiskTierHourly[diskTierKey(loc, sku.StorageClass, sku.Redundancy, tierName)]
+			return ok
+		}
+		pricedTier, ok := pickSizedOrLargerAvailableTier(sku.StorageClass, tier.Name, hasPrice)
+		if ok {
+			if pricedTier.Name != tier.Name {
+				log.Warnf("Azure disk tier meter %s missing for %s %s; using next available larger tier %s",
+					tier.Name, sku.StorageClass, sku.Redundancy, pricedTier.Name)
+			}
+			tierKey := diskTierKey(loc, sku.StorageClass, sku.Redundancy, pricedTier.Name)
+			hourly, ok := az.managedDiskTierHourly[tierKey]
+			if !ok {
+				return 0.0, fmt.Errorf("failed to find pricing for key: %s", tierKey)
+			}
+			return hourly * timeutil.HoursPerMonth, nil
+		}
+		if sku.Redundancy == azureDiskRedundancyZRS {
+			log.Warnf("Azure ZRS disk pricing unavailable for %s size %g; falling back to LRS class rate", loc, sizeGiB)
+		} else {
+			log.Warnf("Azure disk tier pricing unavailable for %s; falling back to linearized class rate",
+				diskTierKey(loc, sku.StorageClass, sku.Redundancy, tier.Name))
+		}
+		// Fall back to linearized class rate × size when no tier meter is available.
+		classKey := diskClassKey(loc, sku.StorageClass)
+		if p, ok := az.Pricing[classKey]; ok && p != nil && p.PV != nil {
+			diskPricePerGBHour, err := parsePrice(p.PV.Cost)
+			if err != nil {
+				return 0.0, fmt.Errorf("error converting to float: %s", err)
+			}
+			return diskPricePerGBHour * timeutil.HoursPerMonth * sizeGiB, nil
+		}
+		return 0.0, fmt.Errorf("failed to find pricing for key: %s", diskTierKey(loc, sku.StorageClass, sku.Redundancy, tier.Name))
+	}
+
+	// Unknown / custom SKU names: preserve legacy class-key lookup.
+	storageClass := string(d.Sku.Name)
+	key := loc + "," + storageClass
 	if p, ok := az.Pricing[key]; !ok || p == nil {
 		return 0.0, fmt.Errorf("failed to find pricing for key: %s", key)
 	}
 	if az.Pricing[key].PV == nil {
 		return 0.0, fmt.Errorf("pricing for key '%s' has nil PV", key)
 	}
-	diskPricePerGBHour, err := strconv.ParseFloat(az.Pricing[key].PV.Cost, 64)
+	diskPricePerGBHour, err := parsePrice(az.Pricing[key].PV.Cost)
 	if err != nil {
 		return 0.0, fmt.Errorf("error converting to float: %s", err)
 	}
-	if d.DiskProperties == nil {
-		return 0.0, fmt.Errorf("disk properties are nil")
-	}
-	if d.DiskSizeGB == nil {
-		return 0.0, fmt.Errorf("disk size is nil")
-	}
-	cost := diskPricePerGBHour * timeutil.HoursPerMonth * float64(*d.DiskSizeGB)
-
-	return cost, nil
+	return diskPricePerGBHour * timeutil.HoursPerMonth * sizeGiB, nil
 }
 
 func (az *Azure) ClusterInfo() (map[string]string, error) {
@@ -1731,6 +1944,10 @@ func (az *Azure) PVPricing(pvk models.PVKey) (*models.PV, error) {
 	az.DownloadPricingDataLock.RLock()
 	defer az.DownloadPricingDataLock.RUnlock()
 
+	if key, ok := pvk.(*azurePvKey); ok {
+		return az.pvPricingFromAzureKey(key)
+	}
+
 	pricing, ok := az.Pricing[pvk.Features()]
 	if !ok {
 		log.Debugf("Persistent Volume pricing not found for %s: %s", pvk.GetStorageClass(), pvk.Features())
@@ -1739,6 +1956,97 @@ func (az *Azure) PVPricing(pvk models.PVKey) (*models.PV, error) {
 	return pricing.PV, nil
 }
 
+// pvPricingFromAzureKey returns an effective $/GiB-hour rate for the PV.
+// Managed disks use Azure size-tier meters; the rate is chosen so that
+// rate × reportedSizeGiB × hours equals the tier's hourly cost.
+func (az *Azure) pvPricingFromAzureKey(key *azurePvKey) (*models.PV, error) {
+	if key.DiskStorageClass == "" {
+		key.resolveSKU()
+	}
+	region := key.region()
+
+	if key.IsAzureFiles || key.DiskStorageClass == AzureFilePremiumStorageClass || key.DiskStorageClass == AzureFileStandardStorageClass {
+		pricing, ok := az.Pricing[diskClassKey(region, key.DiskStorageClass)]
+		if !ok || pricing == nil || pricing.PV == nil {
+			log.Debugf("Persistent Volume pricing not found for %s: %s", key.GetStorageClass(), key.Features())
+			return &models.PV{}, nil
+		}
+		return pricing.PV, nil
+	}
+
+	if key.DiskStorageClass == "" {
+		legacyStorageClass := key.legacyStorageClass()
+		if legacyStorageClass != "" {
+			pricing, ok := az.Pricing[diskClassKey(region, legacyStorageClass)]
+			if ok && pricing != nil && pricing.PV != nil {
+				return pricing.PV, nil
+			}
+		}
+		log.Debugf("Persistent Volume pricing not found for %s: %s", key.GetStorageClass(), key.Features())
+		return &models.PV{}, nil
+	}
+
+	redundancy := key.DiskRedundancy
+	if redundancy == "" {
+		redundancy = azureDiskRedundancyLRS
+	}
+
+	if key.SizeGiB > 0 {
+		tier, ok := selectDiskTier(key.DiskStorageClass, key.SizeGiB)
+		if !ok {
+			log.Warnf("No disk tier for storage class %s size %g; falling back to class rate", key.DiskStorageClass, key.SizeGiB)
+			return az.pvClassFallback(region, key.DiskStorageClass, redundancy)
+		}
+		hasPrice := func(tierName string) bool {
+			_, ok := az.managedDiskTierHourly[diskTierKey(region, key.DiskStorageClass, redundancy, tierName)]
+			return ok
+		}
+		pricedTier, ok := pickSizedOrLargerAvailableTier(key.DiskStorageClass, tier.Name, hasPrice)
+		if !ok {
+			log.Warnf("Persistent Volume tier pricing not found for %s size %g (%s); falling back to class rate",
+				diskTierKey(region, key.DiskStorageClass, redundancy, tier.Name), key.SizeGiB, redundancy)
+			return az.pvClassFallback(region, key.DiskStorageClass, redundancy)
+		}
+		if pricedTier.Name != tier.Name {
+			log.Warnf("Azure disk tier meter %s missing for %s; using next available larger tier %s",
+				tier.Name, diskClassKey(region, key.DiskStorageClass), pricedTier.Name)
+		}
+		tierKey := diskTierKey(region, key.DiskStorageClass, redundancy, pricedTier.Name)
+		hourly, ok := az.managedDiskTierHourly[tierKey]
+		if !ok {
+			log.Warnf("Persistent Volume tier pricing not found for %s size %g (%s); falling back to class rate",
+				tierKey, key.SizeGiB, redundancy)
+			return az.pvClassFallback(region, key.DiskStorageClass, redundancy)
+		}
+		rate := effectiveGiBHourRateFromHourly(hourly, key.SizeGiB)
+		return &models.PV{
+			Cost:   formatPrice(rate),
+			Class:  key.DiskStorageClass,
+			Region: region,
+			Size:   strconv.FormatFloat(key.SizeGiB, 'f', -1, 64),
+		}, nil
+	}
+
+	if redundancy == azureDiskRedundancyZRS {
+		log.Warnf("Persistent Volume size unknown for ZRS volume %s; using LRS linearized class rate", key.Features())
+	} else {
+		log.Debugf("Persistent Volume size unknown for %s; using linearized class rate", key.Features())
+	}
+	return az.pvClassFallback(region, key.DiskStorageClass, redundancy)
+}
+
+func (az *Azure) pvClassFallback(region, storageClass, redundancy string) (*models.PV, error) {
+	if redundancy == azureDiskRedundancyZRS {
+		log.Warnf("Azure ZRS class-level pricing is unavailable; using LRS linearized rate for %s,%s", region, storageClass)
+	}
+	pricing, ok := az.Pricing[diskClassKey(region, storageClass)]
+	if !ok || pricing == nil || pricing.PV == nil {
+		log.Debugf("Persistent Volume pricing not found for %s,%s", region, storageClass)
+		return &models.PV{}, nil
+	}
+	return pricing.PV, nil
+}
+
 func (az *Azure) ServiceAccountStatus() *models.ServiceAccountStatus {
 	return az.ServiceAccountChecks.GetStatus()
 }

+ 319 - 3
pkg/cloud/azure/provider_test.go

@@ -79,16 +79,20 @@ func TestConvertMeterToPricings(t *testing.T) {
 	})
 
 	t.Run("storage", func(t *testing.T) {
-		info := meterInfo("Storage", "Some SSD type", "P4 are good", "US East", 2000)
+		info := meterInfo("Storage", "Premium SSD Managed Disks", "P4 LRS Disk", "US East", 2000)
 		results, err := convertMeterToPricings(info, regions, baseCPUPrice)
 		require.NoError(t, err)
 
+		expectedHourly := formatPrice(tierHourlyFromMonthly(2000))
 		expected := map[string]*AzurePricing{
-			"useast,premium_ssd": {
-				PV: &models.PV{Cost: "0.085616", Region: "useast"},
+			"useast,premium_ssd,LRS,P4": {
+				PV: &models.PV{Cost: expectedHourly, Class: AzureDiskPremiumSSDStorageClass, Region: "useast", Size: "P4"},
 			},
 		}
 		require.Equal(t, expected, results)
+
+		ensureDiskClassFallbacks(results)
+		require.Equal(t, "0.085616", results["useast,premium_ssd"].PV.Cost)
 	})
 
 	t.Run("virtual machines", func(t *testing.T) {
@@ -188,6 +192,304 @@ func TestSelectRetailPrice(t *testing.T) {
 	}
 }
 
+func TestConvertMeterToPricings_PremiumSSDIgnoresDiskMount(t *testing.T) {
+	regions := map[string]string{
+		"centralus": "Central US",
+	}
+	baseCPUPrice := "0.30000"
+
+	meterInfo := func(category, subcategory, name, region string, rate float64) commerce.MeterInfo {
+		return commerce.MeterInfo{
+			MeterCategory:    &category,
+			MeterSubCategory: &subcategory,
+			MeterName:        &name,
+			MeterRegion:      &region,
+			MeterRates:       map[string]*float64{"0": &rate},
+		}
+	}
+
+	// Order matters: Disk first, Disk Mount second mirrors the Azure Rate Card
+	// sort order and reproduces the overwrite bug.
+	meters := []commerce.MeterInfo{
+		meterInfo("Storage", "Premium SSD Managed Disks", "P4 LRS Disk", "US Central", 5.2795),
+		meterInfo("Storage", "Premium SSD Managed Disks", "P4 LRS Disk Mount", "US Central", 0.32),
+	}
+
+	result := map[string]*AzurePricing{}
+	for _, meter := range meters {
+		pricings, err := convertMeterToPricings(meter, regions, baseCPUPrice)
+		require.NoError(t, err)
+		for key, pricing := range pricings {
+			result[key] = pricing
+		}
+	}
+
+	pricing := result["centralus,premium_ssd,LRS,P4"]
+	require.NotNil(t, pricing)
+	require.NotNil(t, pricing.PV)
+	// Must reflect the Disk hourly price, not the Disk Mount price.
+	require.Equal(t, formatPrice(tierHourlyFromMonthly(5.2795)), pricing.PV.Cost,
+		"premium_ssd P4 pricing must use 'P4 LRS Disk' meter and ignore 'P4 LRS Disk Mount'")
+	_, mountPresent := result["centralus,premium_ssd,LRS,P4 Mount"]
+	require.False(t, mountPresent)
+}
+
+func TestRemoveManagedDiskTierEntries_KeepWindowsNodeKey(t *testing.T) {
+	prices := map[string]*AzurePricing{
+		"centralus,premium_ssd,LRS,P4": {
+			PV: &models.PV{
+				Cost:   formatPrice(tierHourlyFromMonthly(5.2795)),
+				Class:  AzureDiskPremiumSSDStorageClass,
+				Region: "centralus",
+				Size:   "P4",
+			},
+		},
+		"centralus,Standard_D2s_v3,ondemand,windows": {
+			Node: &models.Node{
+				Cost:         "0.300000",
+				BaseCPUPrice: "0.30000",
+				UsageType:    "ondemand",
+			},
+		},
+	}
+
+	removeManagedDiskTierEntries(prices)
+
+	_, diskTierPresent := prices["centralus,premium_ssd,LRS,P4"]
+	require.False(t, diskTierPresent)
+
+	_, windowsPresent := prices["centralus,Standard_D2s_v3,ondemand,windows"]
+	require.True(t, windowsPresent)
+}
+
+func TestSelectDiskTier(t *testing.T) {
+	cases := []struct {
+		name         string
+		storageClass string
+		sizeGiB      float64
+		wantTier     string
+		wantOK       bool
+	}{
+		{name: "10 gib premium maps to P3", storageClass: AzureDiskPremiumSSDStorageClass, sizeGiB: 10, wantTier: "P3", wantOK: true},
+		{name: "32 gib premium maps to P4", storageClass: AzureDiskPremiumSSDStorageClass, sizeGiB: 32, wantTier: "P4", wantOK: true},
+		{name: "33 gib premium maps to P6", storageClass: AzureDiskPremiumSSDStorageClass, sizeGiB: 33, wantTier: "P6", wantOK: true},
+		{name: "100 gib premium maps to P10", storageClass: AzureDiskPremiumSSDStorageClass, sizeGiB: 100, wantTier: "P10", wantOK: true},
+		{name: "512 gib standard ssd maps to E20", storageClass: AzureDiskStandardSSDStorageClass, sizeGiB: 512, wantTier: "E20", wantOK: true},
+		{name: "64 gib hdd maps to S6", storageClass: AzureDiskStandardStorageClass, sizeGiB: 64, wantTier: "S6", wantOK: true},
+		{name: "over max premium clamps to P80", storageClass: AzureDiskPremiumSSDStorageClass, sizeGiB: 40000, wantTier: "P80", wantOK: true},
+		{name: "zero size", storageClass: AzureDiskPremiumSSDStorageClass, sizeGiB: 0, wantOK: false},
+		{name: "unknown class", storageClass: "unknown", sizeGiB: 10, wantOK: false},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			tier, ok := selectDiskTier(tc.storageClass, tc.sizeGiB)
+			require.Equal(t, tc.wantOK, ok)
+			if tc.wantOK {
+				require.Equal(t, tc.wantTier, tier.Name)
+			}
+		})
+	}
+}
+
+func TestAzurePVPricing_TierAware(t *testing.T) {
+	az := &Azure{
+		Pricing: map[string]*AzurePricing{
+			"centralus,premium_ssd": {
+				PV: &models.PV{Cost: "0.000226", Class: AzureDiskPremiumSSDStorageClass, Region: "centralus"},
+			},
+			"centralus,standard_ssd": {
+				PV: &models.PV{Cost: "0.000103", Class: AzureDiskStandardSSDStorageClass, Region: "centralus"},
+			},
+			"centralus,Custom_LRS": {
+				PV: &models.PV{Cost: "0.100000", Class: "Custom_LRS", Region: "centralus"},
+			},
+		},
+		managedDiskTierHourly: map[string]float64{
+			"centralus,premium_ssd,LRS,P3":   tierHourlyFromMonthly(2.64),
+			"centralus,premium_ssd,LRS,P4":   tierHourlyFromMonthly(5.2795),
+			"centralus,premium_ssd,LRS,P10":  tierHourlyFromMonthly(19.71),
+			"centralus,premium_ssd,ZRS,P3":   tierHourlyFromMonthly(4.0),
+			"centralus,standard_ssd,LRS,E20": tierHourlyFromMonthly(38.4),
+		},
+	}
+
+	t.Run("10 gib premium uses P3 effective rate", func(t *testing.T) {
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       10,
+			StorageClassParameters: map[string]string{
+				"skuname": "Premium_LRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := az.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, formatPrice(effectiveGiBHourRate(2.64, 10)), pv.Cost)
+		require.Equal(t, "10", pv.Size)
+	})
+
+	t.Run("100 gib premium uses P10 effective rate", func(t *testing.T) {
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       100,
+			StorageClassParameters: map[string]string{
+				"skuname": "Premium_LRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := az.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, formatPrice(effectiveGiBHourRate(19.71, 100)), pv.Cost)
+	})
+
+	t.Run("512 gib standard ssd uses E20", func(t *testing.T) {
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       512,
+			StorageClassParameters: map[string]string{
+				"skuname": "StandardSSD_LRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := az.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, formatPrice(effectiveGiBHourRate(38.4, 512)), pv.Cost)
+	})
+
+	t.Run("premium zrs uses zrs meter", func(t *testing.T) {
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       10,
+			StorageClassParameters: map[string]string{
+				"skuname": "Premium_ZRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := az.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, formatPrice(effectiveGiBHourRate(4.0, 10)), pv.Cost)
+	})
+
+	t.Run("missing size falls back to class rate", func(t *testing.T) {
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       0,
+			StorageClassParameters: map[string]string{
+				"skuname": "Premium_LRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := az.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, "0.000226", pv.Cost)
+	})
+
+	t.Run("missing preferred tier falls back to class rate when no larger tier is available", func(t *testing.T) {
+		azMissing := &Azure{
+			Pricing: map[string]*AzurePricing{
+				"centralus,premium_ssd": {
+					PV: &models.PV{Cost: "0.000226", Class: AzureDiskPremiumSSDStorageClass, Region: "centralus"},
+				},
+			},
+			managedDiskTierHourly: map[string]float64{
+				"centralus,premium_ssd,LRS,P4": tierHourlyFromMonthly(5.2795),
+			},
+		}
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       100,
+			StorageClassParameters: map[string]string{
+				"skuname": "Premium_LRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := azMissing.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, "0.000226", pv.Cost)
+	})
+
+	t.Run("zrs missing size falls back to lrs class rate", func(t *testing.T) {
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       0,
+			StorageClassParameters: map[string]string{
+				"skuname": "Premium_ZRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := az.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, "0.000226", pv.Cost)
+	})
+
+	t.Run("unknown skuname preserves legacy lookup behavior", func(t *testing.T) {
+		key := &azurePvKey{
+			DefaultRegion: "centralus",
+			SizeGiB:       1,
+			StorageClassParameters: map[string]string{
+				"skuname": "Custom_LRS",
+			},
+		}
+		key.resolveSKU()
+		pv, err := az.PVPricing(key)
+		require.NoError(t, err)
+		require.Equal(t, "0.100000", pv.Cost)
+	})
+}
+
+func TestEnsureDiskClassFallbacks_UsesSmallestAvailableTier(t *testing.T) {
+	prices := map[string]*AzurePricing{
+		"useast,premium_ssd,LRS,P4": {
+			PV: &models.PV{Cost: formatPrice(tierHourlyFromMonthly(5.2795)), Class: AzureDiskPremiumSSDStorageClass, Region: "useast", Size: "P4"},
+		},
+		"useast,premium_ssd,LRS,P10": {
+			PV: &models.PV{Cost: formatPrice(tierHourlyFromMonthly(19.71)), Class: AzureDiskPremiumSSDStorageClass, Region: "useast", Size: "P10"},
+		},
+	}
+	ensureDiskClassFallbacks(prices)
+	require.NotNil(t, prices["useast,premium_ssd"])
+	require.Equal(t, formatPrice(effectiveGiBHourRate(5.2795, 32)), prices["useast,premium_ssd"].PV.Cost)
+}
+
+func TestManagedDiskTierPricing_IsNotExportedInPricingMap(t *testing.T) {
+	prices := map[string]*AzurePricing{
+		"centralus,premium_ssd,LRS,P4": {
+			PV: &models.PV{Cost: formatPrice(tierHourlyFromMonthly(5.2795)), Class: AzureDiskPremiumSSDStorageClass, Region: "centralus", Size: "P4"},
+		},
+		"centralus,premium_ssd": {
+			PV: &models.PV{Cost: "0.000226", Class: AzureDiskPremiumSSDStorageClass, Region: "centralus"},
+		},
+	}
+
+	tierHourly := collectManagedDiskTierHourly(prices)
+	removeManagedDiskTierEntries(prices)
+
+	require.Contains(t, tierHourly, "centralus,premium_ssd,LRS,P4")
+	require.NotContains(t, prices, "centralus,premium_ssd,LRS,P4")
+	require.Contains(t, prices, "centralus,premium_ssd")
+}
+
+func TestFindCostForDisk_TierAware(t *testing.T) {
+	var loc = "centralus"
+	var size int32 = 100
+	az := &Azure{
+		managedDiskTierHourly: map[string]float64{
+			"centralus,premium_ssd,LRS,P10": tierHourlyFromMonthly(19.71),
+		},
+	}
+	cost, err := az.findCostForDisk(&compute.Disk{
+		Location: &loc,
+		Sku: &compute.DiskSku{
+			Name: "Premium_LRS",
+		},
+		DiskProperties: &compute.DiskProperties{
+			DiskSizeGB: &size,
+		},
+	})
+	require.NoError(t, err)
+	require.InDelta(t, 19.71, cost, 0.0001)
+}
+
 func TestAzure_findCostForDisk(t *testing.T) {
 	var loc string = "location"
 	var size int32 = 1
@@ -360,6 +662,13 @@ func TestAzurePVKeyFeatures(t *testing.T) {
 			},
 			expected: "eastus,premium_ssd",
 		},
+		{
+			name: "managed disk csi skuname premium zrs",
+			parameters: map[string]string{
+				"skuname": "Premium_ZRS",
+			},
+			expected: "eastus,premium_ssd",
+		},
 		{
 			name: "managed disk csi skuname standard ssd",
 			parameters: map[string]string{
@@ -388,6 +697,13 @@ func TestAzurePVKeyFeatures(t *testing.T) {
 			},
 			expected: "eastus,standard_smb",
 		},
+		{
+			name: "unknown skuname keeps raw legacy class key",
+			parameters: map[string]string{
+				"skuname": "Custom_LRS",
+			},
+			expected: "eastus,Custom_LRS",
+		},
 	}
 
 	for _, tc := range tests {