Two Invoke-Command at the same time - powershell

I'm using this PowerShell script to resume Bitlocker on every active device:
Get-Content "clients.txt" | ForEach-Object {
if (Test-Connection $_ -Count 1 -ErrorAction 0 -Quiet) {
Invoke-Command -ComputerName $_ -ScriptBlock {
Resume-BitLocker -MountPoint "C:"
}
} else {
Write-Host "$_ is OFFLINE" -ForegroundColor Red
}
}
But I also want to trigger a hardware inventory via Invoke-WMIMethod on every active device with this command:
Invoke-WMIMethod -ComputerName $Server -Namespace root\ccm -Class SMS_CLIENT -Name TriggerSchedule "{00000000-0000-0000-0000-000000000001}"
I was able to script the first part but it isn't that well to built in the second command.

you are drifting a bit in the wrong direction.
When using Invoke-Command, it processes the scriptblock, against 32 computers simultaneously (in parallel)!
If you are processing computers with foreach, it would handle them sequentially, which would be much slower.
Same is valid when using *WMI cmdlets. Always try to replace them with the corresponding CIM cmdlets, as the same logic applies - computers are being processed in parallel.
Consider something like:
$ComputerList = Get-Content -Path Clients.txt
Invoke-Command -ComputerName $ComputerList -ErrorAction SilentlyContinue -ScriptBlock {
Resume-BitLocker -MountPoint "C:"
#Add second command
#add third command and so on
}
I am not sure, what would be the alternative command to Invoke-WMIMethod, when executing locally. Maybe Set-WMIInstance, but I am only speculating!
Then if you would like to add second command for execution, just add it into the scriptblock of Invoke-Command.

Related

powershell script restart services on 100s computers

I need to restart the services on hundreds sometimes less depending on if backup jobs failed on the remote machine with
$services = "winmgmt", "cryptsvc", "vss"
$computers = get-content "${env:\userprofile}\servers.txt"
foreach ($srv in $ computers) {
get-service -computername $srv $services | restart-service -force
}
while this works, it does not do it asynchronously, only one at a time, is there a way I can send the job out to all the machines at once?
You can use Invoke-Command to run the command as a Job, you can then wait for all the jobs to complete, something like this should do it.
$computers = get-content "${env:\userprofile}\servers.txt"
$sb = {
$services = "winmgmt", "cryptsvc", "vss"
get-service -Name $services | Restart-Service -PassThru | Get-Service
}
Invoke-Command -ComputerName $computers -ScriptBlock $sb
Demo of invoke-command running in parallel:
# elevated prompt
measure-command {
invoke-command localhost,localhost,localhost { sleep 5 } } |
% seconds
5

restart computer on all hosts at the same time

I have an idea about restarting all of host/VMs in my inventory.
I created some short script which restarting one by one of hosts.
But it's take to long time. How can i do that all hosts to be restarted at the same time?
My code:
foreach ($host in $hosts) {
Restart-Computer -ComputerName $host -Wait
Write-Host "$host restarted "
}
Restart-Computer will accept multiple entries for ComputerName so you can perform the restart on more than one host at a time.
Using this you can initiate the restart in batches, say three at a time, this will significantly decrease the time it takes without adding too much load on the VM Cluster/Host.
$hosts = #("server01","server02","server03","server04","server05","server06","server07","server08","server09","server10")
[int]$batches = 3 #number of computers to restart in each 'batch'
[int]$skip = 0
do {
$selected_hosts = $hosts | Select-Object -First $batches -Skip $skip
Restart-Computer -ComputerName $selected_hosts -Wait -WhatIf
Write-Host "$selected_hosts restarted"
$skip = $skip + $batches
}
while ($selected_hosts)
Note: Remove -WhatIf when you're ready to run the script live, with this in place Restart-Computer will just write to Console informing you what it would do and not actually performing the restart.
is it a good idea ?? - Restart-Computer -asJob
Or maybe are better solutions ??
$j = Restart-Computer -ComputerName "Server01", "Server02" -AsJob
PS C:\> $Results = $j | Receive-Job
PS C:\> $Results
Can you try WMI to reboot the servers. I found one such method on social.technet.microsoft Forum.
$server = get-content c:\Servers.txt
(gwmi -Class Win32_OperatingSystem -ComputerName $server).Win32Shutdown(6)
If ($?) {
Write-Host "$server successfully rebooted"
}Else{
Write-Host "Could not reboot $server"
}
You can find other methods also like using a batch file. Check out this link.

