Posts Tagged ‘Scripting’

Replacing DirSync with Microsoft Azure Active Directory Sync Services

Back in October, MS announced that the new “MS Azure AD Sync Services” (AAD Sync) had gone GA, which means that DirSync is now officially deprecated.

Ironically, I got wind that “Azure AD Connect” would be replacing Azure AD Sync Services, not but a few weeks before:

http://blogs.technet.com/b/ad/archive/2014/08/04/connecting-ad-and-azure-ad-only-4-clicks-with-azure-ad-connect.aspx

Deprecated on release?  Not quite… it appears that Azure AD Sync Services will be at the core of Azure AD Connect, so time spent migrating to Azure AD Sync Services will not be a complete waste of time.

I decided to make this migration happen now, because we will be ramping up to provision faculty and staff with access to Office 365 Pro Plus software in the near future, and I would like to be working with a tool that does not require me to run it in an unsupported configuration (we removed the “Replicating Directory Changes” right from our DirSync service account, which at least at one time was considered a Bad Thing To Do.).

The Good News:

  • The out-of-box configuration does not require a service account with the “Replicating Directory Changes” right, making filtering of sensitive user attributes much easier.
  • The initial setup wizard allows you to de-select attributes that you don’t want to sync to Azure.  No more wading though dense miisclient.exe dialogs and walking off-support to do basic filtering!  The require attributes are listed in grey, so that you don’t risk filtering an attribute that Azure absolutely must have.
  • AAD Sync actually has some documentation available on its installation and use over at MSDN:
    http://msdn.microsoft.com/en-us/library/azure/dn790204.aspx
    …And there actually are quite a few important details in this seemingly cursory documentation.
  • AAD Sync entirely replaces the UI-heavy, un-maintainable attribute filtering tools in DirSync with a new “Declarative Provisioning” engine, that allow robust attribute filtering and transformation rules to be defined by the sys admin.
  • Custom “Inbound” attribute filters will be preserved during product upgrades, according to the documentation.
  • The documentation has been updated with instructions on how to make domain-based sync filters actually work in a multi-domain forest.  This was a key detail that was missing in the DirSync documentation.

The Bad News:

  • The Declarative Provisioning language, while based on VB for Applications (The “best known language by systems administrators”, according to the docs.  Whaaaat?!?!), is not actually VBA.  Debugging methods for dealing with this new VB-like thing are not at all documented, and examples of its usage are few and far between.

So what did I learn during this deployment process that was Blog-worthy?  How to debug Declarative Provisioning!

Debugging Declarative Provisioning (Import Expression Filters):

Let’s take an example.  We want to sync only students, and users who are to be provisioned for use of Lync in Office 365 into Azure.  To accomplish this, we have populated students with the text value “Student” in extensionAttribute1.  Lync users have had extensionAttribute2 populated with the text value “lync”.

When running the Synchronization Rule Editor, we create a new “inbound” rule, assign it to object type “inetOrgPerson”, link type is “join”, and give it a precedence under 100.  We skip the Scoping and Join Rules options, and go to “Transformations”.

The flowType gets set to “Expression”, the Target Attribute to “cloudFiltered”, and we define a VBA-like expression in the “Source” field:

EditSyncRule

My expression follows:

