Private/Get-DMVlanParentPortStatus.ps1

function Get-DMVlanParentPortStatus {
    <#
    .SYNOPSIS
        Read-only idle-port guard for test-owned VLAN lifecycle workflows.
 
    .DESCRIPTION
        Determines whether a candidate parent port is idle enough to be used as
        the parent of a *test-owned* VLAN create/delete workflow. The guard is a
        pure read-only inspection built from the existing network inventory
        getters -- it never mutates, creates, or deletes any object.
 
        A port is treated as unsafe (InUse) when any existing association is
        found: a LIF homed on the port, a VLAN parented on the port, membership
        in a bond, or membership in a failover group. Any inspection failure is
        treated as Unknown (also unsafe): the guard never reports Idle unless it
        positively confirmed every checked association is empty.
 
        The caller must treat both InUse and Unknown as "do not use this port".
 
    .PARAMETER PortId
        ID of the candidate parent port to inspect.
 
    .PARAMETER WebSession
        Optional session for the REST reads. Defaults to the module's cached
        $script:CurrentOceanstorSession session.
 
    .OUTPUTS
        PSCustomObject with:
            PortId - the inspected port ID
            IsIdle - $true only when Status is 'Idle'
            Status - 'Idle', 'InUse', or 'Unknown'
            Reasons - human-readable reasons for InUse/Unknown
            CheckedAssociations - association kinds that were inspected
 
    .NOTES
        Filename: Get-DMVlanParentPortStatus.ps1
        Read-only. Never mutates. Unknown state is unsafe by design.
    #>

    [CmdletBinding()]
    [OutputType('PSCustomObject')]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string]$PortId,

        [Parameter()]
        [pscustomobject]$WebSession
    )

    $session = if ($WebSession) { $WebSession } else { $script:CurrentOceanstorSession }

    $reasons = New-Object System.Collections.Generic.List[string]
    $errors = New-Object System.Collections.Generic.List[string]
    $checked = New-Object System.Collections.Generic.List[string]

    # 1. LIFs homed on the port (server-side HOMEPORTID filter).
    $checked.Add('Lif')
    try {
        $lifs = @(Get-DMLif -WebSession $session -HomePortId $PortId)
        if ($lifs.Count -gt 0) {
            $reasons.Add("Port hosts $($lifs.Count) LIF(s).")
        }
    }
    catch {
        $errors.Add("LIF association check failed: $($_.Exception.Message)")
    }

    # 2. VLANs parented on the port (VLAN 'Port Id' == candidate port).
    $checked.Add('Vlan')
    try {
        $childVlans = @(Get-DMvLan -WebSession $session | Where-Object { $_.'Port Id' -eq $PortId })
        if ($childVlans.Count -gt 0) {
            $reasons.Add("Port is the parent of $($childVlans.Count) VLAN(s).")
        }
    }
    catch {
        $errors.Add("VLAN association check failed: $($_.Exception.Message)")
    }

    # 3. Bond membership (port ID appears in a bond's Ethernet port list).
    $checked.Add('Bond')
    try {
        $memberBonds = @(Get-DMPortBond -WebSession $session |
                Where-Object { $_.'Ethernet Ports' -and ($_.'Ethernet Ports' -match [regex]::Escape($PortId)) })
        if ($memberBonds.Count -gt 0) {
            $reasons.Add("Port is a member of bond(s): $(($memberBonds.Name) -join ', ').")
        }
    }
    catch {
        $errors.Add("Bond membership check failed: $($_.Exception.Message)")
    }

    # 4. Failover-group membership (port ID appears among any group's members).
    $checked.Add('FailoverGroup')
    try {
        foreach ($group in @(Get-DMFailoverGroup -WebSession $session)) {
            $members = @(Get-DMFailoverGroupMember -WebSession $session -Id $group.Id)
            if ($members | Where-Object { $_.Id -eq $PortId }) {
                $reasons.Add("Port is a member of failover group '$($group.Name)'.")
            }
        }
    }
    catch {
        $errors.Add("Failover-group membership check failed: $($_.Exception.Message)")
    }

    # Precedence: a positively-found association is definitively unsafe (InUse).
    # If nothing was found but a check errored, we cannot confirm idle -> Unknown.
    # Only an all-clear, error-free inspection yields Idle.
    if ($reasons.Count -gt 0) {
        $status = 'InUse'
        $isIdle = $false
    }
    elseif ($errors.Count -gt 0) {
        $status = 'Unknown'
        $isIdle = $false
        foreach ($e in $errors) { $reasons.Add($e) }
    }
    else {
        $status = 'Idle'
        $isIdle = $true
    }

    return [pscustomobject]@{
        PortId              = $PortId
        IsIdle              = $isIdle
        Status              = $status
        Reasons             = $reasons.ToArray()
        CheckedAssociations = $checked.ToArray()
    }
}