[PowerShell] Beutzerprofile auflisten und löschen - Wo hakt es bei mir?

xCtrl

Lt. Commander
Registriert
Dez. 2011
Beiträge
1.568
Hallo in die Runde,

​ich sitze aktuell an einem Power Shell Skript, welches auf meinen Terminal Servern (2008R2, Citrix 6.5, PSv1) vor (oder auch nach) dem täglichen nächtlichen Neustart übrig- und hängen gebliebene Benutzerprofile löschen soll. Ein PS Script zum Abmelden der letzten Benutzer vor dem Neustart habe ich bereits erfolgreich umgesetzt. Verwendet werden auf den TSsen Roaming Profiles, die nach der Abmeldung des Benutzers zurück in den Profilestore geschrieben werden. Zu 3/4 klappt das auch, aber teilweise bleiben Profile aus unterschiedlichen Gründen zurück. Ich lasse das mal so stehen...

​Laut Doku auf TechNet eignet sich eine WMI Abfrage, da diese zusammenhängend die Benutzerprofile samt seiner Registry-Einträge ermitteln kann.

​Bis jetzt komme ich soweit, mit entsprechender Abfrage bestimmte Profilordner ausgenommen, mir die aktuell auf dem Server befindlichen Profile abzufragen und in eine Variable zu packen.

Code:
​# Variablen deklarieren
$profilepath = "C:\Users\"
$admin = "administrator"
$ctx_stream = "Ctx_StreamingSvc"
$currentuser = $env:username

​# Watson, kombiniere Profilpfad mit Usernamen der Profilordner, die von der Abfrage ausgenommen werden sollen
$adminpath = $profilepath + $admin
$ctxpath = $profilepath + $ctx_stream
$myself = $profilepath + $currentuser

​# Abfrage der Benutzerprofile mit entsprechenden Filtern, sodass nur Userprofile als Ergebnis rauskommen
$allprofiles = Get-WMIObject -class Win32_UserProfile | where {(!$_.Special) -and $_.LocalPath -ne $adminpath -and $_.LocalPath -ne $ctxpath -and $_.LocalPath -ne $myself} | select localpath, sid, loaded

​# Gib mir das Ganze mal aus
echo $allprofiles

localpath                      sid                                          loaded
---------                      ---                                           ------
C:\Users\BENUTZERORDNER        S-1-5-21-3609199022-3933120324-22675...         False

​und da habe ich als Beispiel schon direkt einen Übeltäter: ein nicht geladenes Profil, da der Benutzer nicht mehr auf dem Server angemeldet ist.

So, laut Dokumentation bei TechNet sollte mit diesem Befehl das Profil, welches ja durch eine WMI Abfrage ermittelt wurde, gelöscht werden können:

Code:
$allprofiles | Remove-WMIObject
​

Und hier hakt es bei mir. Denn PS meldet in schöner roter Schrift darauf folgendes:

Remove-WmiObject : Das Eingabeobjekt kann an keine Parameter des Befehls gebunden werden, da der Befehl keine Pipelineeingaben akzeptiert oder die Eingabe und deren Eigenschaften mit keinem der Parameter übereinstimmen, die Pipelineeingaben akzeptieren.
Bei Zeile:1 Zeichen:32
+ $allprofiles | Remove-WmiObject <<<<
+ CategoryInfo : InvalidArgument: (@{localpath=C:\...; loaded=False}:PSObject) [Remove-WmiObject], Parame
terBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,Microsoft.PowerShell.Commands.RemoveWmiObject

​Habe ich einen Denkfehler? Ich kann mit der Info im Moment nicht viel anfangen :mad:
Das Quellscript habe ich von Hier (SpiceWorks), allerdings wird das Ganze in einem Zug, ohne Variablen, durchgeackert. Ich habe schon vermutet, dass es daran liegt, weil ich abweichend davon mit Variablen arbeite, aber leider Fehlanzeige. Auch mit Angabe des Parameters ($allprofiles.localpath) meckert er rum. Selbst wenn ich das Skript aus der Quelle so anwende, meldet mir Power Shell das gleiche.

​Hat jemand eine Idee?

​Gruß
 
Zuletzt bearbeitet:
Ich sehe da auf Anhieb keinen Fehler. Falls du es noch nicht kennen solltest, ich habe mit Delprof2 ganz gute Erfahrungen gemacht. Möglichweise ist es ja auch für dich interessant:

https://helgeklein.com/free-tools/delprof2-user-profile-deletion-tool/