IIF(CBool(InStr([extensionAttribute1], "Student", 1)), NULL, IIF(CBool(InStr([extensionAttribute2], "lync", 1)), NULL, True)

So what is the intent here? “cloudFiltered” is a metaverse attribute that can be set to suppress synchronization of an account upon export to Azure AD.  If set to “True”, the account should not get synced to Azure.

Our expression uses the “IIF” fuction to check to see if “extensionAttrubute1″ contains “Student”.  If so, then “IIF” returns “NULL”, and NULL is fed to the “cloudFiltered” attribute (which, according to the docs, will cause the attribute to be cleared if it is already set).  However, if extensionAttribute1 does not contain “Student”, we perform a second test to see if “extensionAttribute2″ contains “lync”.  If so, cloudFiltered again gets set to NULL.  If “extensionAttribute2″ does not contain “lync”, then “cloudFiltered” gets set to the boolean value “True”.

Syntax quirks:

  • Items enclosed in square braces [] refer to metaverse attributes, and serve as a sort of automatic variable for provisioning.
  • Note that the InStr() function used here is not the same one documented in the VB for Applications language reference:
    http://msdn.microsoft.com/en-us/library/office/gg264811(v=office.15).aspx
    The “start position” parameter in the docs is not supported here, although it does appear that the “compare” parameter is supported (represented by the “1″ in my example, which means perform a case-insensitive comparison).
  • Everything is case sensitive… even the function names!  “IIF()” is all caps.  CBool(), CStr(), InStr(), and Error() are all “Caml Cased”.
  • IIF() is a bit of a strange beast by itself.  You might need this syntax reference:
    http://msdn.microsoft.com/en-us/library/office/gg264412(v=office.15).aspx

Now that we have a filter, how to we test it?  The docs say “Run a full import followed by a full sync” on the AD management agent.  While this will work, it also is a bit time consuming.  If there are syntax errors in your expression, you will get a lot of errors in your sync logs.  Is there something better?

As it turns out, yes there is.  Special thanks to Brian Desmond (co-author of the o’Riley press “Active Directory Cookbook”) for putting me on the right track:

  1. In the MIISClient.exe, right click your AD management agent, and select “Search Connector Space”:
    SearchConnectorSpace
  2. Specify the DN of a user that you want to test your expression on in the search box.  You can quickly get a DN using PowerShell with the ActiveDirectory module:
    Get-ADUser -Identity [samAccountName]
    Click “Search”, click the returned object, and click “Preview”:
    PreviewUser
  3. In the “Preview” dialog, select either a Full or Delta sync, then click “Generate Preview”, and select the “Import Attribute Flow” option on the left.  Review the list of Sync Rules on the bottom pane to make sure your rule was processed.  Any rule that generates a change will be displayed in the top pane:
    ImportFlow
    In this case, the expression did not generate a change in the existing metaverse object.
  4. If an expression that you have authored generates an error, a full stack trace will appear in the Error Details section of the Preview pane.  Scroll down to the bottom of the stack, where it is likely that the actual problem with your expression will be identified:
    PreviewError
    In this example, I inserted an “Error()” function with a null argument.  The language engine did not like this.
  5. Back in the “Start Preview” tab, you could “Commit Preview” to apply the filter rules for this specific object, and then view the results in the Metaverse Search component of miisclient.exe.

Using the preview tool, I was able to get my filter expressions working correctly with minimal fuss and delay.  The final hurdle to getting filtered attributes set correctly was understanding what the various “run profiles” in AAD Sync actually do.

  • AD Agent – Full Import:  Appears to sync data from AD to the AAD Sync Metaverse without applying Declarative Provisioning Filters.  I think.  Not 100% on this one.
  • AD Agent – Full Synchronization:  Appears to apply Declarative Provisioning filters on all metaverse objects.
  • Azure AD Agent – Full Sync: Appears to apply export filtering logic to metaverse objects entering the Azure connector space.
  • Azure AD Agent – Export:  Appears to sync metaverse objects to Azure without applying filtering logic.  My impression is that you must do a “sync” before an export, if you want logic applied to Azure AD objects as you intended.

My understanding of the run profiles may be flawed, but I will note that if I perform syncs in the following sequence, I get the expected results:

AD Agent:Full Import -> AD Agent:Full Sync -> Azure Agent:Full Sync -> Azure Agent:Export

Although it appears that the more authoritative way to get all of your records reconciled is:

               AD Agent:      MV:   Azure Agent:
               ~~~~~~~~~      ~~~   ~~~~~~~~~~~~
               Full Import => MV
                              MV  MV
                              MV => Export
                              MV  Delta Sync
               Export      <= MV                 (Only needed in Azure write-back scenarios)

However, with other sequences, I have seen strange results such as metaverse objects not getting updated with the intended filter results, or objects getting declared as “disconnectors” by the Azure AD Agent, and thus getting deleted from Azure.  Ugh!

That’s all for today… hopefully this info will help keep someone else out of trouble, too.

Bulk-modification of deployment deadlines in SCCM

A full two years into testing we finally are moving forward with production deployment of our Configuration Manager 2012 (SCCM) environment. Last month we (recklessly?) migrated 1000 workstations into the environment. While the deployment was a technological success, it was a bit of black-eye in the PR department.

Client computers almost uniformly did an unplanned reboot one hour after the SCCM agent got installed on their workstations. In addition to that, many clients experienced multiple reboot requests over the coming days. Many client reported that they did not get the planned 90-minute impending reboot warning, but only the 15-minute countdown timer.

Lots of changes were required to address this situation:

See the “Suppress and required client restarts” setting documented here:

http://technet.microsoft.com/en-us/library/gg682067.aspx#BKMK_EndpointProtectionDeviceSettings

This one was causing clients to reboot following upgrade of their existing Forefront Endpoint Protection client to SCEP. That explained the unexpected 60-minute post-install reboot.

Next, we decided to change the reboot-after deadline grace period from 90 minutes to 9 hours, with the final warning now set to one hour, up from 15 minutes. This should allow people to complete work tasks without having updates interrupt their work day.

Finally, we are planning to reset the deployment deadline for all existing software update deployments to a time several days out from the initial client installation time. Since we have several dozen existing software update group deployments, we need a programmatic approach to completing this task. The key to this was found here:

http://www.scconfigmgr.com/2013/12/01/modify-the-deadline-time-of-an-adr-deployment-with-powershell/

Thanks to Nickolaj Andersen for posting this valuable script.

It did take me a bit of time to decode what Nickolaj was doing with his script (I was not already familiar with the Date/Time format generally used in WMI). I modified the code to set existing update group deployments to a fixed date and time provided by input parameters. I also added some in-line documentation to the script, and added a few more input validation checks:

# Set-CMDeploymentDeadlines script
#   J. Greg Mackinnon, 2014-02-07
#   Updates all existing software update deployments with a new enforcement deadline.
#   Requires specification of: 
#    -SiteServer (an SCCM Site Server name)
#    -SiteCode   (an SCCM Site Code)
#    -DeadlineDate
#    -DeadlineTime
#

[CmdletBinding()]

param(
    [parameter(Mandatory=$true)]
    [string] $SiteServer,

    [parameter(Mandatory=$true)]
    [string] $SiteCode,

    [parameter(Mandatory=$true)]
    [ValidateScript({
        if (($_.Length -eq 8) -and ($_ -notmatch '[a-zA-Z]+')) {
            $true
        } else {
            Throw '-Deadline must be a date string in the format "YYYYMMDD"'
        }
    })]
    [string] $DeadlineDate,

    [parameter(Mandatory=$true)]
    [ValidateScript({
        if (($_.Length -eq 4) -and ($_ -notmatch '[a-zA-Z]+')) {
            $true
        } else {
            Throw '-DeadlineTime must be a time string in the format "HHMM", using 24-hour syntax' 
        }
    })]
    [string] $DeadlineTime
)

Set-PSDebug -Strict

# WMI Date format is required here.  See:
# http://technet.microsoft.com/en-us/library/ee156576.aspx
# This is the "UTC Date-Time Format", sometimes called "dtm Format", and referenced in .NET as "dmtfDateTime"
#YYYYMMDDHHMMSS.000000+MMM
#The grouping of six zeros represents milliseconds.  The last cluster of MMM is minute offset from GMT.  
#Wildcards can be used to for parts of the date that are not specified.  In this case, we will not specify
#the GMT offset, thus using "local time".

# Build new deadline date in WMI Date format:
[string] $newDeadline = $DeadlineDate + $DeadlineTime + '00.000000+***'
Write-Verbose "Time to be sent to the Deployment Object: $newDeadline"
 

# Get all current Software Update Group Deployments.
# Note: We use the WMI class "SMS_UpdateGroupAssignment", documented here:
# http://msdn.microsoft.com/en-us/library/hh949604.aspx
# Shares many properties with "SMS_CIAssignmentBaseClass", documented here:
# http://msdn.microsoft.com/en-us/library/hh949014.aspx 
$ADRClientDeployment = @()
$ADRClientDeployment = Get-WmiObject -Namespace "root\sms\site_$($SiteCode)" -Class SMS_UpdateGroupAssignment -ComputerName $SiteServer

# Loop through the assignments setting the new EnforcementDeadline, 
# and commit the change with the Put() method common to all WMI Classes:
# http://msdn.microsoft.com/en-us/library/aa391455(v=vs.85).aspx
  
foreach ($Deployment in $ADRClientDeployment) {

    $DeploymentName = $Deployment.AssignmentName

    Write-Output "Deployment to be modified: `n$($DeploymentName)"
    try {
        $Deployment.EnforcementDeadline = "$newDeadline"
        $Deployment.Put() | Out-Null
        if ($?) {
            Write-Output "`nSuccessfully modified deployment`n"
        }
    }
    catch {
        Write-Output "`nERROR: $($_.Exception.Message)"
    }
}

We additionally could push out the deployment time for Application updates as well using the “SMS_ApplicationAssignment” WMI class:

http://msdn.microsoft.com/en-us/library/hh949469.aspx

In this case, we would want to change the “UpdateDeadline” property, since we do not set a “deployment deadline” for these updates, but instead are using application supersedence rules to control when the updates are deployed.

Using SQL Logins with “AlwaysOn” Availability Groups

I had some fun today configuring a SQL 2012 “AlwaysOn” Availability Group working with a database that was configured to use SQL Logins.  I am working with the vendor to see if we can use Integrated Authentication instead, but in the meantime I managed to get failover functional.

The problem I was experiencing was that on failover, the database users defined in the database lost their mappings to the SQL Login on the server.  I had created SQL Logins on both nodes of the Availability Group with identical usernames and passwords, but the mapping still failed.  Turns out this was happening because SQL Logins (like AD and NT accounts) have SIDs, and these SIDs are used to map database users to logins.  To correct the problem, I needed to create SQL Logins with identical SIDs on the two servers.

Procedure:

  1. Create SQL Logins on one node of the Availability Group and perform mappings.
  2. Lookup the SIDs of the users using the following query:
    SELECT SUSER_SID (‘[SqlLogin]‘)
  3. Script out creation of the matching logins:
    USE [master]
    GO
    CREATE LOGIN [SqlLogin] WITH PASSWORD=N’[Password]‘, SID=[HexidecimalSID], DEFAULT_DATABASE=[myDatabase], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
    GO
  4. Failover the Availability Group and verify that user mappings are working as planned.  Verify that passwords are working, too.

Another useful tidbit here is a script to re-map SQL database users to local SQL logins:

USE myDatabase
GO
sp_change_users_login @Action=’update_one’, @UserNamePattern=’databaseUserName’, @LoginName=’SqlLoginName’;
GO

Really I am surprised that I did not run into this problem with our SQL 2008 mirrored databases.  The problem should have been present there as well.

Scriptomatic Access to the Start Menu and Taskbar

As promised in my previous post, here is my current VBScript for configuring the Windows 7 Start Menu and Taskbar. Not beautiful, but certainly functional. My thanks to JuliusPIV and cogumel0 for doing the heavy lifting that made this script possible.

Note that you really will need to set the Group Policy option to turn off the Start Menu program history if you want Start Menu pinning to be at all effective in streamlining the Windows 7 “first time” GUI.

'==========================================================================
'
' NAME: Pin & Unpin items to/from Start Menu & Taskbar
'
' AUTHOR: J. Greg Mackinnon
' DATE  : 2013-01-31
'
' COMMENT: Derived from code by JuliusPIV found here:
'   http://social.technet.microsoft.com/Forums/en/w7itproinstall/thread/73eb1c0a-fc78-4ae7-ba6d-356d9a9a5328
'
'   To add items to Start Menu or Taskbar, add a variable defining the 
'   path to the original link in the variables section, then add that 
'   variable to the "aPinSM", "aPinTB", or "aUnpinTB" arrays.
'
'   Note that not all links (such as filesystem shortcuts) can be pinned.
'
'   Uncomment "debugecho" lines to troubleshoot.
'
'==========================================================================
option explicit

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        CONSTANTS
'=-=-=-=-=-=-=-=-=-=-=-=-=
'List of "Shell Special Folder Constants" used in the script.  See:
'http://msdn.microsoft.com/en-us/library/windows/desktop/bb774096(v=vs.85).aspx
const ssfAPPDATA = &H1a
const ssfCOMMONPROGRAMS = &H17
const ssfPROGRAMFILESx86 = &H30
const ssfPROGRAMS = &H2 
const ssfSYSTEM = &H25
const ssfWINDOWS = &H24

'=-=-=-=-=-=-=-=-=-=-=-=-=
'         OBJECTS
'=-=-=-=-=-=-=-=-=-=-=-=-=
dim fso, oShell, oShortcut
set fso = CreateObject("Scripting.FileSystemObject")
set oShell = CreateObject("Shell.Application") 

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        VARIABLES
'=-=-=-=-=-=-=-=-=-=-=-=-=
dim aPinSM, aPinTB, aUnpinTB
dim bEchoOut, bPinItem
dim sAUP, sUP, sRAD, sPFx86, sSys32, sItem, sScriptHost, sFileName
dim sGC, sMOW, sMOE, sMOPP, sMOON, sMOO, sOC, sFZ, sPT, sProj, sCalc, sSnip, sPDN, sMag, sKey, sWMP

'Configure variables for well known folders:
sRAD = oShell.NameSpace(ssfAPPDATA).Self.Path & "\"            'Roaming AppData
sAUP = oShell.NameSpace(ssfCOMMONPROGRAMS).Self.Path & "\"     'Start Menu Programs - All Users
sUP = oShell.NameSpace(ssfPROGRAMS).Self.Path & "\"            'Start Menu Programs - Current User
'sPFx86 = oShell.NameSpace(ssfPROGRAMFILESx86).Self.Path & "\" 'Program Files (x86)
'sSys32 = oShell.NameSpace(ssfSYSTEM).Self.Path & "\"          '%WinDir%\system32

'List of links to be added to the Start Menu or Taskbar, relative to:
' C:\ProgramData\Microsoft\Windows\Start Menu\Programs
sGC = sAUP & "Google Chrome\Google Chrome.lnk"
sMOW = sAUP & "Microsoft Office 2013\Word 2013.lnk"
sMOE = sAUP & "Microsoft Office 2013\Excel 2013.lnk"
sMOPP = sAUP & "Microsoft Office 2013\PowerPoint 2013.lnk"
sMOON = sAUP & "Microsoft Office 2013\Onenote 2013.lnk"
sMOO = sAUP & "Microsoft Office 2013\Outlook 2013.lnk"
sOC = sAUP & "Oracle Calendar\Oracle Calendar.lnk"
sFZ = sAUP & "FileZilla FTP Client\FileZilla.lnk"
sPT = sAUP & "PuTTY\PuTTY.lnk"
sProj = sAUP & "Accessories\displayswitch.lnk"
sCalc = sAUP & "Accessories\Calculator.lnk"
sSnip = sAUP & "Accessories\Snipping Tool.lnk"
sPDN = sAUP & "Paint.NET.lnk"
sMag = sUP & "Accessories\Accessibility\Magnify.lnk"
sKey = sUP & "Accessories\Accessibility\On-Screen Keyboard.lnk"
sWMP = sRAD & "Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\Windows Media Player.lnk"

'Arrays containing links to be added to StartMenu or Taskbar, or to be removed from the Taskbar:
aPinSM = Array(sOC,sFZ,sPT,sPDN,sSnip,sCalc,sProj,sMag,sKey)
aPinTB = Array(sGC,sMOW,sMOE,sMOPP,sMOON)
aUnpinTB = Array(sWMP,sMOW,sMOPP)

'=-=-=-=-=-=-=-=-=-=-=-=-=
'   FUNCTIONS AND SUBS
'=-=-=-=-=-=-=-=-=-=-=-=-=
function PinSM(shortcut)
	dim oFolder, oFolderItem
	dim sFolder, sFile
	dim colVerbs
	dim itemverb
	
	sFolder = fso.GetParentFolderName(shortcut)
	sFile = fso.GetFileName(shortcut)

	'debugecho "Pinning " & sFolder & "\" & sFile & " to Start Menu."
	Err.Clear
					
	set oFolder = oShell.NameSpace(sFolder)
	set oFolderItem = oFolder.ParseName(sFile)
	set colVerbs = oFolderItem.Verbs

	for each itemverb in oFolderItem.Verbs
		if Replace(itemverb.name, "&", "") = "Pin to Start Menu" then itemverb.DoIt
	next
end function

function PinTB(shortcut)
	dim sFolder, sFile
	dim oFolder, oFolderItem
	dim colVerbs, itemverb
	
	sFolder = fso.GetParentFolderName(shortcut)
	sFile = fso.GetFileName(shortcut)

	'debugecho "Pinning " & sFolder & "\" & sFile & " to Taskbar."
	Err.Clear
					
	set oFolder = oShell.NameSpace(sFolder)
	set oFolderItem = oFolder.ParseName(sFile)
	set colVerbs = oFolderItem.Verbs
	
	for each itemverb in oFolderItem.Verbs
		if Replace(itemverb.name, "&", "") = "Pin to Taskbar" then itemverb.DoIt
	next
end function

function UnpinTB(shortcut)
	dim sFolder, sFile
	dim oFolder, oFolderItem
	dim colVerbs, itemverb
	
	sFolder = fso.GetParentFolderName(shortcut)
	sFile = fso.GetFileName(shortcut)

	'debugecho "Unpinning " & sFolder & "\" & sFile & " from Taskbar."
	Err.Clear
					
	set oFolder = oShell.NameSpace(sFolder)
	set oFolderItem = oFolder.ParseName(sFile)
	set colVerbs = oFolderItem.Verbs
	
	for each itemverb in oFolderItem.Verbs
		if Replace(itemverb.name, "&", "") = "Unpin from Taskbar" then itemverb.DoIt
	next
end function

function debugecho(msg)
	if bEchoOut then
		wscript.echo msg
	end if
end function

sub Main
	for each sItem in aUnpinTB
		if not fso.FileExists(sItem) then
			bPinItem = false
			'debugecho "File, " & sItem & ", to unpin does not exist."
			'debugecho "Please check the input and try again."
		else
			UnpinTB(sItem)
		end if
	next
	for each sItem in aPinSM
		if not fso.FileExists(sItem) then
			bPinItem = false
			'debugecho "File, " & sItem & ", to pin does not exist."
			'debugecho "Please check the input and try again."
		else
			PinSM(sItem)
		end if
	next
	for each sItem in aPinTB
		if not fso.FileExists(sItem) then
			bPinItem = false
			'debugecho "File, " & sItem & ", to pin does not exist."
			'debugecho "Please check the input and try again."
		else
			PinTB(sItem)
		end if
	next
end sub

'=-=-=-=-=-=-=-=-=-=-=-=-=
'        MAIN BODY
'=-=-=-=-=-=-=-=-=-=-=-=-=
'Suppress echo if we are in WScript:
sScriptHost = LCase(Wscript.FullName)
if Right(sScriptHost, 11) = "wscript.exe" then
    bEchoOut = false
else
    bEchoOut = true
end if

call Main

Unattended Install and Upgrade of Adobe Reader

Previously we explored how to increase the success rate of unattended application upgrades using our handy “killAndExec” VBScript. This works well for about 80% of our applications. What about Adobe Reader? Well… not so much.

Thankfully, the stock Adobe Reader installer deals with open files quite nicely, and does not care if Reader is in use by a browser during silent installs (this is the main reason that we needed killAndExec.vbs in the first place). However, not all Reader install operations are full installs. Interestingly, Adobe is one of the few vendors that I deal with that actually uses MSI patch files (MSP). To install a patch release of Reader (i.e. 10.1.4), you need first to install the base version (10.1), then one or more patch MSP files. This is easy for new installs… just run “setup.exe” silently, then run “msiexec /p (patchFileName) /qn” to install the patch.

However, the situation gets more complicated for upgrades. If the base product already is installed, setup.exe will return an error code. So, for upgrade scenarios, I have put together another handy VBScript to handle base version detection. The script follows:

option explicit
' Install Adobe Reader Script:
' J. Greg Mackinnon, 2012-06-12
' Intended to perform unattended installations of Adobe Reader by MS SCCM 2012.
' 
' Installs the version of Adobe Reader in the same directory as the script, if not already installed.
' Also installs the current Reader patch, if requested.
' Installer string is specified in "sInstall".
' Requires: 
'     Adobe Reader setup and patch files in the same directory as the script.
'     "setup" and "basever" arguments required.
'     "patch" argument optional.
' Returns:
'     - Code 100 - if required arguments are not provided to the script.
'     - Return code of setup program added to the return code of the patch program, if no other errors occur.

dim oExec, oFS, oLog, oShell
dim cScrArgs
dim iExit
dim sBaseVer, sInstall, sLog, sOut, sPatch, sPath, sPF, sScrArg, sTemp, sVer
dim bDoPatch

sLog = "installAdobeReader.log"
iExit = cLng(0)

' Instantiate objects:
Set oShell = CreateObject( "WScript.Shell" )
Set oFS = CreateObject("Scripting.FileSystemObject")
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Define Functions
'
function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

sub subHelp
	echoAndLog "installAdobeReader.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Runs Adobe Reader silent setup (if not already present), then applies "
	echoAndLog "any specified MSP patch files for Reader."
	echoAndLog "Logs output to 'installAdobeReader.log' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/setup:""[setupFile]"""
	echoAndLog "     The primary Adobe Reader installation program.  If switches "
	echoAndLog "    are required for setup to run silently, they must be provided."
	echoAndLog "/basever:"
	echoAndLog "     The base Adobe Reader product version for which to check (i.e. 10.1)"
	echoAndLog ""
	echoAndLog "Optional arguments and syntax:"
	echoAndLog "/patch:""[patchFile]"""
	echoAndLog "     MSP patch file to install after Adobe Reader setup completes."
end sub
' End Functions
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Parse Arguments
'
if WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
'	For Each sScrArg in cScrArgs
'		echoAndLog sScrArg 'Echo supplied arguments to console
'	Next

	for each sScrArg in cScrArgs
		select case LCase(sScrArg)
			Case "setup"
				sInstall = cScrArgs.Item(sScrArg)
			Case "patch"
				sPatch = cScrArgs.Item(sScrArg)
			Case "basever"
				sBaseVer = CStr(cScrArgs.Item(sScrArg))
			Case Else
				echoAndLog vbCrLf & "Unknown switch or argument: " & sScrArg & "."
				echoAndLog "**********************************" & vbCrLf
				subHelp
				oLog.Close
				WScript.Quit(100)
		end select
	next
	if (IsNull(sInstall) or IsEmpty(sInstall)) then
		echoAndLog "Required argument 'setup' was not provided."
		echoAndLog "**********************************" & vbCrLf
		subHelp
		oLog.Close
		wscript.quit(100)
	elseif (IsNull(sBaseVer) or IsEmpty(sBaseVer)) then
		echoAndLog "Required argument 'basever' was not provided."
		echoAndLog "**********************************" & vbCrLf
		subHelp
		oLog.Close
		wscript.quit(100)
	elseif (IsNull(sPatch) or IsEmpty(sPatch)) then 
		bDoPatch = False
	else
		bDoPatch = True
	end if
elseif WScript.Arguments.Named.Count = 0 then 'Detect if required args are not defined.
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	oLog.Close
	WScript.Quit(100)
end if
'
' End Argument Parsing
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Begin Main
'

' Complete version and installation strings:
sInstall = ".\" & sInstall
sPatch = "msiexec.exe /p " & sPatch & " /qb /norestart"

' Build path to Adobe Reader executable:
sPF = oShell.ExpandEnvironmentStrings( "%ProgramFiles%" )
sPath = sPF & "\Adobe\Reader 10.0\Reader\AcroRd32.exe"
echoAndLog "Acrobat Reader Path: " & sPath

' Get the version string on the currently installed Reader executable:
on error resume next
sVer = oFS.GetFileVersion(sPath)
on error goto 0
echoAndLog "Version of currently installer Adober Reader: " & sVer

' See if we already have the base version installed:
if InStr(Left(cStr(sVer),4),sBaseVer) then
	'Reader 10.1.x is already installed skip base product installation.
	echoAndLog "Base product installed.  Skipping setup..."
else
	'Install the base product.
	echoAndLog "Installing base product with command: " & sInstall
	set oExec = oShell.Exec(sInstall)
	Do While Not oExec.Status  1
		WScript.Sleep(100)
	Loop 
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode)
end if

