Friday, June 9, 2017

Silently Deploy Read & Write 12 via SCCM

Read & Write 12 is an accessibility software we have licensed here at the University. I recently had to create a deployment for version 12 (we have version 11 deployed right now), and I thought I would post the script here in case it helps anyone, including my future self.
The install is fairly straight forward. I discovered some things and tweaked the script until the install was working the way I want. You may not need the entire script if you did not have v11 installed or need the folder cleanup. The script performs the following functions:
  • Uninstall the existing v11.
  • Cleans up a directory (RW Admin) we created during our v11 install.
  • Installs v12.
  • Adds registry entry that licenses product. If you do not have a site license you can omit this, and the user would have to authenticate each time they run the program.
  • Copy RWSettings.xml file to all user profiles during install. Settings in this file will turn off the auto update v12 performs when first run for each user, as well as turn off the check for updates. Keeping the update from running also stops the program from recreating a new shortcut on the Public profile, which I cleaned up during install.
  • Remove the shortcut from the Public Desktop. (Our labs have clean desktops rather than icons for each of the installed application, which can number over 75)
  • Open firewall ports for readandwrite.exe. I am not sure why it tries to open these ports, but I don't want it prompting the user (who is a non-admin and cant open ports anyway) You could also close these ports by using parameter -Action Block instead of Allow if you knew you didn't want that functionality. Opening the firewall ports use the POSH command "New-NetFirewallRule" which only exists in Win8.1 and 10. We are moving completely off Windows 7 this summer, so I don't have to worry about it working on Win7.  If you do, you can use a netsh command instead. Or update to Windows 10.
To get the RWSettings.xml file, I installed Read & Write 12 on a box, and grabbed it from the user's profile\Appdata\Roaming\Texthelp\ReadAndWrite\12\RWSettings.xml. Then I opened it up with my favorite text editor and changed the two lines to true and false respectively:

 

The powershell install script looks like this, or you can grab it from Github.

<#
.AUTHOR
David Pearson, www.dptechjournal.net

.DESCRIPTION
Installs Read & Write 12

.NOTES
Firewall port command New-NetFireWallRule requires Win8.1 or better.
Need to get RWSettings.xml from users profile after install, and set
StartUpWizardHasRun to true and AutoCheckForUpdates to false
Then copy file to C:\Users\profile\AppData\Roaming\Texthelp\ReadAndWrite\12 during install
Program is licensed by adding key to registry.
#>

$currentDirectory = split-path -parent $MyInvocation.MyCommand.Definition

# Uninstall Existing Software
$SoftwareInstalls = get-wmiobject -namespace root\cimv2\sms -query "select * from SMS_InstalledSoftware where ProductName = 'Read And Write 11'"
foreach ($SoftwareInstall in $SoftwareInstalls)
 {
  $SoftwareInstall.productname
  $software = $Softwareinstall.softwarecode
  $arguments = "/x $software /qn /norestart"
  start-process msiexec.exe -ArgumentList $arguments -wait
 }

if (Test-Path "$ENV:SystemDrive\RW Admin")
{
 remove-item -Path "$ENV:SystemDrive\RW Admin" -Force -Recurse
}

# Start Install
Start-Process "$currentDirectory\Read&Write.exe" -ArgumentList "/v/qn" -Wait

reg add "HKLM\SOFTWARE\WOW6432Node\Texthelp\Read&Write" /v "ProductCode" /t REG_SZ /d "MY_LICENSE_CODE" /f

# Copy file to Default Profile that removes first run autoupdate and disables autoupdate for all New Users
if (! (Test-Path "$ENV:SystemDrive\Users\Default\AppData\Roaming\Texthelp\ReadAndWrite\12"))
{
 mkdir "$ENV:SystemDrive\Users\Default\AppData\Roaming\Texthelp\ReadAndWrite\12"
}

Copy-Item -Path "$currentDirectory\RWSettings.xml" -Destination "$ENV:SystemDrive\Users\Default\AppData\Roaming\Texthelp\ReadAndWrite\12\" -Force


