VNet.psm1

<#
.SYNOPSIS
    List subnets for a VNet and show any unallocated gaps

.EXAMPLE
    Find-FreeSubnets.ps1 -ResourceGroup rg-myapp-prod-westus-001 -VNetName vnet-myapp-prod-westus-001
#>

function Find-FreeSubnets {
    [CmdletBinding()]
    param (
        [string]
        $ResourceGroup,
        [string]
        $VNetName
    )

    begin {

        # https://gist.github.com/davidjenni/7eb707e60316cdd97549b37ca95fbe93
        function cidrToIpRange {
            param (
                [string] $cidrNotation
            )

            $addr, $maskLength = $cidrNotation -split '/'
            [int]$maskLen = 0
            if (-not [int32]::TryParse($maskLength, [ref] $maskLen)) {
                throw "Cannot parse CIDR mask length string: '$maskLen'"
            }
            if (0 -gt $maskLen -or $maskLen -gt 32) {
                throw "CIDR mask length must be between 0 and 32"
            }
            $ipAddr = [Net.IPAddress]::Parse($addr)
            if ($ipAddr -eq $null) {
                throw "Cannot parse IP address: $addr"
            }
            if ($ipAddr.AddressFamily -ne [Net.Sockets.AddressFamily]::InterNetwork) {
                throw "Can only process CIDR for IPv4"
            }

            $shiftCnt = 32 - $maskLen
            $mask = -bnot ((1 -shl $shiftCnt) - 1)
            $ipNum = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($ipAddr.GetAddressBytes(), 0))
            $ipStart = ($ipNum -band $mask)
            $ipEnd = ($ipNum -bor (-bnot $mask))

            # return as tuple of strings:
            #([BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($ipStart)) | ForEach-Object { $_ } ) -join '.'
            $bytes = [BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($ipStart))
            New-Object -TypeName "Net.IPAddress" -argumentList (, $bytes)
            #([BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($ipEnd)) | ForEach-Object { $_ } ) -join '.'
            $bytes = [BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($ipEnd))
            New-Object Net.IPAddress -ArgumentList (, $bytes)
        }

        $vnet = Get-AzVirtualNetwork -Name $VNetName -ResourceGroupName $ResourceGroup

        $vnetStart, $vnetEnd = cidrToIpRange $vnet.AddressSpace.AddressPrefixes

        "VNET $vnetStart - $vnetEnd"

        $sorted = $vnet.Subnets.AddressPrefix | Sort-Object -Property {
            $addr, $maskLength = $_ -split '/'

            $ip = ([Net.IPAddress] $addr)
            $ipNum = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($ip.GetAddressBytes(), 0))
            $ipNum
        }

        $maskToAddresses = @{ 28 = 16; 27 = 32; 26 = 64; 25 = 128 }
        $addressToStarts = @{
        }

        $maskToAddresses.Values | ForEach-Object {
            $addressToStarts.Add($_, $(for ($i = 0; $i -lt 255; $i += $_) { $i }))
        }

        $nextAvailableNum = 0

        $notFirst = $false

        foreach ($cidr in $sorted) {

            $start, $end = cidrToIpRange $cidr

            $startNum = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($start.GetAddressBytes(), 0))
            if ($notFirst -and $nextAvailableNum -ne $startNum ) {

                $bytes = [BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($nextAvailableNum))
                $nextAvailable = New-Object Net.IPAddress -ArgumentList (, $bytes)

                "Free block(s) of $($startNum - $nextAvailableNum) starting at $nextAvailable"

                for ($i = $nextAvailableNum; $i -lt $startNum; $i += 8) {
                    $bytes = [BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($i))
                    $freeIp = New-Object Net.IPAddress -ArgumentList (, $bytes)

                    foreach ($mask in $maskToAddresses.Keys) {
                        $address = $maskToAddresses[$mask]
                        if ($addressToStarts[$address] -contains $bytes[3] ) {
                            $freeCidr = $freeIp.IPAddressToString + "/$mask"

                            # Check this doesn't overlap next
                            $possibleFreeStart, $possibleFreeEnd = cidrToIpRange $freeCidr

                            $possibleFreeEndNum = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($possibleFreeEnd.GetAddressBytes(), 0))
                            if ($possibleFreeEndNum -lt $startNum) {
                                " " + $freeCidr
                            }
                        }
                    }
                }
            }

            $notFirst = $true

            "{0,18} : {1,15} - {2,15}" -f $cidr, $start, $end

            $nextAvailableNum = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($end.GetAddressBytes(), 0)) + 1

            $bytes = [BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($ipEnd))
        }

        if ($end -ne $vnetEnd) {
            $bytes = [BitConverter]::GetBytes([Net.IPAddress]::HostToNetworkOrder($nextAvailableNum))
            $nextAvailable = New-Object Net.IPAddress -ArgumentList (, $bytes)

            "Free block(s) starting at $nextAvailable"
        }
    }
}