Powershell Script to go through Partner Center Customers and find Specific Server OS

<#
.SYNOPSIS
    Finds all Windows Server VMs of a given OS year (e.g. 2012, 2016) across
    every customer Azure subscription reachable via your active GDAP
    relationships.

.DESCRIPTION
    Reads the customer tenant list produced by Get-GDAPCustomerTenants.ps1
    (run separately, in its own process, to avoid a Microsoft.Graph/Az.Accounts
    assembly conflict), signs into Azure PowerShell, and for each customer
    tenant:
      - Lists Azure subscriptions. Tenants where your GDAP role doesn't map to
        Azure RBAC (or the mapping hasn't been granted) fail here - logged to
        the errors CSV and skipped, not a script bug.
      - Runs an Azure Resource Graph query across all VMs, flagging any whose
        OS image indicates the target OS year. Matching on the year alone
        also catches R2 variants (e.g. "2012" matches both 2012-Datacenter
        and 2012-R2-Datacenter).
      - Falls back to a per-VM instance-view check (Get-AzVM -Status) for
        Windows VMs whose image reference is ambiguous (custom/generalized
        image, or in-place upgrade).

    Exports two CSVs: confirmed findings, and a log of tenants/subscriptions
    that couldn't be checked (and why).

.PARAMETER OSYear
    The Windows Server release year to search for, e.g. '2012', '2016', '2019'.

.PARAMETER CustomerCsv
    Path to the customer tenant list from Get-GDAPCustomerTenants.ps1.

.PARAMETER OutputPath
    Folder to write the CSV reports to. Defaults to the script's own folder.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string]$OSYear,

    [string]$CustomerCsv = (Join-Path $PSScriptRoot 'GDAPCustomers.csv'),
    [string]$OutputPath  = $PSScriptRoot
)

$ErrorActionPreference = 'Stop'

if (-not (Test-Path $CustomerCsv)) {
    throw "Customer list not found at '$CustomerCsv'. Run Get-GDAPCustomerTenants.ps1 first."
}

# Multiple Az module versions are installed side-by-side on this machine.
# Pin the newest of each explicitly, Az.Accounts first, so a stale auto-picked
# version doesn't get loaded ahead of what the others require.
$moduleVersions = [ordered]@{
    'Az.Accounts'      = '5.5.3'
    'Az.Resources'     = '7.1.0'
    'Az.ResourceGraph' = '1.0.0'
    'Az.Compute'       = '11.9.0'
}
foreach ($m in $moduleVersions.Keys) {
    Import-Module $m -RequiredVersion $moduleVersions[$m] -ErrorAction Stop
}

$customers = Import-Csv -Path $CustomerCsv
if (-not $customers -or $customers.Count -eq 0) {
    Write-Warning "No customer tenants in '$CustomerCsv'. Nothing to scan."
    return
}

$timestamp  = Get-Date -Format 'yyyyMMdd_HHmmss'
$reportPath = Join-Path $OutputPath "Server${OSYear}_Report_$timestamp.csv"
$errorsPath = Join-Path $OutputPath "Server${OSYear}_Report_Errors_$timestamp.csv"

$findings = [System.Collections.Generic.List[object]]::new()
$errors   = [System.Collections.Generic.List[object]]::new()

function Add-ErrorRow {
    param($Stage, $TenantId, $TenantName, $SubscriptionId, $SubscriptionName, $Message)
    $errors.Add([pscustomobject]@{
        Stage             = $Stage
        TenantId          = $TenantId
        TenantName        = $TenantName
        SubscriptionId    = $SubscriptionId
        SubscriptionName  = $SubscriptionName
        Message           = $Message
    })
}

Write-Host "`n=== Sign in to Azure PowerShell ===" -ForegroundColor Cyan
Write-Host "You'll get a device code - open the printed URL in your normal browser and enter it there." -ForegroundColor Cyan
Connect-AzAccount -UseDeviceAuthentication | Out-Null
$homeCtx = Get-AzContext
Write-Host "Signed in to Azure as: $($homeCtx.Account.Id)" -ForegroundColor Green
Write-Host "Customer tenants to check: $($customers.Count)" -ForegroundColor Green
Write-Host "Searching for OS year: $OSYear" -ForegroundColor Green

