Friday, July 12, 2013

Image and deploy 800 machines in 3 days.

Working for HW reseller, large scale deployments are pretty much daily bread and butter. With very thin profit margin on the desktop PCs, one must work quickly and efficiently to make a $$$, satisfy customer with great service and hope he buys higher value items - networking, laptops servers, storage ... Local (Customer's) IT staff is usually stretched to the max to maintain and troubleshoot existing HW, which in time of tight budgets is usually worked to the sad and lonely death.

For this roll-out 800 HP Elite 8300 desktops were delivered to customer's warehouse. We were asked to image and inventory the machines and distribute them to proper locations per specification.

I was in charge of creating portable imaging infrastructure to image and configure machines as quickly as possible, so they could be delivered to proper sites.

This was different from our typical process where machines are prepared in our warehouse, configured, imaged or pre-staged (for SCCM) inventoried, palletized and delivered.

The Setup
Customer was already using MDT 2012 or their deployment but the infrastructure could not support this amount of machines. Option one was to integrate our imaging server with their domain and use Liked  Deployment Shares. I choose the other option to build our own imaging domain and just use the connection to customer's domain to perform the domjoin.

The Configuration
Imaging Server
HW: I did not have pure server available so HP Z200 workstation had to do.
OS: Windows Server 2012 (DC, DNS, DHCP, WDS), MDT 2012 UP1

Networking:
2x24port HP switches (due to lack of power in the warehouse plan was to image no more than 40 machines at a time)
1x access point (our wifi connection for scanning)

Misc:
Power strips, hand scanner, extension cords, networking cables ...

MDT Setup
Let The Fun Begin!
Lite touch deployment was turned to almost zero touch deployment. This allows to run headless imaging. The only prompt is for asset tag - which is setup in BIOS. (This is possible to change to load the information from separate file containing asset tag and serial numbers scanned ahead).

CustomSettings.ini is the key configuration file for the share. Prompt for asset tag is handled by change in the Deployment Wizard (Use http://mdtwizardeditor.codeplex.com/ to edit DeployWiz_Definition_ENU.xml ) Here is great tutorial how to do it from Deployment Bunny .

Imaging Process:
- Start from Boot CD
- CD is ejected - scan Asset Tag
- Set boot order, and asset tag in BIOS (Powershell and WMI)
- pull serial number from BIOS and store it with scanned asset tag in master csv file on the server

To get HP BIOS settings:
Get-WmiObject -Namespace root/hp/instrumentedBIOS -Class hp_biosEnumeration | Format-Table Name,Value –AutoSize

To set HP BIOS settings
$bios =
Get-WmiObject -Namespace root/hp/instrumentedBIOS -Class HP_BIOSSettingInterface
$bios.SetBIOSSetting('After Power Loss', 'On')

- At the end of task sequence BIOS is updated (hpqflash), and configuration is set with biosconfigutility.exe
- Tray is ejected again and machine shutdown to indicate the end of the process.

It is important WDS is used and configured for multi-cast as well as it is turned on for Deployment Share. This way all machines progress and finish at the same pace. Image is downloaded in 15mins via multi-cast and applied in another 5mins. The whole imaging and configuration process takes about 35 minutes for 40 machines, where without multi-cast some machines may take up to an hour or more and the whole batch is delayed.

Imaging started Tuesday mid day - due to misconfiguration on the customer network. (Note to self - always verify the information given by local networking team.)

Last machines were done Thursday afternoon.

Tuesday, June 4, 2013

Monitoring HP Battery Life with PowerShell & SCCM

So you have thousands of HP laptops and need to know health your batteries? May be you purchased an extra year for battery replacement and have 2 months left.

HP offers very easy solution ... Just install HP Battery Check - part of HP Support Assistant bloatware suite, run HP Battery Check Utility and wait for pretty program output page to get the results including the serial number and error code - information that you need to file the replacement claim.

Say this to any field tech and you will have to run - fast!

To avoid this or our customer I have prepared solution which automatically install just silently install the HP Battery Check Utility, silently run it and save results in Access DB on a server. Following description is for Windows 7 x86, but you can easily modify it for Window 8 and/or x64 architecture.
Access DB connection and DB update functions are snippets from Hey Scripting Guy article.

Deploy HP Battery Check
Latest version is a bit hard to find so here is the link to HP Battery Check version 4.3.2.2

  • Unpack and create SCCM Package with installation program command line: setup.exe /S /v/qn
  • Advertise/Deploy to SCCM Collection with your laptops
  • So that was the easy part  and you can reuse this package to install the utility beforehand during imaging process.


Run HP Battery Check and collect the result
Official Battery check information - Battery Check FAQ Page
I contacted the HP level 2 support and officially there are no silent switches for running the utility (BUT there is a silent switch  /S). And it took another three calls to find out where the battery scan result file is stored.

To start the battery silently from PowerShell
Start-Process "${env:ProgramFiles(x86)}\Hewlett-Packard\HP Support Framework\Resources\hpbc.exe /S"


The above line will produce out BCResOut.xml file in ProgramData folder:
%ProgramData%\Hewlett-Packard\HP Support Framework\Resources\Logs\BCResOut.xml


That can be opened by PowerShell:
[xml]$battery = get-content $env:ProgramData"Hewlett-Packard\HP Support Framework\Resources\Logs\BCResOut.xml"

and extract the right information - that took a little bit of experimenting

Then we will need a function to connect to Access database:
Function Connect-Database($fl, $Tables)
{
  $OpenStatic = 3
  $LockOptimistic = 3
  $connection = New-Object -ComObject ADODB.Connection
  $connection.Open("Provider = Microsoft.Jet.OLEDB.4.0;Data Source=$Db" )
  Update-Records($Tables)
} #End Connect-DataBase


The whole script is here:
<#
Name: Battery Check Script
Purpose: Silently run HP Battery Check and save results in Access file on the Server
Version: 1.1 - May, 2013

This script is provided "AS IS" with no warranties, confers no rights and
is not supported by the authors or deployment4everyone.

Author - Milos Pec
Blog : http://deployment4everyone.blogspot.com
Code Snipets from: http://technet.microsoft.com/en-us/magazine/2009.05.scriptingguys.aspx
#>
##########################################################
#Change this to your Access DB file on the server
$Db = "\\yourserver\deploy\batterycheck\BatteryCheck.mdb"
##########################################################

Function Check-Path($fl)
{
 If(!(Test-Path -path (Split-Path -path $fl -parent)))
   { 
     Throw "$(Split-Path -path $fl -parent) Does not Exist"
     exit
   }
  ELSE
  { 
   If(!(Test-Path -Path $fl))
     {
      Throw "$fl does not exist"
      exit
     }
  }
} #End Check-Path

Function Connect-Database($fl, $Tables)
{
  $OpenStatic = 3
  $LockOptimistic = 3
  $connection = New-Object -ComObject ADODB.Connection
  $connection.Open("Provider = Microsoft.Jet.OLEDB.4.0;Data Source=$Db" )
  Update-Records($Tables)
} #End Connect-DataBase

Function Update-Records($Tables)
{
  $RecordSet = new-object -ComObject ADODB.Recordset
   ForEach($Table in $Tables)
     {
      $Query = "Select * from $Table"
      $RecordSet.Open($Query, $Connection, $OpenStatic, $LockOptimistic)
      Invoke-Expression "Update-$Table"
      $RecordSet.Close()
     }
   $connection.Close()
} #End Update-Records

Function Update-BatteryInfo
{
    "Updating Battery Information"
    $RecordSet.AddNew()
    $RecordSet.Fields.Item("ComputerName") = $comp
    $RecordSet.Fields.Item("Model") = $model
    $RecordSet.Fields.Item("ComputerSerial") = $comp_serial
    $RecordSet.Fields.Item("BatterySerialNumber") = $batt_serial
    $RecordSet.Fields.Item("FailureID") = $failID
    $RecordSet.Fields.Item("ResultCode") = $result_code
    $RecordSet.Fields.Item("DesignCharge") = $design_charge
    $RecordSet.Fields.Item("MaxCharge") = $max_charge
    $RecordSet.Fields.Item("ChargeCapacity") = $charge_capacity
    $RecordSet.Fields.Item("DateRun") = $run_date
    $RecordSet.Update()
} #End Update-BatteryInfo


# Checking If Log File Exists
if (Test-Path "$env:ProgramData\Hewlett-Packard\HP Support Framework\Resources\Logs\BCResOut.xml") {
    [xml]$battery = get-content $env:ProgramData"Hewlett-Packard\HP Support Framework\Resources\Logs\BCResOut.xml"
} else {
   Throw "File not found ..." 
}

#Execute Battery Scan
Check-Path -fl "${env:ProgramFiles(x86)}\Hewlett-Packard\HP Support Framework\Resources\hpbc.exe"
Start-Process "${env:ProgramFiles(x86)}\Hewlett-Packard\HP Support Framework\Resources\hpbc.exe /S" -Wait

# Script Start
$comp = $env:COMPUTERNAME
$model = $battery.BatteryTest.ComputerProductName
$comp_serial = $battery.BatteryTest.ComputerSerialNumber
$design_charge = $battery.BatteryTest.result[0].p[0].InnerText
$max_charge = $battery.BatteryTest.result[0].p[1].InnerText
$batt_serial = $battery.BatteryTest.result[0].p[14].InnerText
$failID = $battery.BatteryTest.result[0].FailureID
$charge_capacity = $battery.BatteryTest.result[0].v[9].InnerText
$run_date = $battery.BatteryTest.result[0].r[0].InnerText
$result_code = $battery.BatteryTest.result[0].ResultCode

# If uncommented following will create CVS file on your desktop - to test results
#Start Creating New file
#$batt_status = $Env:USERPROFILE + "\Desktop\" + $failID + "." + $env:COMPUTERNAME + "." +(Get-Date -uFormat "%Y%m%d-%H%M") + ".csv"
#"ComputerName,Model,ComputerSerialNumber,BatterySerialNumber,FailureID,ResultCode,DesignCharge,MaxCharge,Charge_Capacity,Date" | Out-File -FilePath $batt_status -Encoding Ascii
#Add-Content -Encoding Ascii $batt_status ($comp + "," + $model + "," + $comp_serial + "," + $batt_serial + "," + $failID + "," + $result_code + "," + $design_charge + "," + $max_charge + "," + $charge_capacity + "," + $run_date).ToString()
#End Creating New file

if($env:PROCESSOR_ARCHITECTURE -eq 'x86')
  {
    $Tables = "BatteryInfo"
    Check-Path -fl $Db
    Connect-DataBase -fl $Db -tables $Tables
   }
#End Updating Access DB 


Download the script BatteryCheck.ps1
Download the database template BatteryCheck.mdb 

Friday, May 10, 2013

Setting up IP based Printers in SCCM 2007

When setting up migration of Windows XP to Windows 7 or just plain "ordinary" deployment with SCCM 2007, IP based printers are treated as local printers and are not migrated.

This may be problem in environment where majority of printers are setup as IP based printers.

One way to solve this would be to write a script to capture all local printers and import them back on the new machine. The problem with this solution is that most of the old printers may be transferred, sometimes not all the drivers are available for the new platform, etc.

So the decision was to document the current state and decide what printers to keep and create a package and printer definition file to add printers during final stages of the task sequence.

The script must add a local IP Port - here is Windows 7 command:
c:\windows\System32\Printing_Admin_Scripts\en-US\Prnport.vbs -a -r IP_IPAddress -h IPAddress -o raw -n 9100
Adding Printer - with following command:
rundll32 printui.dll,PrintUIEntry /if /b "Base Printer Name" /n "Printer Name" /u /f "inf file" /r "IP_PortName" /m "PrinterDriverModelName"

Note that the PrinterDriverModelName must be exact, no extra spaces or missing characters.
To get more info on the command just run:
rundll32 printui.dll,PrintUIEntry

The idea for the main script is that the script will read the definition CSV file, search for computer name and then create port(s) and printer(s). So the CSV file needs to contain Computer name, IP Address, Printer Name, Printer Driver Name, Inf File Location.

First part of the script reads the file and creates the array - I did not reinvent the wheel and found this universal function made by HunBug.

Download the addprinters.vbs
Download definition template printers.cvs


Wednesday, May 8, 2013

ProCurve Switch configuration for mass deployment - without ProCurve Manager

ProCurveWe were tasked to upgrade networking infrastructure for one of our customers and that meant to setup, configure and roll-out more than 2000 edge devices and core devices, mostly HP ProCurve 2910al, some PoE deices 2610, and 6200yl.
The switches needed to be swapped overnight at each location (average 40 devices per location) replacing old existing device.
Here is a VR image how the warehouse looks like: http://tinyurl.com/lcpqpz9


The configuration was quite simple so I decided to automate the process of config loading and firmware upgrade.

One way would be to use HP ProCurve Manager, but at the time of this project ProCurve was not very stable and would require lot of "waiting" for the switches to show up.

Firmware Upgrade
I created automated way to upgrade the firmware on the switches, but I have found that fastest way is hook up the switches to our setup network, start tftp server with the recent SW update and then use laptop and connect to console and just copy series of commands like this (HP 2810-XXG)

copy tftp flash <Your_TFTP_Server_IP> N_11_25.swi primary
y
copy flash flash secondary
y
boot
y
y


Sometimes you have to do it twice because the boot ROM needs to be updated but the major advantage is that you do not have to wait for the commands to finish - they are buffered and you can move on to the next switch as soon as the text is pasted to the window. This way you can prepare the bare switches to the uniform state for the next step.

Documentation and Configuration
Since we go all the information we needed in Excel (name, IP) and the configuration on edge devices was pretty much the same, I created a macro script with config template for each model. After unpacking switch MACs were scanned into the template and IP modified based on the site network.

Configuration Template

To deploy the config in as few steps as possible I decided not to script all the configuration commands in a telnet session, but rather generate the config file for each switch based on the template and then just telnet to the switch and use tftp to transfer the right file.

Because of the specific configuration let me just show some of the logic and  behind The Config Button from the image above:

  • Individual config files are generated with names like 2800_IPADDRESS.cfg
  • Configuration file defining scopes and reservations for local DHCP is generated based on the target network information.
    This will define the scope 10.123.40.0 on server 10.10.1.9
    # ======================================================================
    #          Scope name: TEST_40
    # ======================================================================
    # Add scope for 10.123.40.0
    Dhcp Server 10.10.1.9 add scope 10.123.40.0 255.255.255.0 "TEST_40" ""
    Dhcp Server 10.10.1.9 Scope 10.123.40.0 set state 1
    # ======================================================================
    #  Add Ipranges to the Scope 10.123.40.0, Server 10.10.1.9
    Dhcp Server 10.10.1.9 Scope 10.123.40.0 Add iprange 10.123.40.1 10.123.40.100
    # ======================================================================
    #  Add Excluderanges to the Scope
    Dhcp Server 10.10.1.9 Scope 10.123.40.0 add excluderange 10.123.40.1 10.123.40.1
    # ======================================================================
    #  Add OptionValues to the Scope
    Dhcp Server 10.10.1.9 Scope 10.123.40.0 set optionvalue 51 DWORD "3600"
    Dhcp Server 10.10.1.9 Scope 10.123.40.0 set optionvalue 3 IPADDRESS "10.123.40.1"


    This will add a reservation to a scope defined above:
    Dhcp Server 10.10.1.9 Scope 10.123.40.0 Add reservedip 10.123.40.10 005056C00008 "XXX-IDFXX-sALXX" "" "BOTH"
  • no the script will remotely execute commands for DHCP configuration
    netsh exec "TEST_scopes.txt"
    netsh exec "TEST_reservations.txt"
  • Next step is to configure the the vlan on the core to ensure connectivity to the dhcp server
    from the configuration vlan:
    I used powershell to script the telnet session to the core using Windows Automation Snapin for Powershell
    <#
    Name: Scripted telnet session 
    Description: Configures VLAN, 
    Version: 1.0 - Jan, 2010
    
    This script is provided "AS IS" with no warranties and
    is not supported by the authors or deployment4everyone.
    WARNING if you do not fully understand this - it will break your network.
    Feel free to use and modify for your needs. 
    
    Author - Milos Pec
    Blog: http://deployment4everyone.blogspot.com
    #>
    cls
    Import-Module "c:\scripts\wasp.dll"
    
    ########################################################
    # Annoyance fix for x64 version of windows - lost path to telnet.exe ...
    # More info: http://www.skyeagle.net/blog/post/Missing-64-bit-native-commands-in-Powershell-%28x86%29.aspx
    if ( (Test-Path "$($env:systemroot)\sysnative") -and (($env:path).split(';') -notcontains "$($env:systemroot)\sysnative") ) {
        $env:path = "$($env:path);$($env:systemroot)\sysnative"
        Write-Host "Path updated to include: $($env:systemroot)\sysnative"
    } 
    ########################################################
    
    
    $switch = "COREIP"
    $user = "ADMINUSER"
    $pwd = ""
    
    #get arguments for the script
    if ($args[0]) {
            $netw = $args[0]
        }else{
            $netw = read-host "Enter second octet network number [10.XXX.40.0]"
    }
    
    if ($netw -eq "") {
        write-host "Panic - network number is needed ..." 
        } else {
    $_switch = read-host "Confirm switch IP [$switch]"
        if($_switch -eq ""){#Default
            }
        elseif($_switch -eq $null) {#Default
            }
        else{$switch = $_switch}
    
    $_user = read-host "Enter username [$user]"
        if($_user -eq ""){#Default
            }
        elseif($_user -eq $null) {#Default
            }
        else{$user = $_user}
    
    $pwd = read-host "Enter password" -AsSecureString
    $pwdenc = [Runtime.InteropServices.Marshal]
    $pwd = $pwdenc::PtrToStringAuto( $pwdenc::SecureStringToBSTR($pwd))
    Start telnet.exe $switch
    Start-Sleep -s 1
    Select-Window telnet | send-keys "~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "$user~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "$pwd~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "config ~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "vlan 40 ~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "no ip add ~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "ip add 10.$netw.40.1 255.255.255.0 ~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "untag c1-c24,d1-d24 ~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "write mem ~"
    Start-Sleep -s .2
    Select-Window telnet | send-keys "logout ~"
    Start-Sleep -s 1
    }
  • The switches can be turned on and they should get the correct IP address
  • The macro is setup to automatically start the TFTP server (I am using opensource TFTPD32) configured with source directory where all the switch configs are stored and then using again PowerShell scripted session to run simple commands to download the right configuration:
    copy tftp startup-config TFTP_IP 2600_SWITCHIP_ip.cfg
Next missing step is 3 hours on site replacing switches ... but that can be fun to :-).

Wednesday, April 3, 2013

Hyper-V and VMware on the same PC

I have finally got my new Ultrabook and was really looking forward to start running my test labs in Hyper-V when using my old VMWare Player setup.

I moved my VMware machines, changed couple settings and tried to start my Ubuntu server only to be spanked by dialog box " VMware Player and Hyper-V are not compatible. Remove the Hyper-V role from the system before running VMware Player.



So with excitement gone from reading blog posts and forums about this, here is the only solution I have found so far:

Default Boot Configuration Data Store needs to be copied to a new one and then hypervisor neds to be disabled. Because the Ultrabook reboots in about 8 seconds, changing the the configuration does not take much time.

bcdedit /copy {default} /d "No Hypervisor"

The above command will create a copy of  the default Windows Boot Loader and returns the GUID of the copy - in my case: {c1b9bb88-e12e-11e2-8be0-de688297e5fd}
Boot Manager and Boot Loader settings can also be listed with a command bcdedit /enum

To change the hypervisorlaunchtype enter another bcdedit command:

bcdedit/set {GUID_OF_THE_COPY_CREATED_ABOVE} hypervisorlaunchtype off


And you are done. Next boot you should see following boot options to choose from:

The only thing left might be changing the default to boot without user intervention. This can be done by a command:

bcdedit /default {GUID_OF_THE_COPY_CREATED_ABOVE}