'Now install the patch:
if bDoPatch = True then
	echoAndLog "Patch installation requested."
	echoAndLog "Installing patch with command: " & sPatch
	set oExec = oShell.Exec(sPatch)
	Do While Not oExec.Status  1
		WScript.Sleep(100)
	Loop 
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from patch installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode) + iExit
else 
	echoAndLog "Patch installation was not requested.  Exiting."
end if

oLog.Close
wscript.quit(iExit)
'
' End Main
'''''''''''''''''''''''''''''''''''''''''''''''''''

KillAndExec.vbs – Ensuring application installer success with VBScript

Today’s scripting challenge…

We are attempting to use SCCM 2012 as a patch management solution for our centrally supported third party applications.  Great new features in SCCM 2012 allow us to write detection rules for applications to determine if superseded versions are present on the client system, and to trigger an immediate upgrade.  Cool Beans.  Problem is, a lot of application installers that ran reliably in our MDT “LiteTouch” environment (which is used to deploy new operating systems with no previously installed software) will not run silently or successfully on systems where previous application versions were already installed, and may currently be running.

This is an old problem for client system management… how can you update in-use files?  In most cases I have seen, the admin will schedule the updates to run when no one is logged in.  Unfortunately, this is an edge case for us.  Most systems are off when no one is logged in.  Another system is to force logoff for application updates.  While this would work, it seems like a “heavy” solution… why force the user to log off to update one application that may or may not be running?  Why force all applications closed on the off chance that one application will need to be terminated.

Our solution?  Kill only the processes that need to be terminated to ensure application installation success.  See the VBScript solution below (I flirted with writing this one in PowerShell, but the code signing requirements still intimidate me, and I may have the odd-duck XP client that still does not have PowerShell).  I have tested the script on Firefox, Thunderbird, VLC, Notepad++, WinSCP, Filezilla, and KeePass.  Rock On!

UPDATE: Since initial publication, I have added some logic to handle execution from “wscript”. If the script is executed from wscript.exe, console output will be suppressed. Additionally, the log file now is named “killAndExec-(exeFileName).log”. (This prevents SCCM from overwriting the log file the next time a program installer runs that also uses this script).

'KillAndExec.vbs script, J. Greg Mackinnon, 2012-09-13
' Kills processes named in the "kill" argument (comma-delimited)
' Runs the executable named in the "exec" argument
' Appends the executable arguments specified in the "args" argument (comma-delimited)
'Requires: "kill" and "exec" arguments.  The executable named in the "exec" arg must be in the same directory as this script.
'Provides:
' RC=101 - Error terminating the requests processes
' RC=100 - Invalid input parameters
' Other return codes - Pass-though of return code from WShell.Exec.Run using the provided input parameters

Option Explicit

const quote = """"

'Declare Variables:
Dim aExeArgs, aKills
Dim bBadArg, bNoArgs, bNoExeArg, bNoExec, bNoKill, bNoKillArg 
Dim cScrArgs
Dim iReturn
Dim oShell, oFS, oLog
Dim sBadArg, sCmd, sExe, sExeArg, sKill, sLog, sScrArg, sTemp

'Set initial values:
bBadArg = false
bNoArgs = false
bNoExeArg = false
bNoExec = false
bNoKill = false
bNoKillArg = false
iReturn = 0

'Instantiate Global Objects:
Set oShell = CreateObject("WScript.Shell")
Set oFS  = CreateObject("Scripting.FileSystemObject")

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Define Functions
'
Sub subHelp
	echoAndLog "KillAndExec.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Kills named processes and runs the provided executable."
	echoAndLog "Logs output to 'KillAndExec.vbs' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/kill:""[process1];[process2]..."""
	echoAndLog "     Specify the image name of one or more processes to terminate."
	echoAndLog "/exe:""[ExecutableFile.exe]"""
	echoAndLog "     Specify the name of the executable to run."
	echoAndLog ""
	echoAndLog "Optional arguments:"
	echoAndLog "/args""[arg1];[arg2];[arg3]..."""
	echoAndLog "     Specify one or more arguments to pass to the executable."
	echoAndLog "/noKill"
	echoAndLog "     Switch to suppress default process termination.  Used for testing."
	echoAndLog "/noExec"
	echoAndLog "     Switch to suppress default program execution.  USed for testing."
End Sub

function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

function fKillProcs(aKills)
' Requires:
'     aKills - an array of strings, with each entry being the name of a running process.   
	Dim cProcs
	Dim sProc, sQuery
	Dim oWMISvc, oProc

	Set oWMISvc = GetObject("winmgmts:{impersonationLevel=impersonate, (Debug)}\\.\root\cimv2")
	sQuery = "Select Name from Win32_Process Where " 'Root query, will be expanded.	
	'Complete the query string using process names in "aKill"
	for each sProc in aKills
		sQuery = sQuery & "Name = '" & sProc & "' OR "
	next
	'Remove the trailing " OR" from the query string
	sQuery = Left(sQuery,Len(sQuery)-3)

	'Create a collection of processes named in the constructed WQL query
	Set cProcs = oWMISvc.ExecQuery(sQuery, "WQL", 48)
	echoAndLog vbCrLf & "----------------------------------"
	echoAndLog "Checking for processes to terminate..."
	'Set this to look for errors that aren't fatal when killing processes.
	On Error Resume Next
	'Cycle through found problematic processes and kill them.
	For Each oProc in cProcs
	   echoAndLog "Found process " & oProc.Name & "."
	   oProc.Terminate()
	   Select Case Err.Number
		   Case 0
			   echoAndLog "Killed process " & oProc.Name & "."
			   Err.Clear
		   Case -2147217406
			   echoAndLog "Process " & oProc.Name & " already closed."
			   Err.Clear
		   Case Else
			   echoAndLog "Could not kill process " & oProc.Name & "! Aborting Script!"
			   echoAndLog "Error Number: " & Err.Number
			   echoAndLog "Error Description: " & Err.Description
			   echoAndLog "Finished process termination function with error."
			   echoAndLog "----------------------------------"
			   echoAndLog vbCrLf & "Kill and Exec script finished."
			   echoAndLog "**********************************" & vbCrLf
			   WScript.Quit(101)
	   End Select
	Next
	'Resume normal error handling.
	On Error Goto 0
	echoAndLog "Finished process termination function."
	echoAndLog "----------------------------------"
end function

function fGetHlpMsg(sReturn)
' Gets known help message content for the return code provided in "sReturn".
' Requires:
'     Existing WScript.Shell object named "oShell"
	Dim sCmd, sLine, sOut
	Dim oExec
	sCmd = "net.exe helpmsg " & sReturn
	echoAndLog "Help Text for Return Code:"
	set oExec = oShell.Exec(sCmd)
	Do While oExec.StdOut.AtEndOfStream  True
		sLine = oExec.StdOut.ReadLine
		sOut = sOut & sLine
	Loop
	fGetHlpMsg = sOut
end function
'
' End Define Functions
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Parse Arguments
If WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
	For Each sScrArg in cScrArgs
		Select Case LCase(sScrArg)
			Case "nokill"
				bNoKill = true
			Case "noexec"
				bNoExec = true
			Case "kill"
				aKills = Split(cScrArgs.Item(sScrArg), ";", -1, 1)
			Case "exe"
				sExe = cScrArgs.Item(sScrArg)
			Case "args"
				aExeArgs = Split(cScrArgs.Item(sScrArg), ";", -1 ,1)
			Case Else
				bBadArg = True
				sBadArg = sScrArg
		End Select
	Next
	If (IsNull(sExe) or IsEmpty(sExe)) Then
		bNoExeArg = True
	ElseIf (IsNull(aKills) or IsEmpty(aKills)) Then
		bNoKillArg = True
	End If
ElseIf WScript.Arguments.Named.Count = 0 Then 'Detect if required args are not defined.
	bNoArgs = True
End If 
' End Argument Parsing
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Initialize Logging
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
sLog = "killAndExec-" & sExe & ".log"
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)
' End Initialize Logging
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
' Process Arguments
if bBadArg then
	echoAndLog vbCrLf & "Unknown switch or argument: " & sBadArg & "."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	WScript.Quit(100)
elseif bNoArgs then
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	WScript.Quit(100)
elseif bNoExeArg then
	echoAndLog "Required argument 'exe' was not provided."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	wscript.quit(100)
elseif bNoKillArg then
	echoAndLog "Required argument 'kill' was not provided."
	echoAndLog "**********************************" & vbCrLf
	subHelp
	wscript.quit(100)
end if
' Log processes to kill:
for each sKill in aKills
	echoAndLog "Process to kill: " & sKill
next
' Log executable arguments:
echoAndLog "Executable to run: " & sExe
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
	for each sExeArg in aExeArgs
		echoAndLog "Executable argument: " & sExeArg
	next
else 
	echoAndLog "Executable has no provided arguments."	
end if
' End Process Arguments
'''''''''''''''''''''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''''''''''''''''''''
'Begin Main
'
'Build full command string:
if inStr(sExe," ") then 'Spaces in the exe file
	sExe = quote & sExe & quote 'Add quotations around the executable.
