Jump to content

Recommended Posts

Posted

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

Posted

You can already do a list of computers, just change

 

[array] $Computers = "LIBRARY-04"

 

to

 

[array] $Computers = "LIBRARY-04","LIBRARY-05","..."

  • Thanks 1
Posted
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!

Posted

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?

  • Thanks 1
Posted

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

  • Thanks 1
Posted

You've quickly hit the top of my Powershell ability! :confused2:

Where would I put that code? (Any chance you could re-post the original code with the adaptations?)

Posted
# 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

  • Thanks 1
Posted

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

Posted
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

  • Thanks 1
Posted

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?

Posted (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 by HPlum78
  • Thanks 1
Posted
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!

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 account

Sign in

Already have an account? Sign in here.

Sign In Now



×
×
  • Create New...