Reputation: 95
I created the following script to reset the password of the local admin account on all machines in a specific host file. This script functions properly and provides a useful output, but it is slow as it only does one machine at a time.
# function to convert a secure string to a standard string
function ConvertTo-String {
param(
[System.Security.SecureString] $secureString
)
$marshal = [System.Runtime.InteropServices.Marshal]
try {
$intPtr = $marshal::SecureStringToBSTR($secureString)
$string = $marshal::PtrToStringAuto($intPtr)
}
finally {
if($intPtr) {
$marshal::ZeroFreeBSTR($intPtr)
}
}
$string
}
$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"
# prompt for password and confirm
do {
$ss1 = Read-Host "Enter new password" -AsSecureString
$ss2 = Read-Host "Enter again to confirm" -AsSecureString
# compare strings - proceed if same - prompt again if different
$ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
Write-Host "Passwords match"
if(-not $ok) {
Write-Host "Passwords do not match"
}
}
until($ok)
# set password variable to string value
$adminPassword = ConvertTo-String $ss1
# setup job to reset password on each client
foreach($client in $clients) {
$status = "OFFLINE"
$isOnline = "OFFLINE"
if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
$isOnline = "ONLINE"
}
# change the password
try {
$localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
$localAdminAccount.SetPassword($adminPassword)
$localAdminAccount.SetInfo()
Write-Verbose "Password change completed successfully"
}
catch {
$status = "FAILED"
Write-Verbose "Failed to change password"
}
# create psobject with system info
$obj = New-Object -TypeName PSObject -Property @{
ComputerName = $client
Online = $isOnline
ChangeStatus = $status
}
$obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append
if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
$stream.writeline("$client -t $status")
}
}
$adminPassword = " "
Write-Verbose "Complete"
Invoke-Item C:\test.txt
To make the script run faster, I set it up to use background jobs so it could run on multiple clients at once. However, now I get no output in my text files. The new script is below. Lines 43 and 79-89 are the changes. What changes do I need to make to this script provide the output it does in the first version? I've tried a number of other ways to get the output than what I currently have on line 89.
# function to convert a secure string to a standard string
function ConvertTo-String {
param(
[System.Security.SecureString] $secureString
)
$marshal = [System.Runtime.InteropServices.Marshal]
try {
$intPtr = $marshal::SecureStringToBSTR($secureString)
$string = $marshal::PtrToStringAuto($intPtr)
}
finally {
if($intPtr) {
$marshal::ZeroFreeBSTR($intPtr)
}
}
$string
}
$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"
# prompt for password and confirm
do {
$ss1 = Read-Host "Enter new password" -AsSecureString
$ss2 = Read-Host "Enter again to confirm" -AsSecureString
# compare strings - proceed if same - prompt again if different
$ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
Write-Host "Passwords match"
if(-not $ok) {
Write-Host "Passwords do not match"
}
}
until($ok)
# set password variable to string value
$adminPassword = ConvertTo-String $ss1
# setup job to reset password on each client
#-----------------------------------------------------------------------------------------
$scriptBlock = {
#-----------------------------------------------------------------------------------------
foreach($client in $clients) {
$status = "OFFLINE"
$isOnline = "OFFLINE"
if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
$isOnline = "ONLINE"
}
# change the password
try {
$localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
$localAdminAccount.SetPassword($adminPassword)
$localAdminAccount.SetInfo()
Write-Verbose "Password change completed successfully"
}
catch {
$status = "FAILED"
Write-Verbose "Failed to change password"
}
# create psobject with system info
$obj = New-Object -TypeName PSObject -Property @{
ComputerName = $client
Online = $isOnline
ChangeStatus = $status
}
$obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append
if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
$stream.writeline("$client -t $status")
}
}
}
#-----------------------------------------------------------------------------------------
Get-Job | Remove-Job -Force
Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset
Get-Job
While(Get-Job -State "Running") {
Start-Sleep -m 10
}
Receive-Job -name AdminPWReset | Out-File C:\test2.txt
#-----------------------------------------------------------------------------------------
$adminPassword = " "
Write-Verbose "Complete"
Invoke-Item C:\test.txt
Invoke-Item C:\test2.txt
Upvotes: 4
Views: 1954
Reputation: 200293
You don't receive output, because you moved your entire loop into the scriptblock:
$scriptBlock = {
foreach ($client in $clients) {
...
}
}
but initialized your client list ($clients
) outside the scriptblock, so that the variable $clients
inside the scriptblock is a different (empty) variable since it's in a different scope. Because of that your job is iterating over an empty list, and is thus not producing any output.
To be able to use the client list inside the scriptblock you'd have to use the using:
scope modifier:
$scriptBlock = {
foreach ($client in $using:clients) {
...
}
}
or pass the client list into the scriptblock as an argument:
$scriptBlock = {
foreach ($client in $args[0]) {
...
}
}
Start-Job -ScriptBlock $scriptBlock -ArgumentList $clients
As I can see from your code you're trying to do the latter:
Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset
However, that doesn't work for two reasons:
Start-Job
there is no current object, so $_
is empty and nothing is passed into the scriptblock.Param()
block, nor does it use the automatic variable $args
, so even if you passed an argument into the scriptblock it would never be used.With that said, you don't want to pass $clients
in the first place, because even if it worked, it wouldn't speed up anything as the entire client list would still be processed sequentially. What you actually want to do is to process the list in parallel. For that you must put just the tests into the scriptblock and then start one job for each client in a loop:
$scriptBlock = {
Param($client)
$status = "OFFLINE"
$isOnline = "OFFLINE"
if (Test-Connection -Computer $client -Quiet -Count 1 -Delay 1) {
$isOnline = "ONLINE"
}
...
}
foreach ($client in $clients) {
Start-Job -Name AdminPWReset -ScriptBlock $scriptBlock -ArgumentList $client
}
while (Get-Job -State "Running") {
Start-Sleep -Milliseconds 100
}
Receive-Job -Name AdminPWReset | Out-File C:\test2.txt
Remove-Job -Name AdminPWReset
Note that if you have a great number of target hosts you may want to use a job queue, so that you don't have hundreds of jobs running in parallel.
Upvotes: 3