# Copy file to all the user profiles that removes first run autoupdate and disables autoupdate for existing users.
$Users = Get-ChildItem -Path $ENV:SystemDrive\Users\ -Exclude "Public","Default.migrated"
foreach ($User in $Users) {
  $profile = $User.Name
 if (! (Test-Path "$ENV:SystemDrive\Users\$profile\AppData\Roaming\Texthelp\ReadAndWrite\12"))
  {
   mkdir "$ENV:SystemDrive\Users\$profile\AppData\Roaming\Texthelp\ReadAndWrite\12"
  }
 Copy-Item -Path "$currentDirectory\RWSettings.xml" -Destination "$ENV:SystemDrive\Users\$profile\AppData\Roaming\Texthelp\ReadAndWrite\12\" -Force

}

# Remove Shortcut from Public Desktop
if (Test-Path "$ENV:PUBLIC\Desktop\Read&Write.lnk")
{
 Remove-Item -Path "$ENV:PUBLIC\Desktop\Read&Write.lnk" -Force
}

# Open Firewall Ports for .exe
New-NetFirewallRule -DisplayName "Read&Write 12" -Direction Inbound -Program "${ENV:ProgramFiles(x86)}\texthelp\read and write 12\readandwrite.exe" -Protocol tcp -Action Allow
New-NetFirewallRule -DisplayName "Read&Write 12" -Direction Inbound -Program "${ENV:ProgramFiles(x86)}\texthelp\read and write 12\readandwrite.exe" -Protocol udp -Action Allow


Gather up all your files, stick them in your Source directory and build your application.



My install string looks like:

powershell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File .\Install_ReadWrite12.ps1


The detection method is based on the msi that gets extracted from the .exe. For it I just grabbed the msi product code and version from the test box I had installed it on first instead of digging around for the source msi in C:\Windows\Installer. (I assume that's where it extracts to, I didn't need to go find it)



Wednesday, January 18, 2017

Powershell Script to Deploy Dell BIOS/Firmware Updates to Computers in the Wild using SCCM

Update 1/31/2017 - I've joined the world and opened a GitHub account. I've posted the XML and my Script there. Makes it much easier to share.

https://github.com/dptechjournal/Dell-Firmware-Updates

Update - 1/27/2017, Script has been modified to create a scheduled task instead of a registry entry in Windows 7, and I changed the -contains parameter to the more appropriate -match parameter.

Update 1/27/2017 - I am testing modifying the script in the Windows 7 section to add a scheduled task on startup, that does two things, one, re-enables bitlocker on startup, and two,  removes itself from scheduled tasks. As Jay pointed out in the comments, with the current script that adds the reg key to HKLM it only re-enables bitlocker when a user logs in, and it runs with the user's permissions. I rather not have bitlocked machines be unprotected between the firmware update and a user logging in. So moving it to Scheduled Tasks on startup seems the way to go. I will post the updated script when it is finished, along with the XML for the scheduled task.

BIOS (or Firmware) updates are rated “Recommended” or "Urgent" by Dell, which means you should probably be performing them regularly. I find they solve a lot of little issues that we see with hardware, and also solve issues with BitLocker. For instance, on the OptiPlex 9010, BitLocker protection would activate after plugging in an additional monitor to a desktop. Updating the BIOS version fixed the issue. There was a time when a BIOS update that went sideways could basically brick your computer, but those days appear to be past. I have successfully updated a few thousand Dell computer BIOS’s in the wild over the past year using the script below without losing a single one. I will say I stick to flashing newer hardware of the past 4 years. I do not try to update GX 280’s for instance. Here is a list of the Dell Models I have successfully been updating:

We use a two pronged approach to updating our BIOS’s. During task sequence imaging we use a package to update the BIOS’s, the package suppresses the reboot, allowing the Task Sequence to control the reboot at a later stage. There are many blog posts about how to accomplish this type of update during a task sequence so I will just focus on the script we use for deploying BIOS updates to computers in the wild. The SCCM application we use to deploy this script is set to only run when no user is logged in. We time the deployments with our monthly Windows updates, which initiate a 10 hour countdown to reboot after installation. This ensures that a majority of desktops and a portion of laptops get the BIOS updates about 10 hours after deployment. The monthly Windows updates are due after work hours, the computers install the updates, reboot, and now have no user logged in. They then install the BIOS update and reboot again. The rest of the computers get the BIOS updates as the application catches the computer without a logged in user. This is hit and miss, but eventually works it way to most of the other computers. Twice I have heard someone mention seeing the Bios flashing and reboot and asked about it, but most of the time the end user either wont be there or wont see it.

