Hello,
Welcome to Microsoft Q&A,
I have attached the Python script below, which includes optional parameters that you can use to filter the VM sizes according to your needs.
# Lists VM sizes you can actually deploy in a region for a subscription.
# No Microsoft.Quota dependency; uses Compute Usage for quotas (stable).
# Filters:
# - Subscription availability (no NotAvailableForSubscription in region)
# - Optional: Encryption-at-Host support
# - Optional: Hyper-V Gen1 support
# - Optional: Restrict to selected families (e.g., B/D/F)
# Quota checks:
# - Family vCPU headroom (if a family row exists)
# - Regional total vCPU headroom (if present)
# Graceful fallback:
# - If a family quota row is missing, keeps the size unless --strict-family-quota is set.
import argparse
import re
import sys
from typing import Dict, List, Optional, Tuple
from azure.identity import DefaultAzureCredential
from azure.mgmt.compute import ComputeManagementClient
from azure.core.exceptions import HttpResponseError
# ---------- helpers ----------
def norm(s: str) -> str:
return re.sub(r"[^a-z0-9]", "", (s or "").lower())
def cap(capabilities, name: str, default=None):
for c in capabilities or []:
if (c.name or "").lower() == name.lower():
return c.value
return default
def is_restricted_in_loc(sku, loc: str) -> bool:
for r in (sku.restrictions or []):
if r.reason_code == "NotAvailableForSubscription":
info = r.restriction_info
if info and info.locations and loc in info.locations:
return True
return False
def parse_args():
p = argparse.ArgumentParser(description="List deployable VM sizes by capabilities & quota.")
p.add_argument("--subscription", required=True, help="Subscription ID")
p.add_argument("--location", required=True, help="Azure region, e.g., eastus")
p.add_argument("--instances", type=int, default=1, help="How many VMs you plan to deploy")
p.add_argument("--families", nargs="*", default=None,
help="Optional family prefixes to keep (e.g., B D F). Omit to allow all.")
p.add_argument("--require-eah", action="store_true", help="Require Encryption-at-Host support")
p.add_argument("--require-gen1", action="store_true", help="Require Hyper-V Gen1 support")
p.add_argument("--strict-family-quota", action="store_true",
help="Drop sizes with no family quota row (default: keep, relying on regional)")
p.add_argument("--verbose", action="store_true", help="Print debug summary")
return p.parse_args()
# ---------- quota via Compute Usage (stable; no RP registration needed) ----------
def get_usage_quota(compute: ComputeManagementClient, location: str
) -> Tuple[Tuple[Optional[int], Optional[int]], Dict[str, Tuple[int, int, str]]]:
"""
Returns:
regional: (used, limit) for total regional vCPUs (if found; otherwise (None,None))
family_quota: dict key=normalized usage name ("standard<family>family") -> (used, limit, localized_name)
"""
usage = list(compute.usage.list(location))
regional_used = regional_limit = None
family_map: Dict[str, Tuple[int, int, str]] = {}
for u in usage:
name_val = (getattr(u.name, "value", None) or "").lower()
name_loc = (getattr(u.name, "localized_value", None) or "").lower()
# Regional total: match “Total Regional vCPUs” or generic cores
if "total" in name_loc and ("vcpu" in name_loc or "core" in name_loc):
regional_used = u.current_value
regional_limit = u.limit
elif "cores" == name_val and ("total" in name_loc or "regional" in name_loc):
regional_used = u.current_value
regional_limit = u.limit
# Family rows usually look like "Standard Dv5 Family vCPUs"
if "family" in name_loc and ("vcpu" in name_loc or "core" in name_loc):
k = norm(getattr(u.name, "value", "") or u.name.localized_value)
family_map[k] = (u.current_value, u.limit, u.name.localized_value or u.name.value)
return (regional_used, regional_limit), family_map
def find_family_quota_row(family_map: Dict[str, Tuple[int,int,str]], family: str
) -> Optional[Tuple[int,int,str]]:
"""
Try exact normalized key; if not found, try fuzzy match against localized names.
"""
exact_key = f"standard{norm(family)}family"
if exact_key in family_map:
return family_map[exact_key]
# fuzzy: some families differ slightly (e.g., Dsv5 vs Dasv5 may share a family row naming)
f_norm = norm(family)
for k, v in family_map.items():
if f_norm in k:
return v
return None
# ---------- main ----------
def main():
args = parse_args()
cred = DefaultAzureCredential()
compute = ComputeManagementClient(cred, args.subscription)
debug = {"skus_total": 0, "candidates_after_caps": 0, "restricted": 0,
"eah_filtered": 0, "gen1_filtered": 0, "fam_filtered": 0, "no_cores": 0,
"quota_family_missing": 0, "quota_family_blocked": 0, "quota_regional_blocked": 0}
# 1) Gather candidate SKUs by capabilities & subscription availability
candidates: List[dict] = []
try:
for sku in compute.resource_skus.list(filter=f"location eq '{args.location}'"):
debug["skus_total"] += 1
if sku.resource_type != "virtualMachines" or args.location not in (sku.locations or []):
continue
if is_restricted_in_loc(sku, args.location):
debug["restricted"] += 1
continue
fam = sku.family or ""
if args.families:
if not any(fam.upper().startswith(p.upper()) for p in args.families):
debug["fam_filtered"] += 1
continue
if args.require_eah:
if (cap(sku.capabilities, "EncryptionAtHostSupported", "False") or "False").lower() != "true":
debug["eah_filtered"] += 1
continue
if args.require_gen1:
if "v1" not in (cap(sku.capabilities, "HyperVGenerations", "") or "").lower():
debug["gen1_filtered"] += 1
continue
candidates.append({"name": sku.name, "family": fam})
except HttpResponseError as e:
print(f"Failed to list SKUs in {args.location}: {e}", file=sys.stderr)
sys.exit(1)
# 2) Add exact vCPU per size and drop unknown
cores_by_size = {s.name: s.number_of_cores for s in compute.virtual_machine_sizes.list(args.location)}
for c in candidates:
c["vcpus"] = cores_by_size.get(c["name"])
candidates = [c for c in candidates if c["vcpus"]]
debug["candidates_after_caps"] = len(candidates)
if not candidates:
print(f"No candidate VM sizes after capability/availability filtering in {args.location}.")
if args.verbose:
print("[DEBUG]", debug)
sys.exit(2)
# 3) Quota headroom (family + regional if available)
regional, family_map = get_usage_quota(compute, args.location)
reg_used, reg_limit = regional
usable: List[dict] = []
for c in candidates:
need = c["vcpus"] * args.instances
fam_row = find_family_quota_row(family_map, c["family"])
if fam_row is None:
debug["quota_family_missing"] += 1
if args.strict_family_quota:
# require an explicit family row; skip
continue
else:
fam_used, fam_limit, _label = fam_row
if fam_limit is not None and fam_used is not None and (fam_used + need > fam_limit):
debug["quota_family_blocked"] += 1
continue
if reg_used is not None and reg_limit is not None:
if reg_used + need > reg_limit:
debug["quota_regional_blocked"] += 1
continue
usable.append(c)
if not usable:
print(f"No VM sizes satisfy capabilities/availability/quota in {args.location}.")
if args.verbose:
print("[DEBUG]", debug)
if not family_map:
print("[DEBUG] No family quota rows found in this region; "\
"either request per-family vCPU quotas or drop --strict-family-quota.")
sys.exit(3)
# 4) Print
usable.sort(key=lambda x: (x["family"], x["vcpus"], x["name"]))
print(f"Deployable VM sizes in {args.location} (instances={args.instances}):")
print(f"{'Name':24} {'Family':10} {'vCPUs':>5}")
print("-" * 44)
for u in usable:
print(f"{u['name']:24} {u['family']:10} {u['vcpus']:>5}")
if args.verbose:
print("\n[DEBUG]", debug)
if reg_used is not None and reg_limit is not None:
print(f"[DEBUG] Regional vCPUs: used {reg_used} / limit {reg_limit}")
# Show a couple of detected family quota labels (for sanity)
shown = 0
for k, (_, lim, lbl) in family_map.items():
if shown >= 6: break
print(f"[DEBUG] Family quota row: {lbl} (limit={lim})")
shown += 1
if __name__ == "__main__":
main()
Please Upvote and accept the answer if it helps!!