Powershell Script to Update DNS on multiple servers - powershell

I have received the following script to update the DNS server on a windows server. This script works great for updating one server. I would like to have this script process a text file with a list of servers to batch update multiple servers at once. I am thinking something like:
script.ps1 -ComputerName (Get-Content c:\serverlist.txt) -OldDns 10.0.0.1 -NewDns 10.0.0.2
This fails if there are multiple servers in the 'serverlist.txt'. My question is how to incorporate a ForEach that allows for every server in the 'serverlist.txt'
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,HelpMessage="Name of the computer to update")]
[String]$ComputerName,
[Parameter(Mandatory=$True,HelpMessage="DNS server to replace")]
[String]$OldDns,
[Parameter(Mandatory=$True,HelpMessage="New DNS server setting")]
[String]$NewDns,
[switch]$TestMode
)
$niclist = Get-WmiObject -Authentication PacketIntegrity -ComputerName $ComputerName -class Win32_NetworkAdapterConfiguration | where { $_.DnsServerSearchOrder -contains $OldDns }
if ($niclist) {
foreach ($nic in $niclist) {
$dns = $nic.DnsServerSearchOrder
$dns
$index = [array]::IndexOf($dns,$OldDns)
$dns[$index] = $NewDns
$dns
$nic
if (!$TestMode) {
$nic.SetDnsServerSearchOrder($dns)
}
}
}

Assuming your serverlist.txt contains a list of servers where one is on each line and no headers you can just pipe the content of the file into a ForEach-Object loop. The -ComputerName will be $_ which is the current item in the pipeline.
Get-Content c:\serverlist.txt | ForEach-Object{
script.ps1 -ComputerName $_ -OldDns 10.0.0.1 -NewDns 10.0.0.2
}

Related

Optimizing Powershell Script to Query Remote Server OS Version

I want optimize a simple task: pull server OS version into a neat table. However, some servers in our environment have Powershell disabled. Below you fill find my script, which works! However, it takes about 20 seconds or so per server, since it waits for the server to return the results of the invoked command before moving onto the next server in the list. I know there's a way to asynchronously pull the results from a PS command, but is this possible when I need to resort to cmd line syntax for servers that can't handle PS, as shown in the catch statement?
$referencefile = "ps_servers_to_query.csv"
$export_location = "ps_server_os_export.csv"
$Array = #()
$servers = get-content $referencefile
foreach ($server in $servers){
#attempt to query the server with Powershell.
try{
$os_version = invoke-command -ComputerName $server -ScriptBlock {Get-ComputerInfo -Property WindowsProductName} -ErrorAction stop
$os_version = $os_version.WindowsProductName
} # If server doesnt have PS installed/or is disabled, then we will resort to CMD Prompt, this takes longer however.. also we will need to convert a string to an object.
catch {
$os_version = invoke-command -ComputerName $server -ScriptBlock {systeminfo | find "OS Name:"} # this returns a string that represents the datetime of reboot
$os_version = $os_version.replace('OS Name: ', '') # Remove the leading text
$os_version = $os_version.replace(' ','') # Remove leading spaces
$os_version = $os_version.replace('Microsoft ','') # Removes Microsoft for data standardization
}
# Output each iteration of the loop into an array
$Row = "" | Select ServerName, OSVersion
$Row.ServerName = $Server
$Row.OSVersion = $os_version
$Array += $Row
}
# Export results to csv.
$Array | Export-Csv -Path $export_location -Force
Edit: Here's what I'd like to accomplish. Send the command out to all the servers (less than 30) at once, and have them all process the command at the same time rather than doing it one-by-one. I know I can do this if they all could take PowerShell commands, but since they can't I'm struggling. This script takes about 6 minutes to run in total.
Thank you in advance!
If I got it right something like this should be all you need:
$referencefile = "ps_servers_to_query.csv"
$export_location = "ps_server_os_export.csv"
$ComputerName = Get-Content -Path $referencefile
$Result =
Get-CimInstance -ClassName CIM_OperatingSystem -ComputerName $ComputerName |
Select-Object -Property Caption,PSComputerName
$Result
| Export-Csv -Path $export_location -NoTypeInformation

How can I stop iterating through entire list multiple times in ForEach?

