Showing posts with label windows 7. Show all posts
Showing posts with label windows 7. Show all posts

Tuesday, May 17, 2016

Winmgmt ResetRepository File Already Exists

Solution

This error comes up because the WMI repository has been reset too many times, and windows runs out of file names.

Under "C:\windows\system32\wbem\" you'll see a bunch of "repository.###" folders. These are the backups windows makes of the repository whenever you reset it. However this seems arbitrarily limited to three digits, so once it hits repository.999 it can no longer back the wmi repo up before the reset, so the reset fails.

The quick and dirty solution is to delete the various repository.### folders then try the reset again. Better solution is to find out why the repository is having to be reset so often and try to fix the underlying cause of the WMI corruption.

Full Story

I have a number of problems with WMI repositories becoming corrupt and requiring to be reset. I've never been able to track down the precise cause of these corruptions, so I've just been doing my best to keep things up and running.

Today I ran into a fun problem. I had a VM that wouldn't allow logins (or logins would take forever). Closer inspection saw that it had the telltale signs of WMI problems (1 CPU core maxed out, high memory usage). I rebooted to safe mode to try the reset, but the reset (winmgmt /resetrepository) failed with the error.

WMI repository reset failed. Windows cannot create a file if that file already exists

Or something along those lines. I forgot to write down the exact error before rebooting the machine, sorry. 

Further investigation found the hundreds of "repository.###" folders in the wbem directory. Deleting these folders allowed the reset to run correctly. VM rebooted to normal mode without issue.

Tuesday, October 21, 2014

PowerShell: Managing User Profiles on Remote Machines

 Introduction

If you're like me, you're a charismatic force of nature whom everyone loves unconditionally. However, if you're job is like mine, you may often finding yourself wanting to remotely remove a user's  profile cache from a machine.

Profile cache is something a roaming domain profile creates when a user first logs into a machine. This holds the user's "CURRENT_USER" registry hive (ntuser.dat) as well as any profile folders that are not being redirected. If you're domain is setup to download redirected folders (offline file sync) it also stores those.

Why would you want to remove that? Various reasons. My usual use case is that one of my terminal servers (RDS) is running out of disk space. We have upwards of several thousand students that could potentially use our servers, but usually only a few hundred will use them in a given week. This means provisioning out enough space to hold several thousand user profiles (this would be several TB) would be too expensive and would never be necessary with proper profile management. Other reasons to remove local caches are: Corrupt profile, Old/bad settings stored in appdata, long login/other login problems, possibly security (but only if password caching is enabled) .

So how do we clear them. Windows has two native ways to do this, manually through the system properties interface, or through group policy using the "remove profiles older than x days" GPO. The first method works, but is limited; you must be logged into the machine (locally or rpd), you can only delete one profile at a time -- it taking several seconds to remove each profile, this method takes a really long time to remove a large number of profiles -- and the "manage user profiles" window can take a really long time ( 30 minutes or more) to actually open when there are hundreds of profiles on the machine. The GPO method is a bit better, but has two major caveats. First, the machine must be rebooted for the purge to run -- this means it cannot be done on demand when the system is in use -- and it cannot target specific accounts.

So we have a situation in which no real tool exists to do what we want. DelProf2 is one option I've looked at before, and while I'm sure it's a perfectly functional tool, as a rule I don't like running software to automate tasks when I don't know exactly what it's doing (from their change log it appears to be a very manual process). So from the short-comings methods above we want the following features
  • Ability to delete multiple profiles quickly
  • No reboot required
  • Command line for batching/automation
  • Ability to target specific profiles, or delete all older profiles
  • Fully remove all registry info and files of user's account
  • Do not have to be logged in / can be done remotely
Turns out all of this can be done via PowerShell and WMI.

 Enter PowerShell

First things first, here is the full code for the three different functions so you can follow along. They should be pretty easy to read, they're commented and have full get-help integration (except the first one, because it's very basic).


edit (7/17/15): Moved code to a local page. There was some issue with pastebin.
http://bisbd.blogspot.com/2015/07/code-dump-get-userprofile-remove.html

The last one I've already talked about on a previous post. It's a simple function to convert UTC time strings into a datetime object that PowerShell understands.

Lets look at the next one then.

Get-UserProfile.