Code:
Delprof2 by Helge Klein (http://helgeklein.com)

Delprof2 deletes inactive Windows user profiles (profiles that are not currently loaded).
Delprof2 is syntax compatible to the original Delprof by Microsoft. Unlike the original it works on Windows 7, too (and XP/2003/Vista/2008).
If possible, Delprof2 uses the backup and restore privileges to bypass security and delete even profiles the executing user does not normally have access to.
Delprof2 has not problem whatsoever deleting files in very long paths (longer than MAX_PATH, 260 characters).
Delprof2 also cleans up stale ProfileList SID.bak registry entires, a common cause of temporary profiles.

Usage: delprof2 [/q] [/i] [/p] [r] [/c:[\]] [/d:]

       /q   Quiet, no confirmation
       /i   Ignore errors, continue deleting
       /p   Prompt for confirmation before deleting each profile
       /r   Delete local caches of roaming profiles only, not local profiles
       /c   Delete on remote computer instead of local machine
       /d   Delete only profiles not used in x days
       /l   List only, do not delete (what-if mode)

Examples:

Delprof2 /c:computername

       Deletes inactive profiles on 'computername'.

Delprof2 /c:computername /l

       Lists inactive profiles on 'computername' without deleting them.

Delprof2 /d:30

       Deletes profiles older than 30 days on the local computer.

Delprof2 /r

       Deletes locally cached roaming profiles only.
 
Och Mensch... das Tool habe ich schon mal irgendwo gesehen *in den Browsertabs rumblättert*. Stimmt, habe ich vollkommen aus den Augen verloren die letzten Tage beim Recherchieren. Ich wollte das eigentlich erst zum Schluss nutzen, wenn sich mein Weg als nicht Möglich herausstellt. Der Grund ist einfach: Wieder ein Tool mehr, was man beim Umstieg der TS auf ein aktuelles Windows irgednwann mit auf dem Schirm haben muss, falls das Problem dort auch bestehen bleibt.

​Dennoch danke fürs erinnern :D Ich mag aber meinem Weg noch nicht aufgeben. Immerhin sieht man schon mal keinen Fehler im Skript. Das schürt Hoffnung.

​Gruß
 
Der Fehler liegt wahrscheinlich in dem "select"-Teil dieser Zeile:
Code:
$allprofiles = Get-WMIObject -class Win32_UserProfile |
 where {(!$_.Special) -and $_.LocalPath -ne $adminpath -and $_.LocalPath -ne $ctxpath -and $_.LocalPath -ne $myself} |
select localpath, sid, loaded
Hier machst du aus dem abgerufenen WMIObject ein generisches Powershell-Objekt mit 3 Variablen (localpath, sid, loaded). Damit kann Remove-WMIObject dann wohl nichts mehr anfangen. Das wäre jedenfalls meine Vermutung.
 
Der Fehler musste das hier sein:

"| select localpath, sid, loaded"
Dadurch ist das Objekt in der Variable "$allprofiles" kein wmiObject mehr sondern ein psCustomObject


@scrcrw verdammt, da war ich knapp zu langsam :D
 
Zuletzt bearbeitet: (Zu langsam)
Ergo muss ich den Select rausnehmen? Darauf bin ich tatsächlich noch nicht gekommen.

Code:
$allprofiles = Get-WMIObject -class Win32_UserProfile | where {(!$_.Special) -and $_.LocalPath -ne $adminpath -and $_.LocalPath -ne $ctxpath -and $_.LocalPath -ne $myself}

​echo $allprofiles

__GENUS           : 2
__CLASS           : Win32_UserProfile
__SUPERCLASS      :
__DYNASTY         : Win32_UserProfile
__RELPATH         : Win32_UserProfile.SID="S-1-5-21-3609199022-3933120324... SID GEKÜRZT"
__PROPERTY_COUNT  : 12
__DERIVATION      : {}
__SERVER          : TSSERVER
__NAMESPACE       : root\cimv2
__PATH            : \\TSSERVER\root\cimv2:Win32_UserProfile.SID="S-1-5-21-3609199022-3933120324... SID GEKÜRZT"
LastDownloadTime  :
LastUploadTime    :
LastUseTime       : 20171121211017.586000+000
Loaded            : False
LocalPath         : C:\Users\BENUTZERORDNER
RefCount          : 1
RoamingConfigured : False
RoamingPath       :
RoamingPreference :
SID               : S-1-5-21-3609199022-3933120324... SID GEKÜRZT
Special           : False
Status            : 1

$allprofiles | Remove-WmiObject

​--> Profilordner und Registry-Eintrag sind weg!

​Und siehe da, es funktioniert. OK, so sieht also der Inhalt einer WMI Abfrage aus. Zugegeben, ich habe mich mit dem Select an meinem logoffscript bedient, da ich ja theoretisch nicht alles an Informationen benötige. Aber dass dadurch gleich ein komplett anderer Typ von Ergebnis rauskommt, habe ich nicht geahnt.

​Nächster Schritt, den Vorgang mit mehreren Profilen durchführen.

Danke euch :-)

