jayteezer
jayteezer

Reputation: 115

Variable not resetting during loop creating misleading output

I have a script here that reads a list of desktops and changes the administrator password of those desktops. It works but I have a minor problem. Once the error shows up in Powershell, the value is never reset. So for example the second DT failed and the third and fourth DT succeeds. Under the error column, it'll show the second DT's error even though the password change was successful. However, it works fine on the output file (outputs.txt). I tried $error.clear() but it didn't solve my problem. Maybe I put it in the wrong place? I'd appreciate if you can help me with this as I am 99.9% done. Thank you.

[cmdletbinding()]
param (
[parameter(mandatory = $true)]
$InputFile,
$OutputDirectory
)

#Log failures in an output file called "outputs.txt" stored in the directory of input file
if(!$outputdirectory) {
    $outputdirectory = (Get-Item $InputFile).directoryname
}   
$failedcomputers    =   Join-Path $outputdirectory "outputs.txt"
$stream = [System.IO.StreamWriter] $failedcomputers
$stream.writeline("ComputerName `t IsOnline `t PasswordChangeStatus `t Error")
$stream.writeline("____________ `t ________ `t ____________________ `t _____")

#Prompt to confirm password twice and read password in a secure manner
$password = Read-Host "Enter the password" -AsSecureString
$confirmpassword = Read-Host "Confirm the password" -AsSecureString

#Convert password into plain text for comparison
$pwd1_text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
$pwd2_text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($confirmpassword))

#If the passwords don't match, script exits; otherwise, it continues.
if($pwd1_text -ne $pwd2_text) {
    Write-Error "Entered passwords are not same. Script is exiting."
    exit
}

#Check if input file exists. If file not found, script exits.
if(!(Test-Path $InputFile)) {
    Write-Error "File ($InputFile) not found. Script is exiting."
    exit
}

#Read contents of input file using Get-Content cmdlet and store list in an array called $computers
$Computers = Get-Content -Path $InputFile

#Loop through each computer account in the array and check if online or not using Test-Connection cmdlet
foreach ($Computer in $Computers) {
    $Computer   =   $Computer.toupper()
    $Isonline   =   "OFFLINE"
    $Status     =   "SUCCESS"
    Write-Verbose "Working on $Computer"
    if((Test-Connection -ComputerName $Computer -count 1 -ErrorAction 0)) {
        $Isonline = "ONLINE"
        Write-Verbose "`t$Computer is Online"
    } else { Write-Verbose "`t$Computer is OFFLINE" }

    #If ping is successful, password is changed
    try {
        $account = [ADSI]("WinNT://$Computer/Administrator,user")
        $account.psbase.invoke("setpassword",$pwd1_text)
        Write-Verbose "`tPassword Change completed successfully"
    }

    #If password change fails, respective error is recorded
    catch {
        $status = "FAILED"
        Write-Verbose "`tFailed to Change the administrator password. Error: $_"
        $e = $_.Exception
        $errmsg = $e.Message
        while ($e.InnerException) {
            $e = $e.InnerException
            $errmsg = $e.Message
        }
    }

    $obj = New-Object -TypeName PSObject -Property @{
        ComputerName = $Computer
        IsOnline = $Isonline
        PasswordChangeStatus = $Status
        Error = $errmsg
    }

    $obj | Select ComputerName, IsOnline, PasswordChangeStatus, Error

    if($Status -eq "FAILED" -or $Isonline -eq "OFFLINE") {
        $stream.writeline("$Computer `t`t $isonline `t`t $status `t $errmsg")
    }else{
        $stream.writeline("$Computer `t`t $isonline `t`t $status")
    }

}
$stream.close()
Write-Host "`n`nFailed computers list is saved to $failedcomputers"

Upvotes: 1

Views: 3716

Answers (2)

TheMadTechnician
TheMadTechnician

Reputation: 36287

I fully support Matt's answer! Either of those two options are simple solutions to your problem. What I offer is more of a re-write with some definite changes to how some things are done. I dropped the stream writer in favor or collecting results, and dumping them to a file at the end with Set-Content (why lock open a file for the whole process and write little bits to it when you can just do it all at once at the end?). Also, I create your computer object at the beginning of each loop, and skip the $IsOnline, $Status, and $errmsg variables all together. Then for each loop I add that object to an array (created before the loop), and at the end I output that.