PowerShell Script to Uninstall multiple programs from a list of servers

I have written a basic PS script in order to uninstall any programs defined in a text file ($appname) on all servers defined in a text file ($servers).
Running this command manually without the variables it works fine, however running the script via Jenkins or from PS command line it just hangs so I can't even debug, anyone have any ideas?
[array]$servers= Get-Content "D:\Jenkins\BuildUtilities\Citrix\CitrixServerList.txt"
[array]$appname= Get-Content "D:\Jenkins\BuildUtilities\Citrix\ProgramsList.txt"
ForEach($server in $servers) {
$prod=gwmi -ComputerName $server Win32_product | ?{$_.name -eq $appname}
$prod.uninstall()
}
To clarify: By running manually I mean running the following:
gwmi -ComputerName CTX-12 Win32_product | ?{_.Name -eq "Microsoft Word"}
Microsoft Word is an example.
It should have been easy to follow Matts hints
[array]$servers= Get-Content "D:\Jenkins\BuildUtilities\Citrix\CitrixServerList.txt"
[array]$appname= Get-Content "D:\Jenkins\BuildUtilities\Citrix\ProgramsList.txt"
ForEach($server in $servers) {
$prod=gwmi -ComputerName $server Win32_product | ?{ $appname -contains $_.name}
$prod.uninstall()
}
You are doing an comparison of two arrays, what you want to do is check for every instance of matching applications in the prod array you fetch from the server, so using the pipeline how you have it is not ideal.
[array]$servers= Get-Content "D:\Jenkins\BuildUtilities\Citrix\CitrixServerList.txt"
[array]$appname= Get-Content "D:\Jenkins\BuildUtilities\Citrix\ProgramsList.txt"
$DebugPreference= 'Continue'
foreach($server in $servers)
{
Write-Debug "Getting all installed applications on server $server"
$prod= Invoke-Command -ComputerName $server -Scriptblock {gwmi Win32_product}
Write-Debug "Installed applications collected, there are $($prod.Count) items in the array."
foreach($p in $prod)
{
Write-Debug "Searching apps array for the name $($p.Name)."
if($appname -contains $p.Name)
{
Write-Verbose -Message "$($p.Name) found on server $server, uninstalling."
$p.uninstall()
}
else
{
Write-Verbose -Message "$($p.Name) was not found on server $server."
}
}
}
EDIT: Since you brought up the speed issue, using Win32_product is very slow. So the faster method would be to get a list of installed applications from the registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall (details how to do that are here). Build a new array of applications found that need uninstalled and then call on Win32_product. Just know that this only speeds up one portion of your script, and if any application is found, the slow down will still occur. The major speed gains will come from when you find a server that doesn't have any applications to uninstall.
EDIT2: Did some experimenting, using a simple Invoke-Command greatly speeds up the Get-WMIObject command. I did it on Services, using the command alone took 9 seconds, using the Invoke-Command took 1 second. Test:
$firstStart = Get-Date
$service1 = Get-WmiObject win32_service -ComputerName $ServerName
$firstEnd = Get-Date
$firstTime = New-TimeSpan -Start $firstStart -End $firstEnd
Write-Host "The first collection completed in $($firstTime.TotalSeconds) seconds." -ForegroundColor Green
$secondStart = Get-Date
$service2 = Invoke-Command -ComputerName $ServerName -ScriptBlock {Get-WmiObject win32_service}
$secondEnd = Get-Date
$secondTime = New-TimeSpan -Start $secondStart -End $secondEnd
Write-Host "The second collection completed in $($secondTime.TotalSeconds) seconds." -ForegroundColor Green

