Get-Content and foreach in two files - powershell

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
}

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

Powershell Script to Update DNS on multiple servers

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
}

Start-job for each computer in my csv

i'm looking for a way to start a new job for each computer in my csv, to launch parallel job on each of them.
I already have this
$job1 = Start-Job -Scriptblock {
$Csv = "C:\springfield\Citrix\CitrixComposants.csv"
$myservers = Import-Csv $Csv
Import-module C:\springfield\Citrix\CitrixDeploymentActivlanModule.ps1
Deploy-Citrix -servers $myservers[0].server -component $myservers[0].component
}
How can i get $job[n] as many as there are servers in my csv ?
I was thinking about something like this, but maybe there is a better way to achieve that.
My idea is to create a dynamic $job variable ($job1, $job2, $job3, $job[n]...)
$Csv = "C:\springfield\Citrix\CitrixComposants.csv"
for ($i=0;$i -lt $csv.count; $i++)
{
$job+"$i" = Start-Job -Scriptblock {
$Csv = "C:\springfield\Citrix\CitrixComposants.csv"
$myservers = Import-Csv $Csv
Import-module C:\springfield\Citrix\CitrixDeploymentActivlanModule.ps1
Deploy-Citrix -servers $myservers[$i].server -component $myservers[$i].component
}
}
I also would like when a job finishes it shows me it's completed, how can i get that?
I'm using powershell v4.
Thanks for your help
Well, currently you are running one job for all the servers. Or you would be if you weren't specifying $Myservers[0].
If any results are returned you can see the results by using receive-job. I would think the results would contain output returned from each server, assuming you removed $myservers[0] and that deploy-citrix cmdlet accepts an array. If you just want to check the status you can use get-job.
If you wanted to instead start a job for each server it should look more like this:
$Csv = "C:\springfield\Citrix\CitrixComposants.csv"
$myservers = Import-Csv $Csv
Foreach ($Server in $MyServers)
{
$SrvName = $Server.Server
$Component = $Server.Component
Start-Job -ArgumentList $SrvName,$Component -Scriptblock {
Param ($SrvName,$Component)
Import-module C:\springfield\Citrix\CitrixDeploymentActivlanModule.ps1
Deploy-Citrix -servers $Srvname -component $component
}
}

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.

'System.OutOfMemoryException' while looping through array in powershell

I was trying to write a function that to look for pool tags in .sys files. I created an array of all the directories that had .sys files then looped through them using the sysinternals Strings utility.
This is the array:
$paths = Get-ChildItem \\$server\c$ *.sys -Recurse -ErrorAction SilentlyContinue |
Select-Object Directory -unique
This was my first attempt at a loop:
foreach ($path in $paths) {
#convert object IO fileobject to string and strip out extraneous characters
[string]$path1 = $path
$path2 = $path1.replace("#{Directory=","")
$path3 = $path2.replace("}","")
$path4 = "$path3\*.sys"
Invoke-Command -ScriptBlock {strings -s $path4 | findstr $string}
}
I found some references to the error indicating that in foreach loops, all of the information is stored in memory until it completes its processing.
So I tried this:
for ($i = 0; $i -lt $paths.count; $i++){
[string]$path1 = $paths[$i]
$path2 = $path1.replace("#{Directory=","")
$path3 = $path2.replace("}","")
$path4 = "$path3\*.sys"
Invoke-Command -ScriptBlock {strings -s $path4 | findstr $string}
}
But it had the same result. I've read that sending an item at a time across the pipeline will prevent this error/issue, but I'm at a loss on how to proceed. Any thoughts?
Yeah, it is usually better to approach this problem using streaming so you don't have to buffer up a bunch of objects e.g.:
Get-ChildItem \\server\c$ -r *.sys -ea 0 | Foreach {
"Processing $_"; strings $_.Fullname | findstr $string}
Also, I'm not sure why you're using Invoke-Command when you can invoke strings and findstr directly. You typically use Invoke-Command to run a command on a remote computer.