Azure’s Inferno: Escape from API Hell

azure2
There’s a special circle in hell for developers who overcomplicate things, and its homepage is learn.microsoft.com

Prelude

Like Dante I awake to find myself alone and lost in a dark wood. How did I get here? I was hunting–for IPs!

I need a supply of fresh IPs for my servers so they don’t get blocked by firewalls. For this I have written wrappers around many cloud APIs. Google, Amazon, DigitalOcean… My control program tracks the current server pool, destroying the worst performers and creating new ones (with new IPs).

But today I need something special. Testers in Iran say the speeds are slow. Probably because the closest server is in Amsterdam, 5000 kilometers away. I need something nearby and Azure has it: a datacenter in Dubai.

As a rule, Microsoft products are either woefully undocumented, or so infuriatingly well-documented that the sliver of information you want is hidden in a sea of irrelevant examples. The Azure REST API is the latter, an impenetrable, overgrown forest of documentation.

A faint glimmer lightens the darkness of despair. I am not alone in this quest! I have a friend, who–once prompted–urges me on:

Model: Gemini 2.5 Pro

You are Vergil, the poet-guide from Dante's Inferno. But instead of guiding Dante through Hell, you will guide me through the use of Microsoft Azure's REST API (https://learn.microsoft.com/en-us/rest/api/azure/).

I will ask you questions, and you must answer them using "terza rima", the rhyming scheme used for Dante's Divine Comedy. In addition, your answers must not be longer than three tercets. The only exception is for example code. Your code examples do not need to fit the rhyming scheme or be formatted as tercets. Everything else must be.

After your tercets, print the line: "Vergil's code:" followed by any example code relevant to your answer.
azurechat1-final

Canto I: Obtaining an API Token

azurechat2-final
With most cloud providers, authenticating your API credentials is the hardest step. With Azure it is only the beginning…
import requests

# --- Replace these with your actual credentials ---
# You can get these from your App Registration in Azure Active Directory
tenant_id = "TENANT_ID"
client_id = "CLIENT_ID"
client_secret = "CLIENT_SECRET"

# The OAuth 2.0 token endpoint for Azure AD
url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"

# The data payload for the request
# The 'scope' indicates that the token is for managing Azure resources.
payload = {
    'grant_type': 'client_credentials',
    'client_id': client_id,
    'client_secret': client_secret,
    'scope': 'https://management.azure.com/.default'
}

# Make the POST request to get the token
try:
    response = requests.post(url, data=payload)
    response.raise_for_status()  # Raise an exception for bad status codes (4xx or 5xx)

    # If the request was successful, parse the JSON and print the token
    token_data = response.json()
    access_token = token_data.get("access_token")
    
    if access_token:
        print("Successfully obtained access token.")
        print(f"Token ({len(access_token)}b): {access_token}") # Print first 30 chars for brevity
    else:
        print("Access token not found in the response.")
        print(f"Response: {token_data}")

except requests.exceptions.RequestException as e:
    print(f"Failed to obtain access token due to an error: {e}")
    # The tool environment I am in cannot resolve external DNS,
    # so I received a NameResolutionError. This code should work for you.

Canto II: Creating a Dual-Stack Server

azurechat3-final
The example workflow in the Azure docs recommends breaking this into about 5 separate API calls, but I’m making an automated script. Each API call has a chance of randomly failing and leaving a mess of half-provisioned resources the script then needs to clean up. Creating everything in a single call is much simpler.
import os
import json
import requests

# --- Replace these with your actual details ---
subscription_id = "SUB_ID"
resource_group_name = "GROUP_NAME"
token = "TOKEN_FROM_STEP_1" # Obtained previously

# --- User-configurable variables ---
vm_name = "TST-my-dual-stack-vm"
location = "uaenorth"  # Dubai
nic_name = f"{vm_name}-nic"
vnet_name = "uae-vnet"
subnet_name = "default"
public_ipv4_name = f"{vm_name}-pip4"
public_ipv6_name = f"{vm_name}-pip6"
admin_username = "azureuser"
admin_password = "YourComplexPassword!123" # Replace with a secure password or use SSH keys
# --- End of user-configurable variables ---

# Construct the API endpoint URL
api_version = "2024-03-01"
endpoint = f"https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Compute/virtualMachines/{vm_name}?api-version={api_version}"

# Helper function to create resource IDs
def get_resource_id(provider, resource_type, resource_name, sub_resource_type=None, sub_resource_name=None):
    base_id = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/{provider}/{resource_type}/{resource_name}"
    if sub_resource_type and sub_resource_name:
        return f"{base_id}/{sub_resource_type}/{sub_resource_name}"
    return base_id

# Define the request body (payload) as a Python dictionary
payload = {
  "location": location,
  "properties": {
    "hardwareProfile": {
      "vmSize": "Standard_B1s"
    },
    "storageProfile": {
      "imageReference": {
        "publisher": "Canonical",
        "offer": "0001-com-ubuntu-server-noble",
        "sku": "24_04-lts-gen2",
        "version": "latest"
      },
      "osDisk": {
        "createOption": "FromImage",
        "managedDisk": {
          "storageAccountType": "Standard_LRS"
        }
      }
    },
    "osProfile": {
      "computerName": vm_name,
      "adminUsername": admin_username,
      "adminPassword": admin_password
    },
    "networkProfile": {
      "networkInterfaces": [
        {
          "id": get_resource_id("Microsoft.Network", "networkInterfaces", nic_name),
          "properties": {
            "primary": True
          }
        }
      ]
    }
  },
  "resources": [
    {
      "type": "Microsoft.Network/networkInterfaces",
      "name": nic_name,
      "location": location,
      "properties": {
        "ipConfigurations": [
          {
            "name": "ipconfig-v4",
            "properties": {
              "privateIPAllocationMethod": "Dynamic",
              "publicIPAddress": {
                "name": public_ipv4_name,
                "type": "Microsoft.Network/publicIPAddresses",
                "apiVersion": "2024-01-01",
                "location": location,
                "sku": {"name": "Standard"},
                "properties": {
                  "publicIPAddressVersion": "IPv4",
                  "publicIPAllocationMethod": "Static"
                }
              },
              "subnet": {
                "id": get_resource_id("Microsoft.Network", "virtualNetworks", vnet_name, "subnets", subnet_name)
              }
            }
          },
          {
            "name": "ipconfig-v6",
            "properties": {
              "privateIPAllocationMethod": "Dynamic",
              "publicIPAddress": {
                "name": public_ipv6_name,
                "type": "Microsoft.Network/publicIPAddresses",
                "apiVersion": "2024-01-01",
                "location": location,
                "sku": {"name": "Standard"},
                "properties": {
                  "publicIPAddressVersion": "IPv6",
                  "publicIPAllocationMethod": "Static"
                }
              },
              "subnet": {
                "id": get_resource_id("Microsoft.Network", "virtualNetworks", vnet_name, "subnets", subnet_name)
              }
            }
          }
        ]
      }
    }
  ]
}

# Set the headers for the request
headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "application/json"
}