end if
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
	sCmd = sExe & " " 
	for each sExeArg in aExeArgs
		if inStr(sExeArg," ") then
			sExeArg = quote & sExeArg & quote 'Add quotations around the argument.
		end if
		sCmd = sCmd & sExeArg & " "
	next
else
	sCmd = sExe
end if
echoAndLog "Command to execute:"
echoAndLog sCmd

'Kill requested processes:
if bNoKill = false then
	fKillProcs aKills
else
	echoAndLog "/noKill switch has been set.  Processes will not be terminated."
end if
'Run the requested command:
echoAndLog vbCrLf & "----------------------------------"
if bNoExec = false then
	echoAndLog "Running the command..."
	on error resume next 'Disable exit on error to allow capture of oShell.Run execution problems.
	iReturn = oShell.Run(sCmd,10,True)
	if err.number  0 then 'Gather error data if oShell.Run failed.
	    echoAndLog "Error: " & Err.Number
		echoAndLog "Error (Hex): " & Hex(Err.Number)
		echoAndLog "Source: " &  Err.Source
		echoAndLog "Description: " &  Err.Description
		iReturn = Err.Number
		Err.Clear
		wscript.quit(iReturn)
	end if
	on error goto 0
	echoAndLog "Return code from the command: " & iReturn
	if iReturn  0 then 'If the command returned a non-zero code, then get help for the code:
		fGetHlpMsg iReturn
	end if 
