Ping and Reboot All Offline CloudPCs using Graph

Photo of author
By Jeff LeBlanc

I use this script to check to see which CloudPCs are online and responding to ping and to restart any that are not. This can be useful if you need to deploy software or configurations to all CloudPCs and you want to make sure they are all only to receive your deployment.

You must have CloudPC Administrator and MgGraph permissions in your Azure tenant for this to work. I usually run this from a PowerShell 7 tab in Terminal and pre-authenticate to my

Tab Command Line: PowerShell-7.5.4-win-x64\pwsh.exe -NoExit -Command Connect-MgGraph -TenantId “<YourTenantID>” -NoWelcome

Modify the $NamePrefix as needed to match your CloudPC naming standards.

###########################################################################################################
#
# Script Name: Ping-Reboot-W365CloudPCs.ps1
#
# Description: Connects to Graph and queries all CloudPCs and reoirts online/offline status.
#              Reboots any computers not responding to ping.
#
# Version History: 1.0 - Initial Script
#
# Command Line: .\Ping-Reboot-W365CloudPCs.ps1 -PingOnly
#               .\Ping-Reboot-W365CloudPCs.ps1
#
###########################################################################################################

param(
  # If set, we only ping + generate OFFLINE.TXT and do NOT reboot anything
  [switch]$PingOnly,

  # Filter: only Cloud PCs whose managedDeviceName starts with this prefix
  [string]$NamePrefix = "W365",

  # Ping target becomes <managedDeviceName>.<DnsSuffix>
  [string]$DnsSuffix = "<YourDomain.com>",

  # Offline list output (one device short name per line)
  [string]$OfflineFile = ".\OFFLINE.TXT",

  # Ping timeout per device (seconds)
  [int]$PingCount = 1,
  [int]$PingTimeoutSeconds = 2,

  # Parallel throttle (only used in PS7+ parallel branch)
  [int]$Throttle = 40,

  # Only used if NOT already connected to Graph
  [switch]$UseDeviceCode,
  [switch]$DisableWam
)

$ErrorActionPreference="Stop"

function Write-Section {
    param([string]$Text)
    Write-Host ""
    Write-Host "==== $Text ====" -ForegroundColor Cyan
}


# Initialize-Graph Function
function Initialize-Graph {
  param(
    [switch]$UseDeviceCode,
    [switch]$DisableWam
  )

  # Ensure MgGraph request cmdlet exists before trying to call it
  # (If cmdlets aren't loaded, Get-MgContext/Invoke-MgGraphRequest won't exist)
  if (-not (Get-Command Invoke-MgGraphRequest -ErrorAction SilentlyContinue)) {
    Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
  }

  # If a context exists, we assume you're already connected and do nothing
  $ctx = $null;

  try {
    $ctx = Get-MgContext
  }
  catch{}

  # Connect only when no active account/tenant is present
  if (-not $ctx -or -not $ctx.Account -or -not $ctx.TenantId) {

    # Optional: force-disable WAM auth flow if your environment requires it
    if ($DisableWam) {
      $env:MSGRAPH_USE_WAM = "false"
    }

    # Connect with CloudPC.ReadWrite.All since reboot is a write operation
    if ($UseDeviceCode) {
      Connect-MgGraph -Scopes "CloudPC.ReadWrite.All" -UseDeviceCode | Out-Null
    }
    else {
      Connect-MgGraph -Scopes "CloudPC.ReadWrite.All" | Out-Null
    }
  }
}


# Get-AllCloudPCs Function
function Get-AllCloudPcs {
  # Pull Cloud PCs from Graph beta endpoint using paging via @odata.nextLink
  $uri="https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs"
  $pcs=@()

  while ($uri) {
    $r=Invoke-MgGraphRequest -Method GET -Uri $uri

    # Some pages may be empty; guard to avoid null issues
    if ($r -and $r.value) {
      $pcs += @($r.value)
    }

    # If no nextLink, this becomes $null and loop ends
    $uri=$r.'@odata.nextLink'
  }

  return $pcs
}


