powershell -parallel not working - powershell

I am writing a PowerShell script to display the status of free space and CPU, below is snippet:
function Get_FreeSpace ($authType, $comp) {
$freespace = Invoke-Command -ScriptBlock {
Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='E:'" |
Select-Object Size,Freespace
} -ComputerName $comp -Credential $authType
$freesize = [String]::Format("{0:P2}" -f ($freespace.Freespace / $freespace.Size))
return $freesize
}
function Get_CPUAverage ($authType, $comp) {
$Cpu_Average = Invoke-Command -ScriptBlock {
Get-WmiObject Win32_Processor |
Measure-Object -Property LoadPercentage -Average |
Select-Object Average
} -ComputerName $comp -Credential $authType
return $Cpu_Average.Average
}
workflow Test-Workflow {
foreach -parallel ($serv1 in $Servers) {
$Size = Get_FreeSpace -authType $cred -comp $serv1
$Cpu_Avg = Get_CPUAverage -authType $cred -comp $serv1
Write-Host $Size
}
}
Test-Workflow
Write-Host $Size
$Size is not returning anything if I use -parallel loop.

should pass $Servers as param as shown below:
workflow Test-Workflow {
param($Servers)
foreach -parallel ($serv1 in $Servers) {
$Size = Get_FreeSpace -authType $cred -comp $serv1
$Cpu_Avg = Get_CPUAverage -authType $cred -comp $serv1
Write-Host $Size
}
}
Test-Workflow $Servers

Alternatively, invoke-command runs in parallel with multiple computers:
invoke-command comp1,comp2,comp3 { sleep 5; "done $env:computername" }

Related

Invoke-Command inside PROCESS not returning anything

I'm trying to use Invoke-Command inside an advanced function to return an object with the workspaces.
This works:
$g = Invoke-Command -ComputerName server1,server2,server3 -ScriptBlock {
$AgentCfg = New-Object -ComObject AgentConfigManager.MgmtSvcCfg
$AgentCfg.GetCloudWorkspaces()
}
$g| Measure-Object
This does not, Measure-Object says there are 0 objects.
function Get-WorkspaceInfo {
[CmdletBinding()]
param(
[string[]]$ComputerName,
[string]$ErrorLog
)
BEGIN {}
PROCESS {
foreach ($Computer in $ComputerName) {
write-verbose "$Computer Iteration"
$RemoteWorkspaces = Invoke-Command -ComputerName $Computer -ScriptBlock {
$AgentCfg = New-Object -ComObject AgentConfigManager.MgmtSvcCfg
$OMSWorkspaces = $AgentCfg.GetCloudWorkspaces() }
$RemoteWorkspaces | Measure-Object
}
}
END {}
}
Get-Workspaceinfo -ErrorLog x.txt -ComputerName server1,server2,server3 -Verbose

Error on running Pester test using foreach-object -parallel

I have this infrastructure pester test. Code for demonstration purpose:
Describe 'WEB-Tests' {
$servers = 'ServerA','ServerB'
$sessions = #()
foreach ($server in $servers) {
$sessions += New-PSSession -ComputerName $server
}
$sessions | foreach-object {
Context " Service is Running on $($_.ComputerName)" {
$service = invoke-command -session $_ -scriptblock { get-service 'some service' }
It "Service $($service.Name) should be Running" {
$service.Status | Should be "Running"
}
}
}
}
This works fine. If I replace the $sessions|foreach-object with $sessions|foreach-object -Parallel, i get this error -
New-PesterState: C:\Program Files\WindowsPowerShell\Modules\Pester\4.9.0\Functions\Context.ps1:77:128
Line |
77 | … '] .) -TestNameFilter $null -TagFilter #() -SessionState SessionState
| ~~~~~~~~~~~~
| Cannot process argument transformation on parameter 'SessionState'. Cannot convert the "SessionState" value of type "System.String" to type
| "System.Management.Automation.SessionState".
Exception: The Context command may only be used from a Pester test script.
Please suggest on how to achieve parallelism here as there are hundreds of servers.
RESOLVED-I figured out a workaround by using jobs and it serves the purpose.
Describe 'WEB-Tests' {
$servers = 'ServerA','ServerB'
$sessions = #()
foreach ($server in $servers) {
$sessions += New-PSSession -ComputerName $server
}
Get-Job | Remove-Job
invoke-command -session $sessions -scriptblock { get-service W3SVC,WAS } -AsJob
$j = Get-Job | wait-job
($results = $j | Receive-Job) | out-null
Context "IIS Services are Running" {
foreach ($result in $results) {
It "Service $($result.Name) should be Running on $($result.PSComputerName)" {
$result.Status | Should be "Running"
}
}
}
}

Options to use different param

