Mick's IT Blogs

My blog is here to help solve issues I have encountered and solved, publish scripts I have written, and educate others in understanding areas that are not well covered.

Mick's IT Blogs

Information Technology Zone

03 October 2014

PowerShell: Windows Reboot Verification Script

The firm I work for does a weekly reboot. As we revamped our SCCM and AD, it was time to revisit the reboot process. I decided to use PowerShell in conjunction with SCCM to handle this process. To make the process easier to maintain and verify, I split it into two parts. There is the reboot script and the verification script. The reboot script creates an empty log file called NotRebooted.log and reboots the PC. It also deletes any file that is from the previous successful reboot. The verification script looks for the NotRebooted.log file and renames it to Rebooted--02-Oct-2014.log for instance. The filename tells that it was rebooted and the date it happened. This provides for an easy verification without having to search through the event viewer logs. To set this up in SCCM, I created two deployments, Reboot and RebootVerify. I setup RebootVerify to run Reboot first. Of course the Reboot is also set to look for a reboot by the program and not SCCM. That is all that is to doing this. IMO, this makes it much easier to track reboots, at least it has here where I work.

Here are the links to download the scripts:


22 August 2014

PowerShell: Enable Bitlocker on a Dell System

After much research and troubleshooting, here is how to enable bitlocker on a Dell system, including clearing the TPM. The documentation by Dell, Trusted Computing Group, and advice from this thread and this one say that it must be done with physical presence through the BIOS screen. There is a way to do this without having to go through the BIOS. Dell has added three additional settings in the BIOS, tpmppiacpi, tpmppipo, and tpmppidpo. If you enable all three of these settings, then you can clear the TPM ownership without having to physically go into the BIOS. There is a catch. When you clear the TPM, you will be prompted with the screenshot below if you want to accept clearing the TPM when the machine reboots. Once you hit F12, the system will continue. I didn't actually find this to be an issue because the Bitlocker process is next to the last process in the build, so once a technician hits F12, it is only a couple more minutes before the build process is complete. Here is a screenshot of what appears:

In the script that I wrote to clear the TPM, I discovered that it only requires one command to clear it. This documentation from Microsoft's developers center has the list of values for SetPhysicalPresenceRequest and what each value does. Unlike what others were posting, I found that I only needed to use value 5, which is clear TPM.

Now to the actual process. I have created a Powershell script for each step in the process with the script sequentially numbered so you know the process to execute them. Here is the process of enabling TPM, including clearing ownership with a hyperlink to each script in the process:

  1. Enable BIOS Password
  2. Restart
  3. Turn TPM On
  4. Restart
  5. Activate TPM ACPI Support
  6. Restart
  7. Activate PPI Provision
  8. Activate PPI Deprovision
  9. Restart
  10. Clear TPM Ownership
  11. Restart
  12. Activate TPM
  13. Restart
  14. Enable Bitlocker (Manual through Control Panel of MDT/SCCM Task Sequence)

21 August 2014

MDT: Windows Could Not parse or process unattend answer file for pass

This error message was caused, at least in my situation, by the addition of Internet Explorer 11 into the build. It was not integrated, but installed in the golden image as an application. When applying the golden image, the above mentioned error occurred. Luckily, this thread came up with the resolution to comment out <IEWelcomeMsg>false</IEWelcomeMsg> located in the unattend.xml file in the %deployroot%\control\<ImageName> folder. Once I did that, the issue was resolved. You can comment it out by changing the line as follows:

<IEWelcomeMsg>false</IEWelcomeMsg>

TO

<!-- <IEWelcomeMsg>false</IEWelcomeMsg> -->

18 August 2014

PowerShell: Transferring data between user profiles

Sometimes when you use USMT, it fails for one reason or another. This script is here to transfer user files from one profile to another. It was written so that in the event USMT fails, there is still a means to automate the transfer of user data.

