Trevor Sullivan's Tech Room

Ramblings of an IT professional

Windows 2003 Print Log Parsing Script [PowerShell]

Posted by pcgeek86 on November 6, 2009

Hello everyone,

Since I’m not aware of any tools that visually log print server information, I wrote a script that parses the log entries from the system event log of a Windows 2003 print server. Apparently I never got around to posting this online, but I hope that someone is able to benefit from it.

Basically, the process flow of the script looks like this:

  1. Retrieve list of all print entries from server event log
  2. Parses information from each log entry and creates an object in memory representing it
  3. Builds a hash table from all objects
  4. Iterates over the hash table and writes each print log entry to an Excel document

If you have any questions about it, please feel free to let me know.

FYI, I commented out the line near the bottom that does the Excel output, so that it could be executed on a server without Excel installed.

#################################################################################
#																				#
#	Author: Trevor Sullivan
#																				#
#	  Date: February 10th, 2009
#																				#
#  Purpose: This script is meant for running against a Windows 2003 event log.
#		The verbage for the print log entries has changed for Windows Vista.
# 																				#
#################################################################################

Function GetPrintEntriesFromTextFile($tFileName)
{
	Get-Content $tFileName
}

Function GetPrintEntriesFromLog()
{
	$PrintEntries = Get-EventLog -LogName System | Where-Object { $_.EventId -eq 10	-and $_.Source -eq "Print" -and $_.TimeGenerated.Month -eq (Get-Date).Month	-and $_.TimeGenerated.Year -eq (Get-Date).Year }
	return $PrintEntries
}

# Parses username from a Windows 2003 Print log message
Function GetUserName($PrintEntry)
{
	If ($PrintEntry -eq "" -or $PrintEntry -eq $null) { return $null }

	$rxUserName = [regex]"owned by ([0-9a-zA-Z]{7})"
	$rxUserName = [regex]"owned by ([0-9a-zA-Z]{1,}) was"
	$rxMatches = $rxUserName.Match($PrintEntry)
	return $rxMatches.Groups[1].Value
}

Function GetPrinterName($PrintEntry)
{
	If ($PrintEntry -eq "" -or $PrintEntry -eq $null) { return $null }

	#Write-Host "Parsing printer name"
	$rxPrinterName = [regex]"printed on ([0-9a-zA-Z-_\s]{5,}) via"
	$rxMatches = $rxPrinterName.Match($PrintEntry)
	return $rxMatches.Groups[1].Value
}

Function GetPrintSize($PrintEntry)
{
	If ($PrintEntry -eq "" -or $PrintEntry -eq $null) { return $null }

	#Write-Host "Getting print size"
	$rxPrintSize = [regex]"Size in bytes: ([0-9]+);"
	$rxMatches = $rxPrintSize.Match($PrintEntry)
	return $rxMatches.Groups[1].Value
}

Function GetPageCount($PrintEntry)
{
	If ($PrintEntry -eq "" -or $PrintEntry -eq $null) { return $null }

	#Write-Host "Getting page count"
	$rxPageCount = [regex]"pages printed\: ([0-9]+)"
	$rxMatches = $rxPageCount.Match($PrintEntry)
	return $rxMatches.Groups[1].Value
}

Function GetDocumentName($PrintEntry)
{
	If ($PrintEntry -eq "" -or $PrintEntry -eq $null) { return $null }

	#Write-Host "Getting print size"
	$rxDocumentName = [regex]", ([a-zA-Z-_:/\[#\]\?\\\=\d\.\s\(\)&-,]{1,}) owned by"
	$rxMatches = $rxDocumentName.Match($PrintEntry)
	return $rxMatches.Groups[1].Value
}

# Retrieves user's full name from AD
Function GetUserFullName($UserId)
{
	if ($UserId -gt "")
	{
		$DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher
		$LdapFilter = "(&(objectClass=user)(samAccountName=${UserId}))"
		#Write-Host "Filter is: ${LdapFilter}"
		$DirectorySearcher.Filter = $LdapFilter
		$UserEntry = [adsi]"$($DirectorySearcher.FindOne().Path)"
		#Write-Host $UserEntry.displayName
		return $UserEntry.displayName
	}

	return
}