I'm looking for a way to to have a choice of a list or a single computername in a foreach loop.
If the user enters in a single computername I want the script to execute for that one computername
but if that user wants to use a path to a list of computers how could I replace $computername with the path that user wants?
function Get-OSInfo {
[CmdletBinding()]
param (
#[Parameter(ValueFromPipeline=$True,
# ValueFromPipelineByPropertyName=$True)]
[string]$computername,
[string]$errorlog = 'c:\errors.txt',
[switch]$logerrors
)
PROCESS {
foreach ($computer in $computername) {
Try {
$os = Get-WmiObject -EA Stop –Class Win32_OperatingSystem –ComputerName $computer
$cs = Get-WmiObject -EA Stop –Class Win32_ComputerSystem –ComputerName $computer
$bios = Get-WmiObject -EA Stop –Class Win32_BIOS –ComputerName $computer
$cpu = Get-WmiObject -EA Stop -class Win32_processor -ComputerName $computer
$props = #{'ComputerName'=$computer;
'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber;
'OSArchitecture'=$os.osarchitecture;
'Manufacturer'=$cs.manufacturer;
'Model'=$cs.model;
'BIOSSerial'=$bios.serialnumber
'CPU Count'=$CPU.Count
'Memory'= [Math]::round(($cs.TotalPhysicalMemory/1gb),2)
'CPU Speed'= $CPU.MaxClockSpeed[0]}
$obj = New-Object -TypeName PSOBject -Property $props
$obj.PSObject.TypeNames.Insert(0,'Get-OS.OSInfo')
#Write-Output $obj
$obj | Export-Csv c:\test4.csv -Append
} Catch {
if ($logerrors) {
$computer | Out-File $errorlog -append
}
Write-Warning "$computer failed"
}
}
}
}
Change the type of the $ComputerName parameter to a string array instead of just a single string:
param(
[string[]]$ComputerName,
[string]$errorlog = 'c:\errors.txt',
[switch]$logerrors
)
Notice the [] after the type name, this denotes an array of strings, rather than a single string.
Now you can do:
PS C:\> $computers = Get-Content C:\computers.txt
PS C:\> Get-OSInfo -ComputerName $computers
If you'd like to be able to specify a path to a file containing the target computers as the argument to the function, you can use multiple parameter sets:
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='ByName',ValueFromPipeline)]
[string[]]$ComputerName,
[Parameter(ParameterSetName='ByFile')]
[string]$InputFile
)
begin {
if($PSCmdlet.ParameterSetName -eq 'ByFile'){
try{
$ComputerName = Get-Content -LiteralPath $InputFile
}
catch{
throw
return
}
}
}
process {
foreach($Computer in $ComputerName){
# Work with $Computer here...
}
}

Passing a variable inside s nested loop