The first step is to login as the end user, or have then login, on the new machine. They can log right back out. This is to create the user profile on the new PC. Once the user has done this, you can now proceed with running the script. It uses robocopy to move the files over. I have also incorporated using PSEXEC to initiate the robocopy command so that the data goes directly from the source to the destination, with no intermediary to slow down the transfer, especially if you are transferring data in a remote office. 

One more feature I have included in the script is the capability of transferring data from an old, renamed profile to a newly created profile on the same machine. 

Before you run this script, you will need to install PSEXEC at some network location for the script to use and also go through and select what you want excluded in the transfer. I have also made the transfer create a log file, which will also need to be customized to your environment. 

You can download the script from here.



 <#   
   Author: Mick Pletcher  
   Date: 04 March 2014  
   Synopsis: This script will robocopy specific user data from one profile to another on  
                   the same machine, or to a new machine. It will require user input in the following  
                 format:  
                 1) Have the user logout  
                 2) Rename the user profile to <profile>.old  
                 3) Have the user log back in  
                 1) Define Global Memory  
                 2) Get the relative path  
                 3) User input  
                      a) User profile to copy  
                     b) Is the profile to be copied to a new machine  
                     c) If yes, what is the computer name of the new machine  
                 4) Get the OS version of the source machine  
                 5) If copying to a new machine, get the OS version of destination machine  
                 6)   
 #>   
   
 #Define Global Variables  
 Set-Variable -Name AdminPassword -Scope Global -Force  
 Set-Variable -Name AdminUsername -Scope Global -Force  
 Set-Variable -Name DestinationComputer -Scope Global -Force  
 Set-Variable -Name DestinationProfile -Scope Global -Force  
 Set-Variable -Name RelativePath -Scope Global -Force  
 Set-Variable -Name SourceComputer -Scope Global -Force  
 Set-Variable -Name SourceProfile -Scope Global -Force  
   
 Function GetRelativePath {   
      $Global:RelativePath = (split-path $SCRIPT:MyInvocation.MyCommand.Path -parent)+"\"   
 }   
   
 Function GetUserInput {  
   
      #Define Local Memory  
      Set-Variable -Name Message -Scope Local -Force  
      Set-Variable -Name No -Scope Local -Force  
      Set-Variable -Name Options -Scope Local -Force  
      Set-Variable -Name Result -Scope Local -Force  
      Set-Variable -Name Title -Scope Local -Force  
      Set-Variable -Name Username -Scope Local -Force  
      Set-Variable -Name Yes -Scope Local -Force  
   
      $Username = Read-Host "Enter the username"  
      $Global:SourceComputer = Read-Host "Enter the computer name of the source system"  
      $Title = ""  
      $message = "Is the profile to be copied to a different machine?"  
      $Yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Copy files to profile on different machine"  
      $No = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Copy files to new profile on same machine"  
      $Options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)  
      $Result = $host.ui.PromptForChoice($title, $message, $options, 0)   
      If ($Result -eq 1) {  
           $Global:DestinationComputer = $Global:SourceComputer  
           $Global:SourceProfile = Read-Host "Enter renamed profile name"  
           $Global:SourceProfile = "\\"+$Global:SourceComputer+"\c$\users\"+$Global:SourceProfile  
           $Global:DestinationProfile = "\\"+$Global:DestinationComputer+"\c$\users\"+$Username  
      } else {  
           $Global:DestinationComputer = Read-Host "Enter the computer name of the new machine"  
           #$Global:SourceProfile = $Env:systemdrive+"\users\"+$Username  
           $Global:SourceProfile = $Username  
           $Global:DestinationProfile = "\\"+$Global:DestinationComputer+"\c$\users\"+$Username  
   
      }  
      $Global:AdminUsername = Read-Host "Enter administrator username"  
      $Global:AdminPassword = Read-Host -AsSecureString "Enter administrator account password"  
      $Global:AdminPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Global:AdminPassword))  
      $Global:AdminUsername = "[Domain]\"+$Global:AdminUsername  
   
      #Cleanup Local Memory  
      Remove-Variable -Name Message -Scope Local -Force  
      Remove-Variable -Name No -Scope Local -Force  
      Remove-Variable -Name Options -Scope Local -Force  
      Remove-Variable -Name Result -Scope Local -Force  
      Remove-Variable -Name Title -Scope Local -Force  
      Remove-Variable -Name Username -Scope Local -Force  
      Remove-Variable -Name Yes -Scope Local -Force  
   
 }  
   
 Function RoboCopyFiles {  
   
      #Define Local Memory  
      Set-Variable -Name ErrCode -Scope Local -Force  
      Set-Variable -Name ExcludeDir -Scope Local -Force  
      Set-Variable -Name ExcludeFiles -Scope Local -Force  
      Set-Variable -Name EXE -Scope Local -Force  
      Set-Variable -Name Logs -Scope Local -Force  
      Set-Variable -Name Parameters -Scope Local -Force  
      Set-Variable -Name RemoteExec -Scope Local -Force  
      Set-Variable -Name Robocopy -Scope Local -Force  
      Set-Variable -Name Switches -Scope Local -Force  
   
      $EXE = "\\BNASANIS01\SupportServices\Tools\PSTools\PsExec.exe"  
      $RemoteExec = "\\"+$Global:SourceComputer+[char]32+"-accepteula -u $Global:AdminUsername -p $Global:AdminPassword"+[char]32  
      $Switches = [char]32+"/e /eta /r:1 /w:0"  
      $ExcludeDir = [char]32+"/xd AppData Application* Downloads LocalService *Games* NetworkService *Links* *temp *TEMPOR~1 *cache Local*"  
      $ExcludeFiles = [char]32+"/xf ntuser.* *.exd *.nk2 *.srs extend.dat *cache* *.oab index.* {* *.ost UsrClass.* SharePoint*.pst history* *tmp*"  
      $Logs = [char]32+"/log:"+$Env:windir+"\waller\Logs\ApplicationLogs\ProfileCopy.log"  
      $Parameters = $Switches+$ExcludeDir+$ExcludeFiles+$Logs  
      $Arguments = $RemoteExec+$Env:windir+"\system32\robocopy.exe"+[char]32+$Env:systemdrive+"\users\"+$Global:SourceProfile+[char]32+$Global:DestinationProfile+$Parameters  
      $ErrCode = (Start-Process -FilePath $EXE -ArgumentList $Arguments -Wait -Passthru).ExitCode  
   
      #Cleanup Local Memory  
      Remove-Variable -Name ErrCode -Scope Local -Force  
      Remove-Variable -Name ExcludeDir -Scope Local -Force  
      Remove-Variable -Name ExcludeFiles -Scope Local -Force  
      Remove-Variable -Name EXE -Scope Local -Force  
      Remove-Variable -Name Logs -Scope Local -Force  
      Remove-Variable -Name Parameters -Scope Local -Force  
      Remove-Variable -Name RemoteExec -Scope Local -Force  
      Remove-Variable -Name Robocopy -Scope Local -Force  
      Remove-Variable -Name Switches -Scope Local -Force  
   
 }  
   
 Function CopyFiles ($FileSource,$FileDest,$FileFilter) {  
      $Dest = $FileDest  
      $Files = Get-ChildItem $FileSource -Filter $FileFilter  
      If ($Files.Count -eq $null) {  
           Write-Host "Copy "$Files.Name"....." -NoNewline  
           Copy-Item $Files.FullName -Destination $Dest -Force  
           $Test = $Dest + "\"+$Files.Name  
           If (Test-Path $Test) {  
                Write-Host "Success" -ForegroundColor Yellow  
           } else {  
                Write-Host "Failed" -ForegroundColor Red  
           }  
      } else {  
           For ($i = 0; $i -lt $Files.Count; $i++) {  
                $File = $Files[$i].FullName  
                Write-Host "Copy"$Files[$i].Name"....." -NoNewline  
                Copy-Item $File -Destination $Dest -Force  
                $Test = $Dest + "\"+$Files[$i].Name  
                If (Test-Path $Test) {  
                     Write-Host "Success" -ForegroundColor Yellow  
                } else {  
                     Write-Host "Failed" -ForegroundColor Red  
                }  
           }  
      }  
 }  
   
 cls  
 GetRelativePath  
 GetUserInput  
 RoboCopyFiles  
 #Copy Outlook Signatures  
 $TempSource = "\\"+$SourceComputer+"\c$\users\"+$SourceProfile+"\AppData\Roaming\Microsoft\Signatures"  
 $TempDestination = "\\"+$DestinationComputer+"\c$\users\"+$DestinationProfile+"\AppData\Roaming\Microsoft\Signatures"  
 CopyFiles $TempSource $TempDestination "*.*"  
   