Function CreatePrintJob()
{
	$PrintJob = New-Object PsObject
	Add-Member -Force -InputObject $PrintJob -MemberType NoteProperty -Name PageCount -Value $null
	Add-Member -Force -InputObject $PrintJob -MemberType NoteProperty -Name UserName -Value $null
	Add-Member -Force -InputObject $PrintJob -MemberType NoteProperty -Name DocumentName -Value $null
	Add-Member -Force -InputObject $PrintJob -MemberType NoteProperty -Name Size -Value $null
	Add-Member -Force -InputObject $PrintJob -MemberType NoteProperty -Name Printer -Value $null
	Add-Member -Force -InputObject $PrintJob -MemberType NoteProperty -Name Time -Value $null
	Add-Member -Force -InputObject $PrintJob -MemberType NoteProperty -Name UserFullName -Value $null
	return $PrintJob
}

Function ParsePrintEntry($PrintEntry)
{
	$NewPrintJob = CreatePrintJob

	if ($PrintEntry.GetType() -eq [System.String])
	{
		$NewPrintJob.PageCount = GetPageCount $PrintEntry
		$NewPrintJob.UserName = GetUserName $PrintEntry
		$NewPrintJob.DocumentName = GetDocumentName $PrintEntry
		$NewPrintJob.Size = GetPrintSize $PrintEntry
		$NewPrintJob.Printer = GetPrinterName $PrintEntry
		$NewPrintJob.UserFullName = GetUserFullName $NewPrintJob.UserName
	}
	elseif ($PrintEntry.GetType() -eq [System.Diagnostics.EventLogEntry])
	{
		$NewPrintJob.PageCount = GetPageCount $PrintEntry.Message
		$NewPrintJob.UserName = GetUserName $PrintEntry.Message
		$NewPrintJob.DocumentName = GetDocumentName $PrintEntry.Message
		$NewPrintJob.Size = GetPrintSize $PrintEntry.Message
		$NewPrintJob.Printer = GetPrinterName $PrintEntry.Message
		$NewPrintJob.Time = $PrintEntry.TimeGenerated.ToString()
		$NewPrintJob.UserFullName = GetUserFullName $NewPrintJob.UserName
	}

	return $NewPrintJob
}

Function Main()
{
	$PrintEntries = GetPrintEntriesFromLog
	#$PrintEntries = GetPrintEntriesFromTextFile "${env:USERPROFILE}\Documents\Powershell Scripts\Print Log Parser\PrintLog.txt"
	$Global:ParsedEntries = @{}; $i = 0

	ForEach ($PrintEntry in $PrintEntries)
	{
		$ParsedEntries.Add($i, $(ParsePrintEntry $PrintEntry))
		#$ParsedEntries += (ParsePrintEntry $PrintEntry)
		$i++

		if ($i % 100 -eq 0)
		{ Write-Host "Processed $i records" }
	}

	# Export information to CSV or Excel document
	$ParsedEntries.Values | Export-Csv "c:\$((Get-Date).Month).csv" -NoTypeInformation
	#WriteToExcel $ParsedEntries
}