How can I apply 1 IP address in a list to 1 server in another list?
Then move onto the next IP and apply it to the next server.
servers.txt looks like:
server1
server2
server3
ip.txt looks like:
10.1.140.80
10.1.140.81
10.1.140.83
I just want to go through the list and apply
10.1.140.80 to server1
10.1.140.81 to server2
10.1.140.83 to server3
Instead, my script is applying all 3 IP addresses to each server.
I dont want to cycle through all IP addresses over and over.
How can I iterate through the lists properly and correct this?
$computers = "$PSScriptRoot\servers.txt"
$iplist = gc "$PSScriptRoot\ip.txt"
function changeip {
get-content $computers | % {
ForEach($ip in $iplist) {
# Set IP address
$remotecmd1 = 'New-NetIPAddress -InterfaceIndex 2 -IPAddress $ip -PrefixLength 24 -DefaultGateway 10.1.140.1'
# Set DNS Servers - Make sure you specify the server's network adapter name at -InterfaceAlias
$remotecmd2 = 'Set-DnsClientServerAddress -InterfaceAlias "EthernetName" -ServerAddresses 10.1.140.5, 10.1.140.6'
Invoke-VMScript -VM $_ -ScriptText $remotecmd1 -GuestUser Administrator -GuestPassword PASSWORD -ScriptType PowerShell
Invoke-VMScript -VM $_ -ScriptText $remotecmd2 -GuestUser Administrator -GuestPassword PASSWORD -ScriptType PowerShell
}
}
}
changeip
Use your the Get-Content cmdlt to place both file contents into an array then pull the individual values by array position. You'll probably want some logic to check if the array size matches and custom handling if it does not. In your above example, you are basically putting a for each loop inside another foreach loop which is giving you the behavior you are seeing.
$computers = GC "C:\server.txt"
$iplist = GC "C:\ip.txt"
for ($i = 0; $i -lt $iplist.Count ; $i++) {
Write-host ("{0} - {1}" -f $computers[$i],$iplist[$i])
}
Or if you are married to using the foreach logic for one list to be all fancy like instead of basic iterating with a for loop then you can add a counter in your foreach loop. Then you can look up your array index of your already parsed iplist array. It's basically doing the same thing though..
$computers = "C:\server.txt"
$iplist = GC "C:\ip.txt"
get-content $computers | % {$counter = 0} {
Write-host ("{0} - {1}" -f $_,$iplist[$counter])
$counter++
}
Just for clarity purposes as well, please note in this line:
"get-content $computers | %"
The % is actually an alias for ForEach-Object which is why you are getting the foreach inside a foreach output that you are seeing.

Get-Content and foreach in two files

I have two files. The first with contains hostnames (Computers.txt) and the second one contains SID (SID.txt). I want to use Get-Content and foreach to execute a command on each computer with the corresponding SID to modify registry.
Let's take for example PC 1 (first line Computers.txt with first line SID.txt) and PC 2 (second line Computers.txt with second line SID.txt).
$Computer = Get-Content D:\Downloads\computers.txt
$SID = Get-Content D:\Downloads\SID.txt
foreach ($pc in $Computer)
{
Invoke-Command -ComputerName $pc {New-Item HKEY_USERS:\$SID -Name -Vaue}
}
Using a foreach-loop doesn't give you the current linenumber so it's impossible to get the same line from the SIDs-list. You should use a while- or for-loop to create an index that increments by one for each run so you know the "current line".
There's no HKEY_USERS: PSDrive. You need to access it using the Registry-provider, like Registry::HKEY_USERS\
Variables in your local scope (ex. $currentsid) aren't accessible inside the Invoke-Command-scriptblock since it's executed on the remote computer. You can pass it in using -ArgumentList $yourlocalvariable and call it with $args[0] (or put param ($sid) at the beginning of the scriptblock). With PS 3.0+ this is much simpler as you can use the using-scope ($using:currentsid) in your script.
Example:
$Computers = Get-Content D:\Downloads\computers.txt
$SIDs = Get-Content D:\Downloads\SID.txt
#Runs one time for each value in computers and sets a variable $i to the current index (linenumer-1 since arrays start at index 0)
for($i=0; $i -lt $Computers.Length; $i++) {
#Get computer on line i
$currentpc = $Computers[$i]
#Get sid on line i
$currentsid = $SIDs[$i]
#Invoke remote command and pass in currentsid
Invoke-Command -ComputerName $currentpc -ScriptBlock { param($sid) New-Item "REGISTRY::HKEY_USERS\$sid" -Name "SomeKeyName" } -ArgumentList $curentsid
#PS3.0+ with using-scope:
#Invoke-Command -ComputerName $currentpc -ScriptBlock { New-Item "REGISTRY::HKEY_USERS\$using:currentsid" -Name "SomeKeyName" }
}
One-liner:
0..($Computers.Length-1) | ForEach-Object { Invoke-Command -ComputerName $Computers[$_] -ScriptBlock { param($sid) New-Item REGISTRY::HKEY_USERS\$sid -Name "SomeKeyName" } -ArgumentList $SIDs[$_] }
On a side-note: Using two files with matching line numbers is a bad idea. What if comptuers has more lines than SIDs? You should be using a CSV-file that maps computer and SID. Ex..
input.csv:
Computer,SID
PC1,S-1-5-21-123123-123213
PC2,S-1-5-21-123123-123214
PC3,S-1-5-21-123123-123215
This is safer, easier to maintain and you can use it like this:
Import-Csv input.csv | ForEach-Object {
Invoke-Command -ComputerName $_.Computer -ScriptBlock { param($sid) New-Item REGISTRY::HKEY_USERS\$sid -Name "SomeKeyName" } -ArgumentList $_.SID
}

