Public/System/Get-VergeInventory.ps1

function Get-VergeInventory {
    <#
    .SYNOPSIS
        Generates a comprehensive inventory report of VergeOS infrastructure.

    .DESCRIPTION
        Get-VergeInventory aggregates inventory data across all resource types in a VergeOS
        system, similar to RVtools for VMware environments. Returns detailed information
        about VMs, networks, storage, nodes, clusters, tenants, and snapshots.

        The inventory can be exported to CSV, JSON, or used with Export-Excel for
        comprehensive Excel reports.

    .PARAMETER ResourceType
        Filter inventory by resource type(s). Valid values:
        - VMs: Virtual machines with CPU, RAM, OS, power state
        - Networks: Virtual networks with DHCP, DNS, IP configuration
        - Storage: Storage tiers with capacity and usage
        - Nodes: Physical nodes with hardware details
        - Clusters: Cluster configuration and resource utilization
        - Tenants: Multi-tenant environments
        - VMSnapshots: Individual VM and tenant point-in-time snapshots
        - CloudSnapshots: System-wide cloud snapshots (with immutability status)
        - NAS: NAS services, volumes, and shares
        - All: All resource types (default)

    .PARAMETER IncludeSnapshots
        Include snapshot VMs in the VM list. By default, VMs that are point-in-time snapshots
        of other VMs are excluded. Snapshot metadata is available via VMSnapshots and
        CloudSnapshots resource types regardless of this setting.

    .PARAMETER IncludePoweredOff
        Include powered-off VMs. By default, all VMs are included regardless of power state.
        Set to $false to exclude powered-off VMs.

    .PARAMETER Summary
        Return summary counts only instead of detailed inventory data.

    .PARAMETER Server
        The VergeOS connection to use. Defaults to the current default connection.

    .EXAMPLE
        Get-VergeInventory

        Returns complete inventory of all resources.

    .EXAMPLE
        Get-VergeInventory -ResourceType VMs, Networks

        Returns inventory of VMs and networks only.

    .EXAMPLE
        Get-VergeInventory -Summary

        Returns summary counts for all resource types.

    .EXAMPLE
        Get-VergeInventory -ResourceType VMs | Export-Csv -Path "VM_Inventory.csv"

        Exports VM inventory to CSV format.

    .EXAMPLE
        Get-VergeInventory | Export-Excel -Path "VergeOS_Inventory.xlsx" -WorksheetName "Inventory"

        Exports full inventory to Excel (requires ImportExcel module).

    .EXAMPLE
        Get-VergeInventory -ResourceType VMs -IncludePoweredOff:$false

        Returns inventory of running VMs only.

    .EXAMPLE
        $inventory = Get-VergeInventory
        $inventory.VMs | Format-Table Name, PowerState, CPUCores, RAM
        $inventory.Networks | Format-Table Name, Type, NetworkAddress
        $inventory.Storage | Format-Table Tier, CapacityGB, UsedGB, UsedPercent

        Access specific resource types from the inventory object.

    .OUTPUTS
        PSCustomObject with PSTypeName 'Verge.Inventory' containing:
        - VMs: Array of VM objects
        - Networks: Array of network objects
        - Storage: Array of storage tier objects
        - Nodes: Array of node objects
        - Clusters: Array of cluster objects
        - Tenants: Array of tenant objects
        - VMSnapshots: Array of VM/tenant snapshot objects
        - CloudSnapshots: Array of cloud snapshot objects (with immutability info)
        - NAS: Array of NAS service/volume objects
        - Summary: Object with counts per resource type
        - GeneratedAt: Timestamp of inventory generation
        - Server: VergeOS server name

        When -Summary is specified, returns only the Summary object.

    .NOTES
        For large environments, consider filtering by ResourceType to reduce query time.
        Use with Export-Excel module for RVtools-style Excel reports with multiple worksheets.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Position = 0)]
        [ValidateSet('All', 'VMs', 'Networks', 'Storage', 'Nodes', 'Clusters', 'Tenants', 'VMSnapshots', 'CloudSnapshots', 'NAS')]
        [string[]]$ResourceType = @('All'),

        [Parameter()]
        [switch]$IncludeSnapshots,

        [Parameter()]
        [bool]$IncludePoweredOff = $true,

        [Parameter()]
        [switch]$Summary,

        [Parameter()]
        [object]$Server
    )

    begin {
        # Resolve connection
        if (-not $Server) {
            $Server = $script:DefaultConnection
        }
        if (-not $Server) {
            throw [System.InvalidOperationException]::new(
                'Not connected to VergeOS. Use Connect-VergeOS to establish a connection.'
            )
        }

        # Determine which resources to collect
        $collectAll = $ResourceType -contains 'All'
        $collectVMs = $collectAll -or $ResourceType -contains 'VMs'
        $collectNetworks = $collectAll -or $ResourceType -contains 'Networks'
        $collectStorage = $collectAll -or $ResourceType -contains 'Storage'
        $collectNodes = $collectAll -or $ResourceType -contains 'Nodes'
        $collectClusters = $collectAll -or $ResourceType -contains 'Clusters'
        $collectTenants = $collectAll -or $ResourceType -contains 'Tenants'
        $collectVMSnapshots = $collectAll -or $ResourceType -contains 'VMSnapshots'
        $collectCloudSnapshots = $collectAll -or $ResourceType -contains 'CloudSnapshots'
        $collectNAS = $collectAll -or $ResourceType -contains 'NAS'
    }

    process {
        try {
            Write-Verbose "Generating inventory from $($Server.Server)"

            # Initialize inventory object
            $inventory = [PSCustomObject]@{
                PSTypeName     = 'Verge.Inventory'
                Server         = $Server.Server
                GeneratedAt    = Get-Date
                VMs            = @()
                Networks       = @()
                Storage        = @()
                Nodes          = @()
                Clusters       = @()
                Tenants        = @()
                VMSnapshots    = @()
                CloudSnapshots = @()
                NAS            = @()
                Summary        = $null
            }

            # Collect VMs
            if ($collectVMs) {
                Write-Verbose "Collecting VM inventory..."
                $vmParams = @{ Server = $Server }
                if ($IncludeSnapshots) {
                    $vmParams['IncludeSnapshots'] = $true
                }

                $vms = Get-VergeVM @vmParams

                # Filter powered-off if requested
                if (-not $IncludePoweredOff) {
                    $vms = $vms | Where-Object { $_.PowerState -eq 'Running' }
                }

                # Bulk fetch all drives and NICs in 2 API calls instead of 2N calls
                # This is much more efficient for large environments
                Write-Verbose "Bulk fetching drives and NICs for all VMs..."

                # Fetch all virtual drives (physical_status eq null excludes physical host drives)
                $driveQuery = @{
                    filter = 'physical_status eq null'
                    fields = '$key,name,machine,disksize,used_bytes,media_source#allocated_bytes as allocated_bytes'
                }
                $driveResponse = Invoke-VergeAPI -Method GET -Endpoint 'machine_drives' -Query $driveQuery -Connection $Server -ErrorAction SilentlyContinue
                $allDrives = if ($driveResponse -is [array]) { $driveResponse } elseif ($driveResponse) { @($driveResponse) } else { @() }

                # Fetch all NICs
                $nicQuery = @{
                    fields = '$key,name,machine'
                }
                $nicResponse = Invoke-VergeAPI -Method GET -Endpoint 'machine_nics' -Query $nicQuery -Connection $Server -ErrorAction SilentlyContinue
                $allNICs = if ($nicResponse -is [array]) { $nicResponse } elseif ($nicResponse) { @($nicResponse) } else { @() }

                # Build lookup tables by machine key for O(1) access
                # API returns 'machine' field, not 'MachineKey'
                $drivesByMachine = @{}
                foreach ($drive in $allDrives) {
                    $machineKey = $drive.machine
                    if ($machineKey -and -not $drivesByMachine.ContainsKey($machineKey)) {
                        $drivesByMachine[$machineKey] = [System.Collections.Generic.List[object]]::new()
                    }
                    if ($machineKey) {
                        $drivesByMachine[$machineKey].Add($drive)
                    }
                }

                $nicsByMachine = @{}
                foreach ($nic in $allNICs) {
                    $machineKey = $nic.machine
                    if ($machineKey -and -not $nicsByMachine.ContainsKey($machineKey)) {
                        $nicsByMachine[$machineKey] = [System.Collections.Generic.List[object]]::new()
                    }
                    if ($machineKey) {
                        $nicsByMachine[$machineKey].Add($nic)
                    }
                }

                # Enrich VMs with drive and NIC counts using lookup tables
                $inventory.VMs = foreach ($vm in $vms) {
                    $drives = if ($drivesByMachine.ContainsKey($vm.MachineKey)) {
                        $drivesByMachine[$vm.MachineKey]
                    } else { @() }

                    $nics = if ($nicsByMachine.ContainsKey($vm.MachineKey)) {
                        $nicsByMachine[$vm.MachineKey]
                    } else { @() }

                    # Calculate total disk size from raw API response
                    # disksize or allocated_bytes, convert from bytes to GB
                    $totalDiskBytes = ($drives | ForEach-Object {
                        if ($_.disksize) { $_.disksize }
                        elseif ($_.allocated_bytes) { $_.allocated_bytes }
                        else { 0 }
                    } | Measure-Object -Sum).Sum
                    $totalDiskGB = $totalDiskBytes / 1GB

                    [PSCustomObject]@{
                        PSTypeName       = 'Verge.Inventory.VM'
                        Key              = $vm.Key
                        Name             = $vm.Name
                        Description      = $vm.Description
                        PowerState       = $vm.PowerState
                        CPUCores         = $vm.CPUCores
                        RAMGB            = [math]::Round($vm.RAM / 1024, 1)
                        RAMMb            = $vm.RAM
                        OSFamily         = $vm.OSFamily
                        GuestAgent       = $vm.GuestAgent
                        UEFI             = $vm.UEFI
                        SecureBoot       = $vm.SecureBoot
                        MachineType      = $vm.MachineType
                        Cluster          = $vm.Cluster
                        Node             = $vm.Node
                        HAGroup          = $vm.HAGroup
                        SnapshotProfile  = $vm.SnapshotProfile
                        DiskCount        = @($drives).Count
                        TotalDiskGB      = [math]::Round($totalDiskGB, 1)
                        NICCount         = @($nics).Count
                        Created          = $vm.Created
                        Modified         = $vm.Modified
                    }
                }
            }

            # Collect Networks
            if ($collectNetworks) {
                Write-Verbose "Collecting network inventory..."
                $networks = Get-VergeNetwork -Server $Server

                $inventory.Networks = foreach ($net in $networks) {
                    [PSCustomObject]@{
                        PSTypeName     = 'Verge.Inventory.Network'
                        Key            = $net.Key
                        Name           = $net.Name
                        Description    = $net.Description
                        Type           = $net.Type
                        PowerState     = $net.PowerState
                        NetworkAddress = $net.NetworkAddress
                        IPAddress      = $net.IPAddress
                        Gateway        = $net.Gateway
                        MTU            = $net.MTU
                        DHCPEnabled    = $net.DHCPEnabled
                        DHCPStart      = $net.DHCPStart
                        DHCPStop       = $net.DHCPStop
                        DNS            = $net.DNS
                        Domain         = $net.Domain
                        Cluster        = $net.Cluster
                        Node           = $net.Node
                    }
                }
            }

            # Collect Storage
            if ($collectStorage) {
                Write-Verbose "Collecting storage inventory..."
                $tiers = Get-VergeStorageTier -Server $Server

                $inventory.Storage = foreach ($tier in $tiers) {
                    [PSCustomObject]@{
                        PSTypeName   = 'Verge.Inventory.Storage'
                        Tier         = $tier.Tier
                        Description  = $tier.Description
                        CapacityGB   = $tier.CapacityGB
                        UsedGB       = $tier.UsedGB
                        FreeGB       = $tier.FreeGB
                        AllocatedGB  = $tier.AllocatedGB
                        UsedPercent  = $tier.UsedPercent
                        DedupeRatio  = $tier.DedupeRatio
                        ReadOps      = $tier.ReadOps
                        WriteOps     = $tier.WriteOps
                    }
                }
            }

            # Collect Nodes
            if ($collectNodes) {
                Write-Verbose "Collecting node inventory..."
                $nodes = Get-VergeNode -Server $Server

                $inventory.Nodes = foreach ($node in $nodes) {
                    [PSCustomObject]@{
                        PSTypeName      = 'Verge.Inventory.Node'
                        Key             = $node.Key
                        Name            = $node.Name
                        Status          = $node.Status
                        Cluster         = $node.Cluster
                        Cores           = $node.Cores
                        RAMGB           = [math]::Round($node.RAM / 1024, 1)
                        RAMMb           = $node.RAM
                        MaintenanceMode = $node.MaintenanceMode
                        NeedsRestart    = $node.NeedsRestart
                        RestartReason   = $node.RestartReason
                        IOMMU           = $node.IOMMU
                        VergeOSVersion  = $node.VergeOSVersion
                        KernelVersion   = $node.KernelVersion
                    }
                }
            }

            # Collect Clusters
            if ($collectClusters) {
                Write-Verbose "Collecting cluster inventory..."
                $clusters = Get-VergeCluster -Server $Server

                $inventory.Clusters = foreach ($cluster in $clusters) {
                    [PSCustomObject]@{
                        PSTypeName         = 'Verge.Inventory.Cluster'
                        Key                = $cluster.Key
                        Name               = $cluster.Name
                        Description        = $cluster.Description
                        Status             = $cluster.Status
                        TotalNodes         = $cluster.TotalNodes
                        OnlineNodes        = $cluster.OnlineNodes
                        OnlineCores        = $cluster.OnlineCores
                        UsedCores          = $cluster.UsedCores
                        OnlineRAMGB        = [math]::Round($cluster.OnlineRAM / 1024, 1)
                        UsedRAMGB          = [math]::Round($cluster.UsedRAM / 1024, 1)
                        RunningMachines    = $cluster.RunningMachines
                        DefaultCPUType     = $cluster.DefaultCPUType
                        NestedVirtualization = $cluster.NestedVirtualization
                    }
                }
            }

            # Collect Tenants
            if ($collectTenants) {
                Write-Verbose "Collecting tenant inventory..."
                $tenantParams = @{ Server = $Server }
                if ($IncludeSnapshots) {
                    $tenantParams['IncludeSnapshots'] = $true
                }
                $tenants = Get-VergeTenant @tenantParams

                $inventory.Tenants = foreach ($tenant in $tenants) {
                    [PSCustomObject]@{
                        PSTypeName  = 'Verge.Inventory.Tenant'
                        Key         = $tenant.Key
                        Name        = $tenant.Name
                        Description = $tenant.Description
                        Status      = $tenant.Status
                        State       = $tenant.State
                        IsRunning   = $tenant.IsRunning
                        Isolated    = $tenant.Isolated
                        URL         = $tenant.URL
                        UIAddress   = $tenant.UIAddress
                        NetworkName = $tenant.NetworkName
                        Created     = $tenant.Created
                        Started     = $tenant.Started
                    }
                }
            }

            # Collect VM Snapshots (individual VM point-in-time snapshots - manually created only)
            if ($collectVMSnapshots) {
                Write-Verbose "Collecting VM snapshot inventory..."
                $vmSnapshotList = @()

                # VM Snapshots - need to iterate over VMs
                # Only include manually created snapshots (not those created by cloud snapshot profiles)
                $allVMs = Get-VergeVM -Server $Server -IncludeSnapshots:$false

                foreach ($vm in $allVMs) {
                    $vmSnapshots = Get-VergeVMSnapshot -VM $vm -Server $Server -ErrorAction SilentlyContinue
                    foreach ($snap in $vmSnapshots) {
                        # Only include VM-only snapshots (not part of cloud snapshots)
                        # If IsCloudSnapshot is true, the snapshot was created as part of a cloud snapshot
                        if (-not $snap.IsCloudSnapshot) {
                            $vmSnapshotList += [PSCustomObject]@{
                                PSTypeName      = 'Verge.Inventory.VMSnapshot'
                                Key             = $snap.Key
                                Name            = $snap.Name
                                Description     = $snap.Description
                                VMName          = $snap.VMName
                                VMKey           = $snap.VMKey
                                Created         = $snap.Created
                                Expires         = $snap.Expires
                                NeverExpires    = $snap.NeverExpires
                                Quiesced        = $snap.Quiesced
                                CreatedManually = $snap.CreatedManually
                            }
                        }
                    }
                }

                # Also collect tenant snapshots that are manually created
                $allTenants = Get-VergeTenant -Server $Server -ErrorAction SilentlyContinue
                foreach ($tenant in $allTenants) {
                    $tenantSnapshots = Get-VergeTenantSnapshot -Tenant $tenant -Server $Server -ErrorAction SilentlyContinue
                    foreach ($snap in $tenantSnapshots) {
                        # Tenant snapshots from manual creation (not part of cloud snapshot profile)
                        # Note: Tenant snapshots don't have CreatedManually field, so we include all
                        # but they should be rare compared to cloud snapshots
                        $vmSnapshotList += [PSCustomObject]@{
                            PSTypeName      = 'Verge.Inventory.VMSnapshot'
                            Key             = $snap.Key
                            Name            = $snap.Name
                            Description     = $snap.Description
                            VMName          = "[Tenant] $($snap.TenantName)"
                            VMKey           = $snap.TenantKey
                            Created         = $snap.Created
                            Expires         = $snap.Expires
                            NeverExpires    = ($snap.ExpiresTimestamp -eq 0)
                            Quiesced        = $null
                            CreatedManually = $null
                        }
                    }
                }

                $inventory.VMSnapshots = $vmSnapshotList
            }

            # Collect Cloud Snapshots (system-wide snapshots)
            if ($collectCloudSnapshots) {
                Write-Verbose "Collecting cloud snapshot inventory..."
                $cloudSnapshotList = @()

                $cloudSnapshots = Get-VergeCloudSnapshot -Server $Server -IncludeExpired -ErrorAction SilentlyContinue
                foreach ($snap in $cloudSnapshots) {
                    # Determine if snapshot is currently expired
                    $isExpired = if ($snap.Expires) {
                        $snap.Expires -lt (Get-Date)
                    } else {
                        $false
                    }

                    $cloudSnapshotList += [PSCustomObject]@{
                        PSTypeName           = 'Verge.Inventory.CloudSnapshot'
                        Key                  = $snap.Key
                        Name                 = $snap.Name
                        Description          = $snap.Description
                        Created              = $snap.Created
                        Expires              = $snap.Expires
                        NeverExpires         = $snap.NeverExpires
                        IsExpired            = $isExpired
                        Profile              = $snap.SnapshotProfileName
                        Status               = $snap.Status
                        Immutable            = $snap.Immutable
                        ImmutableStatus      = $snap.ImmutableStatus
                        ImmutableLockExpires = $snap.ImmutableLockExpires
                        RemoteSync           = $snap.RemoteSync
                    }
                }

                $inventory.CloudSnapshots = $cloudSnapshotList
            }

            # Collect NAS
            if ($collectNAS) {
                Write-Verbose "Collecting NAS inventory..."
                $nasItems = @()

                # NAS Services
                $nasServices = Get-VergeNASService -Server $Server -ErrorAction SilentlyContinue
                foreach ($svc in $nasServices) {
                    $nasItems += [PSCustomObject]@{
                        PSTypeName  = 'Verge.Inventory.NAS'
                        ItemType    = 'Service'
                        Key         = $svc.Key
                        Name        = $svc.Name
                        Description = $svc.Description
                        Status      = $svc.Status
                        IPAddress   = $svc.IPAddress
                        Cluster     = $svc.Cluster
                        Node        = $svc.Node
                    }
                }

                # NAS Volumes
                $nasVolumes = Get-VergeNASVolume -Server $Server -ErrorAction SilentlyContinue
                foreach ($vol in $nasVolumes) {
                    $nasItems += [PSCustomObject]@{
                        PSTypeName  = 'Verge.Inventory.NAS'
                        ItemType    = 'Volume'
                        Key         = $vol.Key
                        Name        = $vol.Name
                        Description = $vol.Description
                        Status      = $vol.MountStatus
                        Tier        = $vol.PreferredTier
                        SizeGB      = $vol.MaxSizeGB
                        UsedGB      = $vol.UsedGB
                        NASService  = $vol.NASService
                    }
                }

                $inventory.NAS = $nasItems
            }

            # Generate summary
            $summaryData = [PSCustomObject]@{
                PSTypeName        = 'Verge.Inventory.Summary'
                Server            = $Server.Server
                GeneratedAt       = $inventory.GeneratedAt
                VMsTotal          = $inventory.VMs.Count
                VMsRunning        = ($inventory.VMs | Where-Object PowerState -eq 'Running').Count
                VMsStopped        = ($inventory.VMs | Where-Object PowerState -eq 'Stopped').Count
                TotalCPUCores     = ($inventory.VMs | Measure-Object -Property CPUCores -Sum).Sum
                TotalRAMGB        = [math]::Round(($inventory.VMs | Measure-Object -Property RAMGB -Sum).Sum, 1)
                TotalDiskGB       = [math]::Round(($inventory.VMs | Measure-Object -Property TotalDiskGB -Sum).Sum, 1)
                NetworksTotal     = $inventory.Networks.Count
                NetworksRunning   = ($inventory.Networks | Where-Object PowerState -eq 'Running').Count
                StorageTiers      = $inventory.Storage.Count
                StorageCapacityGB = [math]::Round(($inventory.Storage | Measure-Object -Property CapacityGB -Sum).Sum, 1)
                StorageUsedGB     = [math]::Round(($inventory.Storage | Measure-Object -Property UsedGB -Sum).Sum, 1)
                NodesTotal        = $inventory.Nodes.Count
                NodesOnline       = ($inventory.Nodes | Where-Object Status -eq 'Running').Count
                ClustersTotal     = $inventory.Clusters.Count
                TenantsTotal      = $inventory.Tenants.Count
                TenantsOnline     = ($inventory.Tenants | Where-Object IsRunning -eq $true).Count
                VMSnapshotsTotal  = $inventory.VMSnapshots.Count
                CloudSnapshotsTotal = $inventory.CloudSnapshots.Count
                NASServices       = ($inventory.NAS | Where-Object ItemType -eq 'Service').Count
                NASVolumes        = ($inventory.NAS | Where-Object ItemType -eq 'Volume').Count
            }

            $inventory.Summary = $summaryData

            # Return summary only if requested
            if ($Summary) {
                Write-Output $summaryData
            }
            else {
                Write-Output $inventory
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}