Skip to content

Commit 3bde61b

Browse files
authored
Merge pull request #34 from PureStorage-OpenConnect/graph-query-fix
Fix Query Bugs
2 parents 930dde8 + b796f21 commit 3bde61b

3 files changed

Lines changed: 201 additions & 30 deletions

File tree

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,68 @@
11
resources
22
| where type =~ 'microsoft.compute/disks'
3-
| extend vmId = tostring(managedBy)
4-
| extend VmName = tostring(split(vmId, '/virtualMachines/')[1])
3+
| extend vmIdRaw = tostring(managedBy)
4+
| extend vmIdKey = tolower(vmIdRaw)
5+
| extend vmIdParts = split(vmIdRaw, '/')
6+
| extend isVmss = vmIdRaw contains '/virtualMachineScaleSets/'
7+
| extend VmName = iff(isVmss,
8+
strcat(tostring(vmIdParts[8]), '_', tostring(vmIdParts[array_length(vmIdParts) - 1])),
9+
tostring(vmIdParts[array_length(vmIdParts) - 1]))
10+
| extend resourceId = tolower(tostring(id))
511
| join kind=leftouter (
612
resources
713
| where type =~ 'microsoft.network/networkinterfaces'
8-
| extend attachedVmId = tostring(properties.virtualMachine.id)
14+
| extend attachedVmId = tolower(tostring(properties.virtualMachine.id))
915
| where isnotempty(attachedVmId)
10-
| extend vnetId = tostring(properties.ipConfigurations[0].properties.subnet.id)
11-
| extend vnetName = tostring(split(vnetId, '/')[8])
12-
| project attachedVmId, vnetName
13-
) on $left.vmId == $right.attachedVmId
16+
| mv-expand ipcfg = properties.ipConfigurations
17+
| extend subnetId = tostring(ipcfg.properties.subnet.id)
18+
| where isnotempty(subnetId)
19+
| extend nicVnet = tostring(split(subnetId, '/')[8])
20+
| extend nicSubnet = tostring(split(subnetId, '/')[10])
21+
| summarize
22+
vnetName = strcat_array(make_set(nicVnet), ';'),
23+
subnetName = strcat_array(make_set(nicSubnet), ';'),
24+
nicNames = strcat_array(make_set(name), ';')
25+
by attachedVmId
26+
) on $left.vmIdKey == $right.attachedVmId
27+
| join kind=leftouter (
28+
resourcecontainers
29+
| where type =~ 'microsoft.resources/subscriptions'
30+
| project subscriptionName = name, subscriptionId
31+
) on subscriptionId
32+
| join kind=leftouter (
33+
resources
34+
| where type =~ 'microsoft.compute/snapshots'
35+
| extend sourceDiskId = tolower(tostring(properties.creationData.sourceResourceId))
36+
| extend snapDiskSizeGB = toreal(properties.diskSizeGB)
37+
| extend isIncremental = tobool(properties.incremental)
38+
| summarize
39+
snapshotCount = count(),
40+
snapshotBilledCapacityGB = sumif(snapDiskSizeGB, isIncremental == false),
41+
snapshotProvisionedCapacityGB = sum(snapDiskSizeGB),
42+
incrementalSnapshotCount = countif(isIncremental == true)
43+
by sourceDiskId
44+
) on $left.resourceId == $right.sourceDiskId
45+
| extend snapshotCount = coalesce(snapshotCount, 0)
46+
| extend snapshotBilledCapacityGB = coalesce(snapshotBilledCapacityGB, 0.0)
47+
| extend snapshotProvisionedCapacityGB = coalesce(snapshotProvisionedCapacityGB, 0.0)
48+
| extend incrementalSnapshotCount = coalesce(incrementalSnapshotCount, 0)
49+
| extend vnetLookupStatus = case(
50+
isempty(vmIdRaw), 'unattached-disk',
51+
isVmss, 'vmss-managed-nic',
52+
isempty(vnetName), 'no-nic-matched',
53+
'ok')
1454
| extend azureRegion = location
15-
| extend azureZone = strcat_array(zones, '"')
55+
| extend azureZone = strcat_array(zones, ',')
56+
| extend resourceName = name
1657
| extend diskName = name
17-
| extend diskType = sku.name
18-
| extend osDisk = properties.osType
58+
| extend diskType = tostring(sku.name)
59+
| extend osDisk = tostring(properties.osType)
1960
| extend diskSize = tostring(properties.diskSizeGB)
2061
| extend diskState = tostring(properties.diskState)
2162
| extend diskBW = tostring(properties.diskMBpsReadWrite)
2263
| extend diskIOPS = tostring(properties.diskIOPSReadWrite)
23-
| extend createdDateTime = properties.timeCreated
24-
//| where isnull(properties.osType)
25-
| summarize arg_min(diskName, *) by diskName
26-
| project subscriptionId, resourceGroup, VmName, diskName, diskType, osDisk, diskSize, diskBW, diskIOPS, diskState, createdDateTime, azureRegion, azureZone, vnetName
64+
| extend createdDateTime = tostring(properties.timeCreated)
65+
| project azureRegion, azureZone, subscriptionName, resourceGroup, resourceId, resourceName,
66+
diskName, diskType, osDisk, diskSize, diskBW, diskIOPS, diskState, managedBy, createdDateTime,
67+
VmName, vnetName, subnetName, nicNames, vnetLookupStatus,
68+
snapshotCount, incrementalSnapshotCount, snapshotBilledCapacityGB, snapshotProvisionedCapacityGB