I'm tying to work out how I pass a variable from an if statement into a nested if statement starting at:
If ($server -eq $env:COMPUTERNAME)
Foreach ($comp in $computer) {
$server = split-FQDN $comp -part H
$domain = Split-FQDN $comp -part D
#Set the domain based parameters
If ($domain -eq '1.fqdn.com') {
set-domparams $domain $userprompt $userpass
}
Elseif ($domain -eq '2.fqdn.com') {
set-domparams $domain $userprompt $userpass
}
Elseif ($domain -eq '3.fqdn.com') {
set-domparams $domain $userprompt $userpass
}
ElseIf ($domain -eq '4.fqdn.com') {
set-domparams $domain $userprompt $userpass
}
ElseIf ($domain -eq '5.fqdn.com') {
set-domparams $domain $userprompt $userpass
}
ElseIf ($domain -eq '6.fqdn.com') {
set-domparams $domain $userprompt $userpass
}
If ($server -eq $env:COMPUTERNAME) {
$server = $_
#clear Kerberos ticket cache
Invoke-Expression -Command:'cmd.exe /c klist purge' | Out-Null
$buildlogsuccess = check-buildlog $domain
$chocologstatus = Invoke-Command -ScriptBlock {check-choco}
$KMSvalues = Invoke-Command -ScriptBlock {get-winlicense}
$parentOU = get-ParentOU (Get-ADComputer -Server $dc -SearchBase $searchbase -Filter {name -eq $server} -Credential $fetchCreds) | select -expand parentou
$SCCMcheck = Invoke-Command -ScriptBlock {get-sccmstatus}
$scomcheck = Invoke-Command -ScriptBlock {get-scomstatus} -argumentlist $scom
$AV = Invoke-Command -ScriptBlock {get-avstatus}
$wfirewall = Invoke-Command -ScriptBlock {(get-service MpsSvc).status}
$net35 = Invoke-Command -ScriptBlock {(Get-WindowsFeature NET-Framework-Core).installed}
$admins = Invoke-Command -ScriptBlock {check-admins}
$DomainComms = Invoke-Command -ScriptBlock {get-domaininfo}
$bigfix = Invoke-Command -ScriptBlock {get-bigfix}
}
Else {
$server = $_
#clear Kerberos ticket cache
Invoke-Expression -Command:'cmd.exe /c klist purge' | Out-Null
#start running functions against target server(s) to get required info
$PSSession = New-PSSession -ComputerName $comp -Credential $fetchCreds -Name $server
$buildlogsuccess = check-buildlog $domain
$chocologstatus = Invoke-Command -Session $pssession -ScriptBlock ${function:check-choco}
$KMSvalues = Invoke-Command -Session $PSSession -ScriptBlock ${Function:get-winlicense}
$parentOU = get-ParentOU (Get-ADComputer -Server $dc -SearchBase $searchbase -Filter {name -eq $server} -Credential $fetchCreds) | select -expand parentou
$SCCMcheck = Invoke-Command -Session $PSSession -ScriptBlock ${Function:get-sccmstatus}
$scomcheck = Invoke-Command -Session $PSSession -ScriptBlock ${function:get-scomstatus} -argumentlist $scom
$AV = Invoke-Command -Session $PSSession -ScriptBlock ${Function:get-avstatus}
$wfirewall = Invoke-Command -Session $PSSession -ScriptBlock {(get-service MpsSvc).status}
$net35 = Invoke-Command -Session $PSSession -ScriptBlock {(Get-WindowsFeature NET-Framework-Core).installed}
$admins = Invoke-Command -Session $PSSession -ScriptBlock ${Function:check-admins}
$DomainComms = Invoke-Command -Session $PSSession -ScriptBlock ${Function:get-domaininfo}
$bigfix = Invoke-Command -Session $PSSession -ScriptBlock ${Function:get-bigfix}
#clean up the remote session
Remove-PSSession -Name $server
}
I already have some global variables in use that are passed by a loaded function, but I'm trying to avoid do the same for this bit of code and understand how to pass my remaining variables ($comp, $computer and $domain) correctly to the next inner loop.
I've tried getting $server to work doing the following but haven't had any success as of yet:
$server = $_
This is more code review opinion instead of an answer, but anyway.
Instead of long if...elseif constructs, why not use table driven programming? If there are lots of domains and they require different credentials, maintaining the if...elseif is tedious. Store the credentials in custom Powershell objects and those in a hashtable. Then lookup can be done with the domain name only. Like so,
# Declare a hashtable and add custom credential objects
$creds=#{}
$creds.Add('foo.fqdn', $(New-Object –TypeName PSObject –Prop #{'Domain'='foo.fqdn'; 'User'='foo.user'; 'Pass'='foo.pass'}))
$creds.Add('bar.fqdn', $(New-Object –TypeName PSObject –Prop #{'Domain'='bar.fqdn'; 'User'='bar.user'; 'Pass'='bar.pass'}))
$creds.Add('zof.fqdn', $(New-Object –TypeName PSObject –Prop #{'Domain'='zof.fqdn'; 'User'='zof.user'; 'Pass'='zof.pass'}))
# Later when credentials are needed, check if the domain is present
# and get the values from the hashtable.
if($creds.ContainsKey($domain) {
set-domparams $domain $creds[$domain].User $creds[$domain].Pass
} else {
write-warning "Domain $domain not found!"
}
I think this is a non-question as your variables are valid within the loops/statements they are created.
Foreach ($comp in $computer) {
$server = split-FQDN $comp -part H
$domain = Split-FQDN $comp -part D
if($true){
# $comp, $server and $domain are all in scope
Write-host "$comp $server $domain"
if($true){
# also true in nested if statements
Write-host "$comp $server $domain"
}
}
Else{
# $comp, $server and $domain are all in scope
Write-host "$comp $server $domain"
}
}
# outside, $comp is not available, only $computer
# $server and $domain will only contain the values from the last run of the foreach loop.

How do I add multi-threading?

Is there a way of getting the below to run in parallel (multi-threading)? I have about 200 servers that need to run and was wondering if there is a way of checking say 10 servers at once rather then one at a time...WMI is very slow in checking this one at a time.
clear
Write-Host "Script to Check if Server is Alive and Simple WMI Check"
$servers = Get-Content -Path c:\Temp\Servers.txt
foreach($server in $servers)
{
if (Test-Connection -ComputerName $server -Quiet)
{
$wmi = (Get-WmiObject -Class Win32_ComputerSystem -ComputerName $server).Name
Write-Host "$server responds: WMI reports the name is: $wmi"
}
else
{
Write-Host "***$server ERROR - Not responding***"
}
}
Use powershell jobs:
$scriptblock = {
Param($server)
IF (Test-Connection $server -Quiet){
$wmi = (gwmi win32_computersystem -ComputerName $server).Name
Write-Host "***$server responds: WMI reports the name is: $wmi"
} ELSE { Write-Host "***$server ERROR -Not responding***" }
}
$servers | % {Start-Job -Scriptblock $scriptblock -ArgumentList $_ | Out-Null}
Get-Job | Wait-Job | Receive-Job