AzVnetTools.psm1

#AzVnetTools.psm1

<#
.SYNOPSIS
Increments an IP address by a specified amount.
 
.DESCRIPTION
This function takes an IP address as a string and increments it by the specified amount.
If no increment is specified, it defaults to 1.
 
.PARAMETER IPAddress
The IP address to increment.
 
.PARAMETER IncrementBy
The number to increment the IP address by. Defaults to 1.
 
.EXAMPLE
Increment-IPAddress -IPAddress "192.168.0.1" -IncrementBy 5
Returns: 192.168.0.6
 
.NOTES
This function wraps around to 0.0.0.0 if the increment goes beyond 255.255.255.255.
#>

function Get-IncrementedIPAddress {
    param (
        [Parameter(Mandatory = $true)]
        [string]$IPAddress,
        [Parameter(Mandatory = $false)]
        [int]$IncrementBy = 1
    )

    try {
        # Convert the IP address string to an IPAddress object
        $ip = [System.Net.IPAddress]::Parse($IPAddress)

        # Convert the IP address to a 32-bit unsigned integer
        $arrAddress = $ip.GetAddressBytes()
        [array]::Reverse($arrAddress)
        $ipInt = [System.BitConverter]::ToUInt32($arrAddress, 0)

        # Increment the integer
        $ipInt += $IncrementBy

        # Convert back to an IP address and reverse array
        $newIP = [System.Net.IPAddress]::new($ipInt)
        $arrAddress = $newIP.GetAddressBytes()
        [array]::Reverse($arrAddress)
        $newIP = [System.Net.IPAddress]::new($arrAddress)

        return $newIP.ToString()
    }
    catch {
        Write-Error "Failed to increment IP address: $_"
        return $null
    }
}

function Get-FlippedAddressBits {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $IPAddress
    )

    try {
        # Convert the IP address string to an IPAddress object
        $ip = [System.Net.IPAddress]::Parse($IPAddress)

        # Convert the IP address to a 32-bit unsigned integer
        $arrAddress = $ip.GetAddressBytes()
        $ipInt = [System.BitConverter]::ToUInt32($arrAddress, 0)

        return $ipInt
    }
    catch {
        Write-Error "Failed to increment IP address: $_"
        return $null
    }
}

<#
.SYNOPSIS
Finds the next available subnet in an Azure Virtual Network.
 
.DESCRIPTION
This function searches for the next available subnet within a specified Azure Virtual Network
that doesn't overlap with existing subnets.
 
.PARAMETER ResourceGroupName
The name of the resource group containing the Virtual Network.
 
.PARAMETER VNetName
The name of the Virtual Network.
 
.PARAMETER NewSubnetMask
The subnet mask for the new subnet (in CIDR notation, e.g., 24 for a /24 subnet).
 
.EXAMPLE
Get-AzNextAvailableSubnet -ResourceGroupName "MyRG" -VNetName "MyVNet" -NewSubnetMask 24
Returns: "10.0.1.0/24" (if this is the next available /24 subnet)
 
.NOTES
Requires the Az PowerShell module to be installed and connected to an Azure account.
#>