How to catch machine name on copy-item error?

I'm trying to write a file and delete a file on multiple remote Windows machines. If the machine is not available, ie. not on line I want to capture that in an error log so that I have a list of problematic machine names to send to a help desk. It's probably ugly but I'm close to having something that works. Any clarification would be appreciated.
$from="D:\whatever\machinfo"
$to="\\$machine\c\\scripts\"
output_file="D:\whatever\reports\$machine_writeerror.txt"
foreach($machine in(gc d:\whatever\machinfo\testworkstations.txt))
{
$machine
IF (!$to)
{
Copy-Item D:\whatever\machinfo\010RunGetmachinfo.bat \\$machine \c\scripts -verbose
# $errormsg="destination not found"
$machine > output_file
}
ELSE
{
# DO NOTHING
Remove-Item \\$machine\c\scripts\000dontrun.bat
}
}
OK, I've rewritten this but I'm not doing something right. I want an unique error file that contains either a single file for each machine connection failure or one file that contains the computername of all machines that could not be connected to. I think the following is close (but not right).
$logfile="D:\Projects\StoreControls\machinfo\reports\"+$machine+"_writeerror.txt"
foreach($machine in(gc d:\projects\StoreControls\machinfo\testworkstations.txt))
{
$machine
If ( (Test-Connection -Computername $machine -Quiet -Count 1) -eq "False"){
$machine > $logfile}
Else{
Remove-Item \\$machine\c\scripts\tasks\000dontStart.bat
Copy-Item D:\Projects\StoreControls\machinfo\010RunPCsNServersGetmachinfo.bat \\$machine\c\scripts\tasks\
}
}
Changed "False" to $False after reading more on Test-Connection. Works! Thank you!
you can test the correct execution of a command by testing the automatic variable $?
so you can use something like
Copy-Item D:\whatever\machinfo\010RunGetmachinfo.bat \\$machine\c\scripts
if($? -eq $false){
# copy has failed
"Copy error on $machine" |out-file d:\whatever\reports\$machine_writeerror.txt
}
by the way, a more efficient way could be to ping the host and see if it's alive :
if ( (Test-Connection -ComputerName $machine -Quiet -Count 1) -eq $false){
#host not available
}

PowerShell: Use Get-Content from multiple text files to send a message

There was very little on the topic of using multiple text files for PowerShell, only found stuff that would take one list and run it against the primary list. Anyway...
My question comes from a need to combine 2 sets of data, equal in the number of rows.
Server.txt & SessionID.txt. Both files are created from another Get-XASession query.
I wanted to combine these in a Send-XAMessage.
Servers.txt = "Server1","Server2","Server3",etc.
SessionIds.txt = "2","41","18",etc.
Here's the code I've tried unsuccessfully...
BTW, "ServerX", is a static connection server required for XA Remote computing.
$Server = Get-Content .\Server.txt
$SessionIds = Get-Content .\SessionIds.txt
ForEach ($s in $Servers -And $i in $SessionIds) {
Send-XASession -ComputerName ServerX -ServerName $s -SessionId $i -MessageTitle "MsgTitle" -MessageBody "MsgBody" }
For normal usability, we can switch the Stop-XASession, with Get-Service, and use the $s for -ComputerName.
And switch SessionId for -ServiceName.
That would look something like this...
ForEach ($s in $Servers -And $i in $Sevices) { Get-Service -ComputerName $s -Name $i } | FT Name,Status
The only thing that matters, is that each line on both text files is ran through simultaneously. No duplicates. Matching line 1 in Servers.txt to line 1 on SessionIds.txt and using it in each command.
Any help would be greatly appreciated.
You can do something like this:
$Server = Get-Content .\Server.txt
$SessionIds = Get-Content .\SessionIds.txt
$i=0
ForEach ($s in $Servers)
{
Send-XASession -ComputerName ServerX -ServerName $s -SessionId $SessionIds[$i++] -MessageTitle "MsgTitle" -MessageBody "MsgBody"
}
That will cycle the $SessionIds elements in synch with the $server elements. The postincrement operator on $SessionIds[$i++] will increment $i each time it goes through the loop.