Check the AppPool status through power-shell and start that on remote machine

The requirement is to extract the server name one by one and check the AppPool status, if that is found to be stopped, make it running. below code is not helping out.
$ErrorActionPreference = "Continue"
$status = gc -path "D:\Servers\server.txt"|ForEach-Object (invoke-command -ComputerName $_ -ScriptBlock {Import-Module Webadministration Get-WebAppPoolState -name (gc "D:\AppPool.txt")})
if ($status.value -eq "Started")
{
Write-Host ("ApppPool already running")
}
else
{
Invoke-Command -ScriptBlock {Start-WebAppPool}
Write-host ("AppPool has started successfully")
}
There were multiple problems with your code, I've gone through them individually so you can see what was stopping it from working correctly.
The syntax for foreach was wrong, you needed to use {} not () in this case. Normal brackets are only used like this ForEach ($number in $numArray ) {CODE} which you aren't.
You were checking $status outside the foreach loop - so it so was evaluating $status only once (with the final computers AppPool status) rather than for each computer.
Your second Invoke-Command didn't have a ComputerName parameter specified so was only running the command locally not against the remote computer, meaning the AppPool would never be started.
As you were specifying the AppPool name using gc "D:\AppPool.txt" this file would have to be present on every remote computer for it to work. I've changed this to be passed into the command as an argument so the file only needs to be on the computer running the script.
$Credentials = Get-Credential
$AppPools = Get-Content "D:\AppPool.txt"
$Servers = Get-Content -Path "D:\Servers\server.txt"
ForEach ($Server in $Servers) {
ForEach ($AppPool in $AppPools) {
$AppPoolState = Invoke-Command -ComputerName $Server -ScriptBlock {Import-Module WebAdministration; Get-WebAppPoolState -Name $args[0] } -ArgumentList $AppPool -Credential $Credentials
if ($AppPoolState.Value -eq "Started")
{
Write-Host "$AppPool AppPool already running on $Server"
}
else
{
Invoke-Command -ComputerName $Server -ScriptBlock {Start-WebAppPool -Name $args[0] } -ArgumentList $AppPool -Credential $Credentials
Write-Host "$AppPool AppPool started on $Server"
}
}
}
Note: I run a non-privileged account so have to supply Credentials. If the account you're running the script as has appropriate permissions to all the remote computers you can remove the three Credentials references.

Using Powershell to stop a service remotely without WMI or remoting

