I've been banging my head against the wall for awhile on this one. No amount of Googling has yielded me any successful results thus far. Wondering if someone can give me a hand?
# Load the PowerShell module for Active Directory
Import-Module ActiveDirectory
# For each computer in AD that is a member of the target group
Get-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" | ForEach {
# Define the new name as the old one, minus one letter at the end
$NewComputerName = $_.Name -replace ".$"
# Perform the rename operation
Rename-Computer -ComputerName $_.Name -NewName $NewComputerName -Force -PassThru -Restart -WhatIf
# Remove the computer from the target group
# THIS SHOULD NOT OCCUR IF THE RENAME ABOVE FAILED!!!
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" -Members $NewComputerName -WhatIf
}
TL;DR: This script finds a bunch of computers in a designated group, renames them, and then removes them from the group. I need help telling the script to NOT remove them from the group in the event that the rename fails (machine is offline, etc.) or throws some error.
Thoughts?
Thanks in advance!
There are a few ways you can check the result of a cmdlet in Powershell.
Run the cmdlet as an if statement condition
You can use the returned object of a cmdlet as an if statement condition, as long as it returns an object on success (which is why we need -PassThru for Rename-Computer, as normally it doesn't return an object). So to use your code as an example (and removing the -WhatIf):
# If Rename-Computer succeeds...
if( Rename-Computer -ComputerName $_.Name `
-NewName $NewComputerName -Force -PassThru -Restart ) {
# then remove the Computer from group
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" `
-Members $NewComputerName
}
This works because objects are truthy in Powershell. Most defined objects and any number other than 0 (zero) are considered $True, but values like $null, "" (an empty string), and 0 (zero) are evaluated as $False. So in the case of your code, if it succeeds an object will be returned ($True), if an error occurs there will be no object returned ($False).
Explicitly check cmdlet result
Alternatively, you could run Rename-Computer first, then check its success:
Rename-Computer -ComputerName $_.Name -NewName $NewComputerName -Force
# Check success of last cmdlet
if( $? ) {
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" `
-Members $NewComputerName
}
$? evaluates the success of the last cmdlet run (don't confuse this with $LASTEXITCODE, which is for checking the last run program result, not cmdlet result). If the cmdlet succeeded, it returns $True, if not, $False.
Catch Errors in a Try/Catch Block
A third way to do it would be to use a try/catch block, though you have to make sure that any error is terminating (will stop the script if uncaught) to catch an exception. You can use the -ErrorAction Stop paremeter for this:
try {
Rename-Computer -ComputerName $_.Name -NewName $NewComputerName -Force `
-PassThru -Restart -ErrorAction Stop
Remove-ADGroupMember -Identity "CN=RenameComputer,CN=Users,DC=int,DC=example,DC=com" `
-Members $NewComputerName
} catch {
# Do something on failure
Write-Warning "An error occurred: $($_.Exception.Message)"
}
If Rename-Computer throws an error within the try block, which it will on failure, the execution jumps to the catch block and will skip over Remove-ADGroupMember. If using a try/catch is your preferred method of error handling, you may consider setting $ErrorActionPreference = 'Stop' in your scripts, which sets the default -ErrorAction behavior to Stop, and you don't have to specify it on every cmdlet.
Additional Information
Here are some official docs about try/catch/finally and if statements in Powershell. I did not cover finally above but this is an optional part of try/catch statements:
About If: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_if?view=powershell-6
About Try/Catch/Finally: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_try_catch_finally?view=powershell-6
Here is also some advanced information about exceptions, errors, and handling them in Powershell: https://kevinmarquette.github.io/2017-04-10-Powershell-exceptions-everything-you-ever-wanted-to-know/
Related
I am hoping for a bit of help on an issue I'm having where once I add -ErrorAction SilentlyContinue to my command, no errors are written to the $Error variable. When I remove the ErrorAction, the command works perfectly fine and I can see the errors in the $Error variable but the errors are also printed to the console, which I don't want. Once I add the ErrorAction, the command still works but does not write any errors to the $Error variable.
Below is the function that contains the command.
function removebutton{
Try{
Write-Host "Please Wait..."
Import-CSV $WPFfile_path.Text | foreach{Remove-DistributionGroupMember -Identity $WPFlist_email.Text -Member $_.email -Confirm:$false -ErrorAction SilentlyContinue}
Write-Host("Completed Processing List")
[System.Windows.MessageBox]::Show("Completed Processing List")
If ($error -ne $null)
{
Write-Host -ForegroundColor Red ($Error | Select-Object Exception -ExpandProperty Exception | Out-String)
[System.Windows.MessageBox]::Show(($Error | Select-Object Exception -ExpandProperty Exception | Out-String))
}
Else
{
}
Get-DistributiongroupMember $WPFlist_email.Text | Select-Object DisplayName, PrimarySMTPAddress | Out-GridView
}
Catch{
[System.Windows.MessageBox]::Show(($Error[0]))
}
}
Any help will be greatly appreciated!
Kind regards,
Dust
When I remove the ErrorAction, the command works perfectly fine and I can see the errors in the $Error variable but the errors are also printed to the console, which I don't want. Once I add the ErrorAction, the command still works but does not write any errors to the $Error variable.
Note:
The following describes PowerShell's normal behavior.
As it turns out, an apparent bug in Remove-DistributionGroupMember prevents non-terminating errors from being recorded in $Error with -ErrorAction SilentlyContinue present, which - for reasons unknown - can be bypassed by using 2>$null to silence error output instead.
-ErrorAction SilentlyContinue continue does record non-terminating errors in the automatic $Error variable (it is only -ErrorAction Ignore that doesn't).
Do not use If ($Error -ne $null) to test if an error occurred in the most recent statement, given that $Error contains all errors that have occurred in the session so far.
As an aside: To test a value for $null, place it on the LHS of -eq / -ne, given that these operators act as filters with collections (arrays) as the LHS - see the docs.
Instead, use the automatic $? variable: if (-not $?)
Alternatively, you could run $Error.Clear() before the statement of interest, which would then allow you to use if ($Error.Count -gt 0), but that comes at the expense of wiping out the session-level history of errors.
The best option may be to use the common -ErrorVariable parameter, which allows you to collect a given call's non-terminating errors in a self-chosen variable (which must be specified without $; e.g., -ErrorVariable errs in order to collect errors in $errs); note that -ErrorAction SilentlyContinue (or redirection 2>$null) is still also needed to prevent error output.
However, non-terminating errors - which is what the common -ErrorAction parameter exclusively acts on - do not trigger the catch block of try ... catch ... finally statements - only terminating errors do.
You can promote non-terminating errors to terminating errors by using -ErrorAction Stop, causing them to trigger the catch block too.
Note that, in a catch script block, you can more simply refer to the triggering error via the automatic $_ variable; that is, instead of $Error[0], you can use $_.
For a comprehensive overview of PowerShell's bewilderingly complex error handling, see GitHub docs issue #1583.
Amazingly, replacing -ErrorAction SilentlyContinue with 2>$null resolved my issue. The errors are no longer written to console and only written to the $Error variable.
Thank you #mklement0 for your help! Much appreciated! I have bought you a coffee to say thanks!
This question already has answers here:
PowerShell output is crossing between functions
(1 answer)
weird delay of the output of an object when followed by start-sleep (or until script end)
(3 answers)
Closed 2 years ago.
I wrote a powershell cmdlet that works great on Server 2012 R2 (powershell 4.0), however on Windows 10 or Server 2016 (powershell 5.1) the commands do not appear to wait for each other to finish, but rather execute asynchronously (?). This is certainly not desired behavior and is causing the cmdlet to not function as intended.
The core of the script starts a transcript, runs Get-ADPrincipalGroupMembership followed by Get-ADUser and then Get-Date, and finally closes the transcript.
try {
Start-Transcript -Path $transactionFilename
Write-Host "GROUP MEMBERSHIP FOR $($targetUsername)"
Get-ADPrincipalGroupMembership -Credential $credential -Identity $Username -Server $domainServer | select name,distinguishedName | format-table
Write-Host "ACCOUNT PROPERTIES FOR $($targetUsername)"
Get-ADUser -Credential $credential -Identity $Username -Server $domainServer -Properties *
Write-Host "CURRENT TIME"
(Get-Date).DateTime
} catch {
} finally {
Stop-Transcript
write-host "Transcript is available at"
write-host $transactionFilename
$Host.UI.RawUI.BufferSize = New-Object Management.Automation.Host.Size ($originalHostWidth, $hostHeight)
}
When run on PS 4.0 each statement is executed in order - each one waiting for the previous to finish.
👍
When run on PS 5.1 the Get-ADPrincipalGroupMembership finishes, then the Write-Host "ACCOUNT PROPERTIES" runs then Write-Host "CURRENT TIME" runs, then everything in the finally block runs then the Get-ADUser and Get-Date commands run.
👎
As you can imagine, having Stop-Transcript run in the middle of the script is a show-stopper!
I've Googled for stopping a cmdlet from executing asynchronously, but all the articles are about how to make it execute async - not how to stop it. I'm not sure where to look for help now.
How can I adjust powershell 5.1 to run the statements synchronously? Backwards compatibility with 4.0 is not strictly necessary, but would be a bonus.
Per comments from #Lee_Dailey and #js2010 I was able to modify the script to function as desired by piping the output from format-table and Get-ADUser to Out-Host:
try {
Start-Transcript -Path $transactionFilename
Write-Host "GROUP MEMBERSHIP FOR $($targetUsername)"
Get-ADPrincipalGroupMembership -Credential $credential -Identity $Username -Server $domainServer | select name,distinguishedName | format-table | out-host
Write-Host "ACCOUNT PROPERTIES FOR $($targetUsername)"
Get-ADUser -Credential $credential -Identity $Username -Server $domainServer -Properties * | out-host
Write-Host "CURRENT TIME"
(Get-Date).DateTime
} catch {
} finally {
Stop-Transcript
write-host "Transcript is available at"
write-host $transactionFilename
$Host.UI.RawUI.BufferSize = New-Object Management.Automation.Host.Size ($originalHostWidth, $hostHeight)
}
Firstly, I'm by no means a PS expert, total newbie - admission done. I have scoured the internet for what I need in order to get the script to do what I want, but I've reached a point where I'm struggling and in need of help.
Basically, I've created a script using ISE that grabs the users in an AD OU, processes them by disabling the accounts, renaming them, stripping out the groups and moving them to another folder. In order to automate the deactivation process for users. But I now need to create a log file every time this runs, to show a) if it found any Users in the original OU (ToBeProcessed) and b) what processes were run and if they were successful. Here is the code.
$OUToBeProcessed = "OU=ToBeProcessed,OU=Users,OU=World,DC=local"
$OURetired = "OU=RetiredUsers,OU=Users,OU=World,DC=local"
$Users = Get-ADUser -SearchBase $OUToBeProcessed -Filter 'name -Like "*"' -Properties MemberOf
ForEach($User in $Users){
$SAN = $User.SamAccountName
#Disable user account
Disable-ADAccount -Identity $SAN
#Remove membership from groups for user
$User.Memberof | Remove-ADGroupMember -Member $User -Confirm:$False
$NewDN = "zzz_" + $User.Name
#Change display name
set-aduser $User -Displayname $newDN -ErrorAction SilentlyContinue
#Change distinguished name
Get-ADUser $SAN | Rename-ADObject -Newname $NewDN
Write-Host "$SAN may already exist."
#Move account to RetiredUsers
Get-Aduser $SAN | Move-ADObject -TargetPath $OURetired
}
I'm assuming I'll need to either use a Write-Output or Log-File cmdlet, though someone had also suggested Transcript, but I don't think that's what I need.
I've tried a number of ways to incorporate the Write-Output into the script, it runs without errors, but no text file is produced. But I'm placing it within the loop which may be the issue. I've placed it outside the loop but I think because it's not being passed anything it's creating the file with nothing in it. Would really appreciate some help as to where the Write-Output might need to go if that is the right cmdlet.
Personally I tend to add a Log function to my scripts. Something like this (where I output to the host and file):
Function Log {
Param (
[Parameter(Mandatory=$true)] [string] $String,
[Parameter(Mandatory=$true)] [string] $LogFilePath,
[Parameter(Mandatory=$false)][ValidateSet("ERROR","WARN","INFO","DEBUG")] [string] $Level = "INFO"
)
$LogString = ((Get-Date -Format "s") +" $Level $env:USERNAME $String")
Write-Host $LogString
Out-File -Append -FilePath $LogFilePath -InputObject $LogString
}
Then you could do logging:
Log "Something wrong!" "c:\mylog.log" "WARN"
Log "Updated stuff" "c:\mylog.log"
Or search the http://www.powershellgallery.com/ for logging modules.
Example (haven't tried this one myself):
https://www.powershellgallery.com/packages/PSLogging/2.5.2
trying to see if anyone has a known workaround for using the test-connection cmdlet in powershell to ping wildcard entries in DNS.
I'm trying to clean out our DNS db and exported a list from our BIND server and am in the process of just pinging through the 600+ machines to see if anything responds. I made my own simple script but have also found one that works slightly better on this forum. The script works but the cmdlet help files state that the -computername parameter does not support wildcards and sure enough, when i run the script all CNAME records are reporting down/false when they actually should be responding. The code I'm using is below and is kind of messy but I just needed something quick and it works, but I've included it below for reference:
Get-Content -path C:\Work\testy.txt | ForEach-Object { Test-Connection -ComputerName $_ -Count 1 -AsJob } | Get-Job | Receive-Job -Wait | Select-Object #{Name='ComputerName';Expression={$_.Address}},#{Name='Reachable';Expression={if ($_.StatusCode -eq 0) { $true } else { $false }}} |out-file -FilePath c:\work\TEST.txt
As pointed out by briantist, any non-existing record name will do. You could generate a GUID to substitute the * in your record name:
"subdomain.domain.tld","*.domain.tld" |ForEach-Object {
Test-Connection -ComputerName $($_ -replace '\*',"$([guid]::NewGuid())")
}
Your expression for whether it's "Reachable" or not can be simplified as well:
#{Name='Reachable'; Expression={[bool]($_.StatusCode -eq 0)}}
I'm working on a script that will build a new group if it doesn't exist. I'm using Get-ADGroup to make sure the group doesn't exist using the following command:
$group = get-adgroup $groupName -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue
But when I do I get the following error (I removed any domain specific data from the error):
Get-ADGroup : Cannot find an object with identity: '*group name*' under: '*domain*'.
At U:\Scripts\Windows\Create-FolderAccessGroup.ps1:23 char:24
+ $group = get-adgroup <<<< $groupName -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue
+ CategoryInfo : ObjectNotFound: (y:ADGroup) [Get-ADGroup], ADIdentityNot
FoundException
+ FullyQualifiedErrorId : Cannot find an object with identity: '' under: ''.,Microsoft.ActiveDirectory.Management.Commands.GetADGroup
I assumed setting ErrorAction and WarningAction to SilentlyContinue would keep this error from being displayed but it hasn't.
I find that this works best:
$Group = Get-ADGroup -Filter {SamAccountName -eq $GroupName}
If the filter returns no results, then $Group is simply set to $null and no error message is generated. Also, since a SAM account name must be unique in Active Directory, there is no risk of $Group being set to an array of more than one object.
I find that using -Filter to get the group rather than -Identity works really well when checking for the existence of groups (or users) in If statements. For example:
If (Get-ADGroup -Filter {SamAccountName -eq $GroupName})
{
Add-ADGroupMember -Identity $GroupName -Members $ListOfUserSamAccountNames
}
Else
{
Write-Warning "Users could not be added to $GroupName because $GroupName
does not exist in Active Directory."
}
I find that is a lot easier to deal with logically (if the group exists, add the users; if not, display a message) than mjolinor's suggestion of try/catch with using the Get-ADGroup cmdlet with the -Identity parameter. Consider the try/catch method of doing the same as above, using the -Identity parameter:
Try
{
Get-ADGroup -Identity $GroupName
Add-ADGroupMember -Identity $GroupName -Members $ListOfUserSamAccountNames
}
Catch
{
Write-Warning "Users could not be added to $GroupName because $GroupName
does not exist in Active Directory."
}
You see if any of the commands in the try block throws a terminating error. If one does, it means the group doesn't exist and will move on and process the command(s) in the catch block. It will work, but I don't think try/catch here flows as well, logically, in comparison to if/else.
Don't get me wrong, mjolinor is a PowerShell genius. It's just that in this case I don't think his solution is the best one.
try {get-adgroup <groupname>}
catch {
<make new group>
}
#mjolinor gives the good answer, but I think some explanation can also help.
Windows PowerShell provides two mechanisms for reporting errors: one mechanism for terminating errors and another mechanism for non-terminating errors.
Internal CmdLets code can call a ThrowTerminatingError method when an error occurs that does not or should not allow the cmdlet to continue to process its input objects. The script writter can them use exception to catch these error.
Internal CmdLets code can call a WriteError method to report non-terminating errors when the cmdlet can continue processing the input objects. The script writer can then use -ErrorAction option to hide the messages.
I realize this is old, but I also had this problem and solved it like this:
If (Get-ADObject -Filter {objectClass -eq "Group -and samAccountName -eq "groupname"}) { //do stuff// }