$idx = 0
foreach ($customer in $customers) {
    $idx++
    $tenantLabel = if ($customer.TenantName) { $customer.TenantName } else { $customer.TenantId }
    Write-Host "`n[$idx/$($customers.Count)] Tenant: $tenantLabel ($($customer.TenantId))" -ForegroundColor Yellow

    $subs = $null
    try {
        $subs = Get-AzSubscription -TenantId $customer.TenantId -ErrorAction Stop
    } catch {
        Write-Host "  Skipped - no Azure RBAC access in this tenant: $($_.Exception.Message)" -ForegroundColor DarkGray
        Add-ErrorRow -Stage 'ListSubscriptions' -TenantId $customer.TenantId -TenantName $tenantLabel `
            -SubscriptionId '' -SubscriptionName '' -Message $_.Exception.Message
        continue
    }

    if (-not $subs -or $subs.Count -eq 0) {
        Write-Host "  No subscriptions visible in this tenant." -ForegroundColor DarkGray
        continue
    }

    foreach ($sub in $subs) {
        Write-Host "  Subscription: $($sub.Name) ($($sub.Id))" -ForegroundColor Gray
        try {
            Set-AzContext -Subscription $sub.Id -Tenant $customer.TenantId -ErrorAction Stop | Out-Null
        } catch {
            Add-ErrorRow -Stage 'SetContext' -TenantId $customer.TenantId -TenantName $tenantLabel `
                -SubscriptionId $sub.Id -SubscriptionName $sub.Name -Message $_.Exception.Message
            continue
        }

        $kql = @"
Resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend osType = tostring(properties.storageProfile.osDisk.osType)
| extend imgPublisher = tostring(properties.storageProfile.imageReference.publisher)
| extend imgOffer = tostring(properties.storageProfile.imageReference.offer)
| extend imgSku = tostring(properties.storageProfile.imageReference.sku)
| extend powerState = tostring(properties.extended.instanceView.powerState.displayStatus)
| project name, resourceGroup, location, osType, imgPublisher, imgOffer, imgSku, powerState, id
"@
        try {
            $vms = Search-AzGraph -Query $kql -Subscription $sub.Id -ErrorAction Stop
        } catch {
            Add-ErrorRow -Stage 'ResourceGraphQuery' -TenantId $customer.TenantId -TenantName $tenantLabel `
                -SubscriptionId $sub.Id -SubscriptionName $sub.Name -Message $_.Exception.Message
            continue
        }

        foreach ($vm in $vms) {
            $isKnownYear = $vm.imgSku -match [regex]::Escape($OSYear) -or $vm.imgOffer -match [regex]::Escape($OSYear)
            $isWindowsUnknown = ($vm.osType -eq 'Windows') -and -not $isKnownYear -and
                                 ([string]::IsNullOrEmpty($vm.imgSku) -or $vm.imgOffer -notmatch '^WindowsServer$' -or $vm.imgSku -notmatch '^\d{4}')

            if ($isKnownYear) {
                $findings.Add([pscustomobject]@{
                    TenantId          = $customer.TenantId
                    TenantName        = $tenantLabel
                    SubscriptionId    = $sub.Id
                    SubscriptionName  = $sub.Name
                    ResourceGroup     = $vm.resourceGroup
                    VMName            = $vm.name
                    Location          = $vm.location
                    PowerState        = $vm.powerState
                    DetectedOS        = "$($vm.imgOffer) $($vm.imgSku)"
                    DetectionMethod   = 'ImageReference'
                })
                Write-Host "    [$OSYear] $($vm.name) - $($vm.imgOffer) $($vm.imgSku)" -ForegroundColor Red
            }
            elseif ($isWindowsUnknown) {
                try {
                    $detail = Get-AzVM -ResourceGroupName $vm.resourceGroup -Name $vm.name -Status -ErrorAction Stop
                    $osName = $detail.OsName
                    if ($osName -match [regex]::Escape($OSYear)) {
                        $findings.Add([pscustomobject]@{
                            TenantId          = $customer.TenantId
                            TenantName        = $tenantLabel
                            SubscriptionId    = $sub.Id
                            SubscriptionName  = $sub.Name
                            ResourceGroup     = $vm.resourceGroup
                            VMName            = $vm.name
                            Location          = $vm.location
                            PowerState        = $vm.powerState
                            DetectedOS        = $osName
                            DetectionMethod   = 'InstanceView'
                        })
                        Write-Host "    [$OSYear] $($vm.name) - $osName (instance view)" -ForegroundColor Red
                    }
                } catch {
                    Add-ErrorRow -Stage 'InstanceViewCheck' -TenantId $customer.TenantId -TenantName $tenantLabel `
                        -SubscriptionId $sub.Id -SubscriptionName $sub.Name -Message "$($vm.name): $($_.Exception.Message)"
                }
            }
        }
    }
}

$findings | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
$errors   | Export-Csv -Path $errorsPath -NoTypeInformation -Encoding UTF8

Write-Host "`n=== Done ===" -ForegroundColor Cyan
Write-Host "GDAP customer tenants checked : $($customers.Count)" -ForegroundColor Green
Write-Host "Server $OSYear VMs found            : $($findings.Count)" -ForegroundColor $(if ($findings.Count -gt 0) { 'Red' } else { 'Green' })
Write-Host "Report                        : $reportPath" -ForegroundColor Green
Write-Host "Tenants/subs skipped or errored (no Azure RBAC via GDAP yet): $($errors.Count)" -ForegroundColor Yellow
Write-Host "Errors log                    : $errorsPath" -ForegroundColor Yellow
(No Ratings Yet)