I came across this one liner that appears to work:
stop-service -inputobject $(get-service -ComputerName remotePC -Name Spooler)
Can anyone explain why, because I thought stop-service didn't work unless you either used remoting or it occurred on the local host.
The output of Get-Service is a System.ServiceProcess.ServiceController .NET class that can operate on remote computers. How it accomplishes that, I don't know - probably DCOM or WMI. Once you've gotten one of these from Get-Service, it can be passed into Stop-Service which most likely just calls the Stop() method on this object. That stops the service on the remote machine. In fact, you could probably do this as well:
(get-service -ComputerName remotePC -Name Spooler).Stop()
Thanks to everyone's contributions to this question, I've come up with the following script. Change the values for $SvcName and $SvrName to suit your needs. This script will start the remote service if it is stopped, or stop it if it is started. And it uses the cool .WaitForStatus method to wait while the service responds.
#Change this values to suit your needs:
$SvcName = 'Spooler'
$SvrName = 'remotePC'
#Initialize variables:
[string]$WaitForIt = ""
[string]$Verb = ""
[string]$Result = "FAILED"
$svc = (get-service -computername $SvrName -name $SvcName)
Write-host "$SvcName on $SvrName is $($svc.status)"
Switch ($svc.status) {
'Stopped' {
Write-host "Starting $SvcName..."
$Verb = "start"
$WaitForIt = 'Running'
$svc.Start()}
'Running' {
Write-host "Stopping $SvcName..."
$Verb = "stop"
$WaitForIt = 'Stopped'
$svc.Stop()}
Default {
Write-host "$SvcName is $($svc.status). Taking no action."}
}
if ($WaitForIt -ne "") {
Try { # For some reason, we cannot use -ErrorAction after the next statement:
$svc.WaitForStatus($WaitForIt,'00:02:00')
} Catch {
Write-host "After waiting for 2 minutes, $SvcName failed to $Verb."
}
$svc = (get-service -computername $SvrName -name $SvcName)
if ($svc.status -eq $WaitForIt) {$Result = 'SUCCESS'}
Write-host "$Result`: $SvcName on $SvrName is $($svc.status)"
}
Of course, the account you run this under will need the proper privileges to access the remote computer and start and stop services. And when executing this against older remote machines, you might first have to install WinRM 3.0 on the older machine.
Based on the built-in Powershell examples, this is what Microsoft suggests. Tested and verified:
To stop:
(Get-WmiObject Win32_Service -filter "name='IPEventWatcher'" -ComputerName Server01).StopService()
To start:
(Get-WmiObject Win32_Service -filter "name='IPEventWatcher'" -ComputerName Server01).StartService()
This worked for me, but I used it as start. powershell outputs,
waiting for service to finshing starting a few times then finishes and then a get-service on the remote server shows the service started.
**start**-service -inputobject $(get-service -ComputerName remotePC -Name Spooler)
Another option; use invoke-command:
cls
$cred = Get-Credential
$server = 'MyRemoteComputer'
$service = 'My Service Name'
invoke-command -Credential $cred -ComputerName $server -ScriptBlock {
param(
[Parameter(Mandatory=$True,Position=0)]
[string]$service
)
stop-service $service
} -ArgumentList $service
NB: to use this option you'll need PowerShell to be installed on the remote machine and for the firewall to allow requests through, and for the Windows Remote Management service to be running on the target machine. You can configure the firewall by running the following script directly on the target machine (one off task): Enable-PSRemoting -force.
You can also do (Get-Service -Name "what ever" - ComputerName RemoteHost).Status = "Stopped"
You could just run a foreach and have logging enabled.
The console will show if something goes wrong and you can look in the log.
That way, you can then handle the errors individually.
I think it works better this way than running a Test-Netconnection for the verification part because firewall rules can create the value false.
For this example you ned a csv file with column ServerName, Populate the column with servername.contoso.com
$ServerList = "$PSScriptRoot\Serverlist.csv"
$Transcriptlog = "$PSScriptRoot\Transcipt.txt"
Start-Transcript -Path $Transcriptlog -Force
Get-Date -Format "yyyy/MM/dd HH:mm"
Try
{ # Start Try
$ImportServerList = Import-Csv $ServerList -Encoding UTF8 | ForEach-Object { # Start Foreach
New-Object PsObject -Prop #{ # Start New-Object
ServerName = $_.ServerName } # End NewObject
Invoke-Command -ComputerName $_.ServerName -ErrorAction Continue -ScriptBlock { # Start ScriptBlock
# Disable Service PrintSpooler
Get-Service -Name Spooler | Stop-Service -Force
} # End ScriptBlock
} # End Foreach
} # End Try
Catch
{ # Start Catch
Write-Warning -Message "## ERROR## "
Write-Warning -Message "## Script could not start ## "
Write-Warning $Error[0]
} # End Catch
Stop-Transcript
stop-service -inputobject $(get-service -ComputerName remotePC -Name Spooler)
This fails because of your variables
-ComputerName remotePC needs to be a variable $remotePC or a string "remotePC"
-Name Spooler(same thing for spooler)
As far as I know, and I cant verify it now, you cannot stop remote services with the Stop-Service cmdlet or with .Net, it is not supported.
Yes it works, but it stopes the service on your local machine, not on the remote computer.
Now, if the above is correct, without remoting or wmi enabled, you could set a scheduled job on the remote system, using AT, that runs Stop-Service locally.