Modules/Public/InventoryModules/Compute/VirtualMachine.ps1
|
<#
.Synopsis Inventory for Azure Virtual Machine .DESCRIPTION This script consolidates information for all microsoft.compute/virtualmachines resource provider in $Resources variable. Excel Sheet Name: Virtual Machines .Link https://github.com/thisismydemo/azure-scout/Modules/Public/InventoryModules/Compute/VirtualMachine.ps1 .COMPONENT This PowerShell Module is part of Azure Scout (AZSC). .CATEGORY Compute .NOTES Version: 3.6.0 First Release Date: 19th November, 2020 Authors: Claudio Merola and Renato Gregio #> <######## Default Parameters. Don't modify this ########> param($SCPath, $Sub, $Intag, $Resources, $Retirements, $Task, $File, $SmaResources, $TableStyle, $Unsupported) If ($Task -eq 'Processing') { $vm = $Resources | Where-Object {$_.TYPE -eq 'microsoft.compute/virtualmachines'} $nic = $Resources | Where-Object {$_.TYPE -eq 'microsoft.network/networkinterfaces'} $vmexp = $Resources | Where-Object {$_.TYPE -eq 'microsoft.compute/virtualmachines/extensions'} $disk = $Resources | Where-Object {$_.TYPE -eq 'microsoft.compute/disks'} $VirtualNetwork = $Resources | Where-Object { $_.TYPE -eq 'microsoft.network/virtualnetworks' } $VMExtraDetails = $Resources | Where-Object { $_.TYPE -eq 'AZSC/VM/SKU' } $VMQuotas = $Resources | Where-Object { $_.TYPE -eq 'AZSC/VM/Quotas' } if($vm) { $tmp = foreach ($1 in $vm) { $ResUCount = 1 # $SUB does not always contain the resource's subscription -- a management-group # scoped run, a subscription the caller lost access to mid-run, or simply a # resource row from a subscription outside the requested scope all leave this # Where-Object matching nothing. An EMPTY result is not $null, and reading # .Name off it throws under StrictMode (AB#5667), so the name is resolved once, # defensively, instead of at the point of use ~400 lines below. $sub1 = $SUB | Where-Object { $_.id -eq $1.subscriptionId } $SubscriptionName = if ($sub1) { @($sub1)[0].Name } else { '' } $data = $1.PROPERTIES # $data.timeCreated: absent entirely on some API versions/resource kinds, not # just $null -- a plain member read throws under StrictMode when the # NoteProperty genuinely does not exist (AB#5667). PSObject.Properties[...] # is a safe existence check either way. $timecreated = if ($data.PSObject.Properties['timeCreated']) { $data.timeCreated } else { $null } $timecreated = if ($timecreated) { ([datetime]$timecreated).ToString("yyyy-MM-dd HH:mm") } else { '' } $dataSize = '' $StorAcc = '' # `extended` is populated by the instance-view expansion and is ABSENT (not # null) on a plain Resource Graph row, and `storageProfile.imageReference` is # absent on a VM created from a specialised/attached disk rather than a # gallery image -- both chains throw under StrictMode (AB#5667). The # fall-back logic is unchanged; only the reads are made safe. $ExtendedOsName = Get-AZSCSafeProperty -InputObject $data -Path 'extended.instanceView.osname' $ExtendedOsVersion = Get-AZSCSafeProperty -InputObject $data -Path 'extended.instanceView.osversion' $ImageOffer = Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.imageReference.offer' $ImageSku = Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.imageReference.sku' $OSName = if(![string]::IsNullOrEmpty($ExtendedOsName)){$ExtendedOsName}else{$ImageOffer} $OSVersion = if(![string]::IsNullOrEmpty($ExtendedOsVersion)){$ExtendedOsVersion}else{$ImageSku} # Extra VM Details # # $VMExtraDetails ($Resources filtered to the synthetic 'AZSC/VM/SKU' type) is # empty on most tenants/fixtures -- member access on an empty collection # throws under StrictMode (AB#5667), so every step of this chain, and every # capability variable it feeds, is reset to a known value up front rather than # left holding a previous VM's value when this VM has no match. $vCPUs = $null; $vCPUsPerCore = $null; $RAM = $null; $MaxDataDiskCount = $null $UncachedDiskIOPS = $null; $UncachedDiskBytesPerSecond = $null; $MaxNetworkInterfaces = $null $VMExtraDetail = if ($VMExtraDetails) { $VMExtraDetails.properties | Where-Object {$_.Location -eq $1.location} } else { $null } # hardwareProfile is absent on some VM shapes (and on any malformed row); # resolved once here so the Where-Object below does not read it per SKU. $VMSize = Get-AZSCSafeProperty -InputObject $data -Path 'hardwareProfile.vmSize' $VMExtraDetail = if ($VMExtraDetail) { $VMExtraDetail.SKUs | Where-Object {$_.Name -eq $VMSize} } else { $null } if ($VMExtraDetail) { foreach ($Capability in $VMExtraDetail.Capabilities) { if ($Capability.Name -eq 'vCPUs') {$vCPUs = $Capability.Value} if ($Capability.Name -eq 'vCPUsPerCore') {$vCPUsPerCore = $Capability.Value} if ($Capability.Name -eq 'MemoryGB') {$RAM = $Capability.Value} if ($Capability.Name -eq 'MaxDataDiskCount') {$MaxDataDiskCount = $Capability.Value} if ($Capability.Name -eq 'UncachedDiskIOPS') {$UncachedDiskIOPS = $Capability.Value} if ($Capability.Name -eq 'UncachedDiskBytesPerSecond') {$UncachedDiskBytesPerSecond = ([math]::Round($Capability.Value / 1024) / 1024)} if ($Capability.Name -eq 'MaxNetworkInterfaces') {$MaxNetworkInterfaces = $Capability.Value} } } # Quotas -- $VMExtraDetail/$VMQuotas/$Quota are all empty on most # tenants/fixtures (the synthetic 'AZSC/VM/SKU' and 'AZSC/VM/Quotas' types are # rarely present), and member access on an empty collection throws under # StrictMode (AB#5667), so every step is guarded rather than chained. $Size = if ($VMExtraDetail) { $VMExtraDetail.Family } else { $null } $Quota = if ($VMQuotas) { $VMQuotas.properties | Where-Object {$_.SubId -eq $1.subscriptionId} } else { $null } $Quota = if ($Quota) { $Quota | Where-Object {$_.Location -eq $1.location} } else { $null } $RemainingQuota = if ($Quota) { (($Quota.Data | Where-Object {$_.Name.Value -eq $Size}).Limit - ($Quota.Data | Where-Object {$_.Name.Value -eq $Size}).CurrentValue) } else { $null } # Same "$VMExtraDetail may be empty" guard as above, for the two remaining # chains off it that are only read once per VM (further down, in $obj). $VMZonesAvailable = if ($VMExtraDetail) { [string]$VMExtraDetail.LocationInfo.ZoneDetails.Name } else { '' } $VMCapabilities = if ($VMExtraDetail) { [string]$VMExtraDetail.LocationInfo.ZoneDetails.Capabilities.Name } else { '' } $Retired = Foreach ($Retirement in $Retirements) { if ($Retirement.id -eq $1.id) { $Retirement } } if ($Retired) { $RetiredFeature = foreach ($Retire in $Retired) { $RetiredServiceID = $Unsupported | Where-Object {$_.Id -eq $Retired.ServiceID} $tmp0 = [PSCustomObject]@{ 'RetiredFeature' = $RetiredServiceID.RetiringFeature 'RetiredDate' = $RetiredServiceID.RetirementDate } $tmp0 } $RetiringFeature = if (@($RetiredFeature.RetiredFeature).count -gt 1) { $RetiredFeature.RetiredFeature | ForEach-Object { $_ + ' ,' } }else { $RetiredFeature.RetiredFeature} $RetiringFeature = [string]$RetiringFeature $RetiringFeature = if ($RetiringFeature -like '* ,*') { $RetiringFeature -replace ".$" }else { $RetiringFeature } $RetiringDate = if (@($RetiredFeature.RetiredDate).count -gt 1) { $RetiredFeature.RetiredDate | ForEach-Object { $_ + ' ,' } }else { $RetiredFeature.RetiredDate} $RetiringDate = [string]$RetiringDate $RetiringDate = if ($RetiringDate -like '* ,*') { $RetiringDate -replace ".$" }else { $RetiringDate } } else { $RetiringFeature = $null $RetiringDate = $null } #Extensions $ext = @() $AzDiag = '' $Azinsights = '' # licenseType is only present at all when Hybrid Benefit is configured -- most # VMs never set it, so a plain $data.licenseType read throws under StrictMode # on the common case, not the exception (AB#5667). $LicenseType = if ($data.PSObject.Properties['licenseType']) { $data.licenseType } else { $null } $Lic = switch ($LicenseType) { 'Windows_Server' { 'Azure Hybrid Benefit for Windows' } 'Windows_Client' { 'Windows client with multi-tenant hosting' } 'RHEL_BYOS' { 'Azure Hybrid Benefit for Redhat' } 'SLES_BYOS' { 'Azure Hybrid Benefit for SUSE' } default { $LicenseType } } $Lic = if($Lic){$Lic}else{'None'} $ext = foreach ($vmextension in $vmexp) { if (($vmextension.id -split "/")[8] -eq $1.name) { $vmextension.properties.Publisher } } if ($null -ne $ext) { $ext = foreach ($ex in $ext) { if ($ex | Where-Object { $_ -eq 'Microsoft.Azure.Performance.Diagnostics' }) { $AzDiag = $true } if ($ex | Where-Object { $_ -eq 'Microsoft.EnterpriseCloud.Monitoring' }) { $Azinsights = $true } $ex + ', ' } $ext = [string]$ext $ext = $ext.Substring(0, $ext.Length - 2) } # Every $data.<chain> below is optional-block territory: a VM that does not # use it does not merely carry a $null value, the whole block is ABSENT from # the API response, and a raw chain throws under StrictMode the moment it # hits the missing segment (AB#5667). Get-AZSCSafeProperty returns $null # instead, exactly like the (unreachable-in-practice) $null a pre-StrictMode # reader of this same chain would have seen. # Neither branch below runs when a VM carries neither a windowsConfiguration # nor a linuxConfiguration block, and reading an unassigned variable is its # own StrictMode failure -- distinct from the missing-property class, and # worse, because without StrictMode it silently reuses the PREVIOUS VM's # value for every VM after the first (this loop reuses one scope). $Autoupdate = $null $WindowsAutoUpdate = Get-AZSCSafeProperty -InputObject $data -Path 'osProfile.windowsConfiguration.enableAutomaticUpdates' $LinuxPatchMode = Get-AZSCSafeProperty -InputObject $data -Path 'osProfile.linuxConfiguration.patchSettings.patchMode' if(![string]::IsNullOrEmpty($WindowsAutoUpdate)) { if($WindowsAutoUpdate -eq 'True') { $Autoupdate = $true } else { $Autoupdate = $false } } elseif(![string]::IsNullOrEmpty($LinuxPatchMode)) { if($LinuxPatchMode -eq 'automaticbyos') { $Autoupdate = $true } else { $Autoupdate = $false } } $AvailabilitySetValue = Get-AZSCSafeProperty -InputObject $data -Path 'availabilitySet' if (![string]::IsNullOrEmpty($AvailabilitySetValue)) { $AVSET = $true }else { $AVSET = $false } $BootDiagEnabled = Get-AZSCSafeProperty -InputObject $data -Path 'diagnosticsProfile.bootDiagnostics.enabled' if ($BootDiagEnabled -eq $true) { $bootdg = $true }else { $bootdg = $false } #Storage $OsDiskVhdUri = Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.osDisk.vhd.uri' $OsDiskManagedDiskId = Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.osDisk.managedDisk.id' $DataDisksManagedIds = Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.dataDisks.managedDisk.id' # Both are left unassigned when the VM uses a managed OS disk that is not in # $disk (a disk outside the queried scope, or simply no matching row) -- # the same reuse-the-previous-VM's-value hazard as $Autoupdate above. $OSDisk = $null $OSDiskSize = $null if($OsDiskVhdUri) { $OSDisk = 'Custom VHD' $OSDiskSize = Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.osDisk.diskSizeGB' } else { foreach ($VMDisk in $disk) { if ($VMDisk.id -eq $OsDiskManagedDiskId) { $OSDisk = $VMDisk.sku.name } if ($VMDisk.id -eq $DataDisksManagedIds) { $OSDiskSize = $VMDisk.properties.diskSizeGB } } } if ($DataDisksManagedIds) { if (@($DataDisksManagedIds).count -ge 2) { $StorAcc = (@($DataDisksManagedIds).count.ToString() + ' Disks found.') foreach ($VMDisk in $disk) { if ($VMDisk.id -in $DataDisksManagedIds) { $dataSize = ($VMDisk.properties.diskSizeGB | Measure-Object -Sum).Sum } } } else { foreach ($VMDisk in $disk) { if ($VMDisk.id -eq $DataDisksManagedIds) { $StorAcc = $VMDisk.sku.name $dataSize = $VMDisk.properties.diskSizeGB } } } } else { $StorAcc = 'None' $dataSize = '0' } # The untagged case still has to emit ONE row per VM, which is why this is a # sentinel rather than an empty collection. It used to be the string '0': # harmless without StrictMode ([string]$Tag.Name on a string quietly yields # ''), but a hard throw with it, since [string] has no Name member. An object # carrying empty Name/Value produces byte-identical output and cannot throw. # `$1.tags` itself is absent (not empty) on rows the Graph never tagged. $TagProperties = Get-AZSCSafeProperty -InputObject $1 -Path 'tags' $Tags = if($null -ne $TagProperties -and ![string]::IsNullOrEmpty($TagProperties.psobject.properties)){$TagProperties.psobject.properties}else{[pscustomobject]@{ Name = ''; Value = '' }} # networkInterfaces is an ARRAY of {id} references -- when it is empty (a # VM row that genuinely has none), enumerating .id off it throws under # StrictMode the same way an empty $Resources match does (AB#5633's rule); # fetch the array itself safely first, then only enumerate when non-empty. $NetworkInterfaceRefs = Get-AZSCSafeProperty -InputObject $data -Path 'networkProfile.networkInterfaces' $VMNICS = if ($NetworkInterfaceRefs) { $NetworkInterfaceRefs.id } else { '0' } foreach ($2 in $VMNICS) { $vmnic = foreach ($netinterface in $nic) { if ($netinterface.id -eq $2) { $netinterface } } $vmnic = $vmnic | Select-Object -Unique # No matching NIC row (common for every VM this harness's fixtures do not # also carry a NIC for) -- $vmnic is $null, and every chain below reads a # property OFF it, which throws under StrictMode on a null base (AB#5667). # Match the values each branch's own "not found" case already produced. if ($vmnic) { # ipConfigurations is an ARRAY -- .properties.publicIPAddress.id relies # on PowerShell's member-enumeration-across-elements magic, which # throws under StrictMode the moment any hop is absent on every element # (e.g. a NIC with no public IP simply omits 'publicIPAddress' rather # than setting it null -- AB#5667/AB#5633's rule). Get-AZSCCollectedValue # walks the same chain one safe hop at a time instead. $IpConfigs = Get-AZSCSafeProperty -InputObject $vmnic -Path 'properties.ipConfigurations' $IpConfigProps = Get-AZSCCollectedValue -InputObject $IpConfigs -Name 'properties' $PublicIpId = Get-AZSCCollectedValue -InputObject (Get-AZSCCollectedValue -InputObject $IpConfigProps -Name 'publicIPAddress') -Name 'id' | Select-Object -First 1 $SubnetId = Get-AZSCCollectedValue -InputObject (Get-AZSCCollectedValue -InputObject $IpConfigProps -Name 'subnet') -Name 'id' | Select-Object -First 1 $PIP = if(![string]::IsNullOrEmpty($PublicIpId)){$PublicIpId.split('/')[8]}else{''} $VNET = if(![string]::IsNullOrEmpty($SubnetId)){$SubnetId.split('/')[8]}else{''} $Subnet = if(![string]::IsNullOrEmpty($SubnetId)){$SubnetId.split('/')[10]} else {''} $vmnet = foreach ($VMVnet in $VirtualNetwork) { if ((Get-AZSCCollectedValue -InputObject (Get-AZSCSafeProperty -InputObject $VMVnet -Path 'subnets') -Name 'id') -eq $SubnetId) { $VMVnet } } # A subnet entry without an `id` is rare but real (an inline subnet # definition on some API versions), and a bare $_.id in the filter # throws under StrictMode for the whole VM rather than skipping that # one entry. # A subnet entry without an `id` -- or a $null entry -- is rare but # real, and a bare $_.id in the filter throws under StrictMode for the # whole VM rather than skipping that one entry. Get-AZSCSafeProperty # rather than $_.PSObject.Properties['id'] because .PSObject itself is # not safe to dereference on a $null element. $vmnetsubnet = (Get-AZSCSafeProperty -InputObject $vmnet -Path 'properties.subnets') | Where-Object { (Get-AZSCSafeProperty -InputObject $_ -Path 'id') -eq $SubnetId } $NicDnsServers = Get-AZSCSafeProperty -InputObject $vmnic -Path 'properties.dnsSettings.dnsServers' if(![string]::IsNullOrEmpty($NicDnsServers)) { $DNSServer = $NicDnsServers } else { $DNSServer = Get-AZSCSafeProperty -InputObject $vmnet -Path 'properties.dhcpoptions.dnsservers' } if(![string]::IsNullOrEmpty($DNSServer)) { $FinalDNS = if (@($DNSServer).count -gt 1) { $DNSServer | ForEach-Object { $_ + ' ,' } }else { $DNSServer } $FinalDNS = [string]$FinalDNS $FinalDNS = if ($FinalDNS -like '* ,*') { $FinalDNS -replace ".$" }else { $FinalDNS } $FinalDNS = ('VNET: ( ' + $FinalDNS + ')') } else { $FinalDNS = 'Default (Azure-provided)' } $NicNsgId = Get-AZSCSafeProperty -InputObject $vmnic -Path 'properties.networkSecurityGroup.id' $SubnetNsgId = Get-AZSCSafeProperty -InputObject $vmnetsubnet -Path 'properties.networksecuritygroup.id' if(![string]::IsNullOrEmpty($NicNsgId)) { $vmnsg = $NicNsgId.split('/')[8] } elseif(![string]::IsNullOrEmpty($SubnetNsgId)) { $vmnsg = ('Subnet: ('+$SubnetNsgId.split('/')[8]+')') } else { $vmnsg = 'None' } $NicAcceleratedNetworking = Get-AZSCSafeProperty -InputObject $vmnic -Path 'properties.enableAcceleratedNetworking' if(![string]::IsNullOrEmpty($NicAcceleratedNetworking)) { $AcceleratedNetwork = $true } else { $AcceleratedNetwork = $false } } else { $PIP = ''; $VNET = ''; $Subnet = ''; $vmnet = $null; $vmnetsubnet = $null $DNSServer = $null; $FinalDNS = 'Default (Azure-provided)' $vmnsg = 'None'; $AcceleratedNetwork = $false # $IpConfigProps is read again further down (Private IP Address/ # Allocation) -- initialised here too so that read is never the FIRST # assignment to it under StrictMode for a VM whose only NIC match # failed (an unset variable throws exactly like a missing property). $IpConfigProps = $null } # ── Phase 17: Performance Metrics (Azure Monitor) ────────────────── $avgCpu = 'N/A' $avgMem = 'N/A' try { $monEnd = (Get-Date).ToUniversalTime().ToString('o') $monStart = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o') $cpuUri = "/subscriptions/$($1.subscriptionId)/resourceGroups/$($1.RESOURCEGROUP)/providers/Microsoft.Compute/virtualMachines/$($1.NAME)/providers/microsoft.insights/metrics?api-version=2019-07-01&metricnames=Percentage+CPU×pan=$monStart/$monEnd&interval=P1D&aggregation=Average" $cpuResp = Invoke-AzRestMethod -Path $cpuUri -Method GET -ErrorAction SilentlyContinue if ($cpuResp.StatusCode -eq 200) { $cpuData = $cpuResp.Content | ConvertFrom-Json $vals = $cpuData.value[0].timeseries[0].data.average | Where-Object { $_ -ne $null } if ($vals) { $avgCpu = [math]::Round(($vals | Measure-Object -Average).Average, 1) } } } catch {} try { $memUri = "/subscriptions/$($1.subscriptionId)/resourceGroups/$($1.RESOURCEGROUP)/providers/Microsoft.Compute/virtualMachines/$($1.NAME)/providers/microsoft.insights/metrics?api-version=2019-07-01&metricnames=Available+Memory+Bytes×pan=$monStart/$monEnd&interval=P1D&aggregation=Average" $memResp = Invoke-AzRestMethod -Path $memUri -Method GET -ErrorAction SilentlyContinue if ($memResp.StatusCode -eq 200) { $memData = $memResp.Content | ConvertFrom-Json $memVals = $memData.value[0].timeseries[0].data.average | Where-Object { $_ -ne $null } $ramBytes = [double]$RAM * 1073741824 if ($memVals -and $ramBytes -gt 0) { $avgAvailBytes = ($memVals | Measure-Object -Average).Average $avgMem = [math]::Round((($ramBytes - $avgAvailBytes) / $ramBytes) * 100, 1) } } } catch {} # ── Phase 17: DR Status (Azure Site Recovery) ───────────────────── $drReplicated = 'N/A' $drTargetRegion = 'N/A' $drHealth = 'N/A' try { $asrUri = "/subscriptions/$($1.subscriptionId)/providers/Microsoft.RecoveryServices/replicationEligibilityResults/$($1.NAME)?api-version=2022-10-01" $asrResp = Invoke-AzRestMethod -Path $asrUri -Method GET -ErrorAction SilentlyContinue if ($asrResp.StatusCode -eq 200) { $asrData = $asrResp.Content | ConvertFrom-Json if ($asrData.properties.eligible -eq $false -and $asrData.properties.errorDetails) { # Not eligible may mean already replicated; check replication $drReplicated = 'Check ASR' } elseif ($asrData.properties.eligible -eq $true) { $drReplicated = $false } } # Try to find replication protected items across vaults $vaultUri = "/subscriptions/$($1.subscriptionId)/providers/Microsoft.RecoveryServices/vaults?api-version=2023-04-01" $vaultResp = Invoke-AzRestMethod -Path $vaultUri -Method GET -ErrorAction SilentlyContinue if ($vaultResp.StatusCode -eq 200) { $vaults = ($vaultResp.Content | ConvertFrom-Json).value foreach ($vault in $vaults) { $rpUri = "/subscriptions/$($1.subscriptionId)/resourceGroups/$($vault.id.split('/')[4])/providers/Microsoft.RecoveryServices/vaults/$($vault.name)/replicationProtectedItems?api-version=2022-10-01" $rpResp = Invoke-AzRestMethod -Path $rpUri -Method GET -ErrorAction SilentlyContinue if ($rpResp.StatusCode -eq 200) { $rpItems = ($rpResp.Content | ConvertFrom-Json).value $vmItem = $rpItems | Where-Object { $_.properties.friendlyName -eq $1.NAME } if ($vmItem) { $drReplicated = $true $drTargetRegion = $vmItem.properties.recoveryAzureResourceGroupId -split '/' | Select-Object -Index 4 $drHealth = $vmItem.properties.replicationHealth break } } } } } catch {} # ── Phase 17: Cost Estimate (Cost Management) ───────────────────── $estMonthlyCost = 'N/A' try { $costUri = "/subscriptions/$($1.subscriptionId)/providers/Microsoft.CostManagement/query?api-version=2023-03-01" $costBody = @{ type = 'Usage' timeframe = 'MonthToDate' dataset = @{ granularity = 'None' filter = @{ dimensions = @{ name = 'ResourceId'; operator = 'In'; values = @($1.id) } } aggregation = @{ totalCost = @{ name = 'PreTaxCost'; function = 'Sum' } } } } | ConvertTo-Json -Depth 10 $costResp = Invoke-AzRestMethod -Path $costUri -Method POST -Payload $costBody -ErrorAction SilentlyContinue if ($costResp.StatusCode -eq 200) { $costData = $costResp.Content | ConvertFrom-Json $rawCost = $costData.properties.rows[0][0] if ($rawCost -ne $null) { $estMonthlyCost = [math]::Round([double]$rawCost, 2) } } } catch {} foreach ($Tag in $Tags) { $obj = @{ 'ID' = $1.id; 'Subscription' = $SubscriptionName; 'Resource Group' = $1.RESOURCEGROUP; 'VM Name' = $1.NAME; 'Location' = $1.LOCATION; 'Retiring Feature' = $RetiringFeature; 'Retiring Date' = $RetiringDate; 'Availability Zone' = [string]$1.ZONES; 'Zones Available in the Region' = $VMZonesAvailable; 'Availability Set' = $AVSET; 'VM Size' = $VMSize; 'Remaining Quota (vCPUs)' = [string]$RemainingQuota; 'vCPUs' = $vCPUs; 'vCPUs Per Core' = $vCPUsPerCore; 'RAM (GiB)' = $RAM; 'Max Remote Storage Disks' = $MaxDataDiskCount; 'Uncached Disk IOPS Limit' = $UncachedDiskIOPS; 'Uncached Disk Throughput Limit (MB/s)' = $UncachedDiskBytesPerSecond; 'Max Network Interfaces' = $MaxNetworkInterfaces; # Same absent-imageReference case as $ImageOffer/$ImageSku above. 'Image Reference' = (Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.imageReference.publisher'); 'Image Version' = (Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.imageReference.exactVersion'); 'Capabilities' = $VMCapabilities; 'Hybrid Benefit' = $Lic; # osProfile is absent on a VM created from a specialised disk (no # provisioning agent ran), osDisk on some attached-disk shapes. 'Admin Username' = (Get-AZSCSafeProperty -InputObject $data -Path 'osProfile.adminUsername'); 'OS Type' = (Get-AZSCSafeProperty -InputObject $data -Path 'storageProfile.osDisk.osType'); 'OS Name' = $OSName; 'OS Version' = $OSVersion; 'Automatic Update' = $Autoupdate; 'Boot Diagnostics' = $bootdg; 'Performance Agent' = if ($azDiag -ne '') { $true }else { $false }; 'Azure Monitor' = if ($Azinsights -ne '') { $true }else { $false }; 'OS Disk Storage Type' = $OSDisk; 'OS Disk Size (GB)' = $OSDiskSize; 'Data Disk Storage Type' = $StorAcc; 'Data Disk Size (GB)' = $dataSize; 'VM generation' = (Get-AZSCSafeProperty -InputObject $data -Path 'extended.instanceview.hypervgeneration'); 'Power State' = (Get-AZSCSafeProperty -InputObject $data -Path 'extended.instanceView.powerState.displayStatus'); 'NIC Name' = [string](Get-AZSCSafeProperty -InputObject $vmnic -Path 'name'); 'NIC Type' = [string](Get-AZSCSafeProperty -InputObject $vmnic -Path 'properties.nicType'); 'DNS Servers' = $FinalDNS; 'Public IP' = $PIP; 'Virtual Network' = $VNET; 'Subnet' = $Subnet; 'NSG' = $vmnsg; 'Accelerated Networking' = $AcceleratedNetwork; 'IP Forwarding' = [string](Get-AZSCSafeProperty -InputObject $vmnic -Path 'properties.enableIPForwarding'); 'Private IP Address' = [string](Get-AZSCCollectedValue -InputObject $IpConfigProps -Name 'privateIPAddress' | Select-Object -First 1); 'Private IP Allocation' = [string](Get-AZSCCollectedValue -InputObject $IpConfigProps -Name 'privateIPAllocationMethod' | Select-Object -First 1); 'Creation Time' = $timecreated; 'VM Extensions' = $ext; 'Avg CPU % (7d)' = $avgCpu; 'Avg Memory % (7d)' = $avgMem; 'DR Replicated' = $drReplicated; 'DR Target Region' = $drTargetRegion; 'DR Replication Health' = $drHealth; 'Est. Monthly Cost (USD)' = $estMonthlyCost; 'Resource U' = $ResUCount; 'Tag Name' = [string]$Tag.Name; 'Tag Value' = [string]$Tag.Value } if ($ResUCount -eq 1) { $ResUCount = 0 } $obj } } } $tmp } } else { If($SmaResources) { $TableName = ('VMTable_'+(($SmaResources.'Resource U' | Measure-Object -Sum).Sum)) $Style = @() $Style += New-ExcelStyle -HorizontalAlignment Center -AutoSize -NumberFormat '0' -VerticalAlignment Center $Style += New-ExcelStyle -HorizontalAlignment Left -Range AW:AW -Width 60 -WrapText $SheetName = 'Virtual Machines' $condtxt = @() #Automatic Updates $condtxt += New-ConditionalText false -Range V:V #Hybrid Benefit $condtxt += New-ConditionalText None -Range Y:Y #Boot Diagnostics $condtxt += New-ConditionalText false -Range AA:AA #Performance Agent $condtxt += New-ConditionalText false -Range AB:AB #Azure Monitor $condtxt += New-ConditionalText false -Range AC:AC #NSG $condtxt += New-ConditionalText None -Range AN:AN #Acelerated Network $condtxt += New-ConditionalText false -Range AQ:AQ #Retirement $condtxt += New-ConditionalText -Range M2:M100 -ConditionalType ContainsText $Exc = New-Object System.Collections.Generic.List[System.Object] $Exc.Add('Subscription') $Exc.Add('Resource Group') $Exc.Add('VM Name') $Exc.Add('VM Size') $Exc.Add('Remaining Quota (vCPUs)') $Exc.Add('vCPUs') $Exc.Add('vCPUs Per Core') $Exc.Add('RAM (GiB)') $Exc.Add('Max Remote Storage Disks') $Exc.Add('Uncached Disk IOPS Limit') $Exc.Add('Uncached Disk Throughput Limit (MB/s)') $Exc.Add('Max Network Interfaces') $Exc.Add('Retiring Feature') $Exc.Add('Retiring Date') $Exc.Add('Availability Zone') $Exc.Add('Zones Available in the Region') $Exc.Add('Capabilities') $Exc.Add('Location') $Exc.Add('OS Type') $Exc.Add('OS Name') $Exc.Add('OS Version') $Exc.Add('Automatic Update') $Exc.Add('Image Reference') $Exc.Add('Image Version') $Exc.Add('Hybrid Benefit') $Exc.Add('Admin Username') $Exc.Add('Boot Diagnostics') $Exc.Add('Performance Agent') $Exc.Add('Azure Monitor') $Exc.Add('OS Disk Storage Type') $Exc.Add('OS Disk Size (GB)') $Exc.Add('Data Disk Storage Type') $Exc.Add('Data Disk Size (GB)') $Exc.Add('VM generation') $Exc.Add('Power State') $Exc.Add('Availability Set') $Exc.Add('Virtual Network') $Exc.Add('Subnet') $Exc.Add('DNS Servers') $Exc.Add('NSG') $Exc.Add('NIC Name') $Exc.Add('NIC Type') $Exc.Add('Accelerated Networking') $Exc.Add('IP Forwarding') $Exc.Add('Private IP Address') $Exc.Add('Private IP Allocation') $Exc.Add('Public IP') $Exc.Add('Creation Time') $Exc.Add('VM Extensions') $Exc.Add('Avg CPU % (7d)') $Exc.Add('Avg Memory % (7d)') $Exc.Add('DR Replicated') $Exc.Add('DR Target Region') $Exc.Add('DR Replication Health') $Exc.Add('Est. Monthly Cost (USD)') $Exc.Add('Resource U') if($InTag) { $Exc.Add('Tag Name') $Exc.Add('Tag Value') } $noNumberConversion = @() $noNumberConversion += 'OS Version' $noNumberConversion += 'Image Version' $noNumberConversion += 'Private IP Address' $noNumberConversion += 'DNS Servers' [PSCustomObject]$SmaResources | ForEach-Object { $_ } | Select-Object $Exc | Export-Excel -Path $File -WorksheetName $SheetName -TableName $TableName -TableStyle $tableStyle -MaxAutoSizeRows 100 -ConditionalText $condtxt -Style $Style -NoNumberConversion $noNumberConversion } } |