else
	echoAndLog "/noExec switch has been set.  Executable will not run."
end if
echoAndLog "----------------------------------"

oLog.Close
wscript.quit(iReturn)
'
' End Main
'''''''''''''''''''''''''''''''''''''''''''''''''''

WiFi Profiles for Windows 8

So Windows 8 is here, to little fanfare at the University.  While I am always happy to have an updated version of Windows to work with, I see that I have yet to blog anything about it.  Perhaps that is because, unlike with the release of Windows 7, there was so little that was relatively “wrong” with the previous release.  I find myself with not much “to do” to get the enterprise ready for Windows 8.  Other reasons for the lack of hype… Windows 7 applications seem, for the most part, to “just work” on Windows 8, thus necessitating very little in the way of application compatibility planning.

Still, we have run into a few hiccups.  I spent most of the last two days updating the UVM WiFi Configuration Tool scripts and experimenting with Group Policy settings to make WPA2-protected wireless working consistently (Previously discussed here, way back in ought-eight.).  In the end, there was very little that I did to the WiFi policies that was Windows 8 specific.  The WiFi profile that we are using maintains backward compatibility with both Windows 7 and Windows Vista.

Here are the details:

  • The 802.1x settings in our WiFi profile was updated to use “user authentication” instead of “user or computer authentication”.  Under XP, this option was called “user reauthentication”.  “ReAuthentication” meant that the computer would attempt to log on as the computer account, but that if the connection was lost, it would re-authenticate as the logged on user.  Under XP, it was not possible to prevent computer authentication attempts.  However, under Win7/Win8, user authentication is just that… only user authentication is attempted, computer authentication is excluded.  We have verified this by looking at the RADIUS server logs.  Switching to “user authentication” will cut down on log errors on the RADIUS servers, and will result in fewer errors on client systems as well.
  • We have added a new trust anchor for our RADIUS server certificate in the WiFi profile.  This was necessitated by mergers and acquisitions on the CA business.  “Equifax” provided our original WPA2/PEAP certificate.  When we went to renew our certificate, we found that Equifax had been acquired by GeoTrust, and that new certificates would be issued from a GeoTrust intermediate CA.  However, this intermediate CA would be cross-signed using the Equifax root CA, so the Equifax trust anchor would still work.  The problem is that if a system has both the GeoTrustandEquifax certs present in the local trusted roots certificate store, it will validate the “radius.uvm.edu” up to the GeoTrust anchor, and will ignore the cross-signing with Equifax.  This results in WiFi connection errors.  When I add the GeoTrust cert as an additional trust anchor, the problem goes away.
  • The VBScript I use to install the WiFi profile is packaged inside a 7-Zip self extractor.  The use of this self-extractor triggers the Windows “Program Compatibility Assistant”, which in turn raises a “This program might not have installed correctly” error after the tool runs.  This problem is corrected by embedding a “manifest” file into the tool.  Typically, this is done using the “mt.exe” tool included in the Windows SDK.  Unfortunately, MT.exe corrupts self-extracting 7-Zip archives (this also is a known problem with WinRAR, and perhaps other similar tools).  Fortunately I was able to work around the problem using “Resource Tuner” from Heaventools.  I needed to add “trustInfo” and “compatibility” sections to the manifest.  My blog engine is really bad about posting XML content in a page, so I will forego posting the manifest here. You can find sample manifests pretty easily though Google.
  • When we run the packaged configuration tool, we get a warning that the application package is unsigned and may not be trustworthy.  I used “signtool.exe” from the Windows SDK to add a signature to the executable, so now it is considered somewhat more trustworthy.  Good instructions on the use of signtool.exe can be found here:
    http://www.tech-pro.net/code-signing-for-developers.html
    I am using a code signing cert that we obtained from the InCommon.org certificate service, hosted by Comodo.  It works.
  • Finally, I updated the profile installer VBScript to make reconfiguration a bit easier (subroutines were converted to functions so that variables set at the start of the script can be passed down to the function.  We then can set things like the trust anchor name, WiFi network name, and log file name at the start of the script where they are more easily edited.  Also, I removed support for Windows XP… no more Service Pack detection, Hotfix installation, or third-party profile installation utilities are needed by the script.  I was able to hack the script down to about a quarter of its original size as a result.  The new script is included below, for those who like that sort of thing…

 


Option Explicit
'On Error Resume Next
'Install UVM WPA2-Enterprise wireless profile
' Version 1.3 by J. Greg Mackinnon, University of Vermont
' Supported platforms:  Windows Vista, 7, and 8
' Requires external tools:  "CertMgr.exe" (from the Windows Platform SDK)
' Requires external files:  Root CA certificate file, 
'                           WiFi XML configuration files for Vista+ Windows OS.
'                            (obtained by running "netsh wlan export profile UVM .\"
' NOTE: modify variables in the "Define variables" section to suit your environment.

'History:
' Version 1.0 - Supported UVM WiFi using WPA2, Equifax certs, Windows XP SP2+ and Vista OS
' Version 1.1 - Updated to support Windows 7
' Version 1.2 - Updated to support Windows 8.  Removed support for XP 
'             - Removed third-party "ZWlanCfg" utility and OS Hotfix installation functions (were only needed for XP support)
' Version 1.3 - Converted existing subroutines to functions to allow for easier switching of CAs and WiFi networks.
'             - Moved Global Variables to the top of the script for easier modification.
'             - Updated CA cert and WPA Profile supporting files to use "GeoTrust" instead of "Equifax".

' Create constants
Const cLogFile = "install_UVM_WiFi.log"

' Declare variables
Dim oShell, oUserEnv, oFSO, oFile, oRegExp
Dim iSPVer
Dim sTempEnv, strComputer, sOSTest, sOS, sCertName, sCertFile, sNetName, sProfileFile
Dim bReRun

' Define variables
bReRun = False
strComputer = "."
sOSTest = "Vista|Windows 7|Windows 8" 'Regular Expression for OS compatibility testing
sCertName = "GeoTrust Global CA"      'Friendly name of the trust anchor certificate
sCertFile = "GeoTrustGlobalCA.cer"    'Name of the trust anchor file
sNetName = "UVM"                      'Name of the WiFi Access Point
sProfileFile = ".\Wi-Fi-UVM.xml"      'Name of the Vista+ wlan profile file.

' Instantiate global objects
Set oShell = WScript.CreateObject("WScript.Shell")
Set oFSO = CreateObject("Scripting.FileSystemObject")
sTempEnv = oShell.ExpandEnvironmentStrings("%TEMP%") & "\"
Set oFile = oFSO.CreateTextFile(sTempEnv & cLogFile,True)
Set oRegExp = New RegExp
oRegExp.IgnoreCase = True
oRegExp.Global = True
oRegExp.Pattern = sOSTest

'''''''''''''''''''''''''''''''''
' Define Functions
'
Function fDetectOS(sOS, iSPVer)
'Detect OS Function - detects OS Caption string and Service Pack integer from WMI WIN32_OperatingSystem.
'Expects to varibles passed, returns the full OS Caption String, and SP Major Version intger
	'Declare variables
	Dim colItems
	Dim objWMIService, objItem
	'Instantiate local objects/collections
	Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\CIMV2") 
	Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

	For Each objItem In colItems
	  sOS = objItem.Caption
	  oFile.WriteLine "Detected Operating System: " & sOS
	  iSPVer = CInt(objItem.ServicePackMajorVersion)
	  oFile.WriteLine "Detected Service Pack Version: " & iSPVer
	  oFile.WriteLine "Service Pack Minor Version: " & objItem.ServicePackMinorVersion
	Next
	
	'Clean local objects/variables
	Set objItem = Nothing
	Set colItems = Nothing
	Set objWMIService = Nothing
