Efficiently finding a matching subnet in a very large pool - powershell

I have been writing a script that gathers IPAM info given a provided IP address.
It is working, but my current implementation is highly inefficient.
I am using this script to run checkSubnet, which determines if an IP is in a subnet.
First, I query IPAM to gather this $allSubnets object:
Address CIDR Description VLAN
------- ---- ----------- ----
10.15.10.0 24 DMZ 3000
10.15.11.0 24 Voice 3010
10.15.12.0 24 Wireless 3020
10.15.13.0 28 Management 3030
... ... ... ...
Which is then searched like so:
$testCon = Test-Connection hostname -Count 1
$allSubnets | ForEach-Object {
if((checkSubnet -addr1 ('{0}/{1}' -f $_.Address, $_.CIDR) -addr2 $testCon.IPV4Address.IPAddressToString).Condition -eq $true)
{
[pscustomobject]#{
subnet = ('{0}/{1}' -f $_.Address, $_.CIDR)
desc = $_.Description
}
}
}
This works perfectly for smaller queries.
However, it can take an extremely long time to run against all the items in $allSubnets! Say I want to test 20 IP addresses against a full 2000 subnets, suddenly this query will take a full 2 minutes to complete.
Does anyone have any ideas of how to improve the efficiency of this?

I don't have access to the full list of subnets, but the tests against the ones posted have shown that this is a bit faster:
first a "simpler" function to see if an IP is in a certain range:
Function Find-Subnet ([IPAddress]$SubnetAddress,[byte]$CIDR,[IPAddress]$MatchIP){
[IPAddress]$Mask = [System.Convert]::ToUInt64(('1'*$CIDR).PadRight(32,'0'),2)
return (($SubnetAddress.Address -band $Mask.Address) -eq ($MatchIP.Address -band $Mask.Address))
}
this takes advantage of bit-shifting, the [System.IPAddress] object can be a bit of a performance hit, but it still seems a lot faster than the linked function, and is much more concise, you could always rewrite the [IPAddress] casting as another binary function if you really need the extra performance, as we only use the numerical representation of the Address from it in the end.
then I look to limit the number of searches, when searching against 2000+ subnets this should be a massive performance increase, searching against only the few in the question it was only a slight negative though.
foreach ($Reg in ('(.*\.).*','(.*\.).*\..*','(.*\.).*\..*\..*')){
$Prefix = $ToMatch -replace $Reg,'$1'
Write-Host "Searching subnets beginning with '$($Prefix)'..." -Fore Yellow
$AllSubnets | ? {$_.Address.StartsWith($Prefix)} | ForEach-Object {
if (Find-Subnet -SubnetAddress $_.Address -CIDR $_.CIDR -MatchIP $ToMatch){
$_ ; break
}
}
}
this loops through the regex snippets '(.*\.).*','(.*\.).*\..*' and '(.*\.).*\..*\..*' which when run through the $Prefix = $ToMatch -replace $Reg,'$1' will result in something like this:
'10.11.12.13' -> '(.*\.).*' -> '10.11.12.'
'10.11.12.13' -> '(.*\.).*\..*' -> '10.11.'
'10.11.12.13' -> '(.*\.).*\..*\..*' -> '10.'
we then go through the whole list and pull out subnets with addresses starting with the prefix, for any class A subnet this will be slower than just not including it, for B and C it will be roughly the same performance or faster, with B being more likely to be slower, and C being significantly more likely to be much much quicker.
your current function also does not stop searching after it finds the IP. by including the ;break after returning the matching IP object, we immediately return it and stop searching, if you'd like to store the subnet in a variable you can do $Result = #(foreach ($Reg in ...) { ... }) to end up with it in $Result.

Related

Loop through multiple array in powershell

