Public/Export-LMDeviceGroupCustomProperties.ps1

<#
.SYNOPSIS
    Exports device group custom properties with inheritance tracking to CSV or returns objects.

.DESCRIPTION
    The Export-LMDeviceGroupCustomProperties function extracts specific custom properties from device groups,
    checking both direct properties and inherited properties from parent groups. It can export results to CSV
    or return objects for further processing.

.PARAMETER PropertyKeys
    Array of custom property keys to extract from device groups. Each property will be checked for both direct
    and inherited values.

.PARAMETER DeviceGroupId
    Optional device group ID to filter results to a specific group and its subgroups.

.PARAMETER DeviceGroupFilter
    Optional filter object to apply when retrieving device groups. Can include multiple conditions combined as AND operations.

.PARAMETER IncludeNumOfProperties
    Switch to include all numOf* properties (numOfHosts, numOfAWSDevices, etc.) in the output.

.PARAMETER ExportPath
    Optional file path to export results to CSV. If not specified, results are returned as objects.

.PARAMETER PassThru
    If specified, returns the results as objects even when ExportPath is provided.

.EXAMPLE
    Export-LMDeviceGroupCustomProperties -PropertyKeys @("nre_filter.application-delivery.gslb.CPU.dataPointName", "nre_filter.application-delivery.gslb.CPU.datasource_name")
    
    Extracts the specified custom properties from all device groups and displays the results.

.EXAMPLE
    Export-LMDeviceGroupCustomProperties -PropertyKeys "environment" -DeviceGroupId 1 -ExportPath "C:\reports\properties.csv"
    
    Extracts the "environment" property from device group 1 and its subgroups, exporting to CSV.

.EXAMPLE
    Export-LMDeviceGroupCustomProperties -PropertyKeys @("location", "team") -IncludeNumOfProperties -PassThru
    
    Extracts "location" and "team" properties from all device groups, includes numOf* properties, and returns objects.

.NOTES
    You must run Connect-LMAccount before running this command. The function checks parent groups by traversing
    the parentId chain to determine if properties are inherited.

.INPUTS
    None. Does not accept pipeline input.

.LINK
    Module repo: https://github.com/logicmonitor/Logic.Monitor.SE

.LINK
    PSGallery: https://www.powershellgallery.com/packages/Logic.Monitor.SE
#>