13 August 2014

PowerShell: Removing Outlook Data Files

Automating the removal of Outlook data files is a tedious process that is difficult to automate. The registry key is a data hash that is unique on each system. Here is a script I wrote that will do just that. This script will remove the data file from Outlook when run under the end-user's credentials. The script parses through the HKU and removes the registry entry for that key. The complete description is in the code below and you can download it from here.


 <#  
 .Author  
      Mick Pletcher  
 .SYNOPSIS  
   Outlook Data Folder  
 .DESCRIPTION  
   This script will remove the Outlook data folder. In order to use this script,  
   you will need to obtain the powershell translated hexadecimal key. The very first  
   thing to do is to go into regedit and manually find the registry key associated with  
   the outlook data folder. You do this by opening up each registry key and to the right  
   is the ASCII translated data. Once you have found this, the next thing to do is to note  
   the name of the key containing the data. Now run the GetHKUBinaryKeyValue function with  
   the DeleteHKUBinaryKeyValue commented out. This will generate the list of keys and  
   give you the translated value that PowerShell is reading. copy the translated value. Now  
   comment out the GetHKUBinaryKeyValue and uncomment the DeleteHKUBinaryKeyValue. Pass the  
   translated value into the function as the second variable. You can see the examples below.  
   This is intended to run as the end user. I ended up running it via a GPO as the user, under  
   their credentials. It worked like a charm.  
 .EXAMPLE  
      GetHKUBinaryKeyValue "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Default Outlook Profile"  
      DeleteHKUBinaryKeyValue "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Default Outlook Profile" "101 0 68 0 79 0 67 0 83 0 32 0 68 0 77 0 0 0"  
 #>  
   
 Function GetHKUBinaryKeyValue ($RegKey, $KeyVal) {  
      Set-Variable -Name HKUsers -Scope Local -Force  
        
      Set-Variable -Name i -Scope Local -Force  
      Set-Variable -Name Key -Scope Local -Force  
      Set-Variable -Name SubKey -Scope Local -Force  
      Set-Variable -Name SubKeys -Scope Local -Force  
      Set-Variable -Name Temp -Scope Local -Force  
      Set-Variable -Name Value -Scope Local -Force  
        
      $Temp = New-PSDrive HKU Registry HKEY_USERS  
      $HKUsers = Get-ChildItem HKU:\ -ErrorAction SilentlyContinue  
      ForEach ($User in $HKUsers) {  
           [string]$Key = $User.Name  
           $Key = $Key -replace "HKEY_USERS", "HKU:"  
           $Key = $Key+"\"+$RegKey  
           If (Test-Path $Key) {  
                $Subkeys = Get-ChildItem $Key -ErrorAction SilentlyContinue  
                Foreach ($SubKey in $SubKeys) {  
                     Write-host $SubKey  
                     For ($i=0; $i -lt $Subkey.ValueCount; $i++) {  
                          [string]$Value = $Subkey.GetValue($Subkey.Property[$i])  
                          Write-Host "  "$Subkey.Property[$i]" : "$Value  
                          If ($Value -eq $KeyVal) {  
                               $DeleteKey = $true  
                          }  
                     }  
                     Write-Host  
                }  
           }  
      }  
   
      Remove-Variable -Name HKUsers -Scope Local -Force  
      Remove-Variable -Name i -Scope Local -Force  
      Remove-Variable -Name Key -Scope Local -Force  
      Remove-Variable -Name SubKey -Scope Local -Force  
      Remove-Variable -Name SubKeys -Scope Local -Force  
      Remove-Variable -Name Temp -Scope Local -Force  
      Remove-Variable -Name Value -Scope Local -Force  
 }  
   
 Function DeleteHKUBinaryKeyValue ($RegKey, $KeyVal) {  
      Set-Variable -Name HKUsers -Scope Local -Force  
      Set-Variable -Name i -Scope Local -Force  
      Set-Variable -Name Key -Scope Local -Force  
      Set-Variable -Name SubKey -Scope Local -Force  
      Set-Variable -Name SubKeys -Scope Local -Force  
      Set-Variable -Name Temp -Scope Local -Force  
      Set-Variable -Name Value -Scope Local -Force  
        
      $Temp = New-PSDrive HKU Registry HKEY_USERS  
      $HKUsers = Get-ChildItem HKU:\ -ErrorAction SilentlyContinue  
      ForEach ($User in $HKUsers) {  
           [string]$Key = $User.Name  
           $Key = $Key -replace "HKEY_USERS", "HKU:"  
           $Key = $Key+"\"+$RegKey  
           If (Test-Path $Key) {  
                $Subkeys = Get-ChildItem $Key -ErrorAction SilentlyContinue  
                Foreach ($SubKey in $SubKeys) {  
                     For ($i=0; $i -lt $Subkey.ValueCount; $i++) {  
                          [string]$Value = $Subkey.GetValue($Subkey.Property[$i])  
                          If ($Value -eq $KeyVal) {  
                               $DeleteKey = $true  
                          }  
                     }  
                     If ($DeleteKey -eq $true) {  
                          [string]$SubKey1 = $SubKey  
                          $SubKey1 = $SubKey1 -replace "HKEY_USERS", "HKU:"  
                          If (Test-Path $SubKey1) {  
                               Remove-Item $SubKey1 -Force  
                          }  
                          $DeleteKey = $false  
                     }  
                }  
           }  
      }  
   
      Remove-Variable -Name HKUsers -Scope Local -Force  
      Remove-Variable -Name i -Scope Local -Force  
      Remove-Variable -Name Key -Scope Local -Force  
      Remove-Variable -Name SubKey -Scope Local -Force  
      Remove-Variable -Name SubKeys -Scope Local -Force  
      Remove-Variable -Name Temp -Scope Local -Force  
      Remove-Variable -Name Value -Scope Local -Force  
 }  
   
 cls  
 #GetHKUBinaryKeyValue "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Default Outlook Profile"  
 DeleteHKUBinaryKeyValue "Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\Default Outlook Profile" "101 0 68 0 79 0 67 0 83 0 32 0 68 0 77 0 0 0"  
   