‎CBS-Azure-Solutions/collect-disk-information-azure-graph/QueryDataDisks‎

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,63 @@
11
resources
22
| where type =~ 'microsoft.compute/disks'
3-
| extend vmId = tostring(managedBy)
4-
| extend VmName = tostring(split(vmId, '/virtualMachines/')[1])
3+
| where isnull(properties.osType)
4+
| extend vmIdRaw = tostring(managedBy)
5+
| extend vmIdKey = tolower(vmIdRaw)
6+
| extend vmIdParts = split(vmIdRaw, '/')
7+
| extend isVmss = vmIdRaw contains '/virtualMachineScaleSets/'
8+
| extend VmName = iff(isVmss,
9+
strcat(tostring(vmIdParts[8]), '_', tostring(vmIdParts[array_length(vmIdParts) - 1])),
10+
tostring(vmIdParts[array_length(vmIdParts) - 1]))
11+
| extend resourceId = tolower(tostring(id))
512
| join kind=leftouter (
613
resources
714
| where type =~ 'microsoft.network/networkinterfaces'
8-
| extend attachedVmId = tostring(properties.virtualMachine.id)
15+
| extend attachedVmId = tolower(tostring(properties.virtualMachine.id))
916
| where isnotempty(attachedVmId)
10-
| extend vnetId = tostring(properties.ipConfigurations[0].properties.subnet.id)
11-
| extend vnetName = tostring(split(vnetId, '/')[8])
12-
| project attachedVmId, vnetName
13-
) on $left.vmId == $right.attachedVmId
17+
| mv-expand ipcfg = properties.ipConfigurations
18+
| extend subnetId = tostring(ipcfg.properties.subnet.id)
19+
| where isnotempty(subnetId)
20+
| extend nicVnet = tostring(split(subnetId, '/')[8])
21+
| extend nicSubnet = tostring(split(subnetId, '/')[10])
22+
| summarize
23+
vnetName = strcat_array(make_set(nicVnet), ';'),
24+
subnetName = strcat_array(make_set(nicSubnet), ';'),
25+
nicNames = strcat_array(make_set(name), ';')
26+
by attachedVmId
27+
) on $left.vmIdKey == $right.attachedVmId
1428
| join kind=leftouter (
1529
resourcecontainers
1630
| where type =~ 'microsoft.resources/subscriptions'
1731
| project subscriptionName = name, subscriptionId
1832
) on subscriptionId
33+
| join kind=leftouter (
34+
resources
35+
| where type =~ 'microsoft.compute/snapshots'
36+
| extend sourceDiskId = tolower(tostring(properties.creationData.sourceResourceId))
37+
| extend snapDiskSizeGB = toreal(properties.diskSizeGB)
38+
| extend isIncremental = tobool(properties.incremental)
39+
| summarize
40+
snapshotCount = count(),
41+
snapshotBilledCapacityGB = sumif(snapDiskSizeGB, isIncremental == false),
42+
snapshotProvisionedCapacityGB = sum(snapDiskSizeGB),
43+
incrementalSnapshotCount = countif(isIncremental == true)
44+
by sourceDiskId
45+
) on $left.resourceId == $right.sourceDiskId
46+
| extend snapshotCount = coalesce(snapshotCount, 0)
47+
| extend snapshotBilledCapacityGB = coalesce(snapshotBilledCapacityGB, 0.0)
48+
| extend snapshotProvisionedCapacityGB = coalesce(snapshotProvisionedCapacityGB, 0.0)
49+
| extend incrementalSnapshotCount = coalesce(incrementalSnapshotCount, 0)
50+
| extend vnetLookupStatus = case(
51+
isempty(vmIdRaw), 'unattached-disk',
52+
isVmss, 'vmss-managed-nic',
53+
isempty(vnetName), 'no-nic-matched',
54+
'ok')
1955
| extend azureRegion = location
20-
| extend azureZone = strcat_array(zones, '"')
21-
| extend resourceId = tolower(tostring(id))
56+
| extend azureZone = strcat_array(zones, ',')
2257
| extend resourceName = name
2358
| extend diskName = name
24-
| extend diskType = sku.name
25-
| extend osDisk = properties.osType
59+
| extend diskType = tostring(sku.name)
60+
| extend osDisk = tostring(properties.osType)
2661
| extend diskSize = tostring(properties.diskSizeGB)
2762
| extend diskState = tostring(properties.diskState)
2863
| extend diskBW = tostring(properties.diskMBpsReadWrite)
Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,103 @@
1-
# Azure Resource Graph Explorer Code
1+
# Collect Azure VM disk information with Resource Graph
22