I'm going to skip over the get-help information, as doing so would be redundant. So the first bit of code is this:

    [CmdletBinding()] 
      param( 
     [Parameter(Mandatory=$False)][string]$UserID="%",
     [Parameter(Mandatory=$False)][string]$Computer="LocalHost",
     [Parameter(Mandatory=$False)][switch]$ExcludeSystemAccounts,
     [Parameter(Mandatory=$False)][switch]$OnlyLoaded,
     [Parameter(Mandatory=$False)][switch]$ExcludeLoaded,
     [Parameter(Mandatory=$False)][datetime]$OlderThan   
     
    )

These are our Cmdlet bindings. They allow us to pass parameters cleanly to the function. Notice nothing here is mandatory. If no parameters are passed to the function it defaults to return all profiles on the local system. There UserID and Computer parameters are given default values if none is specified ('%' is WMI speak for "all" -- analogous to '*' or '.*' in most regex systems)

Next:

if(!(Get-Command Convert-UTCtoDateTime -ErrorAction SilentlyContinue)){
    write-host -BackgroundColor "Black" -ForegroundColor "Red" "################################################################################"
    write-host -BackgroundColor "Black" -ForegroundColor "Red" "#                                                                               "
    write-host -BackgroundColor "Black" -ForegroundColor "Red" "This Program Requires cmdlet ""Convert-UTCtoDateTime""                          "
    write-host -BackgroundColor "Black" -ForegroundColor "Red" "Find it here:                                                                   "
    write-host -BackgroundColor "Black" -ForegroundColor "Red" "http://bisbd.blogspot.com/2014/10/adventures-in-powershell-converting-utc.html  "
    write-host -BackgroundColor "Black" -ForegroundColor "Red" "#                                                                               "
    write-host -BackgroundColor "Black" -ForegroundColor "Red" "################################################################################"
    break;
}

Here we check to make sure our dependent function is loaded. If not, it displays this lovely warning with a link.
Next:
if($Computer.ToLower() -eq "localhost"){
    
    
    $Return = Get-WmiObject -Query "Select * from win32_userprofile where LocalPath like '%\\$UserID'" 
    

}
else{
    $Return = get-wmiobject -ComputerName $Computer -Query "Select * from win32_userprofile where LocalPath like '%\\$UserID'" 
}

OK, first real code. here we do a quick check to see if we're running on the localhost or against a remote machine. The WMI query we run is the same for both, but the parameters we pass to the get-wmiobject function are slightly different.

On my machine doing -ComputerName $Computer actually works with 'localhsot', but only because 'localhost' is defined as 127.0.0.1 in the default hosts file. This might be a safe assumption to make on any windows system, but I try not to make assumptions when I can.

About the WMI query. For anyone familar with SQL this will probably look pretty familiar, the terminology is a bit different though.  We're selecting everything from the class "win32_userprofile" where the class "localpath" matches our UserID with a backslash in front of it. The backslash is there to prevent UIDs which are a substring of another UID from returning erroneous results. for example: if you had users 'bob' and 'jimbob' searching for 'bob' would return both bob and jimbob without the slash in front.

Here's a command to run to see all what is returned by this query.

Get-WmiObject -Query "Select * from win32_userprofile"

