networktools.ps1

function Get-VLSMBreakdown {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Net.IPNetwork]$Network,

        [Parameter(Mandatory = $true,
            ParameterSetName = "SubnetSize")]
        [ValidateNotNullOrEmpty()]
        [array]$SubnetSize,

        [Parameter(Mandatory = $true,
            ParameterSetName = "SubnetSizeCidr")]
        [ValidateNotNullOrEmpty()]
        [array]$SubnetSizeCidr
    )


    function processRecord($net, $cidr, $type) {
        try {
            #try breaking down a network block by CIDR in a form of length
            $subnets = @([System.Net.IPNetwork]::Subnet($net, $cidr))

            #if success push the first resulting subnet to output stack and mark them with associated type
            $outStack.Push(($subnets[0] | add-member -MemberType NoteProperty -Name type -Value $type -PassThru -Force))

            #if there are more subnets generated by "subnet" operation
            #returning the rest subnets to the Ip blocks queue fur further processing
            if ($subnets.count -gt 1) {
                for ($i = 1; $i -le $subnets.count - 1; $i++) {
                    $vlsmStack.Enqueue($subnets[$i])
                }
            }
            # return true if "subnetting was successfull"
            $true
        }
        catch {
            # put the network back to the IP blocks queue
            $vlsmStack.Enqueue($net)
            # return true if "subnetting failed"
            $false
        }
    }



    # Check for correct param usage
    if ("cidr" -in $SubnetSize.Keys) {
        Throw "You cannot use Cidr notation with SubnetSize param. Please use SubnetSizeCidr param instead."
    }
    elseif ("size" -in $SubnetSizeCidr.Keys) {
        Throw "You cannot use Size notation with SubnetSizeCidr param. Please use SubnetSize param instead."
    }

    


    # Hashtable to map CIDR from $SubnetSize Parameter values to usable IPs.
    # Needed for calculations.
    $subnetCidrMap = @{
        16 = 65534
        17 = 32766
        18 = 16382
        19 = 8190
        20 = 4094
        21 = 2046
        22 = 1022
        23 = 510
        24 = 254
        25 = 126
        26 = 62
        27 = 30
        28 = 14
        29 = 6
    }


    # If Cidr is being used, we have to convert Cidr to 'usableIPs' to map calucaltion logic in the script below.
    if ($SubnetSizeCidr) {
        # Map CIDR from $SubnetSize Parameter values to $subnetCidrMap values.
        $SubnetAddressMap = @()
        foreach ($sub in $SubnetSizeCidr) {
            $subnetDef = @{
                type = $sub.type
                size = $subnetCidrMap.($sub.cidr)
            }
            $SubnetAddressMap += $subnetDef
        }
        $SubnetSize = $SubnetAddressMap
    }



    # Check if summarized $SubnetSize fits into network address range
    if (($SubnetSize | ForEach-Object { [PSCustomObject]$_ } | Measure-Object -Property size -Sum).Sum -le $Network.Usable) {
        #queue of masks we wnat to use to break our network
        $vlsmMasks = [System.Collections.Queue]::new()

        #calculating mask length based on subnet sizes we get as an input
        #as an output we get list of objects (type; length) sorted by length
        #list is stored in $vlsmMasks variable
        $SubnetSize | ForEach-Object {
            $length = 32 - [math]::Ceiling([math]::Log($_.size + 2, 2))
            if ($length -lt $Network.Cidr) { throw "The subnet $($_.type) is of wrong size" }
            [PSCustomObject]@{
                type   = $_.type;
                length = $length
            }
        } | Sort-Object -Property length | ForEach-Object { $vlsmMasks.Enqueue($_) }

        #queue of networks to break down
        $vlsmStack = [System.Collections.Queue]::new()
        #stack of subnets we going to break the network to
        $outStack = [System.Collections.Stack]::new()
        #put the very first network to the queue
        $vlsmStack.Enqueue($Network)

        #at this point we got two queues:
        #masks queue and networks queue
        #masks hosls lengths of all subnets we need and netwoks contains our VNET range (network block)

        do {
            $failureCount = 0
            #pick a msk form a queue
            $v = $vlsmMasks.Dequeue()
            try {
                #reordering the queue to keep longest masks up top
                $t = [System.Collections.Queue]::new()
                $vlsmStack.ToArray() | Sort-Object -Property CIDR -Descending | % { $t.Enqueue($_) }
                $vlsmStack = $t
                
                #pick a network block form a queue
                $current = $vlsmStack.Dequeue()
            }
            catch {
                write-verbose -Message "No networks in the queue to process"
            }

            #processing the block against the mask and assosiated type
            #if no success - return mask back to tail of a queue and increase failue count
            if (! (processRecord $current $v.length $v.type) ) { $vlsmMasks.Enqueue($v); $failureCount++ }

            write-verbose ("VLSM MASKS`: " + $vlsmMasks.Count)
            write-verbose ("FAILURES`: " + $failureCount)

            if ($vlsmMasks.Count -eq $failureCount) { break } # break when no records processed during a loop cycle
        } while ($vlsmMasks.Count -ne 0) #leave when we have masks queue emty

        write-verbose ("OUT STACK`: " + $outStack.Count)
        write-verbose ("SubnetSuize`: " + $SubnetSize.count)
        if ($outStack.count -lt $SubnetSize.count) { write-warning -message "subnetting failed" }

        #in case we have more subnets than requested store them
        $reserved = @([System.Net.IPNetwork]::Supernet($vlsmStack.ToArray()))
        $outStack
        #and mark them as 'reserved'
        $reserved | add-member -MemberType NoteProperty -Name type -Value "reserved" -PassThru -Force
    }
    else {
        Throw "The specified address space of $($Network.Network.IPAddressToString)/$($Network.cidr) is too small for the subnets."
    }
}

