@@ -2637,7 +2637,114 @@ def find_by_name(resources: Any, type_name: str) -> Any:
26372637 return next (x for x in resources if x ["type" ] == type_name )
26382638
26392639
2640+ def get_vhd_details_by_resource_graph (
2641+ platform : "AzurePlatform" , vhd_path : str
2642+ ) -> Optional [Dict [str , str ]]:
2643+ """
2644+ Resolve VHD details by querying Azure Resource Graph first.
2645+
2646+ This avoids expensive per-subscription list operations when only a VHD URL is
2647+ provided and subscription/resource group information is needed.
2648+ """
2649+ log = platform ._log
2650+ matched = STORAGE_CONTAINER_BLOB_PATTERN .match (vhd_path )
2651+ assert matched , f"fail to get matched info from { vhd_path } "
2652+ sc_name = matched .group ("sc" )
2653+ container_name = matched .group ("container" )
2654+ blob_name = matched .group ("blob" )
2655+
2656+ try :
2657+ from azure .mgmt .resourcegraph import ResourceGraphClient
2658+ from azure .mgmt .resourcegraph .models import QueryRequest
2659+ except ImportError :
2660+ # Keep backward compatibility when resource graph dependency isn't present.
2661+ return None
2662+
2663+ try :
2664+ subscription_client = SubscriptionClient (platform .credential )
2665+ with global_credential_access_lock :
2666+ subscription_ids = [
2667+ x .subscription_id
2668+ for x in subscription_client .subscriptions .list ()
2669+ if x .subscription_id
2670+ ]
2671+ if not subscription_ids :
2672+ return None
2673+
2674+ rg_client = ResourceGraphClient (platform .credential )
2675+ escaped_name = sc_name .replace ("'" , "''" )
2676+ query = (
2677+ "Resources "
2678+ "| where type =~ 'microsoft.storage/storageaccounts' "
2679+ f"| where name =~ '{ escaped_name } ' "
2680+ "| project subscriptionId, resourceGroup, name, location"
2681+ )
2682+ with global_credential_access_lock :
2683+ response = rg_client .resources (
2684+ QueryRequest (subscriptions = subscription_ids , query = query )
2685+ )
2686+
2687+ data = getattr (response , "data" , None )
2688+ if not data :
2689+ return None
2690+
2691+ records : List [Dict [str , Any ]] = []
2692+ if isinstance (data , list ):
2693+ records = [x for x in data if isinstance (x , dict )]
2694+ elif isinstance (data , dict ):
2695+ columns = data .get ("columns" )
2696+ rows = data .get ("rows" )
2697+ if isinstance (columns , list ) and isinstance (rows , list ):
2698+ column_names = [
2699+ str (x .get ("name" )) for x in columns if isinstance (x , dict )
2700+ ]
2701+ for row in rows :
2702+ if isinstance (row , list ) and len (row ) == len (column_names ):
2703+ records .append (dict (zip (column_names , row )))
2704+
2705+ for item in records :
2706+ if str (item .get ("name" , "" )).lower () != sc_name .lower ():
2707+ continue
2708+
2709+ subscription_id = str (item .get ("subscriptionId" , "" ))
2710+ resource_group_name = str (item .get ("resourceGroup" , "" ))
2711+ location = str (item .get ("location" , "" ))
2712+ if not subscription_id or not resource_group_name or not location :
2713+ continue
2714+
2715+ log .debug (
2716+ "resolved vhd details via resource graph: "
2717+ f"storage_account={ sc_name } , subscription={ subscription_id } , "
2718+ f"resource_group={ resource_group_name } , location={ location } "
2719+ )
2720+
2721+ return {
2722+ "location" : location ,
2723+ "resource_group_name" : resource_group_name ,
2724+ "account_name" : sc_name ,
2725+ "container_name" : container_name ,
2726+ "blob_name" : blob_name ,
2727+ "subscription" : subscription_id ,
2728+ }
2729+
2730+ except Exception :
2731+ # Fall back to existing logic below if Resource Graph is unavailable
2732+ # transiently or blocked by permissions.
2733+ log .debug (
2734+ "resource graph lookup failed; falling back to storage account traversal" ,
2735+ exc_info = True ,
2736+ )
2737+ return None
2738+
2739+ return None
2740+
2741+
26402742def get_vhd_details (platform : "AzurePlatform" , vhd_path : str ) -> Any :
2743+ # Prefer Resource Graph lookup for faster and more reliable resolution.
2744+ rg_result = get_vhd_details_by_resource_graph (platform , vhd_path )
2745+ if rg_result :
2746+ return rg_result
2747+
26412748 matched = STORAGE_CONTAINER_BLOB_PATTERN .match (vhd_path )
26422749 assert matched , f"fail to get matched info from { vhd_path } "
26432750 sc_name = matched .group ("sc" )
@@ -2694,6 +2801,8 @@ def find_storage_account(
26942801 (an object) is not reliably hashable. Instead, we use a module-level
26952802 dict cache keyed by subscription_id.
26962803 """
2804+ log = platform ._log
2805+
26972806 # Check cache first
26982807 if subscription_id not in _storage_account_cache :
26992808 storage_client = get_storage_client (
@@ -2708,9 +2817,26 @@ def find_storage_account(
27082817 # triggers additional API calls during iteration. Frequent or concurrent
27092818 # iterations can exceed Azure's request limits, resulting in throttling
27102819 # errors. To mitigate this, we cache the full list of storage accounts.
2711- _storage_account_cache [subscription_id ] = list (
2712- storage_client .storage_accounts .list ()
2713- )
2820+ for attempt in range (1 , 4 ):
2821+ try :
2822+ sc_list = storage_client .storage_accounts .list ()
2823+ _storage_account_cache [subscription_id ] = list (sc_list )
2824+ break
2825+ except Exception as e :
2826+ log .exception (
2827+ "failed to load storage account cache for "
2828+ f"subscription '{ subscription_id } ' on "
2829+ f"attempt { attempt } /3" ,
2830+ exc_info = e ,
2831+ )
2832+ if attempt < 3 :
2833+ sleep (2 * attempt )
2834+ continue
2835+ raise LisaException (
2836+ "failed to load storage account cache for "
2837+ f"subscription '{ subscription_id } ' "
2838+ f"after 3 attempts: { e } "
2839+ ) from e
27142840
27152841 # Search in cached list
27162842 for sc in _storage_account_cache [subscription_id ]:
0 commit comments