05 August 2014

SCCM: Moving a Computer to different OUs through using PowerShell scripts

I know there have been a lot of blogs out here showing how to move a computer to an OU, but I have taken a different approach. This is a good process for a small to mid-size company that doesn't have a lot of OUs to move machines to during a build. If it is a company the size of HCA, Dell, GM, etc, this isn't a good process because they likely have hundreds of OUs and you would be creating hundreds of PowerShell scripts. The way this process works is that the PowerShell script does not read anything from MDT or SCCM. It is executed, but does the move process on the system and not through MDT or SCCM.

The first thing to do is to get a drop-down window for the organization unit prompt under the join domain portion of the MDT setup process. Andrew provides two different ways of doing this. I ended up using the first one by applying the settings to the customsettings.ini file. When populating this field, it is going to be for informational purposes only. The field itself does not move a system to the new, selected OU.

The next thing to do is to create a separate PowerShell script for each OU you want to move a machine to. This is a really easy process. The script below is what I wrote and use. All you have to do is to change the $NewOU variable to whatever OU you desire.

 Set-Variable -Name CurrentOU -Scope Global -Force  
 Set-Variable -Name NewOU -Scope Global -Force  
   
 cls  
 Import-Module activedirectory  
 [string]$NewOU = "OU=BNA, OU=Computers, OU=Front Office, DC=ACME, DC=COM"  
 $CurrentOU = get-adcomputer $env:computername  
 Write-Host "Computer Name:"$env:computername  
 Write-Host "Current OU:"$CurrentOU  
 Write-Host "Target OU:"$NewOU  
 Move-ADObject -identity $CurrentOU -TargetPath $NewOU  
 $CurrentOU = get-adcomputer $env:computername  
 Write-Host "New OU:"$CurrentOU  
   