​Gruß
 
Zuletzt bearbeitet: (Call of Duden)
Wenn du die Ausgabe beschränken willst musst Du nur die einzelnen Eigenschaften aufrufen:
Bsp.:
Echo $allprofiles.loaded
 
Nachdem ich mir die neue Ausgabe angeschaut habe, sehe ich das mittlerweile auch. So langsam werde ich mit Power Shell warm. Ich steige gerade von Batch um in Richtung PS. An einigen Stellen biege ich vom Denken her noch falsch ab :D
 
Als Grundlage, falls es relevant ist, hätte ich noch ein Script von mir, welches alle (Win32_)UserAccounts auflistet, für die ein Profil (Win32_UserProfile) auf dem Rechner existiert.
Hab das nicht wirklich kommentiert, vermute aber mal, dass es für die Personen, die damit arbeiten,verständlich seien sollte, anhand der Bezeichner. Falls doch Fragen aufkommen, gerne fragen.
Kann man, durch auskommentieren des Filters #-Filter "Special = '$false'" in Zeile 45, auch auf echte User (nicht-SystemUser) beschränken.

Mit deinem Script kannst du z. B. nicht alles abdecken allein durch die Kombination von C:\Users und $UserName.
Gibt ja oft Situationen, bei denen der AD Account neu angelegt wurde (sei es durchs migrieren).
Dadurch bekommt der AD Account eine neue SID und dementsprechend einen neuen UserProfile-Path (= $Username mit Suffix), wie man es auch von der Migration von WinXP auf Win7 kenn => ...v2

Code:
#region user-specific filters
$DomainXY = 'domainxy' 

$ExcludedAccounts = @( # in Pre-Windows2000 Format e.g. MyDomain\MyUserName
	"$env:COMPUTERNAME\administrator" 
	"$DomainXY\MeinName" 
) 
#endregion

#region Definition of several types and status
$ProfileStatusBits = ([ordered] @{  # see Status on https://msdn.microsoft.com/en-us/library/ee886409(v=vs.85).aspx
		'0' = 'Undefined' # The status of the profile is not set.                   
		'1' = 'Temporary' # The profile is a temporary profile and will be deleted after the user logs off.                   
		'2' = 'Roaming' # The profile is set to roaming. If this bit is not set, the profile is set to local.                   
		'4' = 'Mandatory' # The profile is a mandatory profile.                   
		'8' = 'Corrupted' # The profile is corrupted and is not in use. The user or administrator must fix the corruption to use the profile.                   
	} 
) 
$ProfileStatusBitsEnumerator = $ProfileStatusBits.GetEnumerator() 

$AccountTypeBits = ([ordered] @{   # see AccounType on https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
		# Temporary duplicate account         
		'256' = 'UF_TEMP_DUPLICATE_ACCOUNT' # Local user account for users who have a primary account in another domain. This account provides user access to this domain only—not to any domain that trusts this domain.         
		# Normal account         
		'512' = 'UF_NORMAL_ACCOUNT' # Default account type that represents a typical user.         
		# Interdomain trust account          
		'2048' = 'UF_INTERDOMAIN_TRUST_ACCOUNT' # Account for a system domain that trusts other domains.         
		# Workstation trust account         
		'4096' = 'UF_WORKSTATION_TRUST_ACCOUNT' # Computer account for a computer system running Windows that is a member of this domain.         
		# Server trust account         
		'8192' = 'UF_SERVER_TRUST_ACCOUNT' # Account for a system backup domain controller that is a member of this domain.         
	} 
) 
$AccountTypeBitsEnumerator = $AccountTypeBits.GetEnumerator() 

$ProfileHealthStatus = ([Ordered] @{ # see HealthStatus on https://msdn.microsoft.com/en-us/library/ee886409(v=vs.85).aspx
		'0' = 'Healthy' 
		'1' = 'Unhealthy' 
		'2' = 'Caution' 
		'3' = 'Not Applicable' 
	} 
) 
#endregion

$Profiles = Get-CimInstance Win32_Userprofile #-Filter "Special = '$false'"     