Function WriteToExcel($tEntries)
{
	# Load Excel interop assembly
	[Void] [Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Excel")

	# Create Excel application, workbook, and worksheet objects
	$Excel = New-Object Microsoft.Office.Interop.Excel.ApplicationClass
	$Workbook = $Excel.Workbooks.Add()
	$Worksheet = $Workbook.Worksheets.Add()

	# Write Excel worksheet headers
	$Worksheet.Cells.Item(1, 1).Value2 = "Username"
	$Worksheet.Cells.Item(1, 2).Value2 = "Full Name"
	$Worksheet.Cells.Item(1, 3).Value2 = "Time"
	$Worksheet.Cells.Item(1, 4).Value2 = "Page Count"
	$Worksheet.Cells.Item(1, 5).Value2 = "Printer"
	$Worksheet.Cells.Item(1, 6).Value2 = "Size (bytes)"
	$Worksheet.Cells.Item(1, 7).Value2 = "Document Name"
	# End writing Excel worksheet headers	

	# Iterate over each print entry
	$row = 2
	ForEach ($key in $tEntries.Keys)
	{
		$Worksheet.Cells.Item($row, 1).Value2 = $tEntries[$key].UserName
		$Worksheet.Cells.Item($row, 2).Value2 = $tEntries[$key].UserFullName
		$Worksheet.Cells.Item($row, 3).Value2 = $tEntries[$key].Time
		$Worksheet.Cells.Item($row, 4).Value2 = $tEntries[$key].PageCount
		$Worksheet.Cells.Item($row, 5).Value2 = $tEntries[$key].Printer
		$Worksheet.Cells.Item($row, 6).Value2 = $tEntries[$key].Size
		$Worksheet.Cells.Item($row, 7).Value2 = $tEntries[$key].DocumentName
		$row++
	}

	# Do some formatting
	# AutoFit the columns
	[Void] $Excel.ActiveCell.CurrentRegion.Columns.AutoFit()

	# Add table styling and auto-filtering
	[Void] $Excel.ActiveCell.CurrentRegion.Select()
	$ListObject = $Excel.ActiveSheet.ListObjects.Add([Microsoft.Office.Interop.Excel.XlListObjectSourceType]::xlSrcRange, $Excel.ActiveCell.CurrentRegion, $null ,[Microsoft.Office.Interop.Excel.XlYesNoGuess]::xlYes)
	$ListObject.Name = "TableData"
	$ListObject.TableStyle = "TableStyleLight9"

	# Show the Excel window after writing data to spreadsheet
	$Excel.Visible = $true
}

Main

Posted in scripting | Tagged: , , , , , | Leave a Comment »

WMI Repository Corruption / SCCM Client Fix

Posted by pcgeek86 on November 6, 2009

You may have come across the following messages in the execmgr.log file on your SCCM clients:

Failed to open to WMI namespace '\\.\root\ccm\Policy\Machine' (8007045b)
Failed to ConnectSettings for ICcmPolicyAgent in CSoftDistPolicyNamespace::ConnectToNamespace
Failed to ConnectToNamespace in CSoftDistPolicyNamespace::GetMachinePolicy
Failed to connect to machine policy name space. 0x8007045b
Failed to connect to user policy name space
CSoftwareDistPolicyMgr::GetSWDistSiteSettings failed to connect to machine namespace
Failed to instantiate UI Server {C2F23AE4-82D8-456F-A4AF-A2655D8CA726} with error 8000401a
Failed to instantiate UI Server 2 {E8425D59-451B-4978-A2AB-641470EB7C02} with error 8000401a
Failed to instantiate Updates UI Server {2D023958-73D0-4542-8AD6-9A507364F70E} with error 8000401a
Failed to instantiate VApp UI Server {00AAB372-0D6D-4976-B5F5-9BC7605E30BB} with error 0x8000401A

These messages are the result of WMI repository corruption. Other sources online have suggested:

1. Stopping the SMS Agent Host service (net stop ccmexec)
2. Stopping the WMI service (net stop winmgmt)
3. Deleting the WMI repository (rmdir /s /q %WINDIR%\System32\wbem\repository)
4. Starting the WMI service (net start winmgmt)
5. Starting the SMS Agent Host service (net start ccmexec)

This may work, however I have noticed other people in the past suggest that deleting the WMI repository is not ideal, even though it may work. Why exactly that is, I can’t necessarily explain off the top of my head. Either way, I recently encountered a client that was experiencing these errors, and instead of doing the usual WMI repository deletion, I instsead simply ran “winmgmt /resetrepository”. From the description of the command in the help, it would seem that it does more or less the same thing as deleting the repository, although this seems like a “cleaner” way of handling it. After all, the command was written and distributed by Microsoft, so I’m more apt to trust this method. The command help reads:

/resetrepository
The repository is reset to the initial state when the operating system
is first installed. MOF files that contain the #pragma autorecover
preprocessor statement are restored to the repository.

From what I can tell, running this command on the problem client has resolved whatever issues were present. Manually firing off an advertisement worked just fine, and the execmgr.log did not reproduce these messages (yet).

Posted in configmgr, fixes, wmi | 1 Comment »

WinPE 3.0 Storage Driver Issues

Posted by pcgeek86 on November 5, 2009

I was using the 8.9.2.1002 iaAHCI driver (dated 08/07/2009) from Intel in WinPE 3.0, on a Lenovo T60 (and other models), and when I tried to browse the fixed disk (c:\), I get this message: “The volume does not contain a recognized file system. Please make sure that all required file system drivers are loaded and that the volume is not corrupted.” This driver is available from Dell, if you choose for example, the Dell Latitude E6400 from their support site, and select Windows 7 32-bit drivers (see link below). Intel does not currently host this driver version on their website. The latest version of the Windows 7 Matrix Storage Manager Driver that Intel offers directly is 8.9.0.1023, dated 06/04/2009.

Naturally, you may want to use the “newer” driver that Dell offers in your boot image, for better compatibility. Unfortunately, I’ve found that this driver from Dell causes the error referenced above. That said, MDT 2010 is still able to format, partition, and write to the disk, although I’ve been experiencing some performance problems that I believe are related to this incorrect driver.

Be careful about which drivers you add to your WinPE 3.0 / Win7 boot images!

Intel Matrix Storage Manager Driver 8.9.0.1023

Dell Intel Matrix Storage Manager Driver 8.9.2.1002

Update (2009-11-05): After some further testing, it appears that the 8.9.0.1023 driver from Intel does not work with the Latitude E6400 in Intel Rapid Restore Technology (IRRT) mode, which is the factory default setting. I added the 8.9.2.1002 driver back into my boot image, so that the E6400 will work, but at the expense of the error message on the T60. At least this configuration allows MDT to do its work, it just prevents browsing the filesystem from within WinPE 3.0.

I have been spending hours testing various configurations, with different driver versions, and so far I have not been able to come up with anything perfectly stable. There are two other iaStor/iaAHCI driver versions I have seen that seem to work with a full Windows 7 OS, 8.6.x and 8.8.0.1002. I don’t have all the other exact information handy at present, but I believe the 8.6 driver is included in the Windows 7 installation.

That’s all for now …

Posted in Uncategorized | Tagged: , , , , , | Leave a Comment »

PowerShell: AD Workstation Cleanup Script version 2.0

Posted by pcgeek86 on November 3, 2009

A little while ago, I posted a PowerShell script that detects old machine accounts in Active Directory, and disables or deletes them, based on certain ages (in days). I’ve continued work on this script, such that it now logs information to Excel about actions (disable or deletion) that it takes. This requires that Excel 2007 be installed on the computer which you are running it on; I have not tested the script with other versions of Excel. I haven’t really made the script very user friendly (eg. taking command-line parameters) yet, because I have pretty much been the sole user of it, so please keep this in mind.

Directions
1. Set the DisabledDn variable to an OU where you would like disabled accounts to be moved to
2. In the Main() function, near the bottom, set the LDAP DN of the root container you want to evaluate for old computer accounts

Warning: Use this script at your own risk. I take no responsibility for any negative effects this code may have. This code may delete or disable computer accounts in your Active Directory domain, which could cause service outages. Please make sure you understand what it is doing before use.

Note: This script currently only targets workstation accounts, not servers. This can easily be modified.

The Script

#
# Author: Trevor Sullivan
#
# Date: October 28th, 2009
#
# Lessons learned:
#
# 1. When using a DirectorySearcher object, property names are lower case
# 2. Must explicitly cast 64-bit integers from AD
# 3. The Excel API is terrible (already knew that)
# 4. DirectoryEntry property names are all lowercase?
#

${DisabledDn} = 'ou=Disabled,ou=Workstations,dc=subdomain,dc=domain,dc=com'

function DisableOldAccounts(${TargetDn}, ${DisableAge} = 60)
{
${Computers} = GetComputerList ${TargetDn}

foreach (${Computer} in ${Computers})
{
# PwdLastSet is a 64-bit integer that indicates the number of 100-nanosecond intervals since 12:00 AM January 1st, 1601
# The FromFileTime method converts a 64-bit integer to datetime
# http://www.rlmueller.net/Integer8Attributes.htm
${PwdLastSet} = [DateTime]::FromFileTime([Int64]"$(${Computer}.Properties['pwdlastset'])")
${CompAge} = ([DateTime]::Now - $PwdLastSet).Days
if (${CompAge} -gt ${DisableAge})
{
LogMessage "$($Computer.Properties['cn']) age is ${CompAge}. Account will be disabled" 2
WriteDisabledEntry $Computer.Properties['cn'].Item(0) $CompAge $Computer.Properties['distinguishedname'].Item(0) $DisabledDn
DisableAccount $Computer.Properties['distinguishedname'].Item(0)
}
else
{
LogMessage "$($Computer.Properties['cn'].Item(0)) age is ${CompAge}, $($Computer.Properties['pwdlastset'].Item(0)), ${PwdLastSet}" 1
}
}
}

function GetComputerList($TargetDn)
{
# Define the LDAP search syntax filter to locate workstation objects. See this link for info: http://msdn.microsoft.com/en-us/library/aa746475(VS.85).aspx
${tFilter} = '(&(objectClass=computer)(|(operatingSystem=Windows 2000 Professional)(operatingSystem=Windows XP*)(operatingSystem=Windows 7*)))'

# Create a DirectorySearcher using filter defined above
${Searcher} = New-Object System.DirectoryServices.DirectorySearcher $tFilter
# Set the search root to the distinguishedName specified in the function parameter
${Searcher}.SearchRoot = "LDAP://${TargetDn}"
# Search current container and all subcontainers
${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
# See this link for info on why this next line is necessary: http://www.eggheadcafe.com/software/aspnet/32967284/searchall-in-ad-ldap-f.aspx
${Searcher}.PageSize = 1000
$Results = $Searcher.FindAll()
LogMessage "Found $($Results.Count) computer accounts to evaluate for disablement" 1
return $Results
}

# Set description on computer account, disable it, and move it to the Disabled OU
function DisableAccount($dn)
{
# LogMessage "DisableAccount method called with param: ${dn}" 1
# Get a reference to the object at <distinguishedName>
$comp = [adsi]"LDAP://${dn}"
# Disable the account
# LogMessage "userAccountControl ($($comp.Name)) is: $($comp.userAccountControl)"
$comp.userAccountControl = $comp.userAccountControl.Value -bxor 2
# Write the current date to the description field
if ($comp.Description -ne '') { LogMessage "Description attribute of ($comp.Name) is set to: $($comp.Description)" 2 }
$comp.Description = "$(([DateTime]::Now).ToShortDateString())"

# Uncomment these lines to write changes to Active Directory
#[Void] $comp.SetInfo()
#$comp.psbase.MoveTo("LDAP://${DisabledDn}")
}

# Parameter ($DeleteAge): Days from disable date to delete computer account
function DeleteDisabledAccounts($DeleteAge)
{
# Get reference to OU for disabled workstation accounts
${DisabledOu} = [adsi]"LDAP://${DisabledDn}"

${Searcher} = New-Object System.DirectoryServices.DirectorySearcher '(objectClass=computer)'
${Searcher}.SearchRoot = ${DisabledOu}
${Searcher}.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
# Page size is used to return result count > default size limit on domain controllers. See: http://geekswithblogs.net/mnf/archive/2005/12/20/63581.aspx
${Searcher}.PageSize = 1000
LogMessage "Finding computers to evaluate for deletion in container: ${DisabledDn}" 1
${Computers} = ${Searcher}.FindAll()

foreach (${Computer} in ${Computers})
{
${DisableDate} = [DateTime]::Parse(${Computer}.Properties['description'])
trap {
LogMessage "Couldn't parse date for $($Computer.Properties['cn'])" 3
continue
}
${CurrentAge} = ([DateTime]::Now - ${DisableDate}).Days
if (${CurrentAge} -gt ${DeleteAge})
{
LogMessage "$(${Computer}.Properties['cn']) age is ${CurrentAge} and will be deleted" 2
WriteDeletedEntry $Computer.Properties['cn'].Item(0) $CurrentAge $Computer.Properties['distinguishedname'].Item(0) "Note"
#$DisabledOu.Delete('computer', 'CN=' + ${Computer}.Properties['cn'])
}
else
{
LogMessage "$(${Computer}.Properties['cn']) age is ${CurrentAge} and will not be deleted" 1
}
}
}

function LogMessage(${tMessage}, ${Severity})
{
switch(${Severity})
{
1 {
$LogPrefix = "INFO"
$fgcolor = [ConsoleColor]::Blue
$bgcolor = [ConsoleColor]::White
}
2 {
$LogPrefix = "WARNING"
$fgcolor = [ConsoleColor]::Black
$bgcolor = [ConsoleColor]::Yellow
}
3 {
$LogPrefix = "ERROR"
$fgcolor = [ConsoleColor]::Yellow
$bgcolor = [ConsoleColor]::Red
}
default {
$LogPrefix = "DEFAULT"
$fgcolor = [ConsoleColor]::Black
$bgcolor = [ConsoleColor]::White
}
}

Add-Content -Path "AD-Workstation-Cleanup.log" -Value "$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}"
Write-Host -ForegroundColor $fgcolor -BackgroundColor $bgcolor -Object "$((Get-Date).ToString()) ${LogPrefix}: ${tMessage}"
}

function SetupExcel()
{
LogMessage "Setting up Excel logging" 1
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Excel")
$Global:Excel = New-Object Microsoft.Office.Interop.Excel.ApplicationClass
$Excel.Visible = $true
$Global:Workbook = $Excel.Workbooks.Add()

# Delete 3rd worksheet, cuz we don't really need it
$Workbook.Worksheets.Item("Sheet3").Delete()

# Setup worksheet for disabled accounts
$Global:DisabledLog = $Workbook.Worksheets.Item("Sheet2")
$DisabledLog.Name = "Disabled"
$DisabledLog.Cells.Item(1, 1).Value2 = "Date"
$DisabledLog.Cells.Item(1, 2).Value2 = "Name"
$DisabledLog.Cells.Item(1, 3).Value2 = "Age"
$DisabledLog.Cells.Item(1, 4).Value2 = "Source DN"
$DisabledLog.Cells.Item(1, 5).Value2 = "Destination DN"
$Global:tDisabledRow = 2

# Setup worksheet for deleted accounts log
$Global:DeletedLog = $Workbook.Worksheets.Item("Sheet1")
$DeletedLog.Name = "Deleted"
$DeletedLog.Cells.Item(1, 1).Value2 = "Date"
$DeletedLog.Cells.Item(1, 2).Value2 = "Name"
$DeletedLog.Cells.Item(1, 3).Value2 = "Age"
$DeletedLog.Cells.Item(1, 4).Value2 = "DN"
$DeletedLog.Cells.Item(1, 5).Value2 = "Note"
$Global:tDeletedRow = 2
}

# Writes an entry to the global variable used to reference the log for disabled accounts
function WriteDisabledEntry($tName, $tAge, $tSourceDn, $tDestinationDn)
{
LogMessage "Writing disabled computer to Excel log: $tName" 1
Write-Host "Value of tname is $tName"
Write-Host "Value of tage is $tAge"
Write-Host "Value of tsourcedn is $tSourceDn"
Write-Host "Value of tDestinationDn is $tDestinationDn"
$DisabledLog.Cells.Item($tDisabledRow, 1).Value2 = [DateTime]::Now.ToString()
$DisabledLog.Cells.Item($tDisabledRow, 2).Value2 = $tName
$DisabledLog.Cells.Item($tDisabledRow, 3).Value2 = $tAge
$DisabledLog.Cells.Item($tDisabledRow, 4).Value2 = $tSourceDn
$DisabledLog.Cells.Item($tDisabledRow, 5).Value2 = $tDestinationDn
$Global:tDisabledRow++
}

# Writes an entry to the global variable used to reference the log for deleted accounts
function WriteDeletedEntry($tName, $tAge, $tDN, $tNote)
{
LogMessage "Writing deleted computer to Excel log: $tName" 1
Write-Host "Value of tName is $tName"
Write-Host "Value of tAge is $tAge"
Write-Host "Value of tDN is $tDN"
Write-Host "Value of tNote is $tNote"
$DeletedLog.Cells.Item($tDeletedRow,1).Value2 = [DateTime]::Now.ToString()
$DeletedLog.Cells.Item($tDeletedRow,2).Value2 = $tName.ToString()
$DeletedLog.Cells.Item($tDeletedRow,3).Value2 = $tAge.ToString()
$DeletedLog.Cells.Item($tDeletedRow,4).Value2 = $tDN.ToString()
$DeletedLog.Cells.Item($tDeletedRow,5).Value2 = $tNote.ToString()
$Global:tDeletedRow++
return
}

function CloseExcel()
{
LogMessage "Saving and closing Excel workbook" 1
$Global:Workbook.SaveAs("AD Workstation Cleanup.xlsx")
$Global:Excel.Quit()
}

function Main()
{
Clear-Host
LogMessage "Beginning workstation account cleanup script" 1
SetupExcel

# Delete accounts that have been disabled for X days
#DeleteDisabledAccounts 30

# Disable accounts that are older than X days
DisableOldAccounts "dc=subdomain,dc=domain,dc=com" 60

CloseExcel
LogMessage "Completed workstation account cleanup script" 1
}

Main

Posted in scripting | Tagged: , , , , , , , , | 2 Comments »

Troubleshooting Failed Software Updates in ConfigMgr

Posted by pcgeek86 on November 2, 2009

Hello,

I just wanted to take a few moments to share the thought process that I use to troubleshoot failing software updates in Configuration Manager. On several occasions for me, this has been the most straightforward method of figuring out why an update is failing, and determining how to resolve the issue.

How to Manually Troubleshoot a Software Update

  1. Identify the GUID or KB article number of the software update
  2. Find the update in the ConfigMgr console’s Update Repository
  3. Open the update’s property page
  4. Go to the Content Information tab
  5. Find the appropriate line item, click it, and Ctrl + C to copy it
  6. Open Notepad, paste the line into it, and parse out the URL
  7. Download the update with your web browser
  8. Extract the update file(s) from the CAB
  9. Manually execute the update with any command-line options for logging
  10. Examine the resulting log file to determine the failure reason

These steps have usually worked for me, and it is a more direct method of troubleshooting than examining the Configuration Manager log files, since you are manually executing the patch, and can enable additional logging. The SCCM client’s log files can help with initially identifying a failing update, but to dig deeper, you may want to follow the above steps.

ConfigMgr Client Software Update Log Files

  • UpdatesDeployment.log
  • UpdatesHandler.log
  • UpdatesStore.Log
  • WUAHandler.log
  • WindowsUpdate.log (located in the root of %WINDIR%)

Hope this helps!

Posted in configmgr, fixes | Tagged: , , , , , , , , , | Leave a Comment »

MDT 2010 Driver Injection Slowness

Posted by pcgeek86 on October 27, 2009

After upgrading from MDT 2008 to 2010, I recently experienced an issue with MDT / WinPE 3.0 that was simply a driver issue. The initial symptom reported to me was: Driver detection / copying (ZTIDrivers.wsf) is taking a long time in WinPE on a Lenovo T60. The build completed, but hung on the driver injection step for about an hour.

To start diagnosing, I booted WinPE 3.0 off of USB flash drive (UFD) on a Lenovo T60, brought up a command prompt (press F8) and tried to browse the disk (just by typing “c:”) I got: “The volume does not contain a recognized file system. Please make sure that all required file system drivers are loaded and that the volume is not corrupted.“ Upon issuing “format fs=ntfs quick” to diskpart, I received: “DiskPart has encountered an error: The parameter is incorrect. See the System Event Log for more information.” Running “ipconfig” yielded normal results, and I was also able to mount network shares, and browse them without any issue.

Note: I typically use a UFD because they boot significantly faster than off of optical media. :)

After posting on the MDT-OSD discussion list on MyITforum, I discovered that I may not have the correct drivers in my boot image. Most other people have reported that they haven’t had to inject any drivers, and I figured that the Vista drivers I had in my old WinPE 2.x boot images should have worked alright. As it turns out however, the Intel NIC was using a built-in driver from Microsoft according to wpeinit.log, and there was no mass storage driver being loaded according to the output from “devcon driverfiles *“.

Resolution

So, simply updating the mass storage and NIC drivers resolved the issue(s). The network drivers can be found here: http://www.intel.com/support/network/sb/cs-006120.htm. The storage drivers can be found here: http://downloadcenter.intel.com/Detail_Desc.aspx?agr=Y&DwnldID=17882&lang=eng.

Posted in Uncategorized | Tagged: , , , , | Leave a Comment »

Deployment Workbench: Windows Imaging Error

Posted by pcgeek86 on October 27, 2009

I just installed the Windows AIK for Windows7, and MDT 2010 on my 64-bit Windows 7 Release Candidate system, and attempted to open the Deployment Workbench. It opened just fine, but when I selected the Deployment Shares node in the snap-in, I got this error: “The file C:\Windows\system32\wimgapi.dll(version 6.1.7100.0 (winmain_win7rc.090421-1700)) is older than the installed Windows Automated Installation Kit (version 6.1.7600.16385).  Please remove the C:\Windows\system32\wimgapi.dll file so the proper one is used.

Windows Imaging Error

Windows Imaging Error

I am unable to remove the file, or replace it with the newer AIK one, due to a lock on the file (my guess would be that it’s Windows System File Checker).

Most likely, the solution (or at least it might help) would probably be to upgrade to Windows 7 RTM, but I’ve been putting that off for so long, what’s a couple more weeks? :) I just wanted to document this error / scenario in case anyone else comes across it.

