Powershell invoke-command passing an object - powershell

I have created a list of objects, $LIST. Each object in the list has several attributes, including FQDN and Services. FQDN is the fully qualified server name and the services are the list of services I want to check on the remote server.
I'll start with:
$LIST = <CALL to Module function to populate the server information>
Next is the call to the invoke-command
Invoke-Command -ComputerName $LIST.FQDN -ScriptBlock {
Write-Host "Working on $($env:ComputerName)"
Get-Service
}
But what I need to do is pass the list of services that correspond to -ComputerName. I know I can use the -ArgumentList and I've tried:
Invoke-Command -ComputerName $LIST.FQDN -ScriptBlock {
Param ([string[]] $ServiceList)
Write-Host "Working on $($env:ComputerName)"
($ServiceList -split(",")).trim() | %{
$svc =Get-Service $_
$Svc
}
} -ArgumentList $LIST.Services
But this passes a list of all the services for every server. I can do this:
$LIST | %{
$Server = $_
Invoke-Command -ComputerName $Server.FQDN -ScriptBlock {
Param ([string[]] $ServiceList)
Write-Host "Working on $($env:ComputerName)"
($ServiceList -split(",")).trim() | %{
$svc =Get-Service $_
$Svc
}
} -ArgumentList $($Server.SERVICES)
}
But then I loose the advantage of parallelism of the invoke-command CmdLet.
How do I pass the list of services for the specific ComputerName being processed?

If $LIST["COMPUTER"].SERVICES return list service of COMPUTER you can test in your scriptblock for every object passed..maybe This but not tested: (in pseudo-code)
Icm -comp $LIST.FQDN -Script {
Param($obj)
$c=$env:computername
$obj["$c"].services
} -arg $LIST

I don't think there is an easy way to pass your list through Invoke-Command, so perhaps you need to think of alternative approaches that will let each target computer run an identical command.
If the list of services is specific to each remote computer, and is the same every time you run the commands then you could simply store the parameter on each target computer in the registry or a file. Ideally you set that up using a configuration manager such as DSC, Puppet, or Chef.
You could dump the parameters out to files on the local computer and let each target computer connect back to a network share and fetch the file corresponding to its name. If a network share connection isn't possible then a simple web service or a database connection would be other ways you could let each computer fetch the service list.
Are there groups of targets that will receive the same list? Say you have 10 different service lists each to be sent to a group of 100 computers. In that case maybe you could choose an arbitrary computer from each group and send those computers an identical command with all of the data. Each computer you target then figures out which group it is in and distributes the identical command across all of its siblings. This would also have the benefit of increasing the parallelism.
Or just create one set of services that is the union of all the service lists, send that to every machine, and filter the results based on which you wanted. That could be the simplest solution:
PS C:\WINDOWS\system32> $allServices = 'WerSvc','WinRM','ZeroConfigService','AnotherService'
PS C:\WINDOWS\system32> Invoke-Command -ComputerName . -ScriptBlock {
Get-Service -Name $args -ErrorAction Ignore
} -ArgumentList $allServices
Status Name DisplayName PSComputerName
------ ---- ----------- --------------
Stopped WerSvc Windows Error Reporting Service localhost
Running WinRM Windows Remote Management (WS-Manag... localhost
Running ZeroConfigService Intel(R) PROSet/Wireless Zero Confi... localhost
will simply ignore any services you asked for that aren't installed on that particular machine.

For the sake of testing, I've created two objects containing a pointer to localhost, and a list of specific services to verify. Goal is to invoke background jobs that will run remotely and in parallel, and verify the provided list of services.
One of the key parts in the script below is the generated array for the Invoke-Command argumentlist, which contains the services array for the associated computer. The rest is pretty much your code. Finally I use Get-Job | Receive-Job to retrieve the output from the Invoke-Command jobs.
$LIST = #()
$Computer1 = New-Object PSObject -Property #{
FQDN = "localhost";
SERVICES = #()
}
$Computer1.SERVICES += "W32Time"
$Computer1.SERVICES += "vmms"
$LIST += $Computer1
$Computer2 = New-Object PSObject -Property #{
FQDN = "localhost";
SERVICES = #()
}
$Computer2.SERVICES += "ShellHWDetection"
$Computer2.SERVICES += "SharedAccess"
$LIST += $Computer2
$LIST | %{
$Server = $_
$arguments = #(,($Server.SERVICES))
Invoke-Command -ComputerName $Server.FQDN -AsJob -ScriptBlock {
Param ([string[]]$ServiceList)
Write-Host "Working on $($env:ComputerName)"
($ServiceList -split(",")).trim() | %{
$svc =Get-Service $_
$Svc
}
} -ArgumentList $arguments
}
Get-Job | Receive-Job
Output is this:
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
28 Job28 RemoteJob Running True localhost ...
30 Job30 RemoteJob Running True localhost ...
Working on CORSAIR-PC
Status Name DisplayName PSComputerName
------ ---- ----------- --------------
Stopped W32Time Windows Time localhost
Running vmms Hyper-V Virtual Machine Management localhost
Working on CORSAIR-PC
Running ShellHWDetection Shell Hardware Detection localhost
Stopped SharedAccess Internet Connection Sharing (ICS) localhost
So, distinct services per computer were passed to Invoke-Command which runs in parallel jobs using the -AsJob parameter.