Function Export-LMDeviceGroupCustomProperties {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [String[]]$PropertyKeys,

        [Parameter(Mandatory = $false)]
        [Int]$DeviceGroupId,

        [Parameter(Mandatory = $false)]
        [Object]$DeviceGroupFilter,

        [Parameter(Mandatory = $false)]
        [Switch]$IncludeNumOfProperties,

        [Parameter(Mandatory = $false)]
        [String]$ExportPath,

        [Parameter(Mandatory = $false)]
        [Switch]$PassThru
    )

    # Check if we are logged in and have valid api creds
    If ($(Get-LMAccountStatus).Valid) {
        
        Write-Host "[INFO]: Retrieving device groups..."
        
        # Get device groups based on parameters
        If ($DeviceGroupId) {
            $deviceGroups = @()
            $startingGroup = Get-LMDeviceGroup -Id $DeviceGroupId
            $deviceGroups += $startingGroup
            
            # Get all parent groups up to root for proper inheritance checking
            $parentGroups = [System.Collections.ArrayList]@()
            $currentParentId = $startingGroup.parentId
            while ($currentParentId -ne 0) {
                try {
                    $parentGroup = Get-LMDeviceGroup -Id $currentParentId
                    If ($parentGroup) {
                        $parentGroups.Add($parentGroup) | Out-Null
                        $currentParentId = $parentGroup.parentId
                    }
                    else {
                        break
                    }
                }
                catch {
                    break
                }
            }
            $deviceGroups += $parentGroups
            
            # Recursively get all subgroups
            $allGroups = [System.Collections.ArrayList]@()
            $allGroups.AddRange($deviceGroups) | Out-Null
            
            function Get-AllSubGroups {
                param([Int]$ParentId, [System.Collections.ArrayList]$GroupsList)
                $subGroups = Get-LMDeviceGroupGroup -Id $ParentId
                If ($subGroups) {
                    foreach ($subGroup in $subGroups) {
                        $GroupsList.Add($subGroup) | Out-Null
                        Get-AllSubGroups -ParentId $subGroup.id -GroupsList $GroupsList
                    }
                }
            }
            Get-AllSubGroups -ParentId $DeviceGroupId -GroupsList $allGroups
            $deviceGroups = $allGroups
        }
        ElseIf ($DeviceGroupFilter) {
            $deviceGroups = Get-LMDeviceGroup -Filter $DeviceGroupFilter
        }
        Else {
            $deviceGroups = Get-LMDeviceGroup
        }

        If (-not $deviceGroups) {
            Write-Warning "No device groups found matching the specified criteria."
            Return
        }

        Write-Host "[INFO]: Found $(($deviceGroups | Measure-Object).Count) device group(s). Creating lookup dictionary..."

        # Create a lookup dictionary for quick parent group access
        $groupLookup = @{}
        foreach ($group in $deviceGroups) {
            $groupLookup[$group.id] = $group
        }

        # Function to check parent chain for a custom property
        function Get-InheritedProperty {
            param(
                [Object]$CurrentGroup,
                [Hashtable]$GroupLookup,
                [String]$PropertyKey
            )
            
            # Check current group's customProperties first
            if ($CurrentGroup.customProperties) {
                $prop = $CurrentGroup.customProperties | Where-Object { $_.name -eq $PropertyKey }
                if ($prop) {
                    return @{ Value = $prop.value; Source = "Direct" }
                }
            }
            
            # Traverse up the parent chain
            $parentId = $CurrentGroup.parentId
            while ($parentId -ne 0 -and $GroupLookup.ContainsKey($parentId)) {
                $parentGroup = $GroupLookup[$parentId]
                
                if ($parentGroup.customProperties) {
                    $prop = $parentGroup.customProperties | Where-Object { $_.name -eq $PropertyKey }
                    if ($prop) {
                        return @{ Value = $prop.value; Source = "Inherited" }
                    }
                }
                
                $parentId = $parentGroup.parentId
            }
            
            return @{ Value = "N/A"; Source = "N/A" }
        }

        Write-Host "[INFO]: Processing $(($deviceGroups | Measure-Object).Count) device group(s) and extracting $(($PropertyKeys | Measure-Object).Count) custom property(ies)..."

        # Process each device group
        $results = $deviceGroups | ForEach-Object {
            $group = $_
            
            # Get properties (checking direct first, then parent chain)
            $propertyResults = @{}
            foreach ($propertyKey in $PropertyKeys) {
                $result = Get-InheritedProperty -CurrentGroup $group -GroupLookup $groupLookup -PropertyKey $propertyKey
                $propertyResults[$propertyKey] = $result.Value
                $propertyResults["$propertyKey`Source"] = $result.Source
            }
            
            # Build output object
            $outputObject = [PSCustomObject]@{
                id = $group.id
                name = $group.name
                parentId = $group.parentId
            }

            # Add property values and sources
            foreach ($key in $propertyResults.Keys) {
                $outputObject | Add-Member -MemberType NoteProperty -Name $key -Value $propertyResults[$key]
            }

            # Add numOf* properties if requested
            If ($IncludeNumOfProperties) {
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfHosts" -Value $group.numOfHosts
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfAWSDevices" -Value $group.numOfAWSDevices
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfAzureDevices" -Value $group.numOfAzureDevices
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfGcpDevices" -Value $group.numOfGcpDevices
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfOciDevices" -Value $group.numOfOciDevices
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfKubernetesDevices" -Value $group.numOfKubernetesDevices
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfDirectDevices" -Value $group.numOfDirectDevices
                $outputObject | Add-Member -MemberType NoteProperty -Name "numOfDirectSubGroups" -Value $group.numOfDirectSubGroups
            }

            $outputObject
        }

        # Export to CSV if path specified
        If ($ExportPath) {
            Write-Host "[INFO]: Exporting results to CSV: $ExportPath"
            $results | Export-Csv -Path $ExportPath -NoTypeInformation
            Write-Host "[INFO]: Successfully exported $(($results | Measure-Object).Count) record(s) to $ExportPath"
        }

        # Return results if PassThru is specified or no ExportPath
        If ($PassThru -or -not $ExportPath) {
            Return $results
        }
    }
    Else {
        Write-Error "Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again."
    }
}