[cmdletbinding()]
param (
[parameter(mandatory = $true)]
$InputFile,
$OutputDirectory
)

#Log failures in an output file called "failed-computers.txt" stored in the directory of input file
if(!$outputdirectory) {
    $outputdirectory = (Get-Item $InputFile).directoryname
}   
$failedcomputers    =   Join-Path $outputdirectory "failed-computers.txt"

#Prompt to confirm password twice and read password in a secure manner
$password = Read-Host "Enter the password" -AsSecureString
$confirmpassword = Read-Host "Confirm the password" -AsSecureString

#Convert password into plain text for comparison
$pwd1_text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
$pwd2_text = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($confirmpassword))

#If the passwords don't match, script exits; otherwise, it continues.
if($pwd1_text -ne $pwd2_text) {
    Write-Error "Entered passwords are not same. Script is exiting."
    exit
}

#Check if input file exists. If file not found, script exits.
if(!(Test-Path $InputFile)) {
    Write-Error "File ($InputFile) not found. Script is exiting."
    exit
}

#Read contents of input file using Get-Content cmdlet and store list in an array called $computers
$Computers = Get-Content -Path $InputFile

#Create an empty array to populate with the results of the password updates
$Results = @()

#Loop through each computer account in the array and check if online or not using Test-Connection cmdlet
foreach ($ComputerName in $Computers) {
    #Create Computer object
    $Computer   =   New-Object PSObject -Prop @{
        'Name'     =   $ComputerName.toupper()
        'Isonline' =   "OFFLINE"
        'Status'   =   "SUCCESS"
        'Error'    =   ""
    }
    Write-Verbose "Working on $ComputerName"
    if((Test-Connection -ComputerName $Computer.Name -count 1 -ErrorAction 0)) {
        $Computer.Isonline = "ONLINE"
        Write-Verbose "`t$ComputerName is Online"
    } else { Write-Verbose "`t$ComputerName is OFFLINE" }

    #If ping is successful, password is changed
    try {
        $account = [ADSI]("WinNT://$ComputerName/Administrator,user")
        $account.psbase.invoke("setpassword",$pwd1_text)
        Write-Verbose "`tPassword Change completed successfully"
    }

    #If password change fails, respective error is recorded
    catch {
        $Computer.status = "FAILED"
        Write-Verbose "`tFailed to Change the administrator password. Error: $_"
        $e = $_.Exception
        $Computer.Error = $e.Message
        while ($e.InnerException) {
            $e = $e.InnerException
            $Computer.Error = $e.Message
        }
    }
    #Display resutls to screen
    $Computer

    #Add computer's results/errors to results array
    $Results += $Computer

}

#Write results to file
$Results|Format-Table -AutoSize|Out-String|Set-Content $failedcomputers
$Results|Export-Csv ($failedcomputers -replace "txt$","csv") -NoTypeInformation
Write-Host "`n`nFailed computers list is saved to $failedcomputers, with CSV export of the same name and location"

Oh yeah, I also output the whole thing as a CSV so that it can be easily opened in Excel in case you need to work with it later (filter for systems that failed to update, were offline, or whatever).

Upvotes: 2

Matt
Matt

Reputation: 46700

From what I can tell from your tiny picture is that this output line is causing your grief:

$obj | Select ComputerName, IsOnline, PasswordChangeStatus, Error

Specifically the Error property as there is not always an error state. In short you need to address the content of the variable $errmsg which is what populates the above property. On each pass of the loop you should reset or clear the variable. Couple of options there with this train of thought.

  1. Set the variable to an empty string at the beginning of each loop pass

    foreach ($Computer in $Computers) {
        $Computer   =   $Computer.toupper()
        $Isonline   =   "OFFLINE"
        $Status     =   "SUCCESS"
        $errmsg     =   ""
    .... more code stuff...
    
  2. You can also use the cmdlet Clear-Variable in a similar location before the try block or after the output line. Note the $ is omitted on purpose.

    Clear-Variable errmsg
    

Upvotes: 4

Related Questions