Cloud
banshee/cloud-collection/cloud/azure/https/
vms
The power state and the Resource Health availability of each virtual machine of an Azure subscription, by Azure Resource Manager.
by banshee1 installation1 version
Published on October 27, 2026. First version.
// The virtual machines of an Azure subscription, by Azure Resource Manager, logged in as a service principal:
// for each machine, its power state and its availability as Azure Resource Health judges it.
// Reads POST https://login.microsoftonline.com/<tenant>/oauth2/v2.0/token, then GET
// /subscriptions/<id>/providers/Microsoft.Compute/virtualMachines?statusOnly=true and GET
// /subscriptions/<id>/providers/Microsoft.ResourceHealth/availabilityStatuses, page after page.
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 virtual machines are read.',
));
per('vm', 'Virtual machines');
output('power', 'text', array('label' => 'Power state', 'per' => 'vm'));
output('health', 'text', array('label' => 'Resource Health', 'per' => 'vm'));
watch.expect('not-running', 'Virtual machine not running', 'power', array('critical' => array('running'), 'for' => '2 runs'));
watch.state('unavailable', 'Virtual machine unavailable', 'health', array('warning' => array('degraded'), 'critical' => array('unavailable')));
watch.missing('missing', 'Virtual machine missing', 'power', array('enabled' => false));
// 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;
}
$base = 'https://management.azure.com/subscriptions/' . url.encode($subscription);
// {"value": [{"id": "/subscriptions/…/resourceGroups/RG-WEB/providers/Microsoft.Compute/virtualMachines/web-1",
// "name": "web-1", "properties": {"instanceView": {"statuses": [{"code": "ProvisioningState/succeeded"},
// {"code": "PowerState/running", "displayStatus": "VM running"}]}}}], "nextLink": "…"}
$vms = arm_list($base . '/providers/Microsoft.Compute/virtualMachines?api-version=2024-07-01&statusOnly=true',
'the virtual machines', $tenant, $client, $secret);
// {"value": [{"id": "/subscriptions/…/virtualMachines/web-1/providers/Microsoft.ResourceHealth/
// availabilityStatuses/current", "properties": {"availabilityState": "Available", …}}], "nextLink": "…"}: the
// statuses of every resource of the subscription, each under the resource's own id. Ids are compared in lower
// case, as Azure does not keep one case for them.
$statuses = arm_list($base . '/providers/Microsoft.ResourceHealth/availabilityStatuses?api-version=2025-05-01',
'the Resource Health statuses', $tenant, $client, $secret);
$health = array();
foreach ($statuses as $status) {
$state = list.get(list.get($status, 'properties', array()), 'availabilityState', '');
if ($state != '') {
$parts = text.split(text.lower($status['id']), '/providers/microsoft.resourcehealth/');
$health[$parts[0]] = text.lower($state);
}
}
// A machine is named by its resource group and its name, two machines of one name living in two groups.
foreach ($vms as $vm) {
$id = text.lower($vm['id']);
$parts = text.split($id, '/');
$name = $parts[4] . '/' . $vm['name'];
$power = 'unknown';
$view = list.get(list.get($vm, 'properties', array()), 'instanceView', array());
foreach (list.get($view, 'statuses', array()) as $status) {
$code = list.get($status, 'code', '');
if (text.like($code, 'PowerState/*')) {
$words = text.split($code, '/');
$power = text.lower($words[1]);
}
}
value('power', $power, $name);
value('health', list.get($health, $id, 'unknown'), $name);
}