This script does require that remote server administration toolkit is installed and the AD PowerShell feature is enabled. That is what the import-module activedirectory requires. If your security is setup correctly, users will not be able to see or do anything even if they go in and enable other features in RSAT. The list below are the commands for enabling the AD PowerShell on the machines. This really comes in handy for other uses. I am currently deploying a huge upgrade package that requires users be moved to new OUs once the package is installed. RSAT has given me that capability.

 dism.exe /online /enable-feature /featurename:RemoteServerAdministrationTools  
 dism.exe /online /enable-feature /featurename:RemoteServerAdministrationTools-Roles  
 dism.exe /online /enable-feature /featurename:RemoteServerAdministrationTools-Roles-AD  
 dism.exe /online /enable-feature /featurename:RemoteServerAdministrationTools-Roles-AD-Powershell  
   

The next step is to create the MDT or SCCM Applications. The way to do this is to go in and create an application for each OU to execute the powershell script associated with that OU. The catch is that the powershell script will not move the machine under the credentials of MDT because it uses the system account for installing applications and it will not have access to AD. To get around this, you will need to use psexec to run the powershell script as a different user. Here is the command line I use to execute the scripts from an MDT Application:

 \\NetworkLocationOfPsexec\psexec \\%computername% -accepteula -u domain\username -p Password cmd.exe /c "echo . | powershell.exe -executionpolicy bypass -file \\NetworkLocationOfPowerShellScript\MoveOU_AUS.ps1"  
   