End Function

Function fInstCert(sCertName,sCertFile)
' Installs cert with sCertName root CA cert into machine "root" store.
' Requires:  certmgr.exe from the Windows Platform SDK (available with VS .NET or VS 2008 installations), 
'	sCertName variable - contains the friendly name of the root CA
'	sCertFile variable - contains the name of the root CA certificate file
' Requres:  Root CA cert file
' Notes:  We use the "root" argument to certmgr.exe to install into the "Trusted Root Certificate Authorities".  
'		We also could use "ca" to install Intermediate Certificate Authorities.
'		In a previous version of this script we used "oShell.Run", but his returned unexpected results on the
'		Windows 7 platform... using .Exec now.
	
	Dim bCertPresent, bInstSuccess
	Dim oExec
	Dim sOut

	bCertPresent = false
	bInstSuccess = false
	
	set oExec = oShell.Exec("certmgr.exe -c -s -r localMachine root")

	Do Until oExec.StdOut.AtEndOfStream
		sOut = oExec.StdOut.ReadLine()
		if InStr(sOut, sCertName) Then
			'oFile.WriteLine sOut
			'WScript.Echo sOut
			bCertPresent = true
		End If
	Loop

	if bCertPresent = false then
		oFile.WriteLine "Root Certificate for """ & sCertName & """ needs to be installed.  Attempting install..."
		set oExec = oShell.Exec("certmgr.exe -add -c " & sCertFile & " -s -r localMachine root")
		Do Until oExec.StdOut.AtEndOfStream
			sOut = oExec.StdOut.ReadLine()
			if InStr(sOut, "Succeeded") Then
				'oFile.WriteLine sOut
				bInstSuccess = true
			End If
		Loop
		if bInstSuccess = true then
			oFile.WriteLine "Certificate installed successfully"
		else 
			oFile.WriteLine "Certificate failed to install... You will need to install the " _
				& "certificate manually.  See the instructions at https://www.uvm.edu/ets/wireless " _
				& ", then run this script again to compelte installation of the UVM wireless profile."
			WScript.Quit -2
		end if
	else
		oFile.WriteLine "Root Certificate for """ & sCertName & """ is already installed."
	End If
End Function

Function fImportProfile(sProfileFile,sNetName)
'Imports Vista+ Wireless Profile using NETSH command.  
'Requires: a Vista+ wifi profile file exported using NETSH, 
'	sProfileFile - string containing name of the wlan XML profile file to be imported
'	sNetName - string contining the name of the wlan profile name (WiFi Network Name)

	'On Error Resume Next
	Const cUserScope = "all"
	
	Dim iStrMatch
	Dim oExec, oStdOut
	Dim sStdOutLine
	
	oFile.WriteLine "Executing command: netsh wlan add profile filename=""" & sProfileFile & """ user=" & cUserScope & ""
	Set oExec = oShell.Exec("netsh wlan add profile filename=""" & sProfileFile & """ user=" & cUserScope & "")
	Set oStdOut = oExec.stdOut
	While Not oStdOut.AtEndOfStream
		sStdOutLine = oStdOut.ReadLine
		oFile.WriteLine(sStdOutLine)
		iStrMatch = CInt(InStr(sStdOutLine, "Profile " & sNetName & " is added on interface"))
		If iStrMatch > 0 Then
			WScript.Echo "The " & sNetName & " wireless profile was added successfully to your system"
		ElseIf iStrMatch = 0 Then
			WScript.Echo "The wireless profile failed to import.  Please see the manual profile " _
			& "configuration instructions available at http://www.uvm.edu/ets/wireless.  A " _
			& "log file named " & cLogFile & " which contains the full error message can be " _
			& "found in the " & sTempEnv & " directory."
			WScript.Quit -3
		End If
	Wend
	
	Set oStdOut = Nothing
	Set oExec = Nothing
End Function
'
' End Functions
'''''''''''''''''''''''''''''''''

'''''''''''''''''''''''''''''''''
' Begin Main
'

fDetectOS sOS, iSPVer

If oRegExp.Test(sOS) = True Then
	fInstCert sCertName, sCertFile
	fImportProfile sProfileFile, sNetName
Else
	oFile.WriteLine "Your operating system is not supported for use with this script."
	WScript.Quit -4
End If

oFile.close

' Environment cleanup 
Set oFile = Nothing
Set oFSO = Nothing
Set oUserEnv = Nothing
Set oShell = Nothing
Set oRegExp = Nothing

'
' End Main
''''''''''''''''''''''''''''''''''

Thunderbird 13 – The cloud arrives

Mozilla Thunderbird 13 arrived this week.  Guess what?  Our customized build process broke again.  Now, when you start TB for the first time, you get greeted with the option to create a new email account with one of Thunderbird’s “partners” (in other words, email providers who paid for the honor of being put in the “welcome to Thunderbird” start dialog).

With the assistance of the awesome Ben Coddington (who does not keep a blog, but should so that you can bask in his awesomeness), I was able to track down the place that the new-new account dialog is called, and kill it by switching a preference in the “thunderbird-all.js” file.

The preference is a Boolean named “mail.provider.enabled”, set in the thunderbird-all.js file, as documented here:
http://hg.mozilla.org/releases/comm-beta/rev/879e8d044e36
and referenced here:
https://bugzilla.mozilla.org/show_bug.cgi?id=718792#c3
and here:
https://wiki.mozilla.org/index.php?title=Thunderbird/Support/TB13UserChanges

I updated our Thunderbird build script to set this preference to “false”:

Echo modifying default "All Thunderbird" preferences...
..\..\..\bin\sed.exe --binary "s/pref(\"mail.provider.enabled\", true);/pref(\"mail.provider.enabled\", false);/"  .\defaults\pref\all-thunderbird_new.js
if errorlevel 1 goto err
MOVE /Y .\defaults\pref\all-thunderbird_new.js .\defaults\pref\all-thunderbird.js

The whole ugly build script is provided below:

REM Thunderbird customized build script for UVM.
REM Updated June 2012 for Thunderbird 13 support.
REM REQUIRES: 
REM 	- 7z.exe, 7zr.exe and sed.exe in parallel "..\bin" directory
REM     - Unmodified Thunderbird installer in .\source directory
REM		- all required config files in .\config directory
REM     	(including 7z control file, ISP Hook RDF file, and modified prefs.js)
REM		- local JDK install with "jar.exe".  Path to jar.exe will need to be updated in the jdk environment variable
REM OUTPUT: Fully modified Thunderbird installer in .\Installer directory.
REM @echo on

set jdk="c:\Program Files (x86)\Java\jdk1.6.0_27\bin"

Echo Cleaning up old builds...
del .\Installer\*.exe
rmdir /s /q .\build
set /P tbver=Enter Thunderbird version number to build (i.e. "6.0.2"):

Echo Extracting setup files from OEM Installer...
mkdir .\build\temp
..\bin\7zr x .\source\*.exe -o.\build

Echo Extracting omni.ja contents...
mkdir .\build\temp
cd .\build\temp
%jdk%\jar.exe xf ..\core\omni.ja
if errorlevel 1 goto err

Echo modifying messenger functions...
..\..\..\bin\sed.exe --binary "s/NewMailAccount(msgWindow, okCallback);/MsgAccountWizard(okCallback);/"  .\chrome\messenger\content\messenger\msgMail3PaneWindow_new.js
if errorlevel 1 goto err
MOVE /Y .\chrome\messenger\content\messenger\msgMail3PaneWindow_new.js .\chrome\messenger\content\messenger\msgMail3PaneWindow.js

Echo modifying default "All Thunderbird" preferences...
..\..\..\bin\sed.exe --binary "s/pref(\"mail.provider.enabled\", true);/pref(\"mail.provider.enabled\", false);/"  .\defaults\pref\all-thunderbird_new.js
if errorlevel 1 goto err
MOVE /Y .\defaults\pref\all-thunderbird_new.js .\defaults\pref\all-thunderbird.js

Echo modifying default mailnews preferences...
..\..\..\bin\sed.exe --binary "s/try_ssl\", 0)/try_ssl\", 2)/"  .\defaults\pref\mailnews_new.js
if errorlevel 1 goto err
MOVE /Y .\defaults\pref\mailnews_new.js .\defaults\pref\mailnews.js

Echo moving UVM modified prefs.js into place (note that this file is not actually used by Thunderbird!)
copy /Y ..\..\config\prefs.js .\defaults\profile\prefs.js

Echo Repacking omni.ja...
del /f /q ..\core\omni.ja
%jdk%\jar.exe cf ..\core\omni.ja *

Echo Copying UVM Custom ISP file to source...
cd ..\..\
mkdir .\build\core\isp\en-US
copy /Y .\config\UVMMail.rdf .\build\core\isp\en-US\UVMMail.rdf
if errorlevel 1 goto err
Echo Copying UVM default prefs.js to core directory (tbird no longer has a prefs.js by default, but it will be used if present)...
mkdir .\build\core\defaults\profile
copy /Y .\config\prefs.js .\build\core\defaults\profile\prefs.js
if errorlevel 1 goto err

Echo Deleting temporary files that should not be present in the installer...
rmdir /s /q .\build\temp

Echo Repackaging Thunderbird installer...
..\bin\7zr a .\Installer\UVM_Thunderbird_setup_%tbver%.7z .\build\*
copy /b ..\bin\7zS.sfx + .\config\config.txt + .\Installer\UVM_Thunderbird_setup_%tbver%.7z .\Installer\UVM_Thunderbird_setup_%tbver%.exe

Echo Cleaning up installation source...
del /s /f /q .\build\*.*
rmdir /s /q .\build\core
rmdir /s /q .\build
del /f /q .\Installer\UVM_Thunderbird_setup_%tbver%.7z
goto end

:err
echo There was an error running a command.

:end

Driver installation with SCCM Software Distribution

Here we are, working with SCCM again.  Making difficult things possible, and simple things difficult.  Today we wish to distribute a SmartCard driver to all of our managed servers, so that we can require Smart Card for certain classes of logins.  the newer “CNG” Smart Card minidrivers are all simple “.inf” driver packages that you can right-click install.  This ought to be easy, thought the sys admin.  Wrong!

Installation of inf drivers is not a well documented command line procedure (unlike the rather more complicated “.msi” package, which at least is easy to script).

My thanks goes out to the following bloggers and forum users for their assistance with this case:

The script that I cobbled together to install the Athena “ASECard” minidriver is displayed below.  Note that this should work for pretty much any minidriver, as long as it has a “DefaultInstall” section in the inf file.  I just unpack the amd64 and x86 driver cab files into their respective directories, put the batch script one directory above these, and make an SCCM software package of the whole thing.  The installation command line is simply the batch file name.

@echo off
REM Installs the drivers specified in the "DefaultInstall" section
REM of the aseMD.inf that is appropriate for the current (x86 or amd64) platform.
REM Install is silent (4 flag), with no reboot (N flag).
REM The INF is specified to be in the x86 or amd64 subdirectory
REM of the script directory (%~dp0).

echo Detecting platform...
IF EXIST "%programfiles(x86)%" (GOTO :amd64) ELSE (GOTO :i386)

:i386
echo Installing 32-bit driver...
cd x86
%windir%\system32\rundll32.exe advpack.dll,LaunchINFSectionEx "%~dp0x86\aseMD.inf",DefaultInstall,,4,N
goto :EOF

:amd64
REM The command will run in 64-bit mode (%windir%\sysnative\),
REM when called from a 32-bit CMD.exe (as will be the case with SCCM).
echo Installing 64-bit driver...
cd amd64
%windir%\sysnative\rundll32.exe advpack.dll,LaunchINFSectionEx "%~dp0amd64\aseMD.inf",DefaultInstall,,4,N
goto :EOF
REM End of file

Windows Backup Performance Testing with PowerShell

While developing our new Windows file services infrastructure, we wanted to test our pre-production platform to see if there are any file server-side bottlenecks that will cause unacceptable delays in backup processing. Here are UVM we still are using EMC Networker for Enterprise backup (no comments on our satisfaction with EMC will be provided at this time). EMC provides a tool “uasm.exe” that is used at the core of the “save.exe” and “recover.exe” commands on the backup client. If we use “uasm.exe” to backup all of the file server data to “null”, it is possible that we will be able to detect disk, HBA, and other local I/O bottlenecks before they bite us in production.

Since Networker will break up our file server into multiple “save sets”, and run a user-definable number of save set backup processes in parallel, it also is important for us to determine the required number of parallel backup processes required to complete backup in a timely fashion. Thus, we want to run several parallel “uasm.exe” processes in our tests.

PowerShell, with the assistance of “cmd.exe”, and some finesses, can get this job done. Hurdles I ran into while scripting this test follow:

  1. During development, PowerShell consumed huge amounts of CPU while redirecting uasm.exe output to the PowerShell $null object. Interestingly, previous tests using uasm.exe with cmd.exe did not show this problem. To fix this, each uasm job is spawned from a one-line cmd.exe “bat” script, which is included below.
  2. Remember that PowerShell uses the null object “$null”, but that cmd.exe uses the handle “nul” (with one “L”). If you redirect to “null”, you will soon fill up your disk with a file named “null”.
  3. When wanted to examine running jobs, it was difficult to determine which directory a jobs was working on. This was because I initially created a scriptblock object and passed parameters to it when starting a job. For example:
    [scriptblock] $sb = {
    $uasmBlock = {
    	param ([string]$sPath)
    	[string[]] $argList = '/c','c:\local\scripts\uasm_cmd.bat',$sPath
    	& cmd.exe $argList
    }
    $jobs += start-job -Name $myJob -ScriptBlock $sb -ArgumentList $dir1
    

    However, when inspecting the job object’s “command” property, we see “$sPath” in the output. We want the variable expanded. How to do this? Create the scriptblock object in-line when starting the job:

    [string] $cmd = '& cmd.exe "/c","c:\local\scripts\uasm_cmd.bat",' + $dir
    $jobs += Start-Job -Name $jobName `
    	-ScriptBlock ([scriptblock]::create($cmd))
    

    This makes for more compact code, too.

  4. To check on jobs that have completed, I create an array named “$djs” (Done Jobs), populated by piping the $jobs array and filtering for “completed” jobs. I inspect $djs to see if jobs are present. In my first pass, I used the check:
    if ($djs.count -gt 0)

    Meaning, continue if there is anything in the array $djs. However, this check did not work well because output from the $jobs object would put a null item in $djs on creation, meaning that if there were no running jobs, $djs would still have a count of one! I fixed this by changing the test:

    if ($djs[0] -ne $null)

    Meaning, if the first entry in $djs is not a null object, then proceed.

The full script follows:

#uasm_jobQueue.ps1, 2011-09-30, author: J. Greg Mackinnon
#Tests performance of disk when accessed by Networker backup commands.
#   Creates a queue of directories to test ($q), then uses external command 
#   "uasm.exe" to backup these directories to null.
#Change the "$wp" variable to set the number of uasm 'worker processes' to be 
#   used during the test.
#Note: PowerShell $null object causes very high CPU utilization when used for
#   this purpose.  Instead, we call "uasm_cmd.bat" which uses the CMD.exe 'nul'
#   re-director.  'nul' does not have the same problems as $null.

set-psdebug -strict

[int] $wp = 4

# Initialize the log file:
[string] $logfile = "s:\uasm_test.log"
remove-item $logfile -Force
[datetime] $startTime = Get-Date
[string] "Start Time: " + $startTime | Out-File $logfile -Append

##Create work queue array:
# Add shared directories:
[String[]] $q = gci S:\shared | ? {$_.Attributes.tostring() -match "Directory"}`
	| sort-object -Property Name | % {$_.FullName}
# Add remaining targets to queue:
$q += 'H:\','I:\','J:\','K:\','L:\','M:\','S:\sis\','S:\software\','s:\r25\'
	
[int] $dc = 0			#Count of completed (done) jobs.
[int] $qc = $q.Count	#Initial count of jobs in the queue
[int] $qi = 0			#Queue Index - current location in queue
[int] $jc = 0			#Job count - number of running jobs
$jobs = @()				#Jobs array - intended to contain running PS jobs.
	
while ($dc -lt $qc) { # Completed jobs is less than total jobs in queue
	# Keep running jobs until completed jobs is less than total jobs in queue, 
	#  and our queue count is less than the current queue index.
	while (($jobs.count -lt $wp) -and ($qc -gt $qi)) { 
		[string] $jobName = 'qJob_' + $qi + '_';
		[string] $dir = '"' + $q[$qi] + '"'
		[string] $cmd = '& cmd.exe "/c","c:\local\scripts\uasm_cmd.bat",' + $dir
		#Start the job defined in $cmd string.  Use this rather than a pre-
		#  defined scriptblock object because this allows us to see the expanded
		#  job command string when debugging. (i.e. $jobs[0].command)
		$jobs += Start-Job -Name $jobName `
			-ScriptBlock ([scriptblock]::create($cmd))
		$qi++ #Increment the queue index.
	}
	$djs = @(); #Completed jobs array
	$djs += $jobs | ? {$_.State -eq "Completed"} ;
	# $djs array will always have a count of at least 1.  However, if the 
	#    first entry is not empty (null), then there must be completed jobs to
	#    be retrieved.
	if ($djs[0] -ne $null) { 
		$dc += $djs.count;
		$djs | Receive-Job | Out-File $logfile -Append; #Log completed jobs
		$djs | Remove-Job -Force;
		Remove-Variable djs;
		$jobs = @($jobs | ? {$_.State -eq "Running"}); #rebuild jobs array.
	}
	Start-Sleep -Seconds 3
}


# Complete logging:
[datetime] $endTime = Get-Date
[string] "End Time: " + $endTime | Out-File $logfile -Append 
$elapsedTime = $endTime - $startTime
[string] $outstr =  "Elapsed Time: " + [math]::floor($elapsedTime.TotalHours)`
	+ " hours, " + $elapsedTime.minutes + " minutes, " + $elapsedTime.seconds`
	+ " seconds."
$outstr | out-file -Append $logfile

The “uasm_cmd.bat” file called in the above code block contains the following one line:

"c:\program files\legato\nsr\bin\uasm.exe" -s %1 > nul