banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

Cloud

banshee/cloud-collection/cloud/azure/https/

quotas

The compute quotas of an Azure subscription in one region and the part of each limit in use, by Azure Resource Manager.

  • cloud
  • azure
  • quotas
  • limits
  • vcpu
  • compute
  • https
  • api

by banshee1 installation1 version

v1Latest

Published on October 27, 2026. First version.

Keen 1.0.0 · 107 lines
// The compute quotas of an Azure subscription in one region, by Azure Resource Manager, logged in as a service
// principal: for each quota (the vCPUs of the region, of each family of machines, among others), the part of
// its limit in use. A quota reached refuses the next machine, or the next resize.
// Reads POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token, then GET
// /subscriptions/<id>/providers/Microsoft.Compute/locations/<region>/usages on management.azure.com.
need('protocol:http');
$client = need('protocol:http:user');
$secret = need('protocol:http:password');
$tenant = variable('azure:tenant', 'text', array(
    'label' => 'Tenant',
    'help'  => 'The directory (tenant) ID of the service principal, or one of the tenant\'s domains.',
));
$subscription = variable('azure:subscription', 'text', array(
    'label' => 'Subscription',
    'help'  => 'The ID of the subscription whose quotas are read.',
));
$region = variable('azure:region', 'text', array(
    'label' => 'Region',
    'help'  => 'The name of the region, as Azure writes it in an API: westeurope, francecentral, eastus2.',
));

per('quota', 'Quotas');
output('used', 'number', array('label' => 'Part of the limit in use', 'unit' => '%', 'per' => 'quota'));
output('usage', 'number', array('label' => 'In use', 'per' => 'quota'));
output('limit', 'number', array('label' => 'Limit', 'per' => 'quota'));

watch.above('near-limit', 'Quota near its limit', 'used', array('warning' => 80, 'critical' => 95));

// Log in as the service principal, by the client credentials grant. The secret goes in the form, which makes
// the answer secret, and its access token with it. The token is kept in the cache, encrypted on the poller,
// thirty minutes, under the hour or so Microsoft gives it.
function login($tenant, $client, $secret, $scope) {
    $body = url.query(array('grant_type' => 'client_credentials', 'client_id' => $client,
        'client_secret' => $secret, 'scope' => $scope));
    $answer = http.post('https://login.microsoftonline.com/' . url.encode($tenant) . '/oauth2/v2.0/token', $body,
        headers=array('Content-Type' => 'application/x-www-form-urlencoded'));
    if ($answer['status'] == 400 || $answer['status'] == 401) {
        fail('misconfigured', 'Microsoft Entra ID refuses the service principal, code ' . $answer['status']);
    }
    if ($answer['status'] != 200) {
        fail('unreachable', 'Microsoft Entra ID answers the login with code ' . $answer['status']);
    }
    $session = json.read($answer['body']);
    cache.set('token', $session['access_token'], 1800);
    return $session['access_token'];
}

// One GET on Azure Resource Manager with the token kept in the cache; a token Azure no longer takes answers
// 401, and the collector logs in again, once.
function arm_get($url, $what, $tenant, $client, $secret) {
    if (cache.has('token')) {
        $token = cache.get('token');
    } else {
        $token = login($tenant, $client, $secret, 'https://management.azure.com/.default');
    }
    $answer = http.get($url, headers=array('Authorization' => 'Bearer ' . $token));
    if ($answer['status'] == 401) {
        $token = login($tenant, $client, $secret, 'https://management.azure.com/.default');
        $answer = http.get($url, headers=array('Authorization' => 'Bearer ' . $token));
    }
    if ($answer['status'] == 403) {
        fail('misconfigured', 'The service principal may not read ' . $what . ', code 403');
    }
    if ($answer['status'] == 400 || $answer['status'] == 404) {
        fail('misconfigured', 'Azure refuses to read ' . $what . ', code ' . $answer['status'],
            'Check the IDs and names set on the probe.');
    }
    if ($answer['status'] != 200) {
        fail('unreachable', 'Azure answers the reading of ' . $what . ' with code ' . $answer['status']);
    }
    return json.read($answer['body']);
}

// The items of a list, page after page: Azure gives the address of the next page under nextLink, until the
// last. A next page is only followed on Azure Resource Manager itself, where the token may go.
function arm_list($url, $what, $tenant, $client, $secret) {
    $items = array();
    while ($url != '') {
        if (!text.like($url, 'https://management.azure.com/*')) {
            fail('unreachable', 'Azure gives the next page of ' . $what . ' outside management.azure.com');
        }
        $page = arm_get($url, $what, $tenant, $client, $secret);
        foreach (list.get($page, 'value', array()) as $item) {
            $items = list.add($items, $item);
        }
        $url = list.get($page, 'nextLink', '');
    }
    return $items;
}

// {"value": [{"unit": "Count", "currentValue": 17, "limit": 20, "name": {"value": "cores", "localizedValue":
// "Total Regional vCPUs"}}]}. A quota is named by name.value, which does not change with the language.
$usages = arm_list('https://management.azure.com/subscriptions/' . url.encode($subscription)
    . '/providers/Microsoft.Compute/locations/' . url.encode($region) . '/usages?api-version=2024-07-01',
    'the compute quotas', $tenant, $client, $secret);
if (list.count($usages) == 0) {
    fail('misconfigured', 'Azure lists no compute quota in ' . $region, 'Check the name of the region.');
}
foreach ($usages as $usage) {
    $name = $usage['name']['value'];
    value('usage', $usage['currentValue'], $name);
    value('limit', $usage['limit'], $name);
    // A quota at a limit of 0 is a family the subscription may not use at all: nothing is in use of nothing.
    if ($usage['limit'] > 0) {
        value('used', number.round($usage['currentValue'] * 100 / $usage['limit'], 1), $name);
    }
}