3-
This code used in Azure Resource Graph Explorer to collect VM Disk information when considering if Pure Storage Cloud Block Store (CBS) could be an alternate option for data disks
3+
Two Azure Resource Graph queries that inventory managed disks across every
4+
subscription you can read. Use the output to size an Everpure Cloud (Cloud Block
5+
Store) deployment: which data disks exist, how big they are, what they cost to
6+
run today, and which VNets they would need to be reachable from.
47

5-
`where osDisk = null` is a data disk that could be migrated to CBS
8+
Run these in [Azure Resource Graph Explorer](https://portal.azure.com/#blade/HubsExtension/ArgQueryBlade)
9+
in the Azure portal, then export to CSV.
610

7-
Download as a CVS after the query has executed
11+
![Resource Graph Explorer showing the query and results](screenshot.png)
812

9-
![screenshot_1](/CBS-Azure-Solutions/collect-disk-information-azure-graph/screenshot.png)
13+
## Which query to run
14+
15+
| File | Scope | Extra data |
16+
| --- | --- | --- |
17+
| `QueryDataDisks` | Data disks only (`properties.osType` is null) | Subscription name, full resource ID, snapshot rollup |
18+
| `QueryAllDisks` | Every managed disk, OS and data | None. Returns `subscriptionId` rather than the subscription name |
19+
20+
`QueryDataDisks` is the one you want for sizing. OS disks stay on Azure managed
21+
disks; only data disks are candidates to move to Everpure Cloud, so filtering
22+
them out up front keeps the capacity totals honest.
23+
24+
`QueryAllDisks` is the same query with the OS-disk filter commented out at line
25+
24 and the subscription and snapshot joins removed. Reach for it when you need
26+
a full disk inventory rather than a migration candidate list.
27+
28+
## Running it
29+
30+
1. Open Resource Graph Explorer in the Azure portal.
31+
2. Set the scope with the **Subscriptions** picker above the editor. It defaults
32+
to every subscription your account can read, which is usually what you want.
33+
3. Paste the query and select **Run query**.
34+
4. Select **Download as CSV**.
35+
36+
You need `Microsoft.Compute/disks/read`, `Microsoft.Network/networkInterfaces/read`,
37+
and `Microsoft.Compute/snapshots/read` on the subscriptions in scope. Reader at
38+
subscription level covers all three. Reader on only the compute resource groups
39+
returns the disks but not the NICs, which shows up as an empty `vnetName`. See
40+
the next section.
41+
42+
## Reading the output
43+
44+
`osDisk` is empty for a data disk and holds `Windows` or `Linux` for an OS disk.
45+
In `QueryDataDisks` every row is a data disk, so the column is always empty; it
46+
is kept so the two queries produce the same schema.
47+
48+
`diskState` distinguishes an attached disk from one that is provisioned and
49+
billed but connected to nothing. `Unattached` disks bill at full provisioned
50+
capacity and are worth calling out separately in a sizing conversation.
51+
52+
`snapshotBilledCapacityGB` sums only full (non-incremental) snapshots, which bill
53+
at the source disk's provisioned size. Incremental snapshots bill on written
54+
delta, and Resource Graph does not expose that delta, so this column is a floor
55+
rather than a total. `incrementalSnapshotCount` tells you how many snapshots are
56+
missing from the figure. `snapshotProvisionedCapacityGB` counts both kinds at
57+
source-disk size, which is the upper bound.
58+
59+
`diskIOPS` and `diskBW` are the values the disk resource reports. Premium SSD v2
60+
and Ultra Disk set these explicitly per disk; other SKUs report the tier default
61+
for their size. They are not observed throughput. Pull that from Azure Monitor if
62+
you need it.
63+
64+
### When vnetName is empty
65+
66+
`vnetName` comes from the subnet ID on the VM's network interface, reached by
67+
joining `disk.managedBy` to `nic.properties.virtualMachine.id`. Several things
68+
break that join, and the old version of this query returned a blank cell for all
69+
of them. `QueryDataDisks` now reports which one applies in `vnetLookupStatus`:
70+
71+
- **`ok`** - a NIC matched. `vnetName`, `subnetName`, and `nicNames` are
72+
populated. A VM with more than one NIC produces a semicolon-delimited list.
73+
- **`unattached-disk`** - `managedBy` is empty because the disk is not attached
74+
to anything. There is no VNet to report. Expected.
75+
- **`vmss-managed-nic`** - the disk belongs to a Virtual Machine Scale Set
76+
instance. Under Uniform orchestration the NIC is a child of the scale set
77+
rather than a standalone `Microsoft.Network/networkInterfaces` resource, so it
78+
is not in the `resources` table and cannot be joined. `VmName` is reported as
79+
`scaleSetName_instanceId`. Flexible orchestration VMSS resolve normally and
80+
return `ok`.
81+
- **`no-nic-matched`** - a VM ID is present but no NIC matched it. Almost always
82+
RBAC: you can read the compute resource group but not the network resource
83+
group holding the NIC. Common in hub-and-spoke tenants where networking is
84+
owned by a separate team. Widen the subscription scope or get Reader on the
85+
network resource groups, then rerun.
86+
87+
## Limits
88+
89+
CSV export from Resource Graph Explorer stops at **55,000 rows**. This is a
90+
platform cap and cannot be raised through a support ticket. Past that, split the
91+
run by subscription or management group using the scope picker.
92+
93+
These queries do not run under `Search-AzGraph` or `az graph query`. Resource
94+
Graph allows three `join` or `union` operations per query and permits a table to
95+
appear as the right-hand side of a join only once. `QueryDataDisks` uses
96+
`resources` on the right twice, for network interfaces and for snapshots, which
97+
returns `DisallowedMaxNumberOfRemoteTables` through the SDK. Resource Graph
98+
Explorer runs under a higher limit, so the portal is the supported path. To
99+
script it, split the snapshot rollup into a second query and join the two result
100+
sets client-side.
101+
102+
Resource Graph data is eventually consistent. A disk attached or resized in the
103+
last few minutes may not be reflected yet.

0 commit comments

Comments
 (0)