mb2k01 Posted November 27, 2017 Posted November 27, 2017 Hi All, I've got a powershell script (below) which has been slightly adapted for our local needs. This works perfectly if we want to specify a handful of machines and have a technician go through to select profiles manually from GUI, but I am hoping that someone here might have the skills to transform this in to something more automated? Ideally I'd like to supply a list of machines (or even direct at an OU) and have all profiles other than administrator selected for deletion, then hit go and have it work its way through. Does anyone think they can do that? #GUI interface to delete user profiles from remote desktop session host server #v2 - added support to select multiple users at once # added increased error logging # #Setup script variables #add computers to computers variable to search for profiles on those computers [array] $Computers = "LIBRARY-04" $log = "z:\log.txt" $date = Get-Date #Reset variables $selecteduser = "" $profilelist = @() Function SetupForm { #Setup the form [void] [system.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") [void] [system.Reflection.Assembly]::LoadWithPartialName("System.Drawing") $objForm = New-Object System.Windows.Forms.Form $objForm.Text = "Select user(s)" $objForm.Size = New-Object System.Drawing.Size(300,320) $objForm.StartPosition = "CenterScreen" $btnDelete = New-Object System.Windows.Forms.Button $btnDelete.Location = New-Object System.Drawing.Size(120,240) $btnDelete.Size = New-Object System.Drawing.Size(75,23) $btnDelete.Text = "Delete Profile" $objForm.Controls.Add($btnDelete) #When a user clicks the delete button get the details of the logged in users and calls the DeleteProfile function $btnDelete.Add_Click({ #calls the delete profile function DeleteProfile } ) $CancelButton = New-Object System.Windows.Forms.Button $CancelButton.Location = New-Object System.Drawing.Size(200,240) $CancelButton.Size = New-Object System.Drawing.Size(75,23) $CancelButton.Text = "Cancel" $CancelButton.Add_Click({$objForm.Close()}) $objForm.Controls.Add($CancelButton) $objLabel = New-Object System.Windows.Forms.Label $objLabel.Location = New-Object System.Drawing.Size(10,20) $objLabel.Size = New-Object System.Drawing.Size(280,20) $objLabel.Text = "Please select user to delete profile:" $objForm.Controls.Add($objLabel) $objListBox = New-Object System.Windows.Forms.ListBox $objListBox.Location = New-Object System.Drawing.Size(10,40) $objListBox.Size = New-Object System.Drawing.Size(260,300) $objListBox.Height = 180 $objListBox.SelectionMode = "MultiExtended" #Run through each computer in the computers variable to compile a list of unique user accounts across all servers ForEach ($computer in $Computers) { #use WMI to find all users with a profile on the servers Try{ [array]$users = Get-WmiObject -ComputerName $computer Win32_UserProfile -filter "LocalPath Like 'C:\\Users\\%'" -ea stop } Catch { Write-Warning "$($error[0]) " Break } #compile the profile list and remove the path prefix leaving just the usernames $profilelist = $profilelist + $users.localpath -replace "C:\\users\\" #filter the user names to show only unique values left to prevent duplicates from profile existing on multiple computers $uniqueusers = $profilelist | Select-Object -Unique | Sort-Object } #adds the unique users to the combo box ForEach($user in $uniqueusers) { [void] $objListBox.Items.Add($user) } $objForm.Controls.Add($objListBox) $objForm.Topmost = $True $objForm.Add_Shown({$objForm.Activate()}) [void] $objForm.ShowDialog() } Function DeleteProfile { ForEach ($x in $objListBox.SelectedItems) { #Add the path prefix back to the selected user $selecteduser = $x $selectedUser = "C:\Users\$selecteduser" #This section reads through all the computers and deletes the profile from all the computers - it catches any errors. ForEach ($computer in $Computers) { Try { (Get-WmiObject -ComputerName $computer Win32_UserProfile | Where {$_.LocalPath -eq $selecteduser}).Delete() Write-Host -ForegroundColor Green "$selecteduser has been deleted from $computer" Add-Content $log "$date $selecteduser profile has been deleted from $computer" } Catch [system.Management.Automation.MethodInvocationException]{ Write-Host -ForegroundColor Red "ERROR: Profile is currently locked on $computer - please use log off user script first" Add-Content $log "$date $selecteduser Profile is currently locked on $computer - please use log off user script first" } Catch [system.Management.Automation.RuntimeException] { Write-Host -ForegroundColor Yellow -BackgroundColor Blue "INFO: $selecteduser Profile does not exist on $computer" Add-Content $log "$date INFO: $selecteduser Profile does not exist on $computer" } Catch { Write-Host -ForegroundColor Red "ERROR: an unknown error occoured. The error response was $error[0]" Add-Content $log "$date ERROR: an unknown error occoured. The error response was $error[0]" } } } #Add a label to say process is complete $objLabel1 = New-Object System.Windows.Forms.Label $objLabel1.Location = New-Object System.Drawing.Size(10,100) $objLabel1.Size = New-Object System.Drawing.Size(280,20) $objLabel1.Text = "Deletion complete, check log for more details." $objForm.Controls.Add($objLabel1) #Add a view log button to view the log file $LogButton = New-Object System.Windows.Forms.Button $LogButton.Location = New-Object System.Drawing.Size(50,150) $LogButton.Size = New-Object System.Drawing.Size(75,23) $LogButton.Text = "View Log" $LogButton.Add_Click({Invoke-Item $log}) $objForm.Controls.Add($LogButton) } #Check script was run as admin If (-NOT ([security.Principal.WindowsPrincipal] [security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([security.Principal.WindowsBuiltInRole] "Administrator")) { [system.Windows.Forms.MessageBox]::Show("It doesn't appear you have run this PowerShell session with administrative rights, the script may not function correctly. If no users are displayed please ensure you run the script again using administrive rights.") } #Start the form SetupForm
mavhc Posted November 27, 2017 Posted November 27, 2017 You can already do a list of computers, just change [array] $Computers = "LIBRARY-04" to [array] $Computers = "LIBRARY-04","LIBRARY-05","..." 1
mb2k01 Posted November 27, 2017 Author Posted November 27, 2017 You can already do a list of computers, just change [array] $Computers = "LIBRARY-04" to [array] $Computers = "LIBRARY-04","LIBRARY-05","..." Sorry, should have mentioned I knew that one! (I copied my test script rather than the production one we're using at the min!) While I wouldn't mind so much about entering all names in the script, what happens next is that you're presented with a GUI to manually remove each account from each computer. Ideally I'd like the scripts to be able to execute and then either present a total list which you can just click go on, or not present a list at all and just 'know' to remove any profile other than administrator. Essentially cutting down on technician time if we were to use the script over a few hundred PC's. Another way of describing, potentially less wordy... this is a delprof alternative, as even delprof2 seems to break things for us. This script seems to nicely remove profiles without breaking other things, but needs a fair bit of user input when used for multiple stations. If I could automate it, it would be great!
mavhc Posted November 27, 2017 Posted November 27, 2017 According to https://msdn.microsoft.com/en-us/library/ee886409(v=vs.85).aspx you could add to the filter special=False AND loaded=False So something like [array]$users = Get-WmiObject -ComputerName $computer Win32_UserProfile -filter "LocalPath Like 'C:\\Users\\%' AND special=False AND loaded=False" -ea stop That should only list profiles that aren't system services, or in use. To find out if they're local admins is a bit tricky, do they always have a set name, or is it dynamic? 1
mb2k01 Posted November 27, 2017 Author Posted November 27, 2017 According to https://msdn.microsoft.com/en-us/library/ee886409(v=vs.85).aspx you could add to the filter special=False AND loaded=False ...To find out if they're local admins is a bit tricky, do they always have a set name, or is it dynamic? On our domain machines we only have administrator and adlocal as local admin accounts.
mavhc Posted November 27, 2017 Posted November 27, 2017 You could do $List -cnotcontains $ProfileName where $List is a list of admin profile names ForEach($user in $uniqueusers) { if ($AdminList -contcontains $user) { [void] $objListBox.Items.Add($user) } } See if that gives you only users you want to remove, as a starting point, then we can skip the gui 1
mb2k01 Posted November 27, 2017 Author Posted November 27, 2017 You've quickly hit the top of my Powershell ability! Where would I put that code? (Any chance you could re-post the original code with the adaptations?)
mavhc Posted November 27, 2017 Posted November 27, 2017 # GUI interface to delete user profiles from remote desktop session host server #v2 - added support to select multiple users at once # added increased error logging # #Setup script variables #add computers to computers variable to search for profiles on those computers [array] $Computers = "LIBRARY-04","LIBRARY-05" [arary] $AdminUsers = "Administrator", "LocalAdmin" $log = "z:\log.txt" $date = Get-Date #Reset variables $selecteduser = "" $profilelist = @() Function SetupForm { #Setup the form [void] [system.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") [void] [system.Reflection.Assembly]::LoadWithPartialName("System.Drawing") $objForm = New-Object System.Windows.Forms.Form $objForm.Text = "Select user(s)" $objForm.Size = New-Object System.Drawing.Size(300,320) $objForm.StartPosition = "CenterScreen" $btnDelete = New-Object System.Windows.Forms.Button $btnDelete.Location = New-Object System.Drawing.Size(120,240) $btnDelete.Size = New-Object System.Drawing.Size(75,23) $btnDelete.Text = "Delete Profile" $objForm.Controls.Add($btnDelete) #When a user clicks the delete button get the details of the logged in users and calls the DeleteProfile function $btnDelete.Add_Click({ #calls the delete profile function DeleteProfile } ) $CancelButton = New-Object System.Windows.Forms.Button $CancelButton.Location = New-Object System.Drawing.Size(200,240) $CancelButton.Size = New-Object System.Drawing.Size(75,23) $CancelButton.Text = "Cancel" $CancelButton.Add_Click({$objForm.Close()}) $objForm.Controls.Add($CancelButton) $objLabel = New-Object System.Windows.Forms.Label $objLabel.Location = New-Object System.Drawing.Size(10,20) $objLabel.Size = New-Object System.Drawing.Size(280,20) $objLabel.Text = "Please select user to delete profile:" $objForm.Controls.Add($objLabel) $objListBox = New-Object System.Windows.Forms.ListBox $objListBox.Location = New-Object System.Drawing.Size(10,40) $objListBox.Size = New-Object System.Drawing.Size(260,300) $objListBox.Height = 180 $objListBox.SelectionMode = "MultiExtended" #Run through each computer in the computers variable to compile a list of unique user accounts across all servers ForEach ($computer in $Computers) { #use WMI to find all users with a profile on the servers Try{ [array]$users = Get-WmiObject -ComputerName $computer Win32_UserProfile -filter "LocalPath Like 'C:\\Users\\%' AND special=False AND loaded=False" -ea stop } Catch { Write-Warning "$($error[0]) " Break } #compile the profile list and remove the path prefix leaving just the usernames $profilelist = $profilelist + $users.localpath -replace "C:\\users\\" #filter the user names to show only unique values left to prevent duplicates from profile existing on multiple computers $uniqueusers = $profilelist | Select-Object -Unique | Sort-Object } #adds the unique users to the combo box ForEach($user in $uniqueusers) { if ($AdminUsers -cnotcontains $user) { [void] $objListBox.Items.Add($user) } } $objForm.Controls.Add($objListBox) $objForm.Topmost = $True $objForm.Add_Shown({$objForm.Activate()}) [void] $objForm.ShowDialog() } Function DeleteProfile { ForEach ($x in $objListBox.SelectedItems) { #Add the path prefix back to the selected user $selecteduser = $x $selectedUser = "C:\Users\$selecteduser" #This section reads through all the computers and deletes the profile from all the computers - it catches any errors. ForEach ($computer in $Computers) { Try { (Get-WmiObject -ComputerName $computer Win32_UserProfile | Where {$_.LocalPath -eq $selecteduser}).Delete() Write-Host -ForegroundColor Green "$selecteduser has been deleted from $computer" Add-Content $log "$date $selecteduser profile has been deleted from $computer" } Catch [system.Management.Automation.MethodInvocationException]{ Write-Host -ForegroundColor Red "ERROR: Profile is currently locked on $computer - please use log off user script first" Add-Content $log "$date $selecteduser Profile is currently locked on $computer - please use log off user script first" } Catch [system.Management.Automation.RuntimeException] { Write-Host -ForegroundColor Yellow -BackgroundColor Blue "INFO: $selecteduser Profile does not exist on $computer" Add-Content $log "$date INFO: $selecteduser Profile does not exist on $computer" } Catch { Write-Host -ForegroundColor Red "ERROR: an unknown error occoured. The error response was $error[0]" Add-Content $log "$date ERROR: an unknown error occoured. The error response was $error[0]" } } } #Add a label to say process is complete $objLabel1 = New-Object System.Windows.Forms.Label $objLabel1.Location = New-Object System.Drawing.Size(10,100) $objLabel1.Size = New-Object System.Drawing.Size(280,20) $objLabel1.Text = "Deletion complete, check log for more details." $objForm.Controls.Add($objLabel1) #Add a view log button to view the log file $LogButton = New-Object System.Windows.Forms.Button $LogButton.Location = New-Object System.Drawing.Size(50,150) $LogButton.Size = New-Object System.Drawing.Size(75,23) $LogButton.Text = "View Log" $LogButton.Add_Click({Invoke-Item $log}) $objForm.Controls.Add($LogButton) } #Check script was run as admin If (-NOT ([security.Principal.WindowsPrincipal] [security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([security.Principal.WindowsBuiltInRole] "Administrator")) { [system.Windows.Forms.MessageBox]::Show("It doesn't appear you have run this PowerShell session with administrative rights, the script may not function correctly. If no users are displayed please ensure you run the script again using administrive rights.") } #Start the form SetupForm 1
mb2k01 Posted November 27, 2017 Author Posted November 27, 2017 That looks like it works (only tested with a couple of machines so far) I do get the error in PS initially saying: Unable to find type [array]. At C:\script\script.ps1:11 char:9 [Array] $AdminUsers = "Administor", "LocalAdmin" CategoryInfo: Invalid Operation: (array:TypeName) [] RunTimeException FullyQualifiedErrorID: TypeNotFound But afterwards the GUI pops up, allows me to select all and then the script works through
mavhc Posted November 27, 2017 Posted November 27, 2017 That looks like it works (only tested with a couple of machines so far) I do get the error in PS initially saying: Unable to find type [array]. At C:\script\script.ps1:11 char:9 [Array] $AdminUsers = "Administor", "LocalAdmin" CategoryInfo: Invalid Operation: (array:TypeName) [] RunTimeException FullyQualifiedErrorID: TypeNotFound But afterwards the GUI pops up, allows me to select all and then the script works through That's because I can't spell array 1
mb2k01 Posted November 28, 2017 Author Posted November 28, 2017 Ha! ...and I clearly can't spot spelling mistakes! Corrected now and takes away the error. I'm not all that bothered by the GUI still existing, as it gives the tech a final sanity check of profiles that they want to remove. What would be cool to change though... At the minute if I select two machines, the GUI will populate with a list of users from both machines. Once the script runs it will say: C:\Users\User01 has been deleted from Machine01 INFO C:\Users\User01 Profile does not exist on Machine02 If I was to add a few hundred machines in to the process I can see the script being very slow to complete as it would give me up to 199 INFO messages if the profile only existed on one machine. Can you think of a tweak which would change the above behaviour and allow the script to remember which stations the profiles were present on so that it didn't check others once executed?
HPlum78 Posted November 28, 2017 Posted November 28, 2017 (edited) Function Cleanup-UserProfiles <# .Parameter ComputerName The computer on which to clean profiles. .Parameter AgeLimit All profiles older than this number of days will be removed .Parameter Exclude Comma separt #> { [CmdletBinding( SupportsShouldProcess=$true, ConfirmImpact="High" )]param ($computerName= '.',$AgeLimit='60', $Exclude) $dateLimit = (get-date).adddays(-1 * $agelimit) $userprofiles = Get-WmiObject -Class Win32_UserProfile -ComputerName $computerName #the default exclusion list will prevent deletion of Administrator acount, Default accounts, System and Network Service $exclusionlist = @('S-1-5-19','S-1-5-18','S-1-5-20','-500$') + $Exclude | where-object {$_} foreach ($profile in $userprofiles) { #Check if profile is in date range $dateLastUsed = [datetime]::ParseExact(($profile.lastusetime -replace '\..+$',''),'yyyyMMddHHmmss',$null ) if ( $dateLastused -ge $dateLimit){ write-verbose "Skipping $($profile.sid) because it was last used $dateLastUsed" continue; } #Check if profile matches an exclusion $MatchesExclusion = $false foreach ($comparison in $exclusionlist){ if ($profile.sid -match $comparison -or $profile.localpath -match $comparison) { $MatchesExclusion = $true write-verbose "Skipping $($profile.sid) because it matches exclusion '$comparison'" break; } } if ($MatchesExclusion) {continue;} #Use ShouldProcess to prevent accidental removal of profiles $activity = "Remove profile for user $($profile.SID) from computer $Computername with local path $($profile.localpath)" if ($pscmdlet.ShouldProcess($activity)) { Write-Verbose "Attempting to $activity" $profile.Delete() } } } more info here https://gallery.technet.microsoft.com/scriptcenter/Cleanup-UserProfiles-277a8084 I can think of a whole load of extra automation you could add to this script, like building a computer list from AD and some making sure that its accessible before trying to clean up the profiles....... Edited November 28, 2017 by HPlum78 1
mb2k01 Posted November 28, 2017 Author Posted November 28, 2017 Function Cleanup-UserProfiles .... I can think of a whole load of extra automation you could add to this script, like building a computer list from AD and some making sure that its accessible before trying to clean up the profiles....... If you're able to add config in to allow me to specify a list of machines (whether it is in the code itself, or from an external txt), for it to work remotely, I'm happy to give it a go!
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now