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.
by banshee1 installation1 version
Published on October 27, 2026. First version.
// 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);
}
}