function Get-AzNextAvailableSubnet {
    param (
        [Parameter(Mandatory = $true)]
        [string]$ResourceGroupName,
        [Parameter(Mandatory = $true)]
        [string]$VNetName,
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, 32)]
        [int]$NewSubnetMask
    )

    begin {
        # Check if Azure PowerShell module is installed
        if (-not (Get-Module -ListAvailable -Name Az.Network)) {
            throw "Azure PowerShell module 'Az.Network' is not installed. Please install it using: Install-Module -Name Az.Network"
        }

        # Check Azure connection
        try {
            $context = Get-AzContext
            if (-not $context) {
                throw "Not connected to Azure. Please run Connect-AzAccount first."
            }
        }
        catch {
            throw "Failed to check Azure connection: $($_.Exception.Message)"
        }
    }

    process {
        # Get the VNet
        try {
            # Verify resource group exists
            $resourceGroup = Get-AzResourceGroup -Name $ResourceGroupName.Trim() -ErrorAction Stop
            Write-Verbose "Found resource group: $ResourceGroupName in subscription $((Get-AzContext).Subscription.Name)"
        }
        catch {
            Throw "Could not find resource group: $ResourceGroupName in subscription $((Get-AzContext).Subscription.Name)"
        }

        # Get Virtual Network with detailed error handling
        try {
            $vnet = Get-AzVirtualNetwork -ResourceGroupName $ResourceGroupName.Trim() -Name $VNetName.Trim() -ErrorAction Stop
            if (-not $vnet) {
                throw "Virtual Network not found"
            }
            Write-Verbose "Successfully retrieved VNet: $VNetName"
        }
        catch {
            $errMsg = $_.Exception.Message
            switch -Regex ($errMsg) {
                'StatusCode: 404' {
                    throw "Virtual Network '$VNetName' not found in resource group '$ResourceGroupName'"
                }
                'StatusCode: 403' {
                    throw "Access denied. Please check your permissions for Virtual Network '$VNetName'"
                }
                'StatusCode: 429' {
                    throw "Too many requests. Please try again later"
                }
                default {
                    throw "Failed to get Virtual Network: $($errMsg)"
                }
            }
        }

        # Validate VNet has address space
        if (-not $vnet.AddressSpace.AddressPrefixes -or $vnet.AddressSpace.AddressPrefixes.Count -eq 0) {
            throw "Virtual Network has no address space configured"
        }

        # Get existing subnets
        $existingSubnets = $vnet.Subnets.AddressPrefix
        Write-Verbose "Found $($existingSubnets.Count) existing subnets"

        # Get the VNet address space
        $vnetAddressSpace = $vnet.AddressSpace.AddressPrefixes[0]
        $vnetNetwork = [System.Net.IPAddress]::Parse(($vnetAddressSpace -split "/")[0])
        $vnetMask = [System.Convert]::ToInt32(($vnetAddressSpace -split "/")[1])

        # Validate subnet mask is valid for VNet
        if ($NewSubnetMask -lt $vnetMask) {
            throw "New subnet mask /$NewSubnetMask is larger than VNet mask /$vnetMask"
        }

        try {
            # Calculate the number of IP addresses in the new subnet
            $newSubnetSize = [Math]::Pow(2, (32 - $NewSubnetMask))

            # Start from the beginning of the VNet range
            $currentIP = $vnetNetwork.IPAddressToString

            #Get vNet maximum parameters
            $maxAddress = Get-IncrementedIPAddress -IPAddress $vnetNetwork.IPAddressToString -IncrementBy ([Math]::Pow(2, (32 - $vnetMask)))
            $maxBit = [System.Net.IPAddress]::Parse($maxAddress).Address
            $maxBit = Get-FlippedAddressBits -IPAddress $maxBit

            Write-Verbose "Searching for available subnet space..."
            while ((Get-FlippedAddressBits -IPAddress ([System.Net.IPAddress]::Parse($currentIP).Address)) -lt $maxBit) {
                $candidateCIDR = "$currentIP/$NewSubnetMask"

                # Check if this range overlaps with existing subnets
                $overlap = $false
                foreach ($subnet in $existingSubnets) {
                    if (Test-SubnetOverlap -Subnet1 $candidateCIDR -Subnet2 $subnet) {
                        $overlap = $true
                        Write-Verbose "Subnet $candidateCIDR overlaps with existing subnet $subnet"
                        break
                    }
                }

                if (-not $overlap) {
                    Write-Verbose "Found available subnet: $candidateCIDR"
                    return $candidateCIDR
                }

                # Move to the next potential subnet
                $currentIP = Get-IncrementedIPAddress -IPAddress $currentIP -IncrementBy $newSubnetSize
            }
            throw "No available subnet space found within VNet address range"
        }
        catch {
            # Log the error and rethrow
            Write-Error "Error in Get-NextAvailableSubnet: $($_.Exception.Message)"
            throw
        }
    }
}

<#
.SYNOPSIS
Tests if two subnets overlap.
 
.DESCRIPTION
This function checks if two given subnets (in CIDR notation) overlap with each other.
 
.PARAMETER Subnet1
The first subnet in CIDR notation (e.g., "192.168.1.0/24").
 
.PARAMETER Subnet2
The second subnet in CIDR notation (e.g., "192.168.2.0/24").
 
.EXAMPLE
Test-SubnetOverlap -Subnet1 "192.168.1.0/24" -Subnet2 "192.168.1.128/25"
Returns: $true (because these subnets overlap)
 
.NOTES
This function does not validate the correctness of the CIDR notation.
#>

function Test-SubnetOverlap {
    param (
        [string]$Subnet1,
        [string]$Subnet2
    )

    $network1 = [System.Net.IPAddress]::Parse(($Subnet1 -split "/")[0])
    $mask1 = [System.Convert]::ToInt32(($Subnet1 -split "/")[1])
    $net1start = Get-FlippedAddressBits -IPAddress $network1.Address
    $net1end = $net1start + ([Math]::Pow(2, (32 - $mask1))) - 1
    $network2 = [System.Net.IPAddress]::Parse(($Subnet2 -split "/")[0])
    $mask2 = [System.Convert]::ToInt32(($Subnet2 -split "/")[1])
    $net2start = Get-FlippedAddressBits -IPAddress $network2.Address
    $net2end = $net2start + ([Math]::Pow(2, (32 - $mask2))) - 1

    return (($net1start -le $net2end) -and ($net1end -ge $net2start))
}

# Export the public functions
Export-ModuleMember -Function Get-IncrementedIPAddress, Get-AzNextAvailableSubnet, Test-SubnetOverlap