Generate-AzureIPAMTable.ps1
<#PSScriptInfo .VERSION 1.2 .GUID c0401797-836f-4898-b33d-46c8fc4b822c .AUTHOR CooperLutz .COMPANYNAME .COPYRIGHT .TAGS .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES #> <# .DESCRIPTION Azure Runbook to generate an IP table from your Azure environment #> # Define Parameters Param( [Parameter(Mandatory = $true)]$resourceGroup, [Parameter(Mandatory = $true)]$storageAccount, $tableName = "AzureIPAMTable" ) Import-Module AzureRM.Network Import-Module AzureRM.Storage Import-Module AzureRmStorageTable ## Add Azure Automation Login $connectionName = "AzureRunAsConnection" try { # Get the connection "AzureRunAsConnection " $servicePrincipalConnection=Get-AutomationConnection -Name $connectionName "Logging in to Azure..." Add-AzureRmAccount ` -ServicePrincipal ` -TenantId $servicePrincipalConnection.TenantId ` -ApplicationId $servicePrincipalConnection.ApplicationId ` -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint } catch { if (!$servicePrincipalConnection) { $ErrorMessage = "Connection $connectionName not found." throw $ErrorMessage } else{ Write-Error -Message $_.Exception throw $_.Exception } } ## Setup PSipcalc Function function Psipcalc { #requires -version 2 [CmdletBinding()] param( # CIDR notation network address, or using subnet mask. Examples: '192.168.0.1/24', '10.20.30.40/255.255.0.0'. [Parameter(Mandatory=$True)][string] $NetworkAddress, # Causes PSipcalc to return a boolean value for whether the specified IP is in the specified network. Includes network address and broadcast address. [string] $Contains, # Enumerates all IPs in subnet (potentially resource-expensive). Ignored if you use -Contains. [switch] $Enumerate ) # PowerShell ipcalc clone: PSipcalc. # Copyright (c), 2015, Svendsen Tech # All rights reserved. ## Author: Joakim Svendsen # Original release 2015-07-13 (ish) v1.0 (or whatever...) # 2015-07-16: Standardized the TotalHosts and UsableHosts properties to always be of the type int64. # Formely TotalHosts was a string, except for network lengths of 30-32, when it was an int32. UsableHosts used to be int32. # 2015-07-15: Added -Contains and fixed some comment bugs(!) plus commented a bit more and made minor tweaks. v1.1, I guess. Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # This is a regex I made to match an IPv4 address precisely ( http://www.powershelladmin.com/wiki/PowerShell_regex_to_accurately_match_IPv4_address_%280-255_only%29 ) $IPv4Regex = '(?:(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)\.){3}(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)' function Convert-IPToBinary { param( [string] $IP ) $IP = $IP.Trim() if ($IP -match "\A${IPv4Regex}\z") { try { return ($IP.Split('.') | ForEach-Object { [System.Convert]::ToString([byte] $_, 2).PadLeft(8, '0') }) -join '' } catch { Write-Warning -Message "Error converting '$IP' to a binary string: $_" return $Null } } else { Write-Warning -Message "Invalid IP detected: '$IP'." return $Null } } function Convert-BinaryToIP { param( [string] $Binary ) $Binary = $Binary -replace '\s+' if ($Binary.Length % 8) { Write-Warning -Message "Binary string '$Binary' is not evenly divisible by 8." return $Null } [int] $NumberOfBytes = $Binary.Length / 8 $Bytes = @(foreach ($i in 0..($NumberOfBytes-1)) { try { #$Bytes += # skipping this and collecting "outside" seems to make it like 10 % faster [System.Convert]::ToByte($Binary.Substring(($i * 8), 8), 2) } catch { Write-Warning -Message "Error converting '$Binary' to bytes. `$i was $i." return $Null } }) return $Bytes -join '.' } function Get-ProperCIDR { param( [string] $CIDRString ) $CIDRString = $CIDRString.Trim() $o = '' | Select-Object -Property IP, NetworkLength if ($CIDRString -match "\A(?<IP>${IPv4Regex})\s*/\s*(?<NetworkLength>\d{1,2})\z") { # Could have validated the CIDR in the regex, but this is more informative. if ([int] $Matches['NetworkLength'] -lt 0 -or [int] $Matches['NetworkLength'] -gt 32) { Write-Warning "Network length out of range (0-32) in CIDR string: '$CIDRString'." return } $o.IP = $Matches['IP'] $o.NetworkLength = $Matches['NetworkLength'] } elseif ($CIDRString -match "\A(?<IP>${IPv4Regex})[\s/]+(?<SubnetMask>${IPv4Regex})\z") { $o.IP = $Matches['IP'] $SubnetMask = $Matches['SubnetMask'] if (-not ($BinarySubnetMask = Convert-IPToBinary $SubnetMask)) { return # warning displayed by Convert-IPToBinary, nothing here } # Some validation of the binary form of the subnet mask, # to check that there aren't ones after a zero has occurred (invalid subnet mask). # Strip all leading ones, which means you either eat 32 1s and go to the end (255.255.255.255), # or you hit a 0, and if there's a 1 after that, we've got a broken subnet mask, amirite. if ((($BinarySubnetMask) -replace '\A1+') -match '1') { Write-Warning -Message "Invalid subnet mask in CIDR string '$CIDRString'. Subnet mask: '$SubnetMask'." return } $o.NetworkLength = [regex]::Matches($BinarySubnetMask, '1').Count } else { Write-Warning -Message "Invalid CIDR string: '${CIDRString}'. Valid examples: '192.168.1.0/24', '10.0.0.0/255.0.0.0'." return } # Check if the IP is all ones or all zeroes (not allowed: http://www.cisco.com/c/en/us/support/docs/ip/routing-information-protocol-rip/13788-3.html ) if ($o.IP -match '\A(?:(?:1\.){3}1|(?:0\.){3}0)\z') { Write-Warning "Invalid IP detected in CIDR string '${CIDRString}': '$($o.IP)'. An IP can not be all ones or all zeroes." return } return $o } # Not used. function Get-IPRange { param( [string] $StartBinary, [string] $EndBinary ) $StartIPArray = @((Convert-BinaryToIP $StartBinary) -split '\.') $EndIPArray = ((Convert-BinaryToIP $EndBinary) -split '\.') Write-Verbose -Message "Start IP: $($StartIPArray -join '.')" Write-Verbose -Message "End IP: $($EndIPArray -join '.')" $FirstOctetArray = @($StartIPArray[0]..$EndIPArray[0]) $SecondOctetArray = @($StartIPArray[1]..$EndIPArray[1]) $ThirdOctetArray = @($StartIPArray[2]..$EndIPArray[2]) $FourthOctetArray = @($StartIPArray[3]..$EndIPArray[3]) # Four levels of nesting... Slow. $IPs = @(foreach ($First in $FirstOctetArray) { foreach ($Second in $SecondOctetArray) { foreach ($Third in $ThirdOctetArray) { foreach ($Fourth in $FourthOctetArray) { "$First.$Second.$Third.$Fourth" } } } }) $IPs = $IPs | Sort-Object -Unique -Property @{Expression={($_ -split '\.' | ForEach-Object { '{0:D3}' -f [int]$_ }) -join '.' }} return $IPs } # Used. ;) function Get-IPRange2 { param( [string] $StartBinary, [string] $EndBinary ) [int64] $StartInt = [System.Convert]::ToInt64($StartBinary, 2) [int64] $EndInt = [System.Convert]::ToInt64($EndBinary, 2) for ($BinaryIP = $StartInt; $BinaryIP -le $EndInt; $BinaryIP++) { Convert-BinaryToIP ([System.Convert]::ToString($BinaryIP, 2).PadLeft(32, '0')) } } function Test-IPIsInNetwork { param( [string] $IP, [string] $StartBinary, [string] $EndBinary ) $TestIPBinary = Convert-IPToBinary $IP [int64] $TestIPInt64 = [System.Convert]::ToInt64($TestIPBinary, 2) [int64] $StartInt64 = [System.Convert]::ToInt64($StartBinary, 2) [int64] $EndInt64 = [System.Convert]::ToInt64($EndBinary, 2) if ($TestIPInt64 -ge $StartInt64 -and $TestIPInt64 -le $EndInt64) { return $True } else { return $False } } function Get-NetworkInformationFromProperCIDR { param( [psobject] $CIDRObject ) $o = '' | Select-Object -Property IP, NetworkLength, SubnetMask, NetworkAddress, HostMin, HostMax, Broadcast, UsableHosts, TotalHosts, IPEnumerated, BinaryIP, BinarySubnetMask, BinaryNetworkAddress, BinaryBroadcast $o.IP = [string] $CIDRObject.IP $o.BinaryIP = Convert-IPToBinary $o.IP $o.NetworkLength = [int32] $CIDRObject.NetworkLength $o.SubnetMask = Convert-BinaryToIP ('1' * $o.NetworkLength).PadRight(32, '0') $o.BinarySubnetMask = ('1' * $o.NetworkLength).PadRight(32, '0') $o.BinaryNetworkAddress = $o.BinaryIP.SubString(0, $o.NetworkLength).PadRight(32, '0') if ($Contains) { if ($Contains -match "\A${IPv4Regex}\z") { # Passing in IP to test, start binary and end binary. return Test-IPIsInNetwork $Contains $o.BinaryNetworkAddress $o.BinaryNetworkAddress.SubString(0, $o.NetworkLength).PadRight(32, '1') } else { Write-Error "Invalid IPv4 address specified with -Contains" return } } $o.NetworkAddress = Convert-BinaryToIP $o.BinaryNetworkAddress if ($o.NetworkLength -eq 32 -or $o.NetworkLength -eq 31) { $o.HostMin = $o.IP } else { $o.HostMin = Convert-BinaryToIP ([System.Convert]::ToString(([System.Convert]::ToInt64($o.BinaryNetworkAddress, 2) + 1), 2)).PadLeft(32, '0') } #$o.HostMax = Convert-BinaryToIP ([System.Convert]::ToString((([System.Convert]::ToInt64($o.BinaryNetworkAddress.SubString(0, $o.NetworkLength)).PadRight(32, '1'), 2) - 1), 2).PadLeft(32, '0')) #$o.HostMax = [string] $BinaryBroadcastIP = $o.BinaryNetworkAddress.SubString(0, $o.NetworkLength).PadRight(32, '1') # this gives broadcast... need minus one. $o.BinaryBroadcast = $BinaryBroadcastIP [int64] $DecimalHostMax = [System.Convert]::ToInt64($BinaryBroadcastIP, 2) - 1 [string] $BinaryHostMax = [System.Convert]::ToString($DecimalHostMax, 2).PadLeft(32, '0') $o.HostMax = Convert-BinaryToIP $BinaryHostMax $o.TotalHosts = [int64][System.Convert]::ToString(([System.Convert]::ToInt64($BinaryBroadcastIP, 2) - [System.Convert]::ToInt64($o.BinaryNetworkAddress, 2) + 1)) $o.UsableHosts = $o.TotalHosts - 2 # ugh, exceptions for network lengths from 30..32 if ($o.NetworkLength -eq 32) { $o.Broadcast = $Null $o.UsableHosts = [int64] 1 $o.TotalHosts = [int64] 1 $o.HostMax = $o.IP } elseif ($o.NetworkLength -eq 31) { $o.Broadcast = $Null $o.UsableHosts = [int64] 2 $o.TotalHosts = [int64] 2 # Override the earlier set value for this (bloody exceptions). [int64] $DecimalHostMax2 = [System.Convert]::ToInt64($BinaryBroadcastIP, 2) # not minus one here like for the others [string] $BinaryHostMax2 = [System.Convert]::ToString($DecimalHostMax2, 2).PadLeft(32, '0') $o.HostMax = Convert-BinaryToIP $BinaryHostMax2 } elseif ($o.NetworkLength -eq 30) { $o.UsableHosts = [int64] 2 $o.TotalHosts = [int64] 4 $o.Broadcast = Convert-BinaryToIP $BinaryBroadcastIP } else { $o.Broadcast = Convert-BinaryToIP $BinaryBroadcastIP } # I had to create this Get-IPRange function because a 32-digit binary number wouldn't fit in an int64... ### no, I didn't... Get-IPRange2 in effect; significantly faster. if ($Enumerate) { $IPRange = @(Get-IPRange2 $o.BinaryNetworkAddress $o.BinaryNetworkAddress.SubString(0, $o.NetworkLength).PadRight(32, '1')) if ((31, 32) -notcontains $o.NetworkLength ) { $IPRange = $IPRange[1..($IPRange.Count-1)] # remove first element $IPRange = $IPRange[0..($IPRange.Count-2)] # remove last element } $o.IPEnumerated = $IPRange } else { $o.IPEnumerated = @() } return $o } $NetworkAddress | ForEach-Object { Get-ProperCIDR $_ } | ForEach-Object { Get-NetworkInformationFromProperCIDR $_ } } # Declare preliminary variables, set table context $saContext = (Get-AzureRmStorageAccount -ResourceGroupName $resourceGroup -Name $storageAccount -ErrorAction Stop).Context # Determine if the table already exists $table = Get-AzureStorageTable -Name $tableName -Context $saContext -ErrorAction SilentlyContinue -ErrorVariable TableDoesNotExist # Store existing reserved space $ReservedSpaces = Get-AzureStorageTableRowByPartitionKey -table $table -partitionKey "ReservedSpace" -ErrorAction SilentlyContinue -ErrorVariable NoReservedSpaces # The table is either created or has the data removed. If new PartitionKeys are added outside of the provided 4, their data will not be removed if($TableDoesNotExist) { Write-Host "Creating new table $tableName..." New-AzureStorageTable -Name $tableName -Context $saContext $CreateExampleReserve = $true } else { # Remove existing data so that it may be repopulated Write-Host "Removing existing data from $tableName..." Get-AzureStorageTableRowByPartitionKey -table $table -partitionKey "VNet" | Remove-AzureStorageTableRow -table $table Get-AzureStorageTableRowByPartitionKey -table $table -partitionKey "Subnet" | Remove-AzureStorageTableRow -table $table Get-AzureStorageTableRowByPartitionKey -table $table -partitionKey "PublicIP" | Remove-AzureStorageTableRow -table $table Get-AzureStorageTableRowByPartitionKey -table $table -partitionKey "ReservedSpace" | Remove-AzureStorageTableRow -table $table } # Get the table, PIPs, and VNets $table = Get-AzureStorageTable -Name $tableName -Context $saContext $vnets = Get-AzureRmVirtualNetwork $publicIPs = Get-AzureRmPublicIpAddress -ErrorAction SilentlyContinue # Vnets loop - get the Vnet data and its relevant subnets. Write-Verbose "Populating VNets and Subnets..." foreach ($vnet in $vnets) { # Set Val2 to 1 to re-initiate the subnet rowkey $val2 = 1 ## Get the Virtual Network's range and run it through PSipcalc $vnetAddressSpace = Get-AzureRmVirtualNetwork -Name $vnet.Name -ResourceGroupName $vnet.ResourceGroupName | select AddressSpace $vnetAddressSpace = $vnetAddressSpace.AddressSpace.AddressPrefixes $rangeInfo = Psipcalc -NetworkAddress $vnetAddressSpace # Add the table row for the Vnet Add-StorageTableRow -table $table -partitionKey "VNet" -rowKey ("Vnet" + ("{0:d3}" -f $val++)) -property @{` "VirtualNetwork" = $($vnet.Name); ` "Type" = "Range"; ` "Name" = $($vnet.Name); ` "PublicIpAllocationMethod" = "";` "NetworkLength" = $($rangeInfo.NetworkLength);` "IP" = $($rangeInfo.IP);` "TotalHosts" = $($rangeInfo.TotalHosts);` "Broadcast" = $($rangeInfo.Broadcast); ` "AddressSpace" = $($vnetAddressSpace); ` "ResourceGroupName" = $($vnet.ResourceGroupName)` } # Loop through all subnets within a vnet and output these to a table row $subnets = Get-AzureRmVirtualNetwork -Name $vnet.Name -ResourceGroupName $vnet.ResourceGroupName | select subnets $subnetCount = $subnets.Subnets.Name.Count For ($i = 0; $i -lt $subnetCount; $i++) { $rangeInfo = Psipcalc -NetworkAddress $subnets.Subnets[$i].AddressPrefix # Add the table row for each vnet Add-StorageTableRow -table $table -partitionKey "Subnet" -rowKey ("Vnet" + ("{0:d3}" -f ($val-1)) + "Subnet" + ("{0:d3}" -f $val2++)) -property @{` "VirtualNetwork" = $($vnet.Name); ` "Type" = "Range"; ` "Name" = $($subnets.Subnets[$i].Name); ` "PublicIpAllocationMethod" = "";` "NetworkLength" = $($rangeInfo.NetworkLength);` "IP" = $($rangeInfo.IP); ` "TotalHosts" = $($rangeInfo.TotalHosts); ` "Broadcast" = $($rangeInfo.Broadcast);` "AddressSpace" = $($subnets.Subnets[$i].AddressPrefix); ` "ResourceGroupName" = $($vnet.ResourceGroupName)` } } } # Populate all Public IP data if($publicIPs.Count -ge 0) { Write-Host "Populating Public IPs..." $val=0 foreach($pip in $publicIPs) { Add-StorageTableRow -table $table -partitionKey "PublicIP" -rowKey ("Pip" + ("{0:d3}" -f $val++)) -property @{` "VirtualNetwork" = ""; ` "Type" = "PublicIP"; ` "Name" = $($pip.Name); ` "PublicIpAllocationMethod" = $($pip.PublicIpAllocationMethod); ` "AddressSpace" = $($pip.IpAddress); ` "ResourceGroupName" = $($pip.ResourceGroupName); ` "NetworkLength" = "";` "IP" = "";` "TotalHosts" = "";` "Broadcast" = ""` } } } # Populate reserved space data if($NoReservedSpaces) { continue } else { Write-Host "Populating Reserved Space..." $val=1 foreach($plan in $ReservedSpaces) { $rangeInfo = Psipcalc -NetworkAddress $plan.AddressSpace Add-StorageTableRow -table $table -partitionKey "ReservedSpace" -rowKey ("Reserved" + ("{0:d3}" -f $val++)) -property @{ ` "Type" = "Range"; ` "Name" = $($plan.Name); ` "AddressSpace" = $($plan.AddressSpace;); ` "NetworkLength" = $($rangeInfo.NetworkLength); ` "IP" = $($rangeInfo.IP); ` "TotalHosts" = $($rangeInfo.TotalHosts); ` "Broadcast" = $($rangeInfo.Broadcast); ` "VirtualNetwork" = ""; ` "ResourceGroupName" = ""; ` "PublicIpAllocationMethod" = "" } } } # On table's first creation, a reserved space example is generated if($CreateExampleReserve) { Write-Host "Populating Reserved Space Example..." $val=1 $exampleReserveAddress = "192.168.0.0/28" Add-StorageTableRow -table $table -partitionKey "ReservedSpace" -rowKey ("Reserved" + ("{0:d3}" -f $val++)) -property @{ ` "Type" = "Range"; ` "Name" = "ExampleReservedSpace"; ` "AddressSpace" = $exampleReserveAddress; ` "NetworkLength" = "AutoGenerated Next Run"; ` "IP" = "AutoGenerated Next Run"; ` "TotalHosts" = "AutoGenerated Next Run"; ` "Broadcast" = "Auto Generated Next Run"; ` "VirtualNetwork" = ""; ` "ResourceGroupName" = ""; ` "PublicIpAllocationMethod" = "" } } else { continue } |