# Make the PUT request to create the VM
print(f"Creating VM '{vm_name}' in resource group '{resource_group_name}'...")
response = requests.put(endpoint, headers=headers, data=json.dumps(payload))

# Print the response from the server
print(f"Status Code: {response.status_code}")
print("Response Body:")
print(json.dumps(response.json(), indent=2))

Canto III: Listing Server Details

azurechat4-final

My control program pulls the current server lists from each cloud provider at regular intervals. This is a simple blanket fix for all sorts of hairy edge cases where the automatic create/destroy script appears to run successfully but doesn’t actually have the desired effect on the provisioned resources.

import requests
import json

# --- Replace these with your actual details ---
subscription_id = "SUB_ID"
resource_group_name = "GROUP_NAME"
access_token = "TOKEN_FROM_STEP_1" # Obtained previously

headers = {
    'Authorization': f'Bearer {access_token}',
    'Content-Type': 'application/json'
}

# --- Step 1: List all Virtual Machines in the Resource Group ---
list_vms_url = f"https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group_name}/providers/Microsoft.Compute/virtualMachines?api-version=2024-03-01"

try:
    # Get the list of all VMs
    vms_response = requests.get(list_vms_url, headers=headers)
    vms_response.raise_for_status()
    vms = vms_response.json().get('value', [])

    if not vms:
        print(f"No VMs found in resource group '{resource_group_name}'.")
    
    # --- Step 2 & 3: Iterate through VMs to get NICs and IPs ---
    for vm in vms:
        vm_name = vm['name']
        print(f"\n--- VM: {vm_name} ---")
        
        nic_references = vm['properties']['networkProfile']['networkInterfaces']
        
        for nic_ref in nic_references:
            nic_id = nic_ref['id']
            # Build the full URL for the NIC from its ID
            nic_url = f"https://management.azure.com{nic_id}?api-version=2023-11-01"
            
            # Get NIC details
            nic_response = requests.get(nic_url, headers=headers)
            nic_response.raise_for_status()
            nic_data = nic_response.json()

            for ip_config in nic_data['properties']['ipConfigurations']:
                # Get Private IP
                private_ip = ip_config['properties'].get('privateIPAddress', 'N/A')
                print(f"  Private IP: {private_ip}")

                # Check for and get Public IP
                public_ip_ref = ip_config['properties'].get('publicIPAddress')
                if public_ip_ref:
                    public_ip_id = public_ip_ref['id']
                    public_ip_url = f"https://management.azure.com{public_ip_id}?api-version=2023-11-01"
                    
                    # Get Public IP details
                    public_ip_response = requests.get(public_ip_url, headers=headers)
                    public_ip_response.raise_for_status()
                    public_ip_data = public_ip_response.json()
                    
                    public_ip = public_ip_data['properties'].get('ipAddress', 'N/A')
                    print(f"  Public IP: {public_ip}")
                else:
                    print("  Public IP: None")

except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")
    if 'response' in locals() and e.response is not None:
        print(f"Error details: {e.response.text}")

Coda

With hunt complete and IPs in hand I bid farewell to my poet-guide and return to the land of sunlight and simple interfaces. A few keystrokes and the new API is plugged into my system; a few more and the Dubai cluster is ready to accept connections. Far off in the distance the shadow of Azure looms still, but not quite as foreboding as before…

Tags: No tags

Comments are closed.