I have 2 arrays here one contains the servername and other contains the IP.
I need to loop through them and create a key value pair like below for each server
server1:ip1
server2:ip2
I have written below code, but the problem is if i debug the code using F11, it is working fine, but i don't it gives some error which is different every time.
so feeling like it is not that reliable piece to continue.
$NewDNSEntryName = $DNSEntryName.Split(",")
$DNSIPs = $DNSIP.Split(",")
if($DNSEntryName -match "," -or $DNSIP -match ",")
{
0..($NewDNSEntryName.Count - 1) | ForEach-Object {
$fullName=""
$fullName += #("$($NewDNSEntryName[$_]):$($DNSIPs[$_])")
This is the line where i am facing trouble
0..($NewDNSEntryName.Count - 1) | ForEach-Object
Please let me know why this code is behaving like this else any alternate idea is appreciated
Assuming each item in each list corresponds with each other exactly, you can use a for loop and loop through the array indexes.
$NewDNSEntryName = $DNSEntryName.Split(",")
$DNSIPs = $DNSIP.Split(",")
for ($i = 0; $i -lt $DNSIPs.count; $i++) {
"{0}:{1}" -f $NewDNSEntryName[$i],$DNSIPs[$i]
}
For the code above to work, $DNSEntryName and $DNSIP must be single strings with commas between names and IPs. If $DNSEntryName and $DNSIP are already lists or arrays, something else will need to be done.
In your attempt, technically, your logic should work given everything written above is true. However, $fullName is emptied at every single iteration, which may produce undesirable results.

Replace string untill its length is less than limit with PowerShell

I try to update users AD accounts properties with values imported from csv file.
The problem is that some of the properties like department allow strings of length of max length 64 that is less than provided in the file which can be up to 110.
I have found and adopted solution provided by TroyBramley in this thread - How to replace multiple strings in a file using PowerShell (thank You Troy).
It works fine but... Well. After all replaces have place the text is less meaningful than originally.
For example, original text First Department of something1 something2 something3 something4 would result in 1st Dept of sth1 sth2 sth3 sth4
I'd like to have control over the process so I can stop it when the length of the string drops just under the limit alowed by AD property.
By the way. I'd like to have a choice which replacement takes first, second and so on, too.
I put elements in a hashtable alphabetically but it seems that they are not processed this way. I can't figure out the pattern.
I can see the resolution by replacing strings one by one, controlling length after each replacement. But with almost 70 strings it leds to huge portion of code. Maybe there is simpler way?
You can iterate the replacement list until the string reaches the MaxLength defined.
## Q:\Test\2018\06\26\SO_51042611.ps1
$Original = "First Department of something1 something2 something3 something4"
$list = New-Object System.Collections.Specialized.OrderedDictionary
$list.Add("First","1st")
$list.Add("Department","Dept")
$list.Add("something1","sth1")
$list.Add("something2","sth2")
$list.Add("something3","sth3")
$list.Add("something4","sth4")
$MaxLength = 40
ForEach ($Item in $list.GetEnumerator()){
$Original = $Original -Replace $Item.Key,$Item.Value
If ($Original.Length -le $MaxLength){Break}
}
"{0}: {1}" -f $Original.Length,$Original
Sample output with $MaxLength set to 40
37: 1st Dept of sth1 sth2 sth3 something4

Obtain IP address from user input, generates 3 other static IPs

I am looking for a more efficient way to obtain an IP address from a user, splitting the IP address into an array, knocking off the last octet, replacing with a specific number, then joining the array back into a new IP address.
For example: User inputs IP: 10.1.1.50. From this input, I take the first 3 octets, then generate 10.1.1.1, 10.1.1.10 and 10.1.1.11. I can procedurally do this, but seems Im doing the same operations over for 3 iterations, a function could possibly be beneficial, but my attempts are failing.
$ip= $(read-host "Enter any IP Address")
$gateway = $ip.split('.')
$gateway[-1] = 1
$gateway = $ipArray -join '.'
$dns1 = $ip.split('.')
$dns1[-1] = 10
$dns1 = $dns1 -join '.'
$dns2 = $ip.split('.')
$dns2[-1] = 11
$dns2 = $dns2 -join '.'
I've separated out the first 3 octets with $threeOctets = $ip | Select-Object -First 3, which works fine, what any time i operate on it, i just seem to ADD on the 1, then the 10, then the 11 instead of adding on the 1, then going back to the first 3 octets and adding a 10, then going back and adding an 11.
Why are you making the user type in the last octet, only to cut it off and throw it away? And are you sure this is a good design, given that networks aren't all /24?
Anyway, no real need to split and join, you can just:
$ip = (Read-Host "Enter an IP") -replace '\d+$'
$gateway, $dns1, $dns2 = ('1','10','11' | foreach {"$ip$_"})
The replace gets rid of the last digits. The loop puts a string into the pipeline for each of the three numbers in the array. The multiple assignment takes the three values coming out of the pipeline into the three variables.
(PowerShell does have rudimentary IP address types, with [ipaddress]"192.168.0.1", but they're too basic to be of much help here).
I agree with was TessellatingHeckler's answer says. But I wanted to show another way that has some basic error correction as well using [ipaddress].
do{
$ip = $(read-host "Enter any IP Address")
} while(!($ip -as [ipaddress]))
$octets = ([ipaddress]$ip).Getaddressbytes()
$gateway, $dns1, $dns2 = '1','10','11' | ForEach-Object{$octets[3] = $_; $octets -join "."}
This is by no means a better approach. Merely an alternate one. The error correction is not perfect and can break but its better than a kick in the pants in some cases.
Get the ip. Convert it into its octets as an array. replace the last element of the array with the pipeline value. Join again with a period to make it an valid ip again.
It is a shame that the [Ipaddress] class wont let you edit the octets in place. [version] works like this too, which is unfortunate.

Powershell - Call Unique Values in a ForEach loop from Two Variables to execute a PowerCLI command

I am trying to write a script to bring network cards back online, after a port conflict.
I have a list of VMs stored in one variable ($VDILIST) and have a list of available port numbers stored in a second variable ($FREEPORT).
How do I assign a port number to each of the new VM Network cards. I included a line (remarked out) how to do it for one. I know how to either process a single list ForEach loop. However, I can not figure out how to process these values from two seperate variables.
Port VM
---- ----
01 PC-A
02 PC-B
03 PC-C
04 PC-D
I want to assign Port 01 ot PC-A then move to the next and assign Port 02 to PC-B. Then continue on from there.
Thank you for your help.
$VDI = "vdi-win7v5-*"
$PORTGROUP = "Enterprise"
$VDILIST = #(Get-VM $VDI)
$FREEPORT = Get-dvPgFreePort -PortGroup $PortGroup -Number ($VDILIST).count
###USED FOR SINGLE COMPUTER
###Get-VM vdi-win7v2-30 |Get-NetworkAdapter|Set-NetworkAdapter -PortKey $FREEPORT -DistributedSwitch chsdc-nexus1000v -Connected:$TRUE -Confirm:$False
Assuming that $VDILIST and $FREEPORT have exactly the same number of items:
For ($i = 0; $i -lt $VDILIST.count; $i++)
{
Get-VM $VDILIST[$i] |Get-NetworkAdapter|Set-NetworkAdapter -PortKey $FREEPORT[$i] -DistributedSwitch chsdc-nexus1000v -Connected:$TRUE -Confirm:$False
}

Returning a Row Count for a DataRow in PowerShell

My script is populating a datarow from a stored procedure in SQL Server. I then reference specific columns in this datarow throughout the script. What I'm trying to do is add functionality that takes action X if the row count = 0, action Y if the row count = 1, and action Z if the row count > 1.
-- PowerShell script snippet
# $MyResult is populated earlier;
# GetType() returns Name=DataRow, BaseType=System.Object
# this works
ForEach ($MyRow In $MyResult) {
$MyFile = Get-Content $MyRow.FileName
# do other cool stuff
}
# this is what I'm trying to do, but doesn't work
If ($MyResult.Count -eq 0) {
# do something
}
ElseIf ($MyResult.Count -eq 1) {
# do something else
}
Else {
# do this instead
}
I can get $MyResult.Count to work if I'm using an array, but then I can't reference $MyRow.FileName directly.
This is probably pretty simple, but I'm new to PowerShell and object-oriented languages. I've tried searching this site, The Scripting Guy's blog, and Google, but I haven't been able to find anything that shows me how to do this.
Any help is much appreciated.
It has everything to do with how you populate $MyResult. If you query the database like
$MyResult = #( << code that returns results from database >> )
that is, enclosing the code that returns your dataset/datatable from the database within #( ... ), then number of rows returned will be easily checked using $MyResult.count.
Your original code should work as-is if you populate $MyResult this way.
I know this thread is old, but if someone else finds it on Google, this should work also on PS V5:
Replace $MyResult.Count with: ($MyResult | Measure-Object | select -ExpandProperty Count)
For Example:
If (($MyResult | Measure-Object | select -ExpandProperty Count) -eq 0)
I don't have experience with PS and SQL, but I'll try to provide an answer for you. If you're object $myresult is a datarow-object, it means you only got the one row. If the results are empty, then $myresult will usually be null.
If you get one or more rows, you can put them in an array and count it. However, if your $myresult are null, and you put it in an array it will still count as one, so we need to watch out for that. Try this:
If ($MyResult -eq $null) {
# do something if no rows
}
Else If (#($MyResult).Count -eq 1) {
# do something else if there are 1 rows.
# The cast to array was only in the if-test,
# so you can reach the object with $myresult.
}
Else {
# do this if there are multiple rows.
}
Looks like this question gets a lot of views, so I wanted to post how I handled this. :)
Basically, the fix for me was to change the method I was using to execute a query on SQL Server. I switched to Chad Miller's Invoke-SqlCmd2 script: TechNet: Invoke-SqlCmd2, i.e.
# ---------------
# this code works
# ---------------
# Register the function
. .\Invoke-Sqlcmd2.ps1
# make SQL Server call & store results to an array, $MyResults
[array]$MyResults = Invoke-Sqlcmd2 -Serve
rInstance "(local)" -Query "SELECT TOP 1 * FROM sys.databases;"
If ($MyResult -eq $null) {
# do something
}
ElseIf ($MyResult.Count -eq 1) {
# do something else
}
Else {
# do this instead
}