Posted in Uncategorized | Tagged: , , , , , , , | 2 Comments »

Umit

Posted by pcgeek86 on October 20, 2009

Umit is a front end for Nmap.

http://www.umitproject.org/

Posted in Uncategorized | Tagged: | Leave a Comment »

MDT 2008 to 2010 Upgrade Successful!

Posted by pcgeek86 on October 15, 2009

Yesterday, I upgraded MDT from 2008 to 2010. The high-level steps I followed to upgrade it were as follows:

  1. Uninstall Windows AIK 1.1
  2. Uninstall MDT 2008
  3. Install Windows 7 / 2008 R2 AIK
  4. Install MDT 2010
  5. Open MDT 2010 console and upgrade deployment shares
  6. Test task sequence on virtual machine

Please note: The Microsoft .NET Framework and Windows PowerShell are also both required, but we already had them installed.

The application upgrade went flawlessly, and I only got a couple of warning messages during the deployment share upgrades. Here is what I got (and yes, the share still existed):

Unable to transfer settings to deployment share \\<imageserver>\LTIDeploy$.  If the share still exists, the settings will need to be reconfigured.

System.IO.DirectoryNotFoundException: Could not find a part of the path ‘d:\Distribution\Control\{f3123492-005e-499f-9c9b-982a401e1ff9}\Settings.xml’.
   at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite)
   at System.IO.File.Copy(String sourceFileName, String destFileName, Boolean overwrite)
   at Microsoft.BDD.PSSnapIn.DeploymentPoint.TransferSettings(XmlNode deployNode, String settingsFile)
   at Microsoft.BDD.PSSnapIn.DeploymentPoint.UpgradeDeployXML()

