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

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.

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
}

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
}
}

'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.