$Results = @() 
Foreach ($Profile In $Profiles) { 
	If ($Profile.SID -match '^S\-\d*\-\d*\-\d*$') { # If matching well-known SID    
		$Account = Get-CimInstance Win32_SystemAccount -Filter "SID = '$($Profile.SID)'" 
	} Else { 
		$Account = Get-CimInstance Win32_UserAccount -Filter "SID = '$($Profile.SID)'" 
	} 
	
	If (-not($Account.Caption -in $ExcludedAccounts)) { 
		$ProfileStatusInt = $Profile.Status 
		$ProfileStatusText = @() 
		If ($ProfileStatusInt -eq '0') { 
			$ProfileStatusText = $ProfileStatusBits.'0' 
		} Else { 
			Foreach ($Bit In $ProfileStatusBitsEnumerator) { 
				If ($ProfileStatusInt -band $Bit.Key) { 
					$ProfileStatusText+= $Bit.Value 
				} 
			} 
		} 
		
		$AccountTypeInt = $Account.AccountType 
		$AccountTypeText = @() 
		Foreach ($Bit In $AccountTypeBitsEnumerator) { 
			If ($AccountTypeInt -band $Bit.Key) { 
				$AccountTypeText+= $Bit.Value 
			} 
		} 
		
		$Results+= New-Object psobject -Property([ordered] @{ 
				Caption = $Account.Caption 
				FullName = $Account.FullName 
				SID = $Account.SID 
				AccountType = $AccountTypeText
				Disabled = $Account.Disabled 
				LocalAccount = $Account.LocalAccount 
				UserProfileHealthStatus = $ProfileHealthStatus.$($Profile.HealthStatus.toString()) 
				UserProfileLastUseTime = $Profile.LastUseTime 
				UserProfileIsLoaded = $Profile.Loaded 
				UserProfileLocalPath = $Profile.LocalPath 
				UserProfileRoamingConfigured = $Profile.RoamingConfigured 
				UserProfileRoamingPath = $Profile.RoamingPath 
				UserProfileIsSpecial = $Profile.Special 
				UserProfileStatus = $ProfileStatusText
			} 
		) 
	} 
} 

$Results #| ogv

Zum Löschen via WMI Befehl, muss das Ganze als If-Bedingung innerhalb der ForEach-Schleife realisiert werden. Man darf nicht das Custom-PSObject $Results als Grundlage nehmen... wurde ja bereits von scrcrw erwähnt.
 
Zuletzt bearbeitet:
Guten Morgen,

dein Skript ist auf jeden Fall Umfangreich.

Zum Löschen via WMI Befehl, muss das Ganze als If-Bedingung innerhalb der ForEach-Schleife realisiert werden. Man darf nicht das Custom-PSObject $Results als Grundlage nehmen...

​Siehe dazu den Post #6. Genau das hatte mir das Genick gebrochen. Nachdem der Select raus war, funktionierte das Ganze, da ja als Ergebnis WMI Daten kamen.

​Ich hänge mal mein fertiges Skript an.

Code:
# Parameter definieren
param(
[string]$CitrixServer = "0")

Write-Host
Write-Host Server ist $CitrixServer
Write-Host

# Deklariere Variablen
$profilepath = "C:\Users\"
$admin = "administrator"
$ctx_stream = "Ctx_StreamingSvc"
$currentuser = $env:username

# Watson, kombiniere Profilpfad mit Usernamen der Profilordner, die von der Abfrage ausgenommen werden sollen
$adminpath = $profilepath + $admin
$ctxpath = $profilepath + $ctx_stream
$myself = $profilepath + $currentuser


# Abfrage der Benutzerprofile mit entsprechenden Filtern, sodass nur Userprofile, die nicht mehr geladen sind, als Ergebnis rauskommen
$oldprofiles = (Get-WMIObject -computername $CitrixServer -class Win32_UserProfile | where {(!$_.Special) -and $_.LocalPath -ne $adminpath -and $_.LocalPath -ne $ctxpath -and $_.LocalPath -ne $myself -and $_.Loaded -eq $false})

# Gib mir das Ganze mal aus
echo $oldprofiles


# lösche die gefundenen Profile
write-host Profile werden gelöscht
$oldprofiles | Remove-WMIObject

​Die Abfrage filtert die Systemprofile, den "Administrator" und "Ctx_StreamingSvc"​ raus, da diese nicht in der Auflistung auftauchen sollen. Zusätzlich filtere ich nach Status "Loaded" False, denn a.) die Profile sind nach dem Neustart des Servers vorhanden, aber nicht geladen, b.) kann ich das Skript auch während der Arbeitszeit per Hand durchlaufen lassen, um zu sehen, welches Profil mal wieder hängt. Ich kommentiere dann einfach den Löschbefehl aus :D
 
Zuletzt bearbeitet:
Zurück
Oben