I have used powershell for 4 months now and learned some basic stuff e.g. how to loop csv, create functions, export etc. However I am trying to get more complex with it and started building a Leavers script for the company I am currently working for. So far so good I managed to compile a working script where I automatically: import an initial CSV containing usernames then loop them and create folder on the target dest., next comes the mailbox export (where I need help actually and the purpose of this post) and all fine with that but the problem is it cycles everyone from the CSV and pushes the Exchange to export all at once. I don't like that because it overutilize the Server and I saw larger mailboxes failed so I decided to try and build a Queue where Mailboxes are getting exported one by one.
This is what I am using currently where it pushes everyone at once:
$dest = "%\Desktop\#SCRIPTS\Modules\Exports\Temp\Target.csv"
$targetStorage = "\\SERVER\Targetfolders"
$Connection = "http://Exchange-Server.companydomain.com/PowerShell/"
Set-ExecutionPolicy RemoteSigned
$UserCredential = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri $Connection -Authentication Kerberos -Credential $UserCredential
Import-PSSession $Session
#########################################################################
$mails = Import-Csv $dest
ForEach ($mail in $mails)
{
New-MailboxExportRequest -Mailbox "$($mail.Name)#CompanyDomain.com" -FilePath "$($targetStorage)\$($mail.Name)\$($mail.Name).pst" | Out-Null
}
And this is something I came up with yesterday:
$dest = "%\Desktop\#SCRIPTS\Modules\Exports\Temp\Target.csv"
$targetStorage = "\\SERVER\Targetfolders"
$Connection = "http://Exchange-Server.companydomain.com/PowerShell/"
$mails = Import-Csv $dest
ForEach ($mail in $mails) {
Get-MailboxExportRequest -Mailbox $mail.Name
if (Get-MailboxExportRequest -Status InProgress) {
Write-Host "Mailboxes are still exporting, do not cancel the script!!"
}
else { Get-MailboxExportRequestStatistics -Identity $mail.Name | **Some-Cmdlet** -Like 'PercentComplete' -eq '100' | New-MailboxExportRequest -Mailbox $mail.Name -FilePath "$($synStorage)\$($mail.Name)\$($mail.Name).pst"}
}
On this script I added "somecmdlet" -Like 'PercentComplete' -eq '100', where the 100 represents the percentage of the export from Get-MailboxExportRequestStatistics, so when the previous export has reached 100% execute the next export from the CSV.
Any thoughts which cmdlet I could use to "tag" the PercentComplete guys ?
Name StatusDetail SourceAlias PercentComplete
---- ------------ ----------- ---------------
MailboxExport Completed Dummy.User 100
Sorry for the long post but I tried to make it as clear as possible :)
Thanks!
==========================
Edit as per Xiakit's Advice:==========================
$mails = Import-Csv $dest
ForEach ($mail in $mails) {
Get-MailboxExportRequest -Mailbox $mail.Name
if (Get-MailboxExportRequest -Status InProgress) {
Write-Host "Mailboxes are still exporting, do not cancel the script!!"
}
else {
New-MailboxExportRequest -Mailbox $mail.Name -FilePath "$($synStorage)\$($mail.Name)\$($mail.Name).pst"
while($requeststatus.status -notlike "Completed"){
Start-Sleep -seconds 10
$RequestStatus = Get-MailboxExportRequest -Identity $mail.Name
}
}
}
However the script ends and never loops :( the output:
Mailboxes are still exporting, do not cancel the script!!
Name Mailbox Status
---- ------- ------
MailboxExport ommited/Enterprise/Users/Vlatko Completed
MailboxExport1 ommited/Enterprise/Users/Vlatko Completed
MailboxExport2 ommited/Enterprise/Users/Vlatko InProgress
MailboxExport omitted/Enterprise/Users/Dummy User Queued
MailboxExport omitted/Enterprise/Users/Dummy User Queued
MailboxExport omitted/Enterprise/Users/Dummy User Queued
Mailboxes are still exporting, do not cancel the script!!
There is also a Get-MailboxExportRequest, you can check with it if a request is completed:
while($requeststatus.status -notlike "Completed"){
Start-Sleep -seconds 10
$RequestStatus = Get-MailboxImportRequest -Identity "whatever"
}
This will loop as long as your request is not completed.
To cut the saga at the end I asked for TechNet for help as well since of course they are Microsoft and they should know the answer to every Powershell problem :) Bigh thanks to Xiakit for the effort to resolve my problem (on which I am still keen on working that out since I have started it and I want to finish it) however here is the solution in queuing Mailbox Exports:
$mails = Import-Csv $dest
ForEach ($mail in $mails) {
New-MailboxExportRequest -Mailbox $mail.Name -FilePath "$($synStorage)\$($mail.Name)\$($mail.Name).pst"
do {
Write-Host "Queued"
Start-Sleep -s 5
} while(Get-MailboxExportRequest -Mailbox $mail.Name -Status Queued)
do {
Write-Host "InProgress"
Start-Sleep -s 5
} while(Get-MailboxExportRequest -Mailbox $mail.Name -Status InProgress)
If(Get-MailboxExportRequest -Mailbox $mail.Name -Status Complete){
Write-Host "A export request complete"
Get-MailboxExportRequest | Remove-MailboxExportRequest -Confirm:$false
}
If(Get-MailboxExportRequest -Mailbox $mail.Name -Status Failed){
Write-Host "A error occur"
}
}
Hope this will come in handy to other Admins trying to automate leaver processes in their company for multiple users!!
Thanks for the support!
Vlatko
Related
I need to run parallel Search-Mailbox cmdlets against 100's mailboxes to delete the content but they need to fit certain parameters first like certain CAS protocols enabled and a forwarding address present. I've also parameterised it so I can pass a $maxJobCount int to it so the runner can specify a maximum number of concurrently running jobs to allow so as to account for resources on their machine.
Got the thing working then got to the start-job component which is a pretty simple function.
function _StartJob {
param (
$mailAddress
)
Start-Job -Name $mailAddress -Scriptblock {
Get-EXOMailbox $mailAddress -PropertySets Delivery
}
}
That's returning an error saying I need to run Connect-ExchangeOnline before using the cmdlets which is where I learned script blocks in Start-Job are actually new PowerShell.exe processes so don't inherit modules and session options.
Does anyone know an easier way around this? In an MFA environment, it either means sitting there and pasting the password in a few hundred times or convincing the Change board and Secops dept to let me setup a graph application with delete rights... both painful
Thanks for any advice
You just have to pass in the creds into the block however you want.
$kvCertName = 'Cert'
#I am using azure automation here to get the cert its different for keyvault
$kvCertPFX = Get-AutomationCertificate -Name $kvCertName
$tenantid = 'yourcompany.onmicrosoft.com'
$appid = '00000000-46da-6666-5555-33333cfe77ec'
$startDate = ([datetime]::Today).AddDays(-7)
#Build the script block
$block = {
Param(
$kvCert,
$appID,
$tenantID,
$n,
$startdate
)
$newCertPFX = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($kvCert)
Connect-ExchangeOnline -Certificate ([System.Security.Cryptography.X509Certificates.X509Certificate2]$newCertPFX) -AppID $appID -Organization $tenantID -ErrorAction Stop
Search-AdminAuditLog -StartDate $startDate.adddays($n) -EndDate $($startDate.AddDays($n) | get-date -Hour 23 -Minute 59 -Second 59) -ExternalAccess:$false -ResultSize 250000
Disconnect-ExchangeOnline -confirm:$false
}
#Remove all jobs created
Get-Job | Remove-Job
#Run All the Parrallel Jobs
$num = 0..6
$kvCert = $kvCertPFX.Export(3)
foreach($n in $num){Start-Job -Scriptblock $Block -ArgumentList #($kvCert,$appID,$tenantid,$n,$startdate)}
#Wait for all jobs to finish.
do {start-sleep 1}
until ($(Get-Job -State Running).count -eq 0)
#Get information from each job.
$adminPowerShellAuditLog = $null
foreach($job in Get-Job){$adminPowerShellAuditLog+= Receive-Job -Id ($job.Id)}
Write-Output $adminPowerShellAuditLog
Thought I would share this quick function I made for myself, feel free to adapt it and improve it according to your needs.
Sometimes you want to run commands as the logged on user of a remote computer.
As you know, some commands show output for the user who runs it and if you run the same command with Invoke-Command, it won't return the user's information, but yours). Get-Printer is an example amongst many others.
There is no easy, quick way of running commands as the logged on user natively without any third-party apps like PsExec or others so I made this quick function that uses VBS, PS1 and Scheduled Task to make it happen.
It runs completly silently for the user (thanks to the VBS) and the output is shown in your console. Please note it assumes the remote computer has a C:\TEMP.
Created in a Windows 10, powershell v 5.1.17763.503 environement.
I don't pretend it's final and perfect, it's the simplest way I found to do what is needed and I just wanted to share it with you guys as it can be very useful!
Check the comments for explanation of the code and feel free to use it as you wish. Please share your version as I'm curious to see people improve it. A good idea would be to make it support multiple computers, but as I said it's a quick function I did I don't have too much time to put into refining it.
That being said, I had no problems using it multiple times as is :)
*Output returned is in form of a string, if you want to have a proper object, add '| ConvertFrom-String' and play with it :)
PLEASE NOTE: The surefire way of grabbing the username of who is currently logged on is via QWINSTA (since Win32_ComputerSystem - Username is only reliable if a user is logged on LOCALLY, it won't be right if a user is using RDP/RemoteDesktop). So this is what I used to grab the username, however, please note that in our french environement the name of the username property in QWINSTA is "UTILISATEUR",so you have to change that to your needs (english or other language) for it to work. If I remember correctly, it's "USERNAME" in english.
On this line:
$LoggedOnUser = (qwinsta /SERVER:$ComputerName) -replace '\s{2,22}', ',' | ConvertFrom-Csv | Where-Object {$_ -like "*Acti*"} | Select-Object -ExpandProperty UTILISATEUR
See code in the answer below.
function RunAsUser {
Param ($ComputerName,$Scriptblock)
#Check that computer is reachable
Write-host "Checking that $ComputerName is online..."
if (!(Test-Connection $ComputerName -Count 1 -Quiet)) {
Write-Host "$ComputerName is offline" -ForegroundColor Red
break
}
#Check that PsRemoting works (test Invoke-Command and if it doesn't work, do 'Enable-PsRemoting' via WMI method).
#*You might have the adjust this one to suit your environement.
#Where I work, WMI is always working, so when PsRemoting isn't, I enable it via WMI first.
Write-host "Checking that PsRemoting is enabled on $ComputerName"
if (!(invoke-command $ComputerName { "test" } -ErrorAction SilentlyContinue)) {
Invoke-WmiMethod -ComputerName $ComputerName -Path win32_process -Name create -ArgumentList "powershell.exe -command Enable-PSRemoting -SkipNetworkProfileCheck -Force" | Out-Null
do {
Start-Sleep -Milliseconds 200
} until (invoke-command $ComputerName { "test" } -ErrorAction SilentlyContinue)
}
#Check that a user is logged on the computer
Write-host "Checking that a user is logged on to $ComputerName..."
$LoggedOnUser = (qwinsta /SERVER:$ComputerName) -replace '\s{2,22}', ',' | ConvertFrom-Csv | Where-Object {$_ -like "*Acti*"} | Select-Object -ExpandProperty UTILISATEUR
if (!($LoggedOnUser) ) {
Write-Host "No user is logged on to $ComputerName" -ForegroundColor Red
break
}
#Creates a VBS file that will run the scriptblock completly silently (prevents the user from seeing a flashing powershell window)
#"
Dim wshell, PowerShellResult
set wshell = CreateObject("WScript.Shell")
Const WindowStyle = 0
Const WaitOnReturn = True
For Each strArg In WScript.Arguments
arg = arg & " " & strArg
Next 'strArg
PowerShellResult = wshell.run ("PowerShell " & arg & "; exit $LASTEXITCODE", WindowStyle, WaitOnReturn)
WScript.Quit(PowerShellResult)
"# | out-file "\\$ComputerName\C$\TEMP\RAU.vbs" -Encoding ascii -force
#Creates a script file from the specified '-Scriptblock' parameter which will be ran as the logged on user by the scheduled task created below.
#Adds 'Start-Transcript and Stop-Transcript' for logging the output.
$Scriptblock = "Start-Transcript C:\TEMP\RAU.log -force" + $Scriptblock + "Stop-Transcript"
$Scriptblock | out-file "\\$ComputerName\C$\TEMP\RAU.ps1" -Encoding utf8 -force
#On the remote computer, create a scheduled task that runs the .ps1 script silently in the user's context (with the help of the vbs)
Write-host "Running task on $ComputerName..."
Invoke-Command -ComputerName $ComputerName -ArgumentList $LoggedOnUser -ScriptBlock {
param($loggedOnUser)
$SchTaskParameters = #{
TaskName = "RAU"
Description = "-"
Action = (New-ScheduledTaskAction -Execute "wscript.exe" -Argument "C:\temp\RAU.vbs C:\temp\RAU.ps1")
Settings = (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd)
RunLevel = "Highest"
User = $LoggedOnUser
Force = $true
}
#Register and Start the task
Register-ScheduledTask #SchTaskParameters | Out-Null
Start-ScheduledTask -TaskName "RAU"
#Wait until the task finishes before continuing
do {
Write-host "Waiting for task to finish..."
$ScheduledTaskState = Get-ScheduledTask -TaskName "RAU" | Select-Object -ExpandProperty state
start-sleep 1
} until ( $ScheduledTaskState -eq "Ready" )
#Delete the task
Unregister-ScheduledTask -TaskName "RAU" -Confirm:$false
}
Write-host "Task completed on $ComputerName"
#Grab the output of the script from the transcript and remove the header (first 19) and footer (last 5)
$RawOutput = Get-Content "\\$ComputerName\C$\temp\RAU.log" | Select-Object -Skip 19
$FinalOutput = $RawOutput[0..($RawOutput.length-5)]
#Shows output
return $FinalOutput
#Delete the output file and script files
Remove-Item "\\$ComputerName\C$\temp\RAU.log" -force
Remove-Item "\\$ComputerName\C$\temp\RAU.vbs" -force
Remove-Item "\\$ComputerName\C$\temp\RAU.ps1" -force
}
#____________________________________________________
#Example command
#Note: Sometimes Start-Transcript doesn't show the output for a certain command, so if you run into empty output, add: ' | out-host' or '| out-default' at the end of the command not showing output.
$Results = RunAsUser -ComputerName COMP123 -Scriptblock {
get-printer | Select-Object name,drivername,portname | Out-host
}
$Results
#If needed, you can turn the output (which is a string for the moment) to a proper powershell object with ' | ConvertFrom-String'
Been trying to solve this for a bit and can't seem to figure it out.
I have the following script:
$Servers = Get-Content -Path "C:\Utilities_PowerShell\ServerList.txt"
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus # Checks for status of IIS services
{
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3)
{
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
}
else
{
Write-Host " No IIS service(s) were found..." -foreground "red"
}
}
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock ${function:IISServiceStatus} -AsJob -ArgumentList $IISServiceName1, $IISServiceName2, $IISServiceName3, $IISarrService, $IISarrServiceCheck | Wait-Job | Receive-Job
Write-Host " "
}
Whenever I run it, all I get is the output of:
Status of IIS service(s) on *PC* :
If I run the function outside of a loop/invoke-command, the results are absolutely perfect. What is wrong with my remote loop?
I've tried putting the variables inside the function, I've tried running invoke-command without the argument list, etc.
Update: 3/17/16
Turns out...if I run my actual script as is, the result of $EndJobs is weird in that it outputs ALL services in one table and then the three IIS services in another table. This would explain why when I run my invoke-command (stopIIS) scriptblock...I had to reboot the whole server because it took all of the services down.
These functions run PERFECTLY when not run via remote/invoke-command.
What the heck...invoke-command is seriously screwing with my stuff!
Anyone have any ideas/tips on how I can run my local script (which works 100%) on a set of servers from a text file without weird issues like this? Is invoke-command the only way?
do you have the same problem if you wrap it all into the script block like this?
$Servers = Get-Content 'C:\Utilities_PowerShell\ServerList.txt'
$Sessions = New-PSSession -ComputerName $Servers
$EndJobs = $Sessions | ForEach-Object {
Invoke-Command -Session $_ -ScriptBlock {
$IISServiceName1 = 'W3SVC'
$IISServiceName2 = 'IISAdmin'
$IISServiceName3 = 'WAS'
$IISarrService = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3
$IISarrServiceCheck = Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 -ErrorAction SilentlyContinue -ErrorVariable NoService
function IISServiceStatus { # Checks for status of IIS services
param (
$IISServiceName1,
$IISServiceName2,
$IISServiceName3,
$IISarrService,
$IISarrServiceCheck
)
if (Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3) {
Write-Host "Status of IIS service(s) on $env:ComputerName :"
Get-Service -Name $IISServiceName1,$IISServiceName2,$IISServiceName3 | Select Name,DisplayName,Status | Format-Table -AutoSize
} else {
Write-Host ' No IIS service(s) were found...' -ForegroundColor Red
}
}
IISServiceStatus $IISServiceName1 $IISServiceName2 $IISServiceName3 $IISarrService $IISarrServiceCheck
} -AsJob | Wait-Job | Receive-Job
Write-Host ' '
}
$EndJobs
I'm having a similar issue. I'm using credssp to test 2nd hop auth for an automation for shutting down a production environment cleanly. My script has 3 sections; session setup, the invoke, session teardown. If I run each piece separately, I get output. If I run the whole script, I get blank lines matching the amount of output I get when I run them separately... there's nothing fancy in my invoke (backtick line continuation - I prefer Python's formatting paradigm better than Powershell/C#):
Invoke-Command `
-Session $workingSession `
-ScriptBlock {
get-service *spool* -ComputerName server01
}
I have a script that I am setting up to do some migration of users' Exchange mailboxes into .pst file. The idea was that I would have a CSV file that I could put users' names on and then when the script kicked off nightly it would open the CSV file and find users that have been added, perform the requested actions on those users accounts (export, move set permissions etc) and then write back to the CSV file Marking those users as completed and writing the date on which they were completed. Here is what I have so far.
$InPstPath = '\\server1\PST_Store\'
$OutPstPath = '\\server2\PST_Store\'
$User = Get-Content $OutPstPath'login.txt'
$PWord = cat $OutPstPath'pass.txt' | convertto-securestring
$Credentials = New-Object –TypeName System.Management.Automation.PSCredential –ArgumentList $User, $PWord
$PSSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri http://Server1/powershell -Credential $Credentials
Import-PSSession $PSSession
$In_List = Invoke-Command {Import-Csv "\\Server1\PST_Store\Admin\To_Be_Exported.csv"} -computername Server1 -Credential $Credentials
foreach ($objUser in $In_List) {
if ($objUser.Completed -ne "Yes") {
$TargetUser = $objUser.name
$ShortDate = (Get-Date).toshortdatestring()
New-MailboxExportRequest -Mailbox $TargetUser -Filepath "$InPstPath$TargetUser.pst"
$objUser.Completed = "Yes"
$objUser.Date = $ShortDate
}
}
Remove-PSSession -Session (Get-PSSession)
I can't figure out a decent way to write back the $objUser.Completed and $objUser.Date values to the CSV.
Firstly, it's obvious but let me state it anyway. The very first time you run this script, $objUser.name, $objUser.Completed and $objUser.Date will not exist; So, the line
$TargetUser=$objUser.name
will not work, unless you actually have the structure in place in that csv (i.e. have the headers name,completed,date).
Now assuming you got that part done, all you have to do is to create an object that captures the state in an object and then write that back.
$Processed = foreach ($objUser in $In_List) {
if ($objUser.Completed -ne "Yes") {
$TargetUser = $objUser.name
$ShortDate = (Get-Date).toshortdatestring()
New-MailboxExportRequest -Mailbox $TargetUser -Filepath "$InPstPath$TargetUser.pst"
[PSCustomObject]#{Name=$objUser.name;Completed="Yes";Date=$ShortDate}
}
} else {
[PSCustomObject]#{Name=$objUser.name;Completed="No";Date=$null}
}
## export to a temp file
$Processed | export-csv -Path $env:TEMP\processed.csv
## You should probably check to see if original file was modified since you started working on it
# and copy over if not
Copy-Item $env:TEMP\processed.csv $OutPstPath'login.txt' -force
Overview
Looking to call a Powershell script that takes in an argument, runs each job in the background, and shows me the verbose output.
Problem I am running into
The script appears to run, but I want to verify this for sure by streaming the results of the background jobs as they are running.
Code
###StartServerUpdates.ps1 Script###
#get list of servers to update from text file and store in array
$servers=get-content c:\serverstoupdate.txt
#run all jobs, using multi-threading, in background
ForEach($server in $servers){
Start-Job -FilePath c:\cefcu_it\psscripts\PSPatch.ps1 -ArgumentList $server
}
#Wait for all jobs
Get-Job | Wait-Job
#Get all job results
Get-Job | Receive-Job
What I am currently seeing:
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
23 Job23 Running True localhost #patch server ...
25 Job25 Running True localhost #patch server ...
What I want to see:
Searching for approved updates ...
Update Found: Security Update for Windows Server 2003 (KB2807986)
Update Found: Windows Malicious Software Removal Tool - March 2013 (KB890830)
Download complete. Installing updates ...
The system must be rebooted to complete installation.
cscript exited on "myServer" with error code 3.
Reboot required...
Waiting for server to reboot (35)
Searching for approved updates ...
There are no updates to install.
cscript exited on "myServer" with error code 2.
Servername "myServer" is fully patched after 2 loops
I want to be able to see the output or store that somewhere so I can refer back to be sure the script ran and see which servers rebooted, etc.
Conclusion:
In the past, I ran the script and it went through updating the servers one at a time and gave me the output I wanted, but when I started doing more servers - this task took too long, which is why I am trying to use background jobs with "Start-Job".
Can anyone help me figure this out, please?
You may take a look at the module SplitPipeline.
It it specifically designed for such tasks. The working demo code is:
# import the module (not necessary in PS V3)
Import-Module SplitPipeline
# some servers (from 1 to 10 for the test)
$servers = 1..10
# process servers by parallel pipelines and output results immediately
$servers | Split-Pipeline {process{"processing server $_"; sleep 1}} -Load 1, 1
For your task replace "processing server $_"; sleep 1 (simulates a slow job) with a call to your script and use the variable $_ as input, the current server.
If each job is not processor intensive then increase the parameter Count (the default is processor count) in order to improve performance.
Not a new question but I feel it is missing an answer including Powershell using workflows and its parallel possibilities, from powershell version 3. Which is less code and maybe more understandable than starting and waiting for jobs, which of course works good as well.
I have two files: TheScript.ps1 which coordinates the servers and BackgroundJob.ps1 which does some kind of check. They need to be in the same directory.
The Write-Output in the background job file writes to the same stream you see when starting TheScript.ps1.
TheScript.ps1:
workflow parallelCheckServer {
param ($Servers)
foreach -parallel($Server in $Servers)
{
Invoke-Expression -Command ".\BackgroundJob.ps1 -Server $Server"
}
}
parallelCheckServer -Servers #("host1.com", "host2.com", "host3.com")
Write-Output "Done with all servers."
BackgroundJob.ps1 (for example):
param (
[Parameter(Mandatory=$true)] [string] $server
)
Write-Host "[$server]`t Processing server $server"
Start-Sleep -Seconds 5
So when starting the TheScript.ps1 it will write "Processing server" 3 times but it will not wait for 15 seconds but instead 5 because they are run in parallel.
[host3.com] Processing server host3.com
[host2.com] Processing server host2.com
[host1.com] Processing server host1.com
Done with all servers.
In your ForEach loop you'll want to grab the output generated by the Jobs already running.
Example Not Tested
$sb = {
"Starting Job on $($args[0])"
#Do something
"$($args[0]) => Do something completed successfully"
"$($args[0]) => Now for something completely different"
"Ending Job on $($args[0])"
}
Foreach($computer in $computers){
Start-Job -ScriptBlock $sb -Args $computer | Out-Null
Get-Job | Receive-Job
}
Now if you do this all your results will be mixed. You might want to put a stamp on your verbose output to tell which output came from.
Or
Foreach($computer in $computers){
Start-Job -ScriptBlock $sb -Args $computer | Out-Null
Get-Job | ? {$_.State -eq 'Complete' -and $_.HasMoreData} | % {Receive-Job $_}
}
while((Get-Job -State Running).count){
Get-Job | ? {$_.State -eq 'Complete' -and $_.HasMoreData} | % {Receive-Job $_}
start-sleep -seconds 1
}
It will show all the output as soon as a job is finished. Without being mixed up.
If you're wanting to multiple jobs in-progress, you'll probably want to massage the output to help keep what output goes with which job straight on the console.
$BGList = 'Black','Green','DarkBlue','DarkCyan','Red','DarkGreen'
$JobHash = #{};$ColorHash = #{};$i=0
ForEach($server in $servers)
{
Start-Job -FilePath c:\cefcu_it\psscripts\PSPatch.ps1 -ArgumentList $server |
foreach {
$ColorHash[$_.ID] = $BGList[$i++]
$JobHash[$_.ID] = $Server
}
}
While ((Get-Job).State -match 'Running')
{
foreach ($Job in Get-Job | where {$_.HasMoreData})
{
[System.Console]::BackgroundColor = $ColorHash[$Job.ID]
Write-Host $JobHash[$Job.ID] -ForegroundColor Black -BackgroundColor White
Receive-Job $Job
}
Start-Sleep -Seconds 5
}
[System.Console]::BackgroundColor = 'Black'
You can get the results by doing something like this after all the jobs have been received:
$array=#()
Get-Job -Name * | where{$array+=$_.ChildJobs.output}
.ChildJobs.output will have anything that was returned in each job.
function OutputJoblogs {
[CmdletBinding(DefaultParameterSetName='Name')]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[System.Management.Automation.Job] $job,
[Parameter(Mandatory=$true, Position=1)]
[string] $logFolder,
[Parameter(Mandatory=$true, Position=2)]
[string] $logTimeStamp
)
#Output All logs
while ($job.sate -eq "Running" -or $job.HasMoreData){
start-sleep -Seconds 1
foreach($remotejob in $job.ChildJobs){
if($remotejob.HasMoreData){
$output=(Receive-Job $remotejob)
if($output -gt 0){
$remotejob.location +": "+ (($output) | Tee-Object -Append -file ("$logFolder\$logTimeStamp."+$remotejob.Location+".txt"))
}
}
}
}
#Output Errors
foreach($remotejob in $job.ChildJobs){
if($remotejob.Error.Count -gt0){$remotejob.location +": "}
foreach($myerr in $remotejob.Error){
$myerr 2>&1 | Tee-Object -Append -file ("$logFolder\$logTimeStamp."+$remotejob.Location+".ERROR.txt")
}
if($remotejob.JobStateInfo.Reason.ErrorRecord.Count -gt 0){$remotejob.location +": "}
foreach($myerr in $remotejob.JobStateInfo.Reason.ErrorRecord){
$myerr 2>&1 | Tee-Object -Append -file ("$logFolder\$logTimeStamp."+$remotejob.Location+".ERROR.txt")
}
}
}
#example of usage
$logfileDate="$((Get-Date).ToString('yyyy-MM-dd-HH.mm.ss'))"
$job = Invoke-Command -ComputerName "servername1","servername2" -ScriptBlock {
for ($i=1; $i -le 5; $i++) {
$i+"`n";
if($i -gt 2){
write-error "Bad thing happened"};
if($i -eq 4){
throw "Super Bad thing happened"
};
start-sleep -Seconds 1
}
} -asjob
OutputJoblogs -Job $job -logFolder "$PSScriptRoot\logs" -logTimeStamp $logfileDate