In the script below I get it run fine, however once an error occurrs it fails to continue where it left off before the error, I am a little confused as to what I am missing on this, so any help would be great.
First off, it loads a text file that contains a list of distribution groups, this then queries this list with exchange to get the exact match displayname and then cycles through a CSV adding each user to the distribution list, this all works fine, it's when an error occurrs that is the issue.
the catch statement understands the error and then carries out the tasks, once the task is complete it runs the script from the forloop in the catch statement and not in the try.
I have added the function UpdateDistros
To allow the script to run again, but the problem is it rus through the entire text file again instead of starting at where the error occurred, even if its at line 1 or line 25.
Below is the code;
## ##
## Adds users from text file to distributon lists ##
## ##
######################################################
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName 'UserPrincipalName'
Function UpdateDistros{
$ErroractionPreference = 'Stop'
$Path = "C:\INSTALLS\Exchange-Distro_Lists\Data"
$res = $Path+"\Results"
$txt1 = "Distros.txt"
$txt2 = "Results.txt"
$txt3 = "Mail_Contacts.txt"
$txt4 = "Errors.txt"
$DS1 = gc $Path"\"$txt1 # Gets information from a text file
$1 = "Error Occurred"
$R1 = $res+"\"+$txt2
$R2 = $res+"\"+$txt3
$R3 = $res+"\"+$txt4
try{
Foreach($DS in $DS1){ # Cycles through text file
$DSG = Get-DistributionGroup -identity $DS | Select * -ExpandProperty Alias # Gets display name for distribution group
$DS2 = $path+ "\"+ "$DSG.csv"
$NUS = Import-csv $DS2
#Foreach { $_ + '#zenith.co.uk' } | ` # ammends all users in each text file to contain #zenith.co.uk
#Foreach { $_ –replace “ “,”.” } # ammends all users in each text file to contain #zenith.co.uk
Foreach($NU in $NUS){ # Cycles through each record in each text file
$N1 = $NU.DP
$N2 = $NU.EM
#$NU
Add-DistributionGroupMember -identity $DS -Member $N1 #Adds each record to the correct distribution List
"$N1 added to $DS" | Out-file $R1 -Append
Write-host "Completed Script"
}
}
}
Catch{
$Erw = $_.Exception.Message
$Erw = $Erw -replace """",""
$NMC = get-content $DS2
$Rers = "Couldn't find object $N1. Please make sure that it was spelled correctly or specify a different object."
$Erw
$Rers
$A1 = $N1 -replace "\s",""
if($Erw -contains $Rers){
Foreach($NM in $NMC){
Write-host $1
Write-host $Erw
$Unq = 'There are multiple recipients matching identity "$N1". Please specify a unique value.'
$ERM = $_.Exception.Message
if($ERM -contains $Unq){
$NU = $NU + '- Cartwright'
New-MailContact -Name $N1 -ExternalEmailAddress $N2 -Alias $A1
"New Mail contact created for $N1" | Out-file $R2 -Append -ErrorAction continue
UpdateDistros
}
ElseIf($Erw -contains $Rers){
New-MailContact -Name $N1 -ExternalEmailAddress $N2 -Alias $A1
"New Mail contact created for $N1" | Out-file $R2 -Append -ErrorAction continue
UpdateDistros
}
Else{
$Erw | Out-file $R3 -Append -ErrorAction continue
UpdateDistros
}
}
}
Else{
$Erw | Out-file $R3 -Append -ErrorAction continue }
UpdateDistros
}
}
UpdateDistros
You should not put the entire body of your script in a try/catch block. Doing this makes it really hard to tell where the error occured and how to deal with it.
First, delete the try/catch blocks you have.
Next, add one specifically for the issue you want to deal with. You said that you the script fails to continue when it can't add a user to a distribution group, making that a good place to add handling.
Also, I'm moving the comment to the top of the line, instead of the end.
Full lines of text are hard to read, but relatively short columns can be scanned easily.
try{
#Adds each record to the correct distribution List
Add-DistributionGroupMember -identity $DS -Member $N1
}
catch{
$message = "Failed to modify group $($DS) with new member $($N1)"
Write-warning $message
$message | Add-content C:\pathTo\SomeLogfile.txt
CONTINUE
}
The Continue command in the end is a special PowerShell flow control command.
It means 'Stop processing this item in our current loop and move on to the next one`.
Related
Essentially, my script is supposed to check if each user in the administrators group is listed inside of a text file, and if it is then ignore it and move on. If it isn't, it removes the user from the administrator group. However, Get-LocalGroupMember prepends the computer name to the username. This means that the username in the txt file (ex user1), does not match the $._Name variable from the Get-LocalGroupMember command (ex desktop/user1). Here is a copy of the code
$GroupName = "Administrators"
$Exclude = "Administrator","$env:UserName"
$AuthorizedAdmins = Get-Content C:\Users\$env:UserName\admins.txt
Get-LocalGroupMember $GroupName |
ForEach-Object{
if ($_.ObjectClass -eq 'User'){
if ($AuthorizedAdmins -contains $_.Name -or $Exclude -contains $_.Name){
Continue
}
else{
Remove-LocalGroupMember -Group $GroupName -Member $_.Name -Confirm
}
}
}
I have tried several solutions. In the code, I created a new variable that removed the first $env:ComputerName+1 characters of the $._Name string. While this did work to remove the computername, powershell errors out. Here is the error code and changed script:
Get-LocalGroupMember : System error.
At users.ps1:6 char:1
+ Get-LocalGroupMember $GroupName |
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-LocalGroupMember], ContinueException
+ FullyQualifiedErrorId : An unspecified error occurred.,Microsoft.PowerShell.Commands.GetLocalGroupMemberCommand
$GroupName = "Administrators"
$Exclude = "Administrator","$env:UserName"
$AuthorizedAdmins = Get-Content C:\Users\$env:UserName\admins.txt
Get-LocalGroupMember $GroupName |
ForEach-Object{
$User = $_.Name
$length = $env:ComputerName.Length+1
$ShortUser = $User.Remove(0,$length)
if ($_.ObjectClass -eq 'User'){ #ignore groups and select only users
if ($AuthorizedAdmins -contains $ShortUser -or $Exclude -contains $ShortUser){
Continue
}
else{
Remove-LocalGroupMember -Group $GroupName -Member $_.Name -Confirm
}
}
}
The admin.txt file is formatted as follows:
user1
user2
user3
I cannot figure out how to fix this, though it is probably someting simple. Any help would be appreciated.
The real issue with your code is your use of continue in a ForEach-Object loop, see note from the docs. If you want to emulate continue in a pipeline processing function you should use return instead. So your code, with some improvements and simplifications would be:
$GroupName = "Administrators"
$exclude = #(
"Administrator"
$env:UserName
Get-Content C:\Users\$env:UserName\admins.txt
)
Get-LocalGroupMember $GroupName | ForEach-Object{
# if its not a user, skip this logic
if ($_.ObjectClass -ne 'User') {
return
}
# here we assume its a user
if ($_.Name.Split('\')[-1] -in $exclude) {
return
}
Remove-LocalGroupMember -Group $GroupName -Member $_.Name -Confirm
}
Occasionally I forget to log off from a server or am disconnected through an error and I don't remember the name of the server. And my domain account starts getting periodically locked out, so I have to access logs on DC to find out which server(s) keep locking my account and log off from it/them. So I wanted to write to script in powershell that would log me off from all servers in a domain (with the exception of the server where I run the script on of course) without me needing to search which to log off from. This is what I have:
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
$SessionIDs | ForEach-Object
{
Write-Host "Logging off session [$($_)]..."
logoff $_
}
}
}
foreach ($Server in $Servers)
{
if ($Server -isnot $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock {$ScriptBlock}
}
}
But when I launch the script, nothing happens. The script doesn't return any errors but doesn't log me off from any server, nor does it write any of the messages from Write-Host cmdlet, obviously. I noticed the $SessionIDs variable definition only returns ID of the first session. Usually this shouldn't be a problem, since it's unlikely I will have more than one session on a server, but I'd like to have this insurance. Can anyone tell me what's wrong in the script?
I notice a few things...
"First, I don't think quser | Where-Object {$_ -match $env:USERNAME} will ever return anything. The output of quser will not contain the hostname."
Try this for getting logon sessions:
$Sessions = (query user /server:$Env:ComputerName) -split "\n" -replace '\s\s+', ';' |
ConvertFrom-Csv -Delimiter ';'
Next, when you reference the $Server variable on the remote machine in your script block, it is out of scope. You would need to use $Using:Server in the script block.
Lastly, the -isnot operator doesn't compare value, it compares type. So in your last foreach, the if statement evaluates to "if type string is not type string" and will not run. Try -ne or -notlike instead.
Working with objects is much easier if you can just parse the output of QUser.exe. Given your scenario, here's my take on it:
$servers = (Get-ADComputer -Filter '*').Name.Where{$_ -ne $env:COMPUTERNAME}
foreach ($server in $servers)
{
if (-not ($quser = ((QUser.exe /server:$server) -replace '\s{20,39}',',,' -replace '\s{2,}',',' 2>&1) | Where-Object -FilterScript { $_ -match $env:USERNAME })) {
Continue
}
Write-Verbose -Message "$($quser.Count) session(s) found on $server." -Verbose
($quser.Trim() | ConvertFrom-Csv -Header 'USERNAME','SESSIONNAME','ID','STATE','IDLE TIME','LOGON TIME').foreach{
Write-Verbose -Message "Logging user [$($_.UserName)] off." -Verbose
LogOff.exe $_.ID /server:$server
}
}
Filtering should always happen before hand meaning, filter out your computer name on your first call to Get-ADComputer. Since you're using QUser.exe and LogOff.exe to begin with, I'd recommend the use of it all the way through since LogOff accepts an ID value that QUser outputs.
Next, placing the call to quser inside your if statement does two things in this case.
Filters for all users matching $ENV:UserName
Returns $true if anything is found, and $false if not found.
So, switching the results using -not will turn $false into $true allowing the execution of the code block which will just continue to the next server.
This in turn doesn't bother with the rest of the code and continues onto the next computer if no matching names were found.
The use of $quser inside the if statement is so you can save the results to it if more than one name is found; (..) allows this as it turns the variable assignment into an expression having the output pass through onto the pipeline where it is either empty, or not.
Finally, referencing the $quser variable we can convert the strings into objects piping to ConvertFrom-Csv. Only step left to do is iterate through each row and passing it over to LogOff to perform the actual logoff.
If you've noticed, the headers are manually-specified because it is filtered out by the Where-Object cmdlet. This is a better approach seeing as there could be "more than one" RDP Session, now you're just left with those sessions matching the name which can be saved to $quser, so no extra filtering is needed down the line.
So I modified the script this way and it works, sort of. It logs off account from servers, which is the main goal. There are still some glitches, like the message it sends from the first Write-Host doesn't give server's name, the message from second one gives a different value than it should (it gives [1] value after -split instead of [2] for some reason; but those are not really that important things, even though I will try to make at least the first message right) and $SessionIDs still gives only the first value, but usually you shouldn't have more than one RDP session per server. I've seen more sessions of one user, but that is very rare. But I'd also like to fix this if possible. Nevertheless, the script basically does the most important thing. But if someone has a suggestion how to fix the glitches I mentioned I would be grateful.
$ErrorActionPreference = "Silentlycontinue"
$Servers = (Get-ADComputer -Filter *).Name
$ScriptBlock = {
$Sessions = quser | ?{$_ -match $env:USERNAME}
if (($Sessions).Count -ge 1)
{
$SessionIDs = , ($Sessions -split ' +')[2]
Write-Host "Found $(($SessionIDs).Count) user login(s) on $Server."
Foreach ($SessionID in $SessionIDs)
{
Write-Host "Logging off session $SessionID..."
logoff $SessionID
}
}
}
foreach ($Server in $Servers)
{
if ($Server -ne $env:COMPUTERNAME)
{
Invoke-Command -ComputerName $Server -ScriptBlock $ScriptBlock
}
}
I have a directory of .txt files that look like this:
[LINETYPE]S[STARTTIME]00:00:00
[LINETYPE]P[STARTTIME]00:00:00
[LINETYPE]B[STARTTIME]00:59:00
[LINETYPE]C[STARTTIME]00:59:00
[LINETYPE]C[STARTTIME]00:59:30
[LINETYPE]S[STARTTIME]01:00:00
[LINETYPE]P[STARTTIME]01:00:00
[LINETYPE]B[STARTTIME]01:59:00
[LINETYPE]C[STARTTIME]01:59:00
[LINETYPE]C[STARTTIME]01:59:30
[LINETYPE]S[STARTTIME]02:00:00
I'd like to remove all occurrences of [LINETYPE]S except the first, which happens to always be 00:00:00 and on the first line, and then re-save the file to a new location.
That is, [LINETYPE]S[STARTTIME]00:00:00 must always be present, but the other lines that start with [LINETYPE]S need to be removed.
This is what I came up with, which works except it removes all [LINETYPE]S lines, including the first. I can't seem to figure out how to do that part after Googling for a while, so I'm hoping someone can point me in the right direction. Thanks for your help!
Get-ChildItem "C:\Users\Me\Desktop\Samples" -Filter *.txt | ForEach-Object {
Get-Content $_.FullName | Where-Object {
$_ -notmatch "\[LINETYPE\]S"
} | Set-Content ('C:\Users\Me\Desktop\Samples\Final\' + $_.BaseName + '.txt')
}
i couldn't figure out how to do this via a pipeline [blush], so i went with a foreach loop and a compound test.
# fake reading in a text file
# in real life, use Get-Content
$InStuff = #'
[LINETYPE]S[STARTTIME]00:00:00
[LINETYPE]P[STARTTIME]00:00:00
[LINETYPE]B[STARTTIME]00:59:00
[LINETYPE]C[STARTTIME]00:59:00
[LINETYPE]C[STARTTIME]00:59:30
[LINETYPE]S[STARTTIME]01:00:00
[LINETYPE]P[STARTTIME]01:00:00
[LINETYPE]B[STARTTIME]01:59:00
[LINETYPE]C[STARTTIME]01:59:00
[LINETYPE]C[STARTTIME]01:59:30
[LINETYPE]S[STARTTIME]02:00:00
'# -split [System.Environment]::NewLine
$KeepFirst = '[LINETYPE]S'
$FoundFirst = $False
$FilteredList = foreach ($IS_Item in $InStuff)
{
if ($IS_Item.StartsWith($KeepFirst))
{
if (-not $FoundFirst)
{
$IS_Item
$FoundFirst = $True
}
}
else
{
$IS_Item
}
}
$FilteredList
output ...
[LINETYPE]S[STARTTIME]00:00:00
[LINETYPE]P[STARTTIME]00:00:00
[LINETYPE]B[STARTTIME]00:59:00
[LINETYPE]C[STARTTIME]00:59:00
[LINETYPE]C[STARTTIME]00:59:30
[LINETYPE]P[STARTTIME]01:00:00
[LINETYPE]B[STARTTIME]01:59:00
[LINETYPE]C[STARTTIME]01:59:00
[LINETYPE]C[STARTTIME]01:59:30
at that point, you can send the new collection out to a file. [grin]
Try the following:
Get-ChildItem "C:\Users\Me\Desktop\Samples" -Filter *.txt |
Foreach-Object {
$count = 0
Get-Content $_.FullName |
Where-Object { $_ -notmatch '\[LINETYPE\]S' -or $count++ -eq 0 } |
Set-Content ('C:\Users\Me\Desktop\Samples\Final\' + $_.BaseName + '.txt')
}
The script block passed to Where-Object runs in the same scope as the caller, so variable $count can be directly updated.
The 1st line that does contain [LINETYPE]S is included, because $count is 0 at that point, after which $count is incremented ($count++); subsequent [LINETYPE]S are not included, because $count is then already greater than 0.
Update: I am now attempting to use a Get-StartApps command instead of recursively going through the files with a filter. Now I am in need of help re-writing the logic behind the insert sections noted in #3 below. Any suggestions would be amazing!
I found and have been working with a series of scripts that can be used to create a start menu XML. The version I have settled on is below. Unfortunately, I cannot seem to get it to handle ".URL",".Website", ".shortcut" extensions.
I have tried the following (one at a time and together):
On line 90 removing the -filter "*.lnk"
Adding -filter ".website", -Filter ".shortcut"... to line 90
Creating ifelse statements for each of these extensions modeled after lines 107-116
if ($SoftwareLinks.Name -like "$Software.lnk") {
$SoftwareLink = $SoftwareLinks | where {$_ -like "$Software.lnk"}
$child = $StartMenuXml.CreateElement("start","DesktopApplicationTile","http://schemas.microsoft.com/Start/2014/StartLayout")
if ($SoftwareLink.FullName.GetType().BaseType -eq [System.Array]) { ## If multiple links, use the first one
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName[0])
} else {
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName)
}
}
None of this seems to work when trying to pick up links that are placed in the start menu folder. Has anyone come across this? Do you have any advice on how to fix this? Whole script below:
# Where to save start menu xml
#$StartMenuFile = "$ENV:programfiles\DESKTOPENGINEERING\startmenu\startmenu.xml"
$StartMenuFile = "$ENV:programfiles\DESKTOPENGINEERING\startmenu\startmenu.xml"
#$OldStartMenuFile = "$ENV:programfiles\DESKTOPENGINEERING\startmenu\startmenu.xml"
$OldStartMenuFile = "$programfiles\DESKTOPENGINEERING\startmenu\startmenu.old"
# Set this to SilentlyContinue for no debug, or Continue for debug output
$DebugPreference = "SilentlyContinue"
# Remove old startmenu.old file
IF (Test-path $OldStartMenuFile) {
Write-Debug "The file `"$OldStartMenuFile`" already exists and will be removed!"
Remove-item $OldStartMenuFile -Force
} Else {
Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."
}
# Rename startmenu.xml to startmenu.old
IF (Test-path $StartMenuFile) {
Write-Debug "renaming file `"$OldStartMenuFile`"..."
Rename-Item -Path $StartMenuFile -NewName $OldStartMenuFile -Force
} Else {
Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."
}
# One last check to see if file exists or not
IF (Test-path $StartMenuFile) {
Write-Error "Could not rename `"$OldStartMenuFile`", script aborted"
Break
} Else {
Write-Debug "The file `"$OldStartMenuFile`" does not exist! Lets move along then..."
}
# Make sure folder exist and halt if it can't be created
$StartMenuFolder=(Split-path -parent $StartMenuFile)
IF (Test-path $StartMenuFolder) { } ELSE { New-Item -ItemType Directory -Path $StartMenuFolder }
IF (Test-path $StartMenuFolder) { } ELSE { Write-Error "Could not create `"$StartMenuFolder`", script aborted" ; Break }
# Specify number of cols in startmenu
$NumCol = 6
# Add the new group in $MenuGroups
# Format: "order. group title" = "list of Software Links", "Followed by other links"
$MenuGroups = #{
"1. Internet & Network tools" = "Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge"
"2. Microsoft Office" = "Access 2016","Excel 2016","Outlook 2016","PowerPoint 2016","Project 2016","Publisher 2016","Word 2016"
"3. Text, file & programming tools" = "Calculator","Notepad"
"4. Media tools" = "Paint"
"5. Scientific software" = "Calculator"
"6. Administrator" = "Powershell"
"7. Other tools" = "Google Chrome", "Google Link"
}
# Building up base startmenu xml
[xml]$StartMenuXml = '<LayoutModificationTemplate
xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout"
xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout"
xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout"
Version="1">
<LayoutOptions StartTileGroupsColumnCount="1" StartTileGroupCellWidth="'+$NumCol+'" />
<DefaultLayoutOverride LayoutCustomizationRestrictionType="OnlySpecifiedGroups">
<StartLayoutCollection>
<defaultlayout:StartLayout GroupCellWidth="'+$NumCol+'" xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout">
</defaultlayout:StartLayout>
</StartLayoutCollection>
</DefaultLayoutOverride>
<CustomTaskbarLayoutCollection PinListPlacement="Replace">
<defaultlayout:TaskbarLayout>
<taskbar:TaskbarPinList>
<taskbar:UWA AppUserModelID="Microsoft.WindowsCalculator_8wekyb3d8bbwe!App"/>
</taskbar:TaskbarPinList>
</defaultlayout:TaskbarLayout>
</CustomTaskbarLayoutCollection>
</LayoutModificationTemplate>'
# Selecting XML element where all software will be placed
$DefaultLayoutElement = $StartMenuXml.GetElementsByTagName("defaultlayout:StartLayout")
# Fetching all software links on start menu
$SoftwareLinks = Get-ChildItem "$env:PROGRAMDATA\Microsoft\Windows\Start Menu" -recurse -filter "*.lnk"
# Looping all menu groups defined above
foreach ($MenuGroup in $MenuGroups.Keys | Sort-Object) {
# Init xml element for software group
$SoftwareGroupXml = $StartMenuXml.CreateElement("start","Group", "http://schemas.microsoft.com/Start/2014/StartLayout")
$SoftwareGroupXml.SetAttribute('Name',$MenuGroup.Substring(3))
# Init row and col
$col = 0
$row = 0
# Looping all software links in start menu
foreach ($Software in $MenuGroups[$MenuGroup]) {
# Check if it is time for a new col
if (($col%($NumCol-1) -eq 1) -and ($col -ne 1)) {
$row +=1
$col = 0
}
# Check if specified software is found in start menu. If so, add software element
if ($SoftwareLinks.Name -like "$Software.lnk") {
$SoftwareLink = $SoftwareLinks | where {$_ -like "$Software.lnk"}
$child = $StartMenuXml.CreateElement("start","DesktopApplicationTile","http://schemas.microsoft.com/Start/2014/StartLayout")
if ($SoftwareLink.FullName.GetType().BaseType -eq [System.Array]) { ## If multiple links, use the first one
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName[0])
} else {
$child.SetAttribute('DesktopApplicationLinkPath',$SoftwareLink.FullName)
}
}
# Or check if Microsoft app is specified. If so add app element
elseif ($Software -like "Microsoft.*!*") {
$child = $StartMenuXml.CreateElement("start","Tile","http://schemas.microsoft.com/Start/2014/StartLayout")
$child.SetAttribute('AppUserModelID',$Software)
}
# Add common attributes is software or app is found and append xml element
if (($child.HasAttributes) -and (($Software -like "Microsoft.*!*") -or ($SoftwareLinks.Name -like "$Software.lnk"))) {
$child.SetAttribute('Size','2x2')
$child.SetAttribute('Column',$col)
$child.SetAttribute('Row',$row)
$SoftwareGroupXml.AppendChild($child) | Out-Null
$col +=1
}
}
# If a software group is not null, add it!
if ($SoftwareGroupXml.HasChildNodes) {
$DefaultLayoutElement.AppendChild($SoftwareGroupXml) | Out-Null
}
}
# Save to file
$StartMenuXml.Save($StartMenuFile)### Script ends ###
Ok so there was a two part fix. Instead of recursively searching the folders I used the built in $SoftwareLinks = Get-StartApps which solved my first problem and then re-wrote the insert string to match the new format. Thank you everyone for your feedback.
Very very much a PowerShell newbie here I wanted a script to scan devices on the network and report on Local Admins. Found one out there and made some minor modifications to meet my needs - but I have one mod I cant work out how to do. Hoping someone out there will know a simple way to do it ?
The scrip below will read in a list of device names - scan them and output a dated report for all devices that are live and on-line. If the device is not accessible I get the following error on screen but nothing in the report.
I would like when it encounters an error that it writes to the report file - something along the lines of "$computor was not accessible!"
The code I am using is
$date = Get-Date -Format o | foreach {$_ -replace ":", "."}
ECHO "Starting scan"
$Result = #()
foreach($server in (gc .\servers.txt)){
$computer = [ADSI](”WinNT://” + $server + “,computer”)
$Group = $computer.psbase.children.find(”Administrators”)
$Filename = "c:\" + "LocalAdminAudit" + $date + ".txt"
function getAdmins
{
ECHO "SEARCHING FOR DEVICE"
$members = ($Group.psbase.invoke(”Members”) | %
{$_.GetType().InvokeMember(”Adspath”, ‘GetProperty’, $null, $_, $null)}) -
replace ('WinNT://DOMAIN/' + $server + '/'), '' -replace ('WinNT://DOMAIN/',
'DOMAIN\') -replace ('WinNT://', '')
$members}
ECHO "READY TO WRITE OUTPUT"
$Result += Write-Output "SERVER: $server"
$Result += Write-Output ' '
$Result += ( getAdmins )
$Result += Write-Output '____________________________'
$Result += Write-Output ' '
ECHO "Record written"
}
# Added date run to report
$result += Write-Output "Date Reported: $date"
$Result > $Filename
Invoke-Item $Filename
# replace "DOMAIN" with the domain name.
ECHO "Scan Complete"
And the on screen error when a machine is off line or otherwise doesn't respond is
Exception calling "Find" with "1" argument(s): "The network path was not found.
"
At \server\users\User.Name\Powershell Scripts\Get-Local-AdminsV3.ps1:1
0 char:40
+ $Group = $computer.psbase.children.find <<<< (”Administrators”)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
I would like when it encounters an error that it writes to the report file - something along the lines of "$computor was not accessible!" - I am pretty sure there must be an easy way of doing this - but I cant work it out so any tips would be greatly appreciated
As Matt, mentioned in the comments. You can use a Try/Catch block inside your function to catch the error.
I also made some other changes. The most major is that I changed the function to contain all of the code necessary to get the local administrator group. Then the loop just calls the function once per computer with the computer name. This function is then reusable.
Secondly rather than output to a text file, I changed to outputting to a CSV as is a more structured format that can be used better later.
Also rather than relying on writing to the console host, I used Write-Progress to report the progress of the loop.
$Servers = Get-Content .\servers.txt
$ExportFileName = "c:\LocalAdminAudit$date.csv"
function Get-LocalAdministrator {
[cmdletbinding()]
Param(
$ComputerName
)
$Group = [ADSI]("WinNT://$computername/Administrators,group")
try {
$Group.Invoke("Members") | ForEach-Object {
$User = ($_.GetType().InvokeMember("Adspath", 'GetProperty', $null, $_, $null) -split '/')[-2,-1] -join '\'
[PSCustomObject]#{
"User" = $User
"Server" = $ComputerName
"Date" = Get-Date -Format o | ForEach-Object {$_ -replace ":", "."}
}
}
}
catch {
[PSCustomObject]#{
"User" = "Failed to Report"
"Server" = $ComputerName
"Date" = Get-Date -Format o | ForEach-Object {$_ -replace ":", "."}
}
}
}
$LocalAdmins = foreach ($Server in $Servers) {
Write-Progress -Activity "Retrieving Local Administrators" -Status "Checking $Server" -PercentComplete (([array]::indexof($Servers,$Server)/($Server.count))*100)
Get-LocalAdministrator $Server
}
$LocalAdmins | Export-CSV $ExportFileName -NoTypeInformation
Invoke-Item $ExportFileName
Lastly, be careful of smart quotes especially when cutting and pasting between Outlook and word.