Powershell runspace unexpected output (misunderstanding?) - powershell

$ScriptBlock = {
Param (
$Vm,
$vc,
$session)
$VCenter = Connect-VIServer -Server $vc -session $session -force
$status = Get-VM $Vm
Write-host $status.Name
if ($status.PowerState -eq "PoweredOff")
{
$Clone = '_2.2'
$NewVM = $Vm + $Clone
Write-host $NewVM.Name
}
}
$Results = #()
$Throttle = 5 #threads
$sesssionstate = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
$sesssionstate.ImportPSModule("Vmware.VimAutomation.Core")
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $Throttle, $sesssionstate, $host)
$RunspacePool.Open()
$Jobs = #()
$Vms = gc .\vmlist.txt
Foreach ($Vm in $Vms)
{
#Start-Sleep -Minutes 1
$Job = [powershell]::Create().AddScript($ScriptBlock).AddArgument($Vm).AddArgument($con.Name).AddArgument($con.SessionId)
$Job.RunspacePool = $RunspacePool
$Jobs += New-Object PSObject -Property #{
Job = $Job
Result = $Job.BeginInvoke()
}
}
while ($Jobs -Contains $false) {}
foreach($Job in $jobs)
{
$results += $Job.Job.EndInvoke($Job.Result)
$Job.Job.Dispose()
}
$results
Input File:
INTCLWK8
INTCLWK82
INTCLWK83
Output:
INTCLWK8
INTCLWK8_2.2
INTCLWK82
INTCLWK82_2.2
INTCLWK83
INTCLWK83_2.2
2nd run:
INTCLWK8
INTCLWK8_2.2
INTCLWK82_2.2
INTCLWK83
INTCLWK82
INTCLWK83_2.2
Why is the output different at run? This caused confusion to write multithread.
The output should be always like the below.
INTCLWK8
INTCLWK8_2.2
INTCLWK82
INTCLWK82_2.2
INTCLWK83
INTCLWK83_2.2

Related

get local admin users with password age

I am working on once assignment where want to get a list of local Windows admin users with X password age. Got below function for local admin users and other one for age. Please help me integrate these.
I have below command can work with users list to fetch details from specific groups and hostnames.
Get-Content -Path "D:\Groups.txt" | ForEach-Object {
Get-GroupMember -ComputerName (Get-Content -Path "D:\servers.txt") -LocalGroup $_
} | Export-Csv -Path D:\Getgroupmembers_$(Get-Date -Format ddMMyyyy).csv -NoTypeInformation
List of users:
function Get-GroupMember {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[Alias('Group')]
[string]$LocalGroup,
[Alias('CN','Computer')]
[string[]]$ComputerName = '.'
)
foreach ($Computer in $ComputerName) {
Write-Verbose "Checking membership of localgroup: '$LocalGroup' on $Computer"
try {
([adsi]"WinNT://$Computer/$LocalGroup,group").psbase.Invoke('Members') | ForEach-Object {
New-Object -TypeName PSCustomObject -Property #{
ComputerName = $Computer
LocalGroup = $LocalGroup
Member = $_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null)
}
}
Write-Verbose "Successfully checked membership of localgroup: '$LocalGroup' on $Computer"
} catch {
Write-Warning $_
}
}
}
TO check Password age we can use below code and we need to integrate these two using one command:
function Get-PwdAge {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false,
Position=1,
ValueFromPipeline=$false,
ValueFromPipelineByPropertyName=$false)]
[String]$Usr,
[Switch]$All
)
$filter = "(&(objectCategory=person)(objectClass=user)(name=$Usr))"
if ($All) {
$filter = '(&(objectCategory=person)(objectClass=user))'
}
$root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$searcher = New-Object System.DirectoryServices.DirectorySearcher $filter
$SearchRoot = $root.defaultNamingContext
$searcher.SearchRoot = "LDAP://CN=Users,$SearchRoot"
$searcher.SearchScope = 'SubTree'
$searcher.SizeLimit = 0
$searcher.PageSize = 1000
$searcher.FindAll() | ForEach-Object {
$account = $_.GetDirectoryEntry()
$pwdset = [DateTime]::FromFileTime($_.Properties.Item("pwdLastSet")[0])
$age = (New-TimeSpan $pwdset).Days
$info = 1 | Select-Object Name, Login, AgeInDays, LastSet
$info.Name = $account.DisplayName[0]
$info.Login = $account.SamAccountName[0]
$info.AgeInDays = $age
$info.LastSet = $pwdset
$info
}
}
Param
(
[Parameter(Position=0,Mandatory=$false)]
[ValidateNotNullorEmpty()]
[Alias('cn')][String[]]$ComputerName=$Env:COMPUTERNAME,
[Parameter(Position=1,Mandatory=$false)]
[Alias('un')][String[]]$AccountName,
[Parameter(Position=2,Mandatory=$false)]
[Alias('cred')][System.Management.Automation.PsCredential]$Credential
)
$Obj = #()
$now = Get-Date
Foreach($Computer in $ComputerName)
{
If($Credential)
{
$AllLocalAccounts = Get-WmiObject -Class Win32_UserAccount -Namespace "root\cimv2" `
-Filter "LocalAccount='$True'" -ComputerName $Computer -Credential $Credential -ErrorAction Stop
}
else
{
$AllLocalAccounts = Get-WmiObject -Class Win32_UserAccount -Namespace "root\cimv2" `
-Filter "LocalAccount='$True'" -ComputerName $Computer -ErrorAction Stop
}
$Obj = $AllLocalAccounts | ForEach-Object {
$user = ([adsi]"WinNT://$computer/$($_.Name),user")
$pwAge = $user.PasswordAge.Value
$maxPwAge = $user.MaxPasswordAge.Value
$pwLastSet = $now.AddSeconds(-$pwAge)
New-Object -TypeName PSObject -Property #{
'Account Name' = $_.Name
'Disabled' = $_.Disabled
'Password Expires' = $_.PasswordExpires
'Password Last Set' = $pwLastSet
'Password Expiry Date' = $now.AddSeconds($maxPwAge - $pwAge)
'Password Required' = $_.PasswordRequired
'Domain' = $_.Domain
'Password Age' = ($now - $pwLastSet).Days
}
}
If($AccountName)
{
Foreach($Account in $AccountName)
{
$Obj|Where-Object{$_.Name -like "$Account"}
}
}
else
{
$Obj
}
}