# Get-PingTargets Function
function Get-PingTargets {
  param(
    [object[]]$CloudPcs,
    [string]$NamePrefix,
    [string]$DnsSuffix
  )

  # Build a simple list of targets to ping:
  # Short = managedDeviceName, Target = FQDN used for ping, Id = CloudPC id used for reboot
  $targets=@()

  foreach ($pc in @($CloudPcs)) {
    $sn=[string]$pc.managedDeviceName

    # Skip objects without a managedDeviceName
    if (-not $sn) { continue }

    # Prefix filter (case-insensitive)
    if ($sn.StartsWith($NamePrefix,[System.StringComparison]::OrdinalIgnoreCase)) {
      $targets += [pscustomobject]@{
        Short=$sn
        Target=("$sn.$DnsSuffix".TrimEnd('.')) # protects if suffix is empty or ends with dot
        Id=$pc.id
      }
    }
  }

  return $targets
}


# Invoke-PingTargets Function
function Invoke-PingTargets {
  param(
    [object[]]$Targets,
    [int]$PingTimeoutSeconds,
    [int]$Throttle
  )

  # Ping each target and return objects with Online=$true/$false.
  # In PS7+, use ForEach-Object -Parallel for faster runs on large sets.
  if ($PSVersionTable.PSVersion.Major -ge 7) {

    # NOTE: this is the built-in PS7 parallel loop mechanism
    $ping = $Targets | ForEach-Object -Parallel {
      [pscustomobject]@{
        # what gets returned
        Short=$_.Short; Target=$_.Target; Id=$_.Id
        Online=(Test-Connection -ComputerName $_.Target -Count 1 -TimeoutSeconds $using:PingTimeoutSeconds -Quiet -ErrorAction SilentlyContinue)
      }
    } -ThrottleLimit $Throttle

    return @($ping)
  }

  # PS5.1 fallback (sequential)
  $ping=@()

  foreach ($t in @($Targets)) {
    $ping += [pscustomobject]@{
      # what gets returned
      Short=$t.Short; Target=$t.Target; Id=$t.Id
      Online=(Test-Connection -ComputerName $t.Target -Count $PingCount -Quiet -ErrorAction SilentlyContinue)
    }
  }

  return $ping
}


# Show-PingResults Function
function Show-PingResults {
  param([object[]]$PingResults)

  # Optional alphabetical sort (disabled by default)
  # $sorted = @($PingResults | Sort-Object Short)

  # Use original order unless sorting is re-enabled above
  $list = if ($sorted) { $sorted } else { $PingResults }

  foreach ($r in @($list)) {
    if ($r.Online) {
      Write-Host ("ONLINE: {0} ({1})" -f $r.Short, $r.Target) #-ForegroundColor Green
    }
    else {
      Write-Warning ("OFFLINE (ping failed): {0} ({1})" -f $r.Short, $r.Target)
    }
  }
}


# Get-OfflineList Function
function Get-OfflineList {
  param([object[]]$PingResults)

  # Build unique offline short-name list for OFFLINE.TXT
  $offline=@()

  foreach ($r in @($PingResults)) {
    if (-not $r.Online) {
      $offline += $r.Short
    }
  }

  $offline = @($offline | Sort-Object -Unique)
  return $offline
}


# Get-OfflinePingRows Function
function Get-OfflinePingRows {
  param([object[]]$PingResults)

  # Keep the full rows for offline devices (includes Id for reboot)
  $rows=@()

  foreach ($r in @($PingResults)) {
    if (-not $r.Online) {
      $rows += $r
    }
  }

  return $rows
}


# Restart-CloudPc Function
function Restart-CloudPc {
  param([string]$Id)

  # Calls the Graph Cloud PC reboot action:
  # POST /deviceManagement/virtualEndpoint/cloudPCs/{id}/reboot
  $u="https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/cloudPCs/$Id/reboot"

  # Retry a few times because Graph can return 409 conflict if the Cloud PC is busy
  for ($a=1;$a -le 5;$a++) {
    try {
      Invoke-MgGraphRequest -Method POST -Uri $u | Out-Null
      return $true
    }
    catch {

      # Handle the common "busy/conflict" cases with a small backoff
      if ($_.Exception.Message -match '409|conflict') {
        Start-Sleep -Seconds ([math]::Min(120,5*$a))
        continue
      }

      # Any other error: bubble up to the caller
      throw
    }
  }

  # Ran out of retries
  return $false
}


# ----------------------------------------------------------------------
# MAIN SCRIPT
# ----------------------------------------------------------------------
Write-Host "Starting Script Execution..." -ForegroundColor Cyan

# 1) Ensure Graph cmdlets exist + connect only if needed
Write-Host "Checking graph connection, if none found, prompt for login." -ForegroundColor White
Initialize-Graph -UseDeviceCode:$UseDeviceCode -DisableWam:$DisableWam