Related

PowerShell Invoke-Command shows different output than running the same command locally

Using PowerShell, if I run this command on my local computer, the output is "Running"
(Get-Service -Name Spooler).Status
If I run the same command inside an Invoke-Command script block, to get the status of the service on a remote computer
Invoke-Command -ComputerName 10.131.173.71 -Credential $cred -ScriptBlock {(Get-Service -Name Spooler).Status}
the output is
PSComputerName RunspaceId Value
-------------- ---------- -----
10.131.173.71 5f1b9d02-4c60-47a9-b783-01fa89eb1d58 Running
How can I get the same output with the second command? (Running)
Here is one way to do it. You don't get the same value when remoting because the Status Property is an enum and it gets de-serialized that way. See this answer for more details.
[ServiceProcess.ServiceControllerStatus] (
Invoke-Command -ComputerName 10.131.173.71 -Credential $cred -ScriptBlock {
[int] (Get-Service Spooler).Status
}
)

Running a script against multiple servers, but the outcome is out of order

So, I'm using invoke-command to run a script against more than one server simultaneously:
Invoke-Command -ComputerName $Servers -Credential $Cred -FilePath "C:\RemoteScript.ps1"
However the results appear to be out of order:
User: username
FQDN: DEVSERVER01.localnet.int
IP: 10.0.0.101
Server uptime: 20 days
======================
User: username
FQDN: DEVSERVER02.localnet.int
IP: 10.0.0.202
Server uptime: 20 days
======================
User: username
FQDN: DEVSERVER03.localnet.int
IP: 10.0.0.303
Server uptime: 20 days
======================
List of updates for DEVSERVER01.localnet.int:
No updates
List of updates for DEVSERVER02.localnet.int:
No updates
List of updates for DEVSERVER03.localnet.int:
No updates
I also want to log the results for each server but this is what I came up with:
Start-Process -FilePath Powershell -ArgumentList "
Invoke-Command -ComputerName $Server -Credential $Cred -FilePath "C:\RemoteScript.ps1"
" -RedirectStandardOutput "$Server Log.txt" -Wait
How should I be able to run my script on two servers but should try to get the results from each server in order?
Indeed, when targeting multiple computers in parallel with Invoke-Command, the order of the output objects across computers is not guaranteed.
You can use Group-Object to group the output objects by their (implicitly added) PSComputerName property, which reflects the computer of origin:
Invoke-Command -ComputerName $Servers -Credential $Cred -FilePath C:\RemoteScript.ps1 |
Group-Object PSComputerName |
ForEach-Object { Set-Content "$($_.Name) Log.txt" -Value $_.Group }
Note that Group-Object implicitly and invariably outputs the groups sorted by the grouping criterion/a, so that the groups are sorted by computer name in this case.
Caveat: In your case, given that you're writing the results to files, you have no expectation of console output anyway, but if you do want to see the results in the console instead, note that Group-Object has to collect all output, across all computers, before it can display the results.

Issue getting pssessions to loop properly for multiple servers listed in a txt file

