-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathazure_common.py
More file actions
421 lines (333 loc) · 14 KB
/
azure_common.py
File metadata and controls
421 lines (333 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
#!/usr/bin/env python3
# ex: set filetype=python:
"""
Common utilities for Azure Kconfig generation scripts.
This module provides shared functionality for Azure-specific Kconfig
generation, including region discovery, default region detection, and
Jinja2 template management.
"""
import os
import sys
import json
from configparser import ConfigParser
from jinja2 import Environment, FileSystemLoader
class AzureNotConfiguredError(Exception):
"""Raised when Azure credentials are not available."""
pass
def get_default_region():
"""
Get the default Azure region from Azure configuration.
Returns:
str: Default region, or 'westus' if no default is found.
"""
try:
from azure.common.credentials import get_cli_profile
# Check if user is authenticated by getting the CLI profile
# This reuses the 'az login' session without subprocess calls
profile = get_cli_profile()
_, _, _ = profile.get_login_credentials(resource="https://management.azure.com")
# Azure doesn't have a per-account default region like AWS
# Check environment variable first
if "AZURE_DEFAULTS_LOCATION" in os.environ:
return os.environ["AZURE_DEFAULTS_LOCATION"]
# Try to read from azure config
config_path = os.path.expanduser("~/.azure/config")
if os.path.exists(config_path):
try:
config = ConfigParser()
config.read(config_path)
if "defaults" in config and "location" in config["defaults"]:
return config["defaults"]["location"]
except Exception:
pass
# Default fallback
return "westus"
except Exception as e:
print(f"Warning: Error reading Azure config: {e}", file=sys.stderr)
print("Ensure you are logged in with 'az login'", file=sys.stderr)
return "westus"
def get_jinja2_environment(template_path=None):
"""
Create a standardized Jinja2 environment for template rendering.
Args:
template_path (str): Path to template directory. If None, uses caller's directory.
Returns:
Environment: Configured Jinja2 Environment object
"""
if template_path is None:
template_path = sys.path[0]
return Environment(
loader=FileSystemLoader(template_path),
trim_blocks=True,
lstrip_blocks=True,
)
def get_all_regions(quiet=False):
"""
Retrieve the list of all Azure regions using the Azure SDK.
Returns:
list: List of region dictionaries with name, displayName, and metadata
"""
if not quiet:
print("Querying Azure for available regions...", file=sys.stderr)
try:
from azure.common.credentials import get_cli_profile
from azure.mgmt.resource import SubscriptionClient
# Get credentials from Azure CLI profile (reuses 'az login' session)
profile = get_cli_profile()
credentials, subscription_id, _ = profile.get_login_credentials(
resource="https://management.azure.com"
)
# Query regions using SDK
subscription_client = SubscriptionClient(credentials)
locations = subscription_client.subscriptions.list_locations(subscription_id)
# Convert SDK objects to dict format
region_list = []
for location in locations:
# Skip logical regions (not for general use)
if location.metadata and location.metadata.region_type == "Logical":
continue
# Convert metadata to dict with camelCase keys
# Only convert the fields we actually use to minimize overhead
metadata = {}
if location.metadata:
meta = location.metadata
metadata = {
"physicalLocation": meta.physical_location,
"pairedRegion": meta.paired_region,
}
region_list.append(
{
"name": location.name or "",
"displayName": location.display_name or "",
"regionalDisplayName": location.regional_display_name or "",
"metadata": metadata,
}
)
return sorted(region_list, key=lambda x: x["name"])
except Exception as e:
if not quiet:
print(f"Error: Failed to query Azure regions: {e}", file=sys.stderr)
print("Ensure you are logged in with 'az login'", file=sys.stderr)
return []
def get_region_kconfig_name(region_name):
"""
Convert an Azure region name to a Kconfig variable name.
Args:
region_name (str): Azure region name (e.g., 'westus', 'eastus2')
Returns:
str: Kconfig-safe name (e.g., 'WESTUS', 'EASTUS2')
"""
return region_name.upper().replace("-", "_")
def get_compute_client():
"""
Get an authenticated Azure Compute Management client.
Returns:
tuple: (ComputeManagementClient, subscription_id)
Raises:
Exception: If authentication fails or SDK is not available
"""
from azure.common.credentials import get_cli_profile
from azure.mgmt.compute import ComputeManagementClient
# Get credentials from Azure CLI profile (reuses 'az login' session)
profile = get_cli_profile()
credentials, subscription_id, _ = profile.get_login_credentials(
resource="https://management.azure.com"
)
client = ComputeManagementClient(credentials, subscription_id)
return client, subscription_id
def get_vm_sizes_and_skus(region, quiet=False):
"""
Get all VM sizes and capabilities for a region using a single Azure SDK call.
This function uses the resource SKUs API which provides all the information
from both VM sizes and SKU capabilities in a single efficient API call.
Args:
region (str): Azure region name
quiet (bool): Suppress debug messages
Returns:
tuple: (sizes_list, capabilities_dict) where:
- sizes_list: List of VM size dictionaries in CLI-compatible format
- capabilities_dict: Dict mapping VM size names to their capabilities
"""
if not quiet:
print(f"Fetching VM sizes and capabilities from {region}...", file=sys.stderr)
try:
client, _ = get_compute_client()
# Query resource SKUs using SDK with location filter
# This single API call provides all information we need
skus = list(client.resource_skus.list(filter=f"location eq '{region}'"))
# Filter to VM SKUs only
vm_skus = [s for s in skus if s.resource_type == "virtualMachines"]
# Build both data structures
size_list = []
sku_capabilities = {}
for sku in vm_skus:
if not sku.capabilities:
continue
# Convert capabilities list to dictionary for easy lookup
caps = {cap.name: cap.value for cap in sku.capabilities}
# Extract size information from capabilities
# The resource SKUs API provides the same data as the VM sizes API
try:
cores = int(caps.get("vCPUs", 0))
memory_mb = int(float(caps.get("MemoryGB", 0)) * 1024)
max_disks = int(caps.get("MaxDataDiskCount", 0))
resource_disk_mb = int(caps.get("MaxResourceVolumeMB", 0))
os_disk_mb = int(caps.get("OSVhdSizeMB", 0))
# Build VM size dict in CLI-compatible format
size_list.append(
{
"name": sku.name,
"numberOfCores": cores,
"memoryInMB": memory_mb,
"maxDataDiskCount": max_disks,
"resourceDiskSizeInMB": resource_disk_mb,
"osDiskSizeInMB": os_disk_mb,
}
)
# Store capabilities for this size
sku_capabilities[sku.name] = caps
except (ValueError, TypeError) as e:
if not quiet:
print(
f"Warning: Could not parse capabilities for {sku.name}: {e}",
file=sys.stderr,
)
continue
if not quiet:
print(f" Found {len(size_list)} VM sizes in {region}", file=sys.stderr)
return size_list, sku_capabilities
except Exception as e:
if not quiet:
print(f"Error: Failed to query VM sizes and SKUs: {e}", file=sys.stderr)
print("Ensure you are logged in with 'az login'", file=sys.stderr)
return [], {}
def get_all_offers_and_skus(publisher_id, region, quiet=False, max_workers=10):
"""
Get all offers and SKUs for a publisher using Azure SDK with parallel execution.
This function uses parallel SKU fetching for optimal performance with
publishers that have many offers. The parallelization is safe because
each SKU query is independent and the Azure API supports concurrent requests.
Args:
publisher_id (str): Azure publisher identifier (e.g., "Debian", "RedHat")
region (str): Azure region name
quiet (bool): Suppress debug messages
max_workers (int): Maximum concurrent workers for parallel SKU fetching
Returns:
dict: Dictionary mapping offer names to lists of SKU names
Example: {"debian-12": ["12", "12-arm64"], ...}
"""
from concurrent.futures import ThreadPoolExecutor
if not quiet:
print(f"Querying offers for {publisher_id} in {region}...", file=sys.stderr)
try:
client, _ = get_compute_client()
# Get all offers (single fast API call)
offers = list(client.virtual_machine_images.list_offers(region, publisher_id))
if not offers:
return {}
if not quiet:
print(
f" Found {len(offers)} offers, fetching SKUs in parallel...",
file=sys.stderr,
)
def fetch_skus_for_offer(offer):
"""Helper function to fetch SKUs for a single offer."""
offer_name = offer.name
# Skip test offers and other non-production offers
offer_lower = offer_name.lower()
if any(
skip in offer_lower
for skip in ["test", "preview", "experimental", "-dev", "staging"]
):
return (offer_name, [])
try:
skus = list(
client.virtual_machine_images.list_skus(
region, publisher_id, offer_name
)
)
sku_names = [sku.name for sku in skus if sku.name]
return (offer_name, sku_names)
except Exception as e:
if not quiet:
print(
f" Warning: Failed to get SKUs for {offer_name}: {e}",
file=sys.stderr,
)
return (offer_name, [])
# Fetch SKUs for all offers in parallel
offers_dict = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = executor.map(fetch_skus_for_offer, offers)
for offer_name, sku_names in results:
if sku_names:
offers_dict[offer_name] = sku_names
if not quiet:
print(
f" Found {len(offers_dict)} offers with available SKUs",
file=sys.stderr,
)
return offers_dict
except Exception as e:
if not quiet:
print(
f"Error: Failed to query offers and SKUs for {publisher_id}: {e}",
file=sys.stderr,
)
print("Ensure you are logged in with 'az login'", file=sys.stderr)
return {}
def exit_on_empty_result(result, context, quiet=False):
"""
Exit with error if result is empty or None.
This consolidates the common pattern of checking if an API query
returned results and exiting with appropriate error messaging if not.
Args:
result: Result from API query (list, dict, or other iterable)
context (str): Description of what operation failed
quiet (bool): Suppress error messages
Returns:
Does not return; exits the process with status 1
"""
if not result:
if not quiet:
print(
f"Error: Cannot perform {context}. Check Azure authentication status.",
file=sys.stderr,
)
print("Run 'az login' to authenticate with Azure.", file=sys.stderr)
sys.exit(1)
def require_azure_credentials():
"""
Require Azure credentials, raising an exception if not configured.
This function should be called early in main() to validate Azure
credentials. If Azure is not configured, it raises AzureNotConfiguredError
to let the caller decide how to handle it.
This centralizes the handling of missing Azure credentials and avoids
TOCTOU race conditions from manual file existence checks.
Returns:
str: Subscription ID if credentials are valid
Raises:
AzureNotConfiguredError: If Azure credentials are not found
"""
try:
from azure.common.credentials import get_cli_profile
profile = get_cli_profile()
credentials, subscription_id, _ = profile.get_login_credentials(
resource="https://management.azure.com"
)
return subscription_id
except ImportError as e:
raise AzureNotConfiguredError("Azure SDK not installed") from e
except Exception as e:
# Only treat as "not configured" if it looks like an auth/login issue
error_msg = str(e).lower()
auth_indicators = [
"login",
"logged in",
"authenticate",
"credential",
"az login",
]
if any(phrase in error_msg for phrase in auth_indicators):
raise AzureNotConfiguredError("Azure credentials not found") from e
raise