Making variables visible inside Powershell workflow

I have five variables defined before a workflow that need to be available to the workflow, but I can't find out how to do it.
Putting the variables inside the workflow makes them visible, but that causes an issue with the CSV import that means extra properties are added to the object relating to the workflow that I don't want.
Code as follows:
$source = 'C:\Users\Koozer\a place\'
$rotateParams = 90, 90, 270
$cropParams = #(64, 64), (32, 0)
$images = Import-Csv "${source}images.csv"
$colNames = $images[0].psobject.properties.Name
Workflow StitchCropWorkflow {
foreach -parallel ($imageSet in $images) {
$magickRotParams = ''
$n = 0
foreach ($image in $colNames) {
$magickRotParams += '`( '''+$source+$imageSet.($image)+''' -rotate '+$rotateParams[$n]+' `) '
$n++
}
$finfo = [io.fileinfo]$imagePathSets[0]
$command = 'magick '+$magickRotParams+' +append -crop '+$cropParams[0][0]+'x'+$cropParams[0][1]+'+'+$cropParams[1][0]+'+'+$cropParams[1][1]+' +repage '''+$finfo.DirectoryName+'\'+$finfo.BaseName+'_stitch_crop'+$finfo.Extension+''''
echo $command
Invoke-Expression $command
}
}
StitchCropWorkflow
You can pass parameters to a workflow like you would do it for a function:
$source = 'C:\Users\Koozer\a place\'
$rotateParams = 90, 90, 270
$cropParams = #(64, 64), (32, 0)
$images = Import-Csv "${source}images.csv"
$colNames = $images[0].psobject.properties.Name
Workflow StitchCropWorkflow {
param (
$source,
$rotateParams,
$cropParams,
$images,
$colNames
)
# your code here
}
StitchCropWorkflow $source $rotateParams $cropParams $images $colNames
If you define a variable in a script containing a workflow, to access the variable within the workflow the syntax is $using:variable
As an example, here is a powershell script containing a workflow with a single param.
param([string]$ComputerName)
$VerbosePreference = "Continue"
workflow Start-Reboot {
param($server)
Write-Verbose "Input server is $server"
InlineScript{
New-Item -Path C:\Scripts\Logging\Start-Reboot-Single.log -ItemType File -ErrorAction SilentlyContinue | Out-Null
}
Sequence{
InlineScript
{
Try
{
Get-WmiObject -ComputerName $using:server -Class Win32_Service -Filter "Name='HealthService'" | Invoke-WmiMethod -Name PauseService | Out-Null
$operatingSystem = Get-WmiObject Win32_OperatingSystem -ComputerName $using:server -ErrorAction stop
$LastReboot = [Management.ManagementDateTimeConverter]::ToDateTime($operatingSystem.LastBootUpTime).ToString().Trim()
Write-Verbose "Last reboot time for $using:server is $LastReboot"
Write-Verbose "Here we restart $using:server, for testing no reboot is done"
Restart-Computer -ComputerName $using:server -Wait -For Wmi -Force
$OSRecheck = Get-WmiObject Win32_OperatingSystem -ComputerName $using:server -ErrorAction stop
$CurrentReboot = [Management.ManagementDateTimeConverter]::ToDateTime($OSRecheck.LastBootUpTime).ToString().Trim()
$props = [Ordered]#{
Server=$using:server
LastReboot=$LastReboot
CurrentReboot=$CurrentReboot
}
} Catch {
Write-Verbose "Oh no, problem with $using:server"
$rnd = Get-Random -Minimum 1 -Maximum 5
Start-Sleep -Seconds $rnd
$err = $_.Exception.GetType().FullName
$props = [Ordered]#{
Server=$using:server
LastReboot=$err
CurrentReboot=$null
}
} Finally {
$object = New-Object -TypeName PSObject -Property $props
$random = Get-Random -Minimum 2 -Maximum 15
Start-Sleep -Seconds $random
Write-Output $object | Out-File -Append -FilePath C:\Scripts\Logging\Start-Reboot-Single.log
}
}#inline end
}#sequence end
}#end workflow block
Start-Reboot -server $ComputerName

Creating workflow for parallel scheduled server reboots with logging

I'm currently using the following code to schedule a server reboot. This works pretty well for a handful of servers but becomes a problem when there are many servers (over 80) because Register-ScheduledJob takes a long time per server.
$user = Get-Credential -UserName $env:USERNAME -Message "UserName/password for scheduled Reboot"
$trigger = New-JobTrigger -once -at $date
$script = [ScriptBlock]::Create("D:\Scripts\Scheduled-Reboot-Single.ps1 -server $server")
Register-ScheduledJob -Name $server -Credential $user -Trigger $trigger -ScriptBlock $script
My research pointed to using workflow and foreach -parallel.
The problem I run into is accurate logging. My log file is created but the columns are not ordered correctly.
workflow Do-ScheduledReboot{
Param([string[]]$servers)
foreach -parallel($server in $servers) {
InlineScript {
try {
$LastReboot = Get-EventLog -ComputerName $using:server -LogName system |
Where-Object {$_.EventID -eq '6005'} |
Select -ExpandProperty TimeGenerated |
select -first 1
#New loop with counter, exit script if server did not reboot.
$max = 20; $i = 0
do {
if ($i -gt $max) {
$hash = #{
"Server" = $using:server
"Status" = "FailedToReboot!"
"LastRebootTime" = "$LastReboot"
"CurrentRebootTime" = "FailedToReboot!"
}
$newRow = New-Object PsObject -Property $hash
$rnd = Get-Random -Minimum 5 -Maximum 40
Start-Sleep -Seconds $rnd
Export-Csv D:\workflow-results.csv -InputObject $newrow -Append -Force
exit
}#exit script and log failed to reboot.
$i++
Start-Sleep -Seconds 15
} while (Test-path "\\$using:server\c$")
$max = 20; $i = 0
do {
if ($i -gt $max) {
$hash = #{
"Server" = $using:server
"Status" = "FailedToComeOnline!"
"LastRebootTime" = "$LastReboot"
"CurrentRebootTime" = "FailedToReboot!"
}
$newRow = New-Object PsObject -Property $hash
$rnd = Get-Random -Minimum 5 -Maximum 40
Start-Sleep -Seconds $rnd
Export-Csv D:\workflow-results.csv -InputObject $newrow -Append -Force
exit
}#exit script and log failed to come online.
$i++
Start-Sleep -Seconds 15
} while (-not(Test-path "\\$using:server\c$"))
$CurrentReboot = Get-EventLog -ComputerName $using:server -LogName system | Where-Object {$_.EventID -eq '6005'} | Select -ExpandProperty TimeGenerated | select -first 1
$hash = #{
"Server" = $using:server
"Status" = "RebootSuccessful"
"LastRebootTime" = $LastReboot
"CurrentRebootTime" = "$CurrentReboot"
}
$newRow = New-Object PsObject -Property $hash
$rnd = Get-Random -Minimum 5 -Maximum 40
Start-Sleep -Seconds $rnd
Export-Csv D:\workflow-results.csv -InputObject $newrow -Append -Force
} catch {
$errMsg = $_.Exception
"Failed with $errMsg"
}#end catch
}#end inline script
}#end foreach parallel
}#end workflow
$mylist = gc D:\Servers.txt
Do-ScheduledReboot -servers $mylist
Create ordered hashtables:
$hash = [ordered]#{
'Server' = $using:server
'Status' = ...
"LastRebootTime" = ...
'CurrentRebootTime' = ...
}

Number of table count is wrong in dataset

I am supplying 3 servers to loop however the $mdtable.table.count is only 1. I must be missing a simple thing here. Can anyone please help me resolve this?
Get-Content 'C:\test\computers.txt' | ? { $_.Trim() -ne "" } | ForEach-Object {
$value = Invoke-Command -Computer $_ -ScriptBlock {
Param($computer)
# Connect to SQL and query data, extract data to SQL Adapter
$SqlQuery = "xp_fixeddrives"
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = "Data Source=$computer;Initial Catalog='Secaudit';Integrated Security = True";
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter($SqlQuery, $Sqlconnection)
$mdtable = New-Object System.Data.Dataset
$nRecs = $SqlAdapter.Fill($mdtable) | Out-Null
$nRecs | Out-Null
$res = $mdtable.Tables.Count
$res
} -ArgumentList $_ -Credential $cred
}
$value
The thing you're missing is that
... | ForEach-object {
$value = Invoke-Command -Computer $_ -ScriptBlock {
...
} -ArgumentList $_ -Credential $cred
}
replaces the value of $value with each iteration when you actually want to accumulate the values.
You can achieve this for instance like this:
... | ForEach-object {
$value += Invoke-Command -Computer $_ -ScriptBlock {
...
} -ArgumentList $_ -Credential $cred
}
or like this:
$value = ... | ForEach-object {
Invoke-Command -Computer $_ -ScriptBlock {
...
} -ArgumentList $_ -Credential $cred
} | Measure-Object -Sum | Select-Object -Expand Sum

Add new values to PSObject - Is this the best approach?

I have the following code and it works. But I was wondering if this was the best approach as it repeats the structure over and over again.
$Computers = Get-Content -Path C:\scripts\Computers.txt
$AllComputers = #()
foreach ($machine in $Computers) {
$objCompSys = Get-WmIObject -class "Win32_ComputerSystem" -cn $machine -EA silentlyContinue -namespace "root\CIMV2"
foreach ($item in $objCompSys) {
$compname = $item.Name
$physicalRAM = [Math]::Ceiling($item.TotalPhysicalMemory / 1MB)
$numProcs = $item.NumberOfProcessors
}
$objProc = Get-WMIObject -class "Win32_Processor" -cn $machine -EA silentlyContinue -namespace "root\CIMV2"
foreach ($item in $objProc) {
$procType = $item.Name
$procSpeed = $item.MaxClockSpeed
}
if (procSpeed -eq "486") {
$Data1 = #{
MachineName = $compname
CPUSpeed = $procSpeed
"Num CPUs" = $numProcs
"Physical RAM" = $physicalRAM
"Summary" = "486 process - check"
New-Object PSObject -Property $data1
}
$AllComputers += $Data1
} elseif (procSpeed -eq "586") {
$Data2 = #{
MachineName = $compname
CPUSpeed = $procSpeed
"Num CPUs" = $numProcs
"Physical RAM" = $physicalRAM
"Summary" = "586 process - check"
New-Object PSObject -Property $data2
}
$AllComputers += $Data2
} else (procSpeed -eq "6*") {
$Data3 = #{
MachineName = $compname
CPUSpeed = $procSpeed
"Num CPUs" = $numProcs
"Physical RAM" = $physicalRAM
"Summary" = "New Models - Good to go"
New-Object PSObject -Property $data3
}
$AllComputers += $Data3
}
}
$AllComputers | Out-GridView
Yes. Set up your "Data" table once, and then change just the "Summary" field if necessary:
$Computers = Get-Content -Path C:\scripts\Computers.txt
$AllComputers = #()
foreach($machine in $Computers)
{
$objCompSys = Get-WmIObject -class "Win32_ComputerSystem" -cn $machine -EA silentlyContinue -namespace "root\CIMV2"
foreach($item in $objCompSys)
{
$compname = $item.Name
$physicalRAM = [Math]::Ceiling($item.TotalPhysicalMemory / 1MB)
$numProcs = $item.NumberOfProcessors
}
$objProc = Get-WMIObject -class "Win32_Processor" -cn $machine -EA silentlyContinue -namespace "root\CIMV2"
foreach($item in $objProc)
{
$procType = $item.Name
$procSpeed = $item.MaxClockSpeed
}
$Data = #{
MachineName = $compname
CPUSpeed = $procSpeed
"Num CPUs" = $numProcs
"Physical RAM" = $physicalRAM
}
$Data["Summary"] = switch($procSpeed)
{
"486" { "486 process - check" }
"586" { "586 process - check" }
default { "New Models - Good to go" }
}
$AllComputers += New-Object psobject -Property $Data
}
$AllComputers | Out-GridView
The switch above is comparable to this if/else construct:
if($procSpeed -eq "486") {
"486 process - check"
} elseif($procSpeed -eq "486") {
"586 process - check"
} else {
"New Models - Good to go"
}