function Get-IPRanges {
    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = "ByBaseNet")]
        $Networks,
        [Parameter(Mandatory = $true, ParameterSetName = "ByBaseNet")]
        [System.Net.IPNetwork] $BaseNet,
        [Parameter(Mandatory = $true, ParameterSetName = "ByBaseNet")]
        [ValidateRange(8, 30)]
        [int] $CIDR
    )
    function NextSubnet ($network, $newCIDR ) {
        $net = [System.Net.IPNetwork]::Parse($network)
        $netmask = [System.Net.IPNetwork]::ToNetmask($newCIDR, $net.AddressFamily)

        [bigint] $nextAddr = [System.Net.IPNetwork]::ToBigInteger($net.Broadcast) + 1
        [bigint] $mask = [System.Net.IPNetwork]::ToBigInteger($netmask)
        [bigint] $masked = $nextAddr -band $mask

        if ($masked -eq $nextAddr) {
            Write-Verbose "Masked"
            $next = $masked
        }
        else {
            Write-Verbose "Magic!"
            $next = $masked + [bigint]::Pow(2, 32 - $newCIDR)
        }

        $nextNetwork = [System.Net.IPNetwork]::ToIPAddress($next, [System.Net.Sockets.AddressFamily]::InterNetwork)
        $nextSubnet = [System.Net.IPNetwork]::Parse($nextNetwork, $netmask)
        $nextSubnet
    }
    function getIPRanges {
        for ($i = 0; $i -le ($outNets.Count - 1); $i++ ) {
            $n = $outNets[$i]
            $ns = NextSubnet $n $CIDR
            Write-Verbose "subnet is`: $n; next subnet is $ns"

            # test if 'next subnet' is a part of a base range
            if (-not [System.Net.IPNetwork]::Contains($BaseNet, $ns)) {
                Write-Verbose "$ns is not in a $BaseNet"
                # skip the network if it is not in a base range
                continue;
            }
            else { Write-Verbose "$ns is in a $BaseNet" }

            # test if 'next subnet' overlaps any of the existing below it in a sorted list
            $k = $i; $isoverlap = $false
            do {
                if ([System.Net.IPNetwork]::Overlap($outNets[$k], $ns)) {
                    Write-Verbose "$ns overlaps $($outNets[$k])"
                    # break this loop if there is an overlap
                    $isoverlap = $true
                    break
                }
                $k++
            } while ($k -lt ($outNets.Count))

            # when there were no overlaps -> output
            if (! $isoverlap) {
                Write-Verbose "$ns did not overlap"
                $ns
            }
        }
    }

    $calcNets = {
        # put all networks we've found to the out collection, sort them
        # for some reason sort cmdlet does not work well, so use Lists and internal comparer.
        $outNets = [System.Collections.Generic.List[System.Net.IPNetwork]]::new()
        $Networks | ForEach-Object {
            if ($_) {
                $outNets.add($_)
            }
        }
        $outNets.Sort()

        # create a collection of unused nets
        $freeNets = [System.Collections.Generic.List[System.Net.IPNetwork]]::new()

        # mark all existing as 'used'
        $outNets | Add-Member -MemberType NoteProperty -Name IsFree -Value $false -Force

        # search for free ranges in between the items of the 'used' nets list and after the last one
        $n = getIPRanges
        Write-Verbose "ip ranges we've got`: $n"

        # if there is something - add them to 'unused' list
        # mark them as 'unused'
        if ($n.count -gt 0) {
            $n | ForEach-Object {
                $net = $_ | Add-Member -MemberType NoteProperty -Name IsFree -Value $true -Force -PassThru;
                $freeNets.add($net)
            }
        }

        # TODO: search networks in front of the provided set of used networks
        $firstFree = [System.Net.IPNetwork]::ToBigInteger($BaseNet.FirstUsable)
        if ($outNets.Count -gt 0) {
            $lastFree = [System.Net.IPNetwork]::ToBigInteger($outNets[0].FirstUsable)
        }
        else {
            $lastFree = [System.Net.IPNetwork]::ToBigInteger($BaseNet.FirstUsable)
        }
        $diff = $lastFree - $firstFree 
        if ($diff -gt 0) {
            Write-Verbose "there are addresses in front of occupied blocks, number is`: $diff"
            $frontNets = [System.Collections.Generic.List[System.Net.IPNetwork]]::new()
            $firstNetToTest = "$($BaseNet.FirstUsable.ToString())/$CIDR"
            $netToTest = [System.Net.IPNetwork]::Parse($firstNetToTest)


            while (-not [System.Net.IPNetwork]::Overlap($outNets[0], $netToTest)) {
                $netToTest | Add-Member -MemberType NoteProperty -Name IsFree -Value $true -Force
                $frontNets.Add($netToTest)
                $netToTest = NextSubnet $netToTest $CIDR
            }

            Write-Verbose "$frontNets"
            $freeNets.AddRange( $frontNets )
            $freeNets.Sort()
        }
        # join used and unused together to output them properly
        $outNets.AddRange( $freeNets )
        $outNets.Sort()

        # sometimes two consequent subnets may end up with the same next free subnet. selecting unique values to avoid confusion
        $outNets | Select-Object -Unique

    }

    & $calcNets
}