What you'll see, if this runs correctly, is a mess. There's only a few properties in here that are useful (which we'll filter out in a minute). A noticeable exclusion here, you'll notice, is that there is no Username type field. I just wanted to point this out here because it might seems strange to be looking at the LocalPath property otherwise.

So after this block of code we have a full list of user profiles stored in our "$Return" variable. Now it's time for some filtering.

#Filter System Accounts
if($ExcludeSystemAccounts){
    $Return = $Return | Where-Object -Property Special -eq $False
}
#Filter out Loaded Accounts
if($ExcludeLoaded){
    $Return = $Return | Where-Object -Property Loaded -eq $False
}
#Filter otherthan loaded accounts
if($OnlyLoaded){
    $Return = $Return | Where-Object -Property Loaded -eq $True
}


Here are the first three filters. They're all pretty much the same. First they check if they're switch has been set, then use where-object to filter out certain properties. I do these all as individual if statements that modify the $return variable so that they can be chained together.

The two properties we're looking at here are "Special" and "Loaded". The Special property tells us if the account if an account is a non-user (i.e. system) account. You'll see things like "system", "network service", etc. listed as special. The "Loaded" property tells us if the account is currently in use. This property will be important later as you can't remove accounts that are currently loaded.

My inclusion of a "OnlyLoaded" flag might seem strange here. This is not directly related to the removal of user accounts, but an additional functionality. Combine "-OnlyLoaded" and "-ExcludeSystemAccounts" and you can find out what user(s) is(are) logged into the machine. Neat!

Let's look at the last filter now.
#Filter on lastusetime
if([bool]$OlderThan){
$Return | Where-Object -property LastUseTime -eq $Null | % {Write-Host -BackgroundColor "Black" -ForegroundColor "Yellow" $_.LocalPath " Has no 'LastUseTime', omitting" }
$Return = $Return | Where-Object -property LastUseTime -ne $Null
$Return = $Return | Where-Object {$(Convert-UTCtoDateTime $_.LastUseTime -ToLocal) -lt $OlderThan }
}

This one has a bit more going on.

The if statement looks a bit different. Because the variable is a "system.datetime" object rather than a boolean, I'm typecasting it as a boolean. If the variable has been populated, this returns true, if the variable is $Null (that is, was not set), then it returns false. The type casting isn't strictly necessary simply doing "if($OlderThan)" would return the same thing. This is mostly just for readability.

The next lines warn the user that it's skipping over any user accounts with a $Null "lastusttime" property. This is one aspect that may need to be modified in the future, but I don't think so. I have never seen a $Null LastUseTime on an actual user account. Mostly it shows up on accounts created by programs. For example, my computer has ".Net v4.5 Classic", "DefaultAppPool",  and ".Net v4.5" as accounts with no lastusetime. Even local users who have been created, but never logged in, won't get caught by this; this is because they don't show up at all until their first logon, at which point they'll get a "lastusetime".

Finally, after filtering out the $Null entries, we convert the LastUseTime to a datetime object and compare it to the datetime passed to the -OlderThan parameter. By default the lastusetime is a very ugly string that is difficult to make sense of at a glance. More importantly, to do any sort of date math, powershell needs it in a datetime object. So this is where the Convert-UTCtoDateTime function comes in to play. This function takes the ugly UTC string and turns it into something powershell can understand.

One caveat here, the -ToLocal flag turns out to be important. When doing date math, powershell evidantly doesn't take time zones into consideration. So it is necessary to have both dates be in local time before doing math, otherwise it might not behave as expected. See the following example:



Next, and final block:

if($PSBoundParameters['Verbose'])
{
Write-Output $Return
}
else{
 Write-Output $Return | Select SID,LocalPath,@{Label="Last Use Time";Expression={Convert-UTCtoDateTime $_.LastUseTime -ToLocal}}    
}


Here we process our output. I've added support here for the powershell verbose flag. -Verbose is a native flag in powershell, which all Cmdlets have, even if they don't implement them. Normally this is used with the Write-Verbose Cmdlet, but I've done a little more. I didn't just want to write something additional when verbose is set, but wanted it formatted differently, that is, unformatted. This is necessary for this Cmdlets integration with the Remove-UserProfile Cmdlet. So I check to see if verbose has been set, if it has simply return the $Return variable. If it is not set by verbose, I format the output to look nice and show only the relevant information.

The relevant information here is the SID (Profile unique identifier), the LocalPath (c:\users\myuser), and the LastUseTime. The LastUseTime I modify with Convert-UTCtoDateTime to make it look nicer and be more useful at a glance.

That's about all there is to Get-UserProfile. Next We'll look at Remove-UserProfile, which uses Get-UserProfile.

Remove-UserProfile

Remove-UserProfile is very much an extension of Get-UserProfile. At a high level, it uses Get-UserProfile to obtain a list of user profiles then deletes them. That's really about it. Obviously there's a few checks and things in here as well, so lets go through that.

$ProfileList = Get-UserProfile -Verbose -UserID $UserID -Computer $Computer -ExcludeSystemAccounts -OlderThan $OlderThan

Since we've already done all the big work in the Get-UserProfile Cmdlet, all we need to do is call it with the appropriate flags. We use verbose so we get the full object, not just the filtered information. We exclude system accounts because we don't want to delete those for what I hope are obvious reasons -- I'm not sure that it'd actually let you, but better to be safe. We also use the -OlderThan flag regardless of whether the user has actually specified this.

Looking back at the parameter bindings, you see I've included a default value for $OlderThan that is one day in the future. This is for a couple of reasons. First, it's way more readable, no nested if statements with different querys. Second, this filters out the not-system-but-also-not-user accounts. I haven't tried removing these accounts to see what would actually happen, but I'm sure .net would be none too happy about it.

Next block

    if(!$ProfileList){
        Write-Warning "NO USER PROFILES WERE FOUND"
        RETURN;
    }

This is a simple $Null check to make sure the query actually returned something. If no profiles matched the criteria, the script exits.


    if(!$Batch){
        Write-Warning "ABOUT TO REMOVE THE FOLLOWING USER ACCOUNTS"
        Foreach($User in $ProfileList){
            $User | Select SID,LocalPath,@{Label="Last Use Time";Expression={Convert-UTCtoDateTime $_.LastUseTime -ToLocal}}
        }
        $Title = "PROCEED?"
        $Message = "ARE YOU SURE YOU WANT TO REMOVE THE LISTED USER ACCOUNTS?"
        $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes","Removes User Accounts"
        $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No","Exits Script, No Changes Will be Made"
        $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
        $result = $host.ui.PromptForChoice($title, $message, $options, 1) 
        switch ($result)
        {
            0 {}
            1 {return;}
        }

    }

The next bit here is a confirmation dialog. This is a built in PowerShell feature you can read more about here, but a few quick notes about my implementation. First, if the -Batch flag is set, it skips this. This is important as otherwise the script would always require user confirmation which would make it far less useful from an automation standpoint.

The foreach loop here lists out (in a nice format) all the user profiles to be deleted. This is a nice sanity check for the user to make sure they know what they're deleting.

On the choices, "$yes"/0 does nothing, and $no/1 exits the script, with the default being no. I wrote it this way to make the coding easier. With this continue/exit method, the rest of the Cmdlet doesn't have to be imbedded within the "switch($result)" block; which makes the mode much more readable and the -Batch code easier to write.


    Foreach($User in $ProfileList){
        if($User.Loaded){
            if(!$Batch){
            Write-Host -BackgroundColor "Black" -ForegroundColor "Red" "User Account " $User.LocalPath "is Currently in user on" $Computer ":`tSkipping"
            }
            else{
            Write-Output "User $($User.LocalPath) on $($Computer) was in use and could not be removed"
            }
            continue;
        }
        if(!$Batch){
        Write-Host -BackgroundColor "Blue" -ForegroundColor "Green" "Removing User $($UserID.LocalPath) from $($Computer)"
        }
        else{
        Echo "Deleting $($User.LocalPath) from $($Computer)"
        }
        $User.delete()


    }

Now we get into the actual deleting. A simple foreach loop that deletes everything that was returned by the Get-UserProfile Cmdlet. A few things to look at in here. I use if(!$Batch) in couple places. This is for formatting reasons. The only difference between the batch and non-batch output is the method of writing. Batch uses Write-Ouput (aka echo) which is nice because it can be redirected to a log file. However Write-Ouput lacks a lot of formatting options. So in non-batch mode I use Write-Host, which cannot be redirected to a file, but gives us some formatting/coloring options to make the output more readable.

Next, lets look at the if($User.Loaded). As discussed in Get-UserProfile, the loaded property tells us whether or not the profile is currently in use. It's important to filter these out otherwise PowerShell will throw errors when you try to delete the profile. Why not use the -ExcludeLoaded flag we created in Get-UserProfile? I debated about this for awhile actually, but decided it would be frustrating if you were trying to delete a specific profile and the script kept saying "no profiles found". This way provides more information, even if it wastes a bit more time.

And lastly, we delete the profile. "$User.Delete()" is really all it takes.

These three functions are about 250 lines all together. And you could get all the same functionality in this.

  Get-WmiObject -Computer MyComputer.mydomain -Query "Select * from win32_userprofie where LocalPath like '%\\MyUser'" | % {$_.Delete()}

Not entirely sure my aim in pointing this out. Maybe that there's a tradeoff between writing something you know, and something other people could use?

Anyway, hope someone else can get some use out of this. I know it's something that's bugged me for a long time. 

Tuesday, March 25, 2014

Upgrading DE45-HG to windows 7 with bootable usb

Here's an interesting problem I ran into trying to upgrade our Visix (Digital Signage) player to windows 7. With the XP EoL coming up, they're pushing for us to get everything upgraded off XP. Problem is the Visix players stayed on XP way longer than they should have, so even relatively new devices are running XP.

Most of the upgrades have been pretty seamless, but hit a snag with a small form-factor device. Its model number is DE45-HG, from what I can tell it's made by AOpen, but with some customization from Visix. Problem is that it doesn't have a DVD drive and won't boot from USB.

First issue is that the POST is hidden behind a "Digital Engine" logo so you can't see what keystrokes get you into the BIOS. Through a little trial and error I found it was "delete" to get into the BIOS. Unfortunately under the boot options only the main SATA drive and the network are listed as boot devices.

The workaround is something like this. With the usb plugged in, get into the BIOS and go to boot > boot device priority; here you will only see the SATA drive (and network, if that's enabled) listed. Go back and select boot > Drive configuration. Here you should see the SATA drive and the USB drive listed. Change the USB drive to "drive 1" (Sata will be reassigned drive 2). Go back to boot priority and now you should see only the USB drive listed as a boot device.

Save changes and exit and the machine should boot to the USB to do the install. Proceed with the install as normal. The first time the machine reboots to continue install, you will need to switch the boot devices back. Get into BIOS, got to boot > drive configuration, and change the SATA drive back to drive 1. Verify it now shows up as the only boot device. Save changes and exit. The setup should now continue like a normal win 7 install.

Update: Also found that when updating the display drivers the screen would go black until the computer was rebooted. Recommend installing display driver separately (without other updates) then hold down power button to reboot after HDD activity light stops.

Monday, September 23, 2013

Powershell Rename-Computer and Add-Computer firewall -- non-domain joined machine

Had a challenge today where I needed to join a bunch of machines to our domain (a bunch of cloned VMs to be exact). I found I could do this through a pretty simple powershell script as follows.

$credlocal = get-credential
$creddomain = get-credential

$machinelist = import-csv c:\temp\machines.csv

foreach($line in $machinelist)
{
 
    add-computer -computername $line.IP -localcredential $credlocal -domain my.domain -domaincredential $creddomain -Newname $line.Hostname -restart

}
Where machines.csv is a CSV file with Hostname/IP pairs and a header row. It prompts for credentials twice, once for the local/built-in admin, then one with domain-join permission

However, when I actually tried to run this I got the following error.

add-computer : Cannot establish the WMI connection to the computer 'xxx.xxx.xxx.xxx' with the following error
message: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)). 
At C:\temp\renamecomputer.ps1:8 char:5
+     add-computer -newname $line.Hostname -computername $line.IP -localcredent ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (xxx.xxx.xxx.xxx:String) [Rename-Computer], InvalidOperationException
    + FullyQualifiedErrorId : RenameComputerException,Microsoft.PowerShell.Commands.RenameComputerCommand
