Public/Device/Update-PanDeviceLocation.ps1

function Update-PanDeviceLocation {
<#
.SYNOPSIS
Refresh the PanDevice device-group or vsys (and shared) layout in the PanDevice Location property.
.DESCRIPTION
Refresh the PanDevice device-group (Panorama) or vsys (NGFW) layout (and shared) in the PanDevice Location property. Not saved to disk. Refreshed at runtime.
.NOTES
Update-PanDeviceLocation doe *not* add new device-groups or vsys's. It simply refreshes what already exists on-device into the PanDevice Location property.
Update runs at most every 900 seconds. Can force a manual update with -Force.
Refresh- is not an approved verb. Update- it is.
.INPUTS
PanDevice[]
   You can pipe a PanDevice to this cmdlet
.OUTPUTS
None
.EXAMPLE
#>

   [CmdletBinding(SupportsShouldProcess,ConfirmImpact='Low')]
   param(
      [parameter(Mandatory=$true,ValueFromPipeline=$true,HelpMessage='PanDevice(s) on which location layout (vsys, device-group) will be determined')]
      [PanDevice[]] $Device,
      [parameter(HelpMessage='Force location layout update, regardless of elapsed time since last update')]
      [Switch] $Force
   )

   Begin {
      # Propagate -Debug and -Verbose to this module function, https://tinyurl.com/y5dcbb34
      if($PSBoundParameters.Debug) { $DebugPreference = 'Continue' }
      if($PSBoundParameters.Verbose) { $VerbosePreference = 'Continue' }
      # Announce
      Write-Debug ($MyInvocation.MyCommand.Name + ':')
      
      # For comparison
      $Now = Get-Date
      $UpdateInterval = New-TimeSpan -Seconds 900
   } # Begin block

   Process {
      foreach($DeviceCur in $PSBoundParameters.Device) {
         Write-Debug ('{0}: Device: {1}' -f $MyInvocation.MyCommand.Name,$DeviceCur.Name)
         if( (-not $PSBoundParameters.Force.IsPresent) -and $DeviceCur.LocationUpdated.AddSeconds($UpdateInterval.TotalSeconds) -gt $Now ) {
            # If PanDevice has been updated and interval has not passed, no need to update again
            Write-Debug ('{0}: Device: {1} locations updated already. Next update after {2}' -f
               $MyInvocation.MyCommand.Name,$DeviceCur.Name,$DeviceCur.LocationUpdated.AddSeconds($UpdateInterval.TotalSeconds))
            # Next iteration of foreach (next PanDevice)
            continue
         }

         # Ordered, case sensitive hashtable. Must initialize this way and *not* with [ordered]@{} to maintain case sensitivity
         $DeviceCurLocation = [System.Collections.Specialized.OrderedDictionary]::new()
         # Update shared first as it is the same for both Panorama and Ngfw
         $DeviceCurLocation.Add("shared", "/config/shared")

         # For broader compatibility between Panorama and NGFW, using the config action=complete capability with the XML-API
         # Originally, used an "@name" ending XPath to determine vsys and device-group list. Not ideal
         # $XPath = "/config/devices/entry[@name='localhost.localdomain']/vsys/entry/@name"
         # https://live.paloaltonetworks.com/t5/automation-api-discussions/retrieve-device-list-and-vsys-names-using-pan-rest-api/m-p/15238
         if($DeviceCur.Type -eq [PanDeviceType]::Panorama) {
            $XPath = "/config/devices/entry[@name='localhost.localdomain']/device-group"
            Write-Debug ('{0}: Device: {1} Panorama XPath: {2}' -f $MyInvocation.MyCommand.Name,$DeviceCur.Name,$XPath)
         }
         else {
            $XPath = "/config/devices/entry[@name='localhost.localdomain']/vsys"
            Write-Debug ('{0}: Device: {1} NGFW XPath: {2}' -f $MyInvocation.MyCommand.Name,$DeviceCur.Name,$XPath)
         }
         
         # Fetch the valid device-group (Panorama) or vsys (NGFW) using config action=complete
         $R = Invoke-PanXApi -Device $DeviceCur -Config -Complete -XPath $XPath
         if($R.Status -eq 'success') {
            # XML-API response to config action=complete is in <response><completions> and NOT <response><result> like everything else
            # The [PanResponse] type does NOT include a named "Completions" property (like it does "Result")
            # We can get at <completions> through the [PanResponse] "WRContent" property (that exists for just this type of obscure purpose)
            # <response status="success" code="19"><completions>
            # <completion value="Child" vxpath="/config/devices/entry[@name='localhost.localdomain']/device-group/entry[@name='Child']"/>
            # <completion value="Parent" vxpath="/config/devices/entry[@name='localhost.localdomain']/device-group/entry[@name='Parent']"/>
            # <completion value="Grandparent" vxpath="/config/devices/entry[@name='localhost.localdomain']/device-group/entry[@name='Grandparent']"/>
            # </completions></response>
            foreach($CompletionCur in $R.Response.completions.completion) {
               # Add each entry's name to an aggregate
               $DeviceCurLocation.Add($CompletionCur.value, $CompletionCur.vxpath)
            }

            # Update the PanDevice
            if($PSCmdlet.ShouldProcess('PanDeviceDb','Update ' + $DeviceCur.Name + ' vsys/device-group layout')) {
               $DeviceCur.Location = $DeviceCurLocation
               Write-Debug ('{0}: Device: {1} Location (Update): {2}' -f $MyInvocation.MyCommand.Name,$DeviceCur.Name,($DeviceCurLocation.keys -join ','))
               $DeviceCur.LocationUpdated = Get-Date
            }
         } # End if PanResponse success
      } # End foreach DeviceCur
   } # Process block

   End {
   } # End block
} # Function