Now you may ask, why not just deploy the bios update and suppress the reboot? Well, we tried that, and found that if a computer installed a bios update and went to sleep before rebooting to finish the update, it would cause an error when Windows resumed from sleep. Therefore I created this script to try to handle any contingencies I could think of.

The script has gone through a number of versions before I felt it was ready to share. It performs the following functions, while logging steps to the computers Application Log, so we can check for certain error codes in SCOM.
  1. Checks if computer is a laptop, if it is, exits script if battery life is less than 30%.
  2. Gets the OS of the Computer.
    1. For Windows 7 – Uses Manage-bde.exe to suspend BitLocker if the C: drive is BitLocked. We found that computers that are not BitLocked but ran the -disable command, informed the user that BitLocker could not be resumed upon logon, even though its not Bitlocked to being with. So we only run the suspend command if its actually BitLocked. Also writes to the RunOnce registry key a command to re-enable BitLocker on next startup. Also adds a Scheduled Task to enable bitlocker on startup, and then remove itself. This was necessary because in rare cases, BitLocker did not resume on its own after using the “Manage-bde.exe –protectors –disable” command.
    2. Windows 8 and 10 – Uses PowerShell commands to check for and disable BitLocker on the volume with the Operating System.
  3. Checks if BitLocker was successfully suspended. If it was not, writes the error to the Application Log and exists. I have not seen this error happen yet.
  4. Updates the BIOS and forces a reboot of the machine, which re-enables Bitlocker