I start with a txt file named vms.txt.
It contains 2 servers like so:
server1
server2
When I run the script shown below, the command that is invoked to install VMware tools only runs on server2 and server1 gets skipped. Does anyone have any suggestions on how to modify this script to make it run on both servers in the txt file? I will have to run this in the future for hundreds of VMs, so I am trying to find an easy way to get this to loop properly.
$cred = Get-Credential
$vms = Get-Content C:\Scripts\Tools\vms.txt
foreach($vm in $vms){
$sessions = New-PSSession -ComputerName $vm -Credential $cred
}
foreach($session in $sessions)
{
Invoke-Command -Session $session -ScriptBlock {
c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI”
}
}
In your loop-based approach, the problem is your variable assignment:
# !! This only ever stores the *last* session created in $sessions,
# !! because the assignment is performed in *each iteration*.
foreach($vm in $vms){
$sessions = New-PSSession -ComputerName $vm -Credential $cred
}
The immediate fix is to move the assignment out of the loop:
# OK - captures *all* session objects created in $sessions
$sessions = foreach($vm in $vms){
New-PSSession -ComputerName $vm -Credential $cred
}
Taking a step back:
Both New-PSSession -ComputerName and Invoke-Command -Session accept an array of computer names / sessions, so there's no need for loops.
Passing multiple sessions / computer names to Invoke-Command has the big advantage that the operations run in parallel.
Note:
Invoke-Command has built-in throttling to avoid targeting too many machines at once. It defaults to 32, but can be modified with the -ThrottleLimit parameter.
Output from the targeted computers will arrive in no predictable order, but the output objects are decorated with (among others) a .PSComputerName property reflecting the originating computer - see the bottom section of this answer.
That is, your code can be simplified to:
$cred = Get-Credential
$vms = Get-Content C:\Scripts\Tools\vms.txt
$sessions = New-PSSession -ComputerName $vms -Credential $cred
Invoke-Command -Session $sessions -ScriptBlock {
c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI”
}
Important:
Sessions should eventually be cleaned up with Remove-PSSession when no longer needed.
However, that stops any commands running in those sessions, so if you've launched asynchronous operations via your Invoke-Command call, you need to ensure that those operations have finished first - see the comments re potentially asynchronous execution of your VMware-tools-10.3.5.exe application below.
Or, even simpler, if you only need to execute one command on each machine, in which case there is no need to create sessions explicitly, pass all computer names directly to Invoke-Command's -ComputerName parameter:
$cred = Get-Credential
$vms = Get-Content C:\Scripts\Tools\vms.txt
# Note the use of -ComputerName
Invoke-Command -ComputerName $vms -Credential $cred -ScriptBlock {
# Note the use of | Write-Output to ensure synchronous execution.
c:\users\jsmith\documents\VMware-tools-10.3.5.exe /v "/qn REBOOT=R Remove=AppDefense,VMCI” | Write-Output
}
Important:
If your application (VMware-tools-10.3.5.exe) runs asynchronously, you must ensure its
synchronous execution, otherwise it may not run to completion, because the implicitly created remote session is discarded when a script block returns from a given computer.
A simple trick to ensure synchronous execution of any external (GUI-subsystem) executable is to pipe it to Write-Output, as shown above (or Wait-Process, if it doesn't produce console output) - see this answer for an explanation.

Powershell output for multiple dhcp servers

I am trying to get the mibinfo for multiple dhcp servers in our infrastructure. My problem is that when i run the command
invoke-command -computername $dhcpserver -credential $Cred -scriptblock{netsh dhcp server show mibinfo}
I get MIBinfo for all the servers, but i don't get the name of the server in the output. So i need a way where i get the output as
Server1
mibinfo
server2
mibinfo
($dhcpserver has the list of all the dhcp servers.)
Is $dhcpserver an array of strings? I didn't know Invoke-Command could do this...
If so, try this:
Foreach ($server in $dhcpserver) {
$mibOutput = invoke-command -computername $server -credential $Cred -scriptblock{netsh dhcp server show mibinfo}
Write-Output "$server $mibOutput"
}

Powershell: waiting for changed directory

To make it short, I want to connect to a server that is running Virtual Machines and then get a List of all installed machines, the command I use for this is:
Invoke-Command -ScriptBlock {enter-pssession -ComputerName <name>}; Invoke-Command -ScriptBlock {Get-VM} | select-Object -Property name
This line contains two commands at first:
Invoke-Command -ScriptBlock {enter-pssession -ComputerName <name>};
this part connects to the server, and then:
Invoke-Command -ScriptBlock {Get-VM} | select-Object -Property name
This command gets a list of the VMs currently on the server and returns specific properties of these servers.
However, because the connection needs a short time until it is set up, the "get-vm" command is still set in the previous direction and results in an error report.
I want to know if there is a way to wait for ether a command to be finished or for a change in the directory, without having an extra loop running for this time, or waiting for a hard set time.
I don't know why are you trying to do what you are trying to do, what you should do is:
Invoke-Command -SessionName (or -ComputerName) -ScriptBlock {Get-VM | Select-Object -Property name}