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/

vms

The power state and the Resource Health availability of each virtual machine of an Azure subscription, by Azure Resource Manager.

  • cloud
  • azure
  • virtual-machines
  • vm
  • resource-health
  • power-state
  • https
  • api

by banshee1 installation1 version

v1Latest

Published on October 27, 2026. First version.

Keen 1.0.0 · 128 lines
// 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);
}