We have Collections for each Hardware Model we have. I use those as  limiting collections for the Updating Collection that I will use to deploy the application script, with a query looking for older bio’s versions that need updating. I do this for each model I am updating. Here is an example of my Optiplex 3020 shrinking collection (models that are up to date get removed from the collection based a query), of which we have 575 of this model.  You can see, only 29 still have a BIOS that needs updating. (Most of them haven't been online, or in rare cases, the user is one who turns off the computer every night before leaving, so its never had time to install when no user is logged in. Eventually I will get them!)



Create an XML file named "sTask_Details.xml" to contain the Scheduled Task that will get imported when the script is ran. This will create 2 actions on the startup trigger. The first action will enable bitlocker, the 2nd action will remove the tasks from the task scheduler. Copy and pasting from the code below wont work, either recreate the task on a machine and export it as an XML, or better yet, grab it from my GitHub repository. https://github.com/dptechjournal/Dell-Firmware-Updates


  
    2017-01-30T08:47:18.2600256
    dave
  
  
    
      true
    
  
  
    
      S-1-5-18
      LeastPrivilege
    
  
  
    IgnoreNew
    false
    true
    true
    false
    false
    
      true
      false
    
    true
    true
    false
    false
    false
    false
    false
    P3D
    7
  
  
    
      Manage-bde.exe
      -protectors -enable c:
    
    
      cmd
      /c schtasks /delete /f /tn "Bitlock"
    
  


Here is the script itself, replace "Latitude_E7x70_1.12.3.exe" on the last line with the BIOS Update executable you want to run. If you have a Firmware Password, put it in the $args, replace /p=password with your firmware password. Yes this means that your firmware password is in your SCCM cache. But what can you do huh? The script is also at my GitHub repository https://github.com/dptechjournal/Dell-Firmware-Updates
<# 
 .NOTES
 ==========================================================================
  Created on:    3/25/2016 12:44 PM
  Created by:    David Pearson http://www.dptechjournal.net
 ===========================================================================
 .DESCRIPTION
  Installs BIOS Update's for Dell Computers.
  1. Determines the OS of the computer
  2. Suspends Bitlocker if needed
  3. Updates BIOS
  4. Writes Log to Application Log
 Function Get-Laptop from https://blogs.technet.microsoft.com/heyscriptingguy/2010/05/15/hey-scripting-guy-weekend-scripter-how-can-i-use-wmi-to-detect-laptops/
#>
Function Get-Laptop
{
 Param (
  [string]$computer = “localhost”
 )
 $isLaptop = $false
 if (Get-WmiObject -Class win32_systemenclosure -ComputerName $computer |
 Where-Object {
  $_.chassistypes -eq 9 -or $_.chassistypes -eq 10 `
  -or $_.chassistypes -eq 14
 })
 { $isLaptop = $true }
 if (Get-WmiObject -Class win32_battery -ComputerName $computer)
 { $isLaptop = $true }
 $isLaptop
} # end function Get-Laptop
$currentDirectory = split-path -parent $MyInvocation.MyCommand.Definition

# Setup Logging
$ErrorActionPreference = "SilentlyContinue"
if (!(Get-Eventlog -LogName "Application" -Source "ConfigMgr Team"))
{
 New-Eventlog -LogName "Application" -Source "ConfigMgr Team" | Out-Null
}
$ErrorActionPreference = "Continue"


Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 1 -Message "ConfigMgr detected No User Logged In, Starting BIOS Upgrade Script."

# Check if Laptop, exit if Battery life is less than 30
If (get-Laptop)
{
 Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 3 -Message "Laptop detected, checking battery life."
 if ((Get-WmiObject win32_battery).estimatedChargeRemaining -le 30)
 {
  Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 4 -Message "Battery below 30%, canceling BIOS Upgrade...this time."
  exit
 }
}

# Get Operating System version and performs the appropriate actions to bitlocker
[int]$computerOS = (Get-WmiObject -namespace Root\cimv2 -Query "SELECT BuildNumber FROM win32_operatingSystem").buildnumber


if ($computerOS -ge 8000)
{
 $drive = Get-BitLockerVolume | where { $_.ProtectionStatus -eq "On" -and $_.VolumeType -eq "OperatingSystem" }
    if ($drive)
 {
     Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 2 -Message "Attempting to Suspend Bitlocker on drive $drive."
     Suspend-BitLocker -Mountpoint $drive -RebootCount 1
     if (Get-BitLockerVolume -MountPoint $drive | where ProtectionStatus -eq "On")
     {
      #Bitlocker Suspend Failed, Exit Script
      Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 13 -Message "Failed to Suspend Bitlocker on drive $drive , Exiting."
      exit
     }
    }
}
else
{
 $drive = manage-bde.exe -status c:
 if ($drive -match "    Protection Status:    Protection On")
 {
  Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 2 -Message "Attempting to Suspend Bitlocker on drive C: ."
  manage-bde.exe -protectors -disable c:
  $verifydrive = manage-bde.exe -status c:
  if ($verifydrive -match "    Protection Status:    Protection On")
  {
   #Bitlocker Suspend Failed, Exit Script
   Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 13 -Message "Failed to Suspend Bitlocker on drive C: , Exiting."
   exit
  }
  # Create a Scheduled Task to resume Bitlocker on startup, then remove
  cmd /c schtasks /create /f /tn "Bitlock" /XML $currentDirectory\sTask_Details.xml
 }
 
}

#Install BIOS Update
Write-EventLog -LogName "Application" -Source "ConfigMgr Team" -EntryType "Information" -EventId 7 -Message "Configmgr starting BIOS Update and rebooting. For more information, examine update.log in $currentDirectory"

$args = "/s /r /f /p=password /l=$currentDirectory\Update.log"
$install = Start-Process Latitude_E7x70_1.12.3.exe -ArgumentList $args -WorkingDirectory $currentDirectory -Wait


Name the script "Install_Dell_Bios_upgrade.ps1" and put it in your application source with the Bios Update executable and your XML file that holds the Scheduled Tasks details.

Here are the screenshots of how I setup my Application in ConfigMgr. The installation program install line is
powershell.exe -ExecutionPolicy Unrestricted -File "Install_Dell_Bios_upgrade.ps1"



My detection method checks the BIOS version in the registry, this briefly makes the application report that it failed to install as this does not update until after the reboot, but I am only concerned with the BIOS not upgrading if its already at the correct version.
I should mention that the parameters in the install command WILL NOT install the Firmware update if a laptop is not plugged into power. If you are feeling adventurous, you can add the switch to force the update, usually its /f . I am debating adding this, as I will rely on the estimated battery life part of the script to ensure there is enough juice left for the update to happen. That would go a long way towards getting the update to laptops in a more timely manner.