This error was pretty simple, because these machines are not domain joined, the $c/$admin/etc shares are not enabled. You can enable the pretty simply see here:

http://everydaynerd.com/how-to/fix/windows-7-enable-admin-share

After that though, I was getting:

 add-computer : Cannot establish the WMI connection to the computer 'xxx.xxx.xxx.xxx' with the following error
message: The RPC server is unavailable. (Exception from HRESULT: 0x800706BA).
At C:\temp\renamecomputer.ps1:8 char:5
+     add-computer -newname $line.Hostname -computername $line.IP -localcredent ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (xxx.xxx.xxx.xxx:String) [Rename-Computer], InvalidOperationException
    + FullyQualifiedErrorId : RenameComputerException,Microsoft.PowerShell.Commands.RenameComputerCommand
This error turned out to be less straight-forward to fix. The short version is, you have to allow RPC calls through the firewall (something AD does automatically, either by design or by settings that have been in our domain long before I started working here) which isn't really easy because of Asynchronous callback. Basically, all incoming RPC calls go to port 135, no problem, you can open that. But so that calls don't tie up that port, all the first request returns is a new port for all future calls to come in on - this is more problematic because this appears to be ANY non-standard port; you can't very well open the firewall to all ports about 5000. Also, all of the remote-management rules that are in advfirewall (disabled) don't see to have any effect on this problem. Luckily after some digging, I found you can use program-based firewall rules to get things going.

This is article is for windows 8.1 embeded, so step #2 doesn't work on win 7, but everything else seems to run. Also, step #1 is the same reg key created from the last link.

http://msdn.microsoft.com/en-us/library/jj980508(v=winembedded.81).aspx

I re-ran the command after each step, mine started working after step 5, so I think that's the key rule, but the others don't seem to hurt. Of course, this is something that has to be in the master image, so now I have reclone about 50 VMs but hey, testing things like this before the cloning is overrated.