For whatever reason, this didn’t negatively impact the deployment point, so I proceeded with testing out our standard Windows XP task sequence for workstation builds. The only errors I experienced were actually in a couple of custom scripts that we wrote, one of which handles Windows XP HAL injection, and the other which sets the computer name for virtual machines by truncating the serial number (we name some computers based on their serial #). After doing some research, it turns out that the DeploySystemDrive task sequence variable no longer existed for some reason. I searched BDD.log for ‘c:’ to see if there was another variables that contained the target OS drive, and sure enough, I found one named DestinationLogicalDrive. After updating the two scripts with this new TS variable, the task sequence completed successfully!

All in all, I would say the experience upgrading from MDT 2008 to 2010 was quite simple. It was completed in under 4 hours of my time. Thanks to the MDT team at Microsoft for making upgrading simple!

Posted in Uncategorized | Tagged: , , , , , , , , , | Leave a Comment »

GemBox’s Excel .NET Library

Posted by pcgeek86 on October 13, 2009

If you’re at all familiar with the Excel APIs, you’ll know that they’re often quite frustrating to work with. The fact that the Office 2007 .NET API is simply a wrapper around the COM interface doesn’t make it very appealing either.

I e-mailed my dad, who develops software, and he sent me a link to this handy .NET library by GemBox Software. It simplifies the Excel API, and allows you to get things done much more logically than the Excel API does. Here is a link to their product page:

http://www.gemboxsoftware.com/GBSpreadsheet.htm

Posted in Uncategorized | Tagged: | Leave a Comment »