The final step is to put the applications into the task sequence. I created a folder and put an individual application task for each OU and associated the matching application with it. Here is an example:



You will need to filter each task sequence by creating a conditional statement in the options of the task sequence application. You will create a Task sequence variable, use MachineObjectOU, and associate with the matching OU in the customsettings.ini file. This will limit them to run only if the selected OU in the Windows Deployment Wizard matches.

That is everything you need to do to get this process to work. 

25 July 2014

MSI Error 1325: is not a valid short file name

This error in my case was being caused because the MSI was being executed through SCCM. The MSI file had to be executed locally and not from a network source. It would install with no issues if you double-clicked on the MSI. It only happened when SCCM tried to install it. During my troubleshooting, I tried a few different alternatives:

  • I use a PowerShell script to install the package. I thought it could be causing the issue, so I created a new SCCM package that only executed the MSI with the following command line, but the error persisted: msiexec.exe /I iDocID_for_32bit_Office.msi /qb-
  • I created a GPO that installed the MSI

These options did not resolve the issue. I then decided that if the issue was because the MSI needs to execute locally, then why not use psexec? I created the following command line to incorporate into an SCCM program. It failed the first time I ran it. The second time was successful after I went back into the environment tab and ran it with user rights. Basically this executes the package on the local machine under the user profile of the logged on user but runs the installer with the SCCM admin account. This resolved my issue.


\\    <network share>\psexec.exe \\%computername% -accepteula -u <username> -p <password> cmd.exe /c "echo . | powershell.exe -executionpolicy bypass -file <network share>\install.ps1"