# 2) Pull full Cloud PC inventory and filter to target prefix
Write-Section "Fetch Cloud PC Inventory"
Write-Host "Fetching Cloud PC inventory (paged)..." -ForegroundColor Cyan

$pcs = Get-AllCloudPcs
$targets = Get-PingTargets -CloudPcs $pcs -NamePrefix $NamePrefix -DnsSuffix $DnsSuffix

if ($targets) {
  #Write-Host ("Filtered to {0} Cloud PCs where managedDeviceName starts with '{1}'." -f $w365Pcs.Count, $NamePrefix) -ForegroundColor Cyan
  Write-Host ("Filtered to {0} Cloud PCs total where managedDeviceName starts with '$NamePrefix'." -f $pcs.Count) -ForegroundColor Cyan
}
else {
  Write-Warning "No Cloud PCs matched prefix '$NamePrefix'"
  return
}


# 3) Ping all targets
Write-Section "Ping Cloud PCs & Generate OFFLINE.TXT"
Write-Host ("Pinging using suffix: .{0}" -f $DnsSuffix) -ForegroundColor Cyan
Write-Host ("PingCount={0}, TimeoutSeconds={1}, Parallel={2}, Throttle={3}" -f $PingCount, $PingTimeoutSeconds, (-not $NoParallelPing), $Throttle) -ForegroundColor Cyan

$ping = Invoke-PingTargets -Targets $targets -PingTimeoutSeconds $PingTimeoutSeconds -Throttle $Throttle


# 4) Display per-device ping results (ONLINE/OFFLINE)
Write-Host ""
Show-PingResults -PingResults $ping


# 5) Write OFFLINE.TXT for any ping failures
$offline = Get-OfflineList -PingResults $ping
$offline | Set-Content $OfflineFile
Write-Host ("OFFLINE saved: {0} ({1} systems)" -f (Resolve-Path $OfflineFile), $offline.Count) -ForegroundColor Yellow


# 6) If PingOnly, stop here (no reboot actions)
if ($PingOnly) {
  Write-Host "PingOnly enabled - skipping reboots." -ForegroundColor Yellow
  
  $totalPinged  = $ping.Count
  $onlineCount  = ($ping | Where-Object { $_.Online }).Count
  $offlineCount = ($ping | Where-Object { -not $_.Online }).Count

  Write-Host "PING SUMMARY   : TOTAL: $totalPinged  ONLINE: $onlineCount  OFFLINE: $offlineCount" -ForegroundColor DarkCyan
  return
}
else {
  Write-Host "Rebooting all offline Cloud PCs..." -ForegroundColor White
}


# 7 Set Summary counters
$totalPinged  = $ping.Count
$onlineCount  = ($ping | Where-Object { $_.Online }).Count
$offlineCount = ($ping | Where-Object { -not $_.Online }).Count

$rebootAttempted = 0
$rebootSuccess   = 0
$rebootFailed    = 0


# 8) Reboot Cloud PCs that failed ping (best-effort, logs to console)
$offlineRows = Get-OfflinePingRows -PingResults $ping

foreach ($row in $offlineRows) {
  $rebootAttempted++

  try {
    $ok = Restart-CloudPc -Id $row.Id

    if ($ok) {
      $rebootSuccess++
      Write-Host ("{0}: REBOOT SENT" -f $row.Short) -ForegroundColor Cyan
    }
    else {
      $rebootFailed++
      Write-Host ("{0}: RETRY LIMIT" -f $row.Short) -ForegroundColor Yellow
    }
  }
  catch {
    $rebootFailed++
    Write-Warning ("{0}: REBOOT FAILED - {1}" -f $row.Short, $_.Exception.Message)
  }
}


# Display Summary and Finish
Write-Host ""

if ($PingOnly) {
  Write-Host "PING SUMMARY   : TOTAL: $totalPinged  ONLINE: $onlineCount  OFFLINE: $offlineCount" -ForegroundColor DarkCyan
}
else {
  Write-Host "PING SUMMARY   : TOTAL: $totalPinged  ONLINE: $onlineCount  OFFLINE: $offlineCount" -ForegroundColor DarkCyan
  Write-Host "REBOOT SUMMARY : ATTEMPTED: $rebootAttempted  SUCCEEDED: $rebootSuccess  FAILED: $rebootFailed" -ForegroundColor DarkCyan
}

Write-Host "Finished script execution." -ForegroundColor Cyan

Leave a Comment