tag:blogger.com,1999:blog-44114065008017974002024-03-20T07:51:03.796-04:00Actively DirectThings I find interesting from the daily work I do.Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.comBlogger55125tag:blogger.com,1999:blog-4411406500801797400.post-57773570500428183922011-11-28T21:30:00.000-05:002012-02-22T17:06:41.477-05:00Exchange Mailbox ReportWhether in an environment I am familiar or one that is new to me, information about it is important to my management of it. In a large environment, having timely information readily available is key to making good decisions. It can take hours to generate that data and that decision might be time sensitive enough that I can't wait. That is why I run daily, weekly and monthly audits of key systems and store them away for later retrieval. <a href="http://www.microsoft.com/exchange/en-us/default.aspx">Microsoft Exchange</a> is just one of those systems where I am constantly making business decisions based on daily audits. Who are the employees with the biggest mailboxes? Which server has the most messages? How active is a mailbox? Which mailbox server is under-utilized and which is overloaded? What is the combined storage used by all messages in the organization?
<br />
<br />
Below is a PowerShell script that I have developed over time to provide relevant information about Exchange mailbox servers and the employee mailboxes contained within. It assumes that you have at least the <a href="http://technet.microsoft.com/en-us/library/bb124413(EXCHG.80).aspx">Exchange 2007</a> <a href="http://msdn.microsoft.com/en-us/library/system.management.automation.pssnapin(v=VS.85).aspx">PSSnapin</a> available to execute but will detect if the Exchange 2010 PSSnapin is available and load it. Because I run into diverse installations of Exchange, the script can report on Exchange 2003, Exchange 2007 and Exchange 2010 mailbox servers. The largest environment I have successfully tested had all three versions in an Exchange organization, 40 mailbox servers and covered installations in multiple countries worldwide. The end result will be a nicely formatted Excel spreadsheet with an overview worksheet hyperlinking to a worksheet for each of the mailbox servers with at least 1 mailbox in the workbook. Because I can run into new and unknown environments, the PowerShell script has fairly detailed logging. You need the permission to read mailboxes on each of the Exchange mailbox servers with security context you execute the script. The logging will catch permission errors and log them. Unmodified, it will store the logs, work files and final spreadsheet in child directories of the root of the executed script. Review the <a href="http://technet.microsoft.com/en-us/library/dd347562.aspx">Set-Variable</a> statements after the Functions and update them to your needs. In its current state, it will not delete work files allowing you to see the raw product used to produce the Excel spreadsheet.
<pre class="brush:powershell;toolbar:false">#--------------------------------------------------------------------------------------------------#
Function Get-LocalDomainController($objectDomain) {
return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
#--------------------------------------------------------------------------------------------------#
Function Get-ObjectADDomain($distinguishedName) {
return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
#--------------------------------------------------------------------------------------------------#
Function Get-ActiveDirectoryObject($distinguishedName) {
return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Function Write-LogEntry($logEntry,$logFile) {
Add-Content -Path $logFile -Value ((Get-TimeStamp) + " $logEntry")
}
#--------------------------------------------------------------------------------------------------#
Function Write-LogEntryLineBreak($logFile) {
Write-LogEntry "#-----------------------------------------------------------------------------------#" $logFile
}
#--------------------------------------------------------------------------------------------------#
Function Get-TimeStamp {
return (Get-Date -format yyyyMMddHHmmss)
}
#--------------------------------------------------------------------------------------------------#
Function Get-DayStamp {
return (Get-Date -format yyyyMMdd)
}
#--------------------------------------------------------------------------------------------------#
Function Get-RunTime($runTime) {
$runTimeHours = ($runTime.Hours).ToString()
$runTimeMinutes = ($runTime.Minutes).ToString()
$runTimeSeconds = ($runTime.Seconds).ToString()
if($runTimeHours.Length -eq 1) {
$runTimeHours = "0$runTimeHours"
}
if($runTimeMinutes.Length -eq 1) {
$runTimeMinutes = "0$runTimeMinutes"
}
if($runTimeSeconds.Length -eq 1) {
$runTimeSeconds = "0$runTimeSeconds"
}
return ($runTimeHours + ":" + $runTimeMinutes + ":" + $runTimeSeconds)
}
#--------------------------------------------------------------------------------------------------#
Function Initialize-Directory($directoryPath) {
if(!(Test-Path -Path $directoryPath)) {
New-Item -Path $directoryPath -Type Directory | Out-Null
}
if(Test-Path -Path $directoryPath) {
$success = $true
} else {
$success = $false
}
return $success
}
#--------------------------------------------------------------------------------------------------#
Function Load-ExchangeSnapin {
$registeredPsSnapins = @(Get-PSSnapin -registered | ForEach-Object { $_.Name })
$loadedPsSnapins = Get-PSSnapin
if($registeredPsSnapins -contains "Microsoft.Exchange.Management.PowerShell.E2010") {
$snapinLoaded = $false
foreach($snapin in $loadedPsSnapins) {
if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.E2010") {
$snapinLoaded = $true
}
}
if($snapinLoaded -eq $false) {
Add-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.E2010'
}
} elseif($registeredPsSnapins -contains "Microsoft.Exchange.Management.PowerShell.Admin" -and $registeredPsSnapins -contains "Microsoft.Exchange.Management.PowerShell.Support") {
$adminPsSnapinLoaded = $false
$supportPsSnapinLoaded = $false
foreach($snapin in $loadedPsSnapins) {
if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Admin") {
$adminPsSnapinLoaded = $true
}
if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Support") {
$supportPsSnapinLoaded = $true
}
}
if($adminPsSnapinLoaded -eq $false) {
Add-PSSnapin -Name "Microsoft.Exchange.Management.PowerShell.Admin"
}
if($supportPsSnapinLoaded -eq $false) {
Add-PSSnapin -Name "Microsoft.Exchange.Management.PowerShell.Support"
}
}
}
#--------------------------------------------------------------------------------------------------#
Function Unload-ExchangeSnapin {
$registeredPsSnapins = @(Get-PSSnapin -registered | ForEach-Object { $_.Name })
$loadedPsSnapins = Get-PSSnapin
if($registeredPsSnapins -contains "Microsoft.Exchange.Management.PowerShell.E2010") {
$snapinLoaded = $false
foreach($snapin in $loadedPsSnapins) {
if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.E2010") {
$snapinLoaded = $true
}
}
if($snapinLoaded -eq $true) {
Remove-PSSnapin -Name 'Microsoft.Exchange.Management.PowerShell.E2010'
}
} elseif($registeredPsSnapins -contains "Microsoft.Exchange.Management.PowerShell.Admin" -and $registeredPsSnapins -contains "Microsoft.Exchange.Management.PowerShell.Support") {
$adminPsSnapinLoaded = $false
$supportPsSnapinLoaded = $false
foreach($snapin in $loadedPsSnapins) {
if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Admin") {
$adminPsSnapinLoaded = $true
}
if($snapin.name -eq "Microsoft.Exchange.Management.PowerShell.Support") {
$supportPsSnapinLoaded = $true
}
}
if($adminPsSnapinLoaded -eq $true) {
Remove-PSSnapin -Name "Microsoft.Exchange.Management.PowerShell.Admin"
}
if($supportPsSnapinLoaded -eq $true) {
Remove-PSSnapin -Name "Microsoft.Exchange.Management.PowerShell.Support"
}
}
}
#--------------------------------------------------------------------------------------------------#
Function Get-UserInformation($legacyExchangeDn) {
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.SearchRoot = (New-Object System.DirectoryServices.DirectoryEntry("GC://$forestRootDn"))
$directorySearcher.Filter = ("(&(objectclass=user)(objectCategory=person)(legacyExchangeDN=$legacyExchangeDn))")
$directorySearcher.PropertiesToLoad.Clear()
$directorySearcher.PropertiesToLoad.Add("distinguishedName") | Out-Null
$searchResult = $directorySearcher.FindOne()
$directorySearcher.Dispose()
if($searchResult) {
return $searchResult.Properties.Item("distinguishedName")[0]
} else {
return $null
}
}
#--------------------------------------------------------------------------------------------------#
Function Get-StorageLimitInfo($storageLimitInfo, $issuedWarning, $prohibitedSend, $mailboxDisabled, $unlimitedMailboxes) {
if($storageLimitInfo -eq 1) {
$storageLimitInfo = "Below Limit"
} elseif($storageLimitInfo -eq 2) {
$storageLimitInfo = "Issued Warning"
$issuedWarning++
} elseif($storageLimitInfo -eq 4) {
$storageLimitInfo = "Prohibited Send"
$prohibitedSend++
} elseif($storageLimitInfo -eq 8) {
$storageLimitInfo = "Unlimited Mailbox"
$unlimitedMailboxes++
} elseif($storageLimitInfo -eq 16) {
$storageLimitInfo = "Mailbox Disabled"
$mailboxDisabled++
} else {
$storageLimitInfo = "Disabled Account w/ No Permissions"
$orphanedMailboxes++
}
return $storageLimitInfo, $issuedWarning, $prohibitedSend, $mailboxDisabled, $unlimitedMailboxes, $orphanedMailboxes
}
#--------------------------------------------------------------------------------------------------#
Function Get-StorageLimitStatus($storageLimitStatus, $issuedWarning, $prohibitedSend, $mailboxDisabled, $unlimitedMailboxes) {
if($storageLimitStatus -eq "BelowLimit") {
$storageLimitStatus = "Below Limit"
} elseif($storageLimitStatus -eq "IssueWarning") {
$storageLimitStatus = "Issued Warning"
$issuedWarning++
} elseif($storageLimitStatus -eq "ProhibitSend") {
$storageLimitStatus = "Prohibited Send"
$prohibitedSend++
} elseif($storageLimitStatus -eq "NoChecking") {
$storageLimitStatus = "Unlimited Mailbox"
$unlimitedMailboxes++
} elseif($storageLimitStatus -eq "MailboxDisabled") {
$storageLimitStatus = "Mailbox Disabled"
$mailboxDisabled++
} else {
$storageLimitStatus = "Disabled Account w/ No Permissions"
$orphanedMailboxes++
}
return $storageLimitStatus, $issuedWarning, $prohibitedSend, $mailboxDisabled, $unlimitedMailboxes, $orphanedMailboxes
}
#--------------------------------------------------------------------------------------------------#
Function Get-ExcelTableStyle($current,$tableStyles) {
$tableStyle = $tableStyles[$current]
if($current -ge ($tableStyles.Count - 1)) {
$current = 0
} else {
$current++
}
return $current, $tableStyle
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
Set-Variable -name ping -option Constant -value (New-Object -typeName System.Net.NetworkInformation.Ping)
#--------------------------------------------------------------------------------------------------#
# Typically I set to 2x larger than the standard mailbox size.
Set-Variable -name largeMailboxSize -option Constant -value (1GB / 1KB)
#--------------------------------------------------------------------------------------------------#
Set-Variable -name logDirectory -option Constant -value "$pwd\Logs"
#--------------------------------------------------------------------------------------------------#
# Must be an absolute path for Excel.
Set-Variable -name workDirectory -option Constant -value ($env:temp + "\Work")
#--------------------------------------------------------------------------------------------------#
# Must be an absolute path for Excel.
Set-Variable -name reportDirectory -option Constant -value "$pwd\Reports"
#--------------------------------------------------------------------------------------------------#
Set-Variable -name logFilename -option Constant -value ($logDirectory + "\" + (Get-DayStamp) + "-" + ($myinvocation.mycommand.name -Replace ".ps1",".txt"))
Set-Variable -name masterExchangeServerFilename -option Constant -value ($workDirectory + "\Master Exchange Servers List.csv")
Set-Variable -name filteredExchangeServerFilename -option Constant -value ($workDirectory + "\Filtered Exchange Servers List.csv")
Set-Variable -name excelFilename -option Constant -value ($reportDirectory + "\Exchange Mailbox Report-" + (Get-DayStamp) + ".xlsx") # Assuming Excel 2007 or better
#--------------------------------------------------------------------------------------------------#
# Style Cheat Sheet in French/English: http://msdn.microsoft.com/fr-fr/library/documentformat.openxml.spreadsheet.tablestyle.aspx
Set-Variable -name excelTableStyles -option Constant -value @("TableStyleMedium16","TableStyleMedium17","TableStyleMedium18","TableStyleMedium19","TableStyleMedium20","TableStyleMedium21")
#--------------------------------------------------------------------------------------------------#
Set-Variable -name excelCurrentTableStyle -value 0
#--------------------------------------------------------------------------------------------------#
# Set to $false to review data generated by the mailbox scans.
# Set to $true to delete the work directory at completion.
Set-Variable -name cleanupWorkDirectory -option Constant -value $false
#--------------------------------------------------------------------------------------------------#
# If $true, no screen output will be displayed. Best for a scheduled task.
# $false if you like staring at progress bars along with superfluous and gratuitous information.
Set-Variable -name beQuiet -option Constant -value $false
#--------------------------------------------------------------------------------------------------#
Set-Variable -name sheetNumber -value 1
Set-Variable -name serverCount -value 1
#--------------------------------------------------------------------------------------------------#
$startTime = Get-Date
if(!$beQuiet) { Clear-Host }
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Setup Environment" -percentComplete "50" }
if((Initialize-Directory $logDirectory) -eq $false) {
Write-Host "Unable to access $logDirectory for writing logs. Exiting." -Foregroundcolor Red
Set-Content -path ("$pwd\" + ($myinvocation.mycommand.name -Replace ".ps1","") + " MAJOR ERROR.txt") -value "Unable to access $logDirectory for writing logs. Exiting."
exit
}
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Setup Environment" -percentComplete "75" }
if((Initialize-Directory $workDirectory) -eq $false) {
Write-Host "Unable to access $workDirectory for writing temp files. Exiting." -Foregroundcolor Red
Write-LogEntry "[ERROR] Unable to access $workDirectory for writing temp files. Exiting." $logFilename
exit
}
if((Initialize-Directory $reportDirectory) -eq $false) {
Write-Host "Unable to access $reportDirectory for writing the Excel file. Exiting." -Foregroundcolor Red
Write-LogEntry "[ERROR] Unable to access $reportDirectory for writing the Excel file. Exiting." $logFilename
exit
}
Write-LogEntry ("Log Directory: " + $logDirectory) $logFilename
Write-LogEntry ("Work Directory: " + $workDirectory) $logFilename
Write-LogEntry ("Report Directory: " + $reportDirectory) $logFilename
Write-LogEntry ("Log Filename: " + $logFilename) $logFilename
Write-LogEntry ("Master Server List Filename: " + $masterExchangeServerFilename) $logFilename
Write-LogEntry ("Filtered Server List Filename: " + $filteredExchangeServerFilename) $logFilename
Write-LogEntry ("Excel Filename: " + $excelFilename) $logFilename
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Setup Environment" -percentComplete "100" }
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Loading Exchange Snapin" -percentComplete "50" }
Write-LogEntry "Loading Exchange Snapin" $logFilename
Load-ExchangeSnapin
Write-LogEntry "Exchange Snapin loaded" $logFilename
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Loading Exchange Snapin" -percentComplete "100" }
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Finding all Exchange Servers" -percentComplete "50" }
Write-LogEntry "Discover Exchange Servers" $logFilename
$exchangeServers = @((Get-ExchangeServer | Where-Object { $_.ServerRole -match "Mailbox" -or $_.ServerRole -eq "None" } ) | Select-Object -property Name,Fqdn,AdminDisplayVersion,Edition,ExchangeVersion,DistinguishedName,WhenChanged,WhenCreated | Sort-Object -property Name)
Write-LogEntry ($exchangeServers.Count.ToString() + " Exchange Servers found") $logFilename
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Finding all Exchange Servers" -percentComplete "100" }
$serversScanned = 0
foreach($exchangeServer in $exchangeServers) {
$serversScanned++
$exchangeServer.Name = ($exchangeServer.Name).ToUpper()
$exchangeServer.Fqdn = ($exchangeServer.Fqdn).ToLower()
Write-LogEntry $exchangeServer.Fqdn $logFilename
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status ("Gathering information on " + $exchangeServer.Fqdn) -percentComplete (($serversScanned / $exchangeServers.Count) * 100) }
Write-LogEntry "IPV4 Address Discovery" $logFilename
try {
$hostAddresses = [System.Net.Dns]::GetHostAddresses($exchangeServer.Fqdn)
Add-Member -inputObject $exchangeServer -type NoteProperty -name "ipV4Address" -value $hostAddresses[0].IPAddressToString
} catch {
Add-Member -inputObject $exchangeServer -type NoteProperty -name "ipV4Address" -value "0.0.0.0"
}
Write-LogEntry ("IPV4 Address:" + $exchangeServer.ipV4Address) $logFilename
Write-LogEntry "Testing if server online" $logFilename
if($exchangeServer.ipV4Address -ne "0.0.0.0") {
$onlineStatus = $ping.send($exchangeServer.ipV4Address)
if($onlineStatus.Status -eq "Success") {
Add-Member -inputObject $exchangeServer -type NoteProperty -name "online" -value $true
} else {
Add-Member -inputObject $exchangeServer -type NoteProperty -name "online" -value $false
}
} else {
Add-Member -inputObject $exchangeServer -type NoteProperty -name "online" -value $false
}
Write-LogEntry ("Online status:" + $exchangeServer.online) $logFilename
Write-LogEntry "Discovering number of databases" $logFilename
if($exchangeServer.online -eq $true) {# -and $exchangeServer.configurationDistinguishedName -ne "Unknown" -and $exchangeServer.version -ne "Unknown") {
if($exchangeServer.AdminDisplayVersion -match "Version 6.") {
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.SearchRoot = (New-Object System.DirectoryServices.DirectoryEntry("GC://" + $exchangeServer.DistinguishedName))
$directorySearcher.Filter = "(&(objectclass=msExchStorageGroup))"
$directorySearcher.PropertiesToLoad.Clear()
$directorySearcher.PropertiesToLoad.Add("distinguishedName") | Out-Null
$exchangeSearchResults = $directorySearcher.FindAll()
$directorySearcher.Dispose()
Add-Member -inputObject $exchangeServer -type NoteProperty -name "numberOfStores" -value $exchangeSearchResults.Count
} else {
Add-Member -inputObject $exchangeServer -type NoteProperty -name "numberOfStores" -value @(Get-MailboxDatabase -server $exchangeServer.Fqdn).Count
}
} else {
Add-Member -inputObject $exchangeServer -type NoteProperty -name "numberOfStores" -value "0"
}
if($exchangeServer.numberOfStores -eq "" -or $exchangeServer.numberOfStores -eq $null) { $exchangeServer.numberOfStores = 0 }
Write-LogEntry ("Databases:" + $exchangeServer.numberOfStores) $logFilename
Write-LogEntryLineBreak $logFilename
}
$mailboxes = @()
$serversScanned = 0
foreach($exchangeServer in $exchangeServers) {
Write-LogEntry ("Scanning Mailboxes on " + $exchangeServer.Fqdn) $logFilename
$serversScanned++
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status ("Scanning Mailboxes: " + $exchangeServer.Fqdn) -percentComplete (($serversScanned / $exchangeServers.Count) * 100) }
$mailboxesScanned = 0
$mailboxCount = 0
$totalStorage = 0
$totalMessages = 0
$issuedWarning = 0
$prohibitedSend = 0
$mailboxDisabled = 0
$unlimitedMailboxes = 0
$orphanedMailboxes = 0
$largeMailboxes = 0
$userMailboxes = @()
if($exchangeServer.numberOfStores -eq 0) {
Add-Member -inputObject $exchangeServer -type NoteProperty -name "mailboxCount" -value $mailboxCount
Write-LogEntry ("There are no mailboxes on " + $exchangeServer.Fqdn) $logFilename
Write-LogEntryLineBreak $logFilename
continue
}
if($exchangeServer.AdminDisplayVersion -match "Version 6.") {
Write-LogEntry "Exchange 2003 Server" $logFilename
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status ("Loading Mailboxes on " + $exchangeServer.Fqdn) -percentComplete (($serversScanned / $exchangeServers.Count) * 100) }
$error.clear()
try {
$mailboxes = @(Get-WmiObject -ComputerName $exchangeServer.Fqdn -Namespace "root\MicrosoftExchangeV2" -Class Exchange_Mailbox)
} catch {
Write-LogEntry ("[ERROR] Unable to obtain mailboxes on " + $exchangeServer.Fqdn) $logFilename
foreach($errorEntry in $error) {
Write-LogEntry $errorEntry $logFilename
}
$mailboxes = @()
Add-Member -inputObject $exchangeServer -type NoteProperty -name "totalMessages" -value $totalMessages
Add-Member -inputObject $exchangeServer -type NoteProperty -name "totalStorage" -value $totalStorage
Add-Member -inputObject $exchangeServer -type NoteProperty -name "mailboxCount" -value $mailboxCount
Add-Member -inputObject $exchangeServer -type NoteProperty -name "issuedWarning" -value $issuedWarning
Add-Member -inputObject $exchangeServer -type NoteProperty -name "prohibitedSend" -value $prohibitedSend
Add-Member -inputObject $exchangeServer -type NoteProperty -name "mailboxDisabled" -value $mailboxDisabled
Add-Member -inputObject $exchangeServer -type NoteProperty -name "unlimitedMailboxes" -value $unlimitedMailboxes
Add-Member -inputObject $exchangeServer -type NoteProperty -name "orphanedMailboxes" -value $orphanedMailboxes
Add-Member -inputObject $exchangeServer -type NoteProperty -name "largeMailboxes" -value $largeMailboxes
Write-LogEntryLineBreak $logFilename
continue
}
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status ("Reviewing Mailboxes on " + $exchangeServer.Fqdn) -percentComplete (($serversScanned / $exchangeServers.Count) * 100) }
Write-LogEntry ($mailboxes.Count.ToString() + " mailboxes found") $logFilename
foreach($mailbox in $mailboxes) {
$error.clear()
$mailboxesScanned++
if($mailbox.LegacyDN -and $mailbox.LegacyDN -notmatch "CN=MICROSOFT SYSTEM ATTENDANT" -and $mailbox.LegacyDN -notmatch "CN=SMTP" -and $mailbox.LegacyDN -notmatch "CN=SYSTEMMAILBOX") {
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity (($mailboxes.Count).ToString() + " Mailboxes to Review") -status ("Scanning Mailbox: " + $mailbox.MailboxDisplayName) -percentComplete (($mailboxesScanned / $mailboxes.Count) * 100) }
if($mailbox.LastLogonTime -ne $null) {
$lastLogonTime = Get-Date -year $mailbox.LastLogonTime.ToString().Substring(0,4) -month $mailbox.LastLogonTime.ToString().Substring(4,2) -day $mailbox.LastLogonTime.ToString().Substring(6,2) -hour $mailbox.LastLogonTime.ToString().Substring(8,2) -minute $mailbox.LastLogonTime.ToString().Substring(10,2) -second $mailbox.LastLogonTime.ToString().Substring(12,2) -format G
} else {
$lastLogonTime = ""
}
$storageLimitInfo, $issuedWarning, $prohibitedSend, $mailboxDisabled, $unlimitedMailboxes, $orphanedMailboxes = Get-StorageLimitInfo $mailbox.StorageLimitInfo $issuedWarning $prohibitedSend $mailboxDisabled $unlimitedMailboxes $orphanedMailboxes
if($mailbox.Size -gt $largeMailboxSize) {
$largeMailboxes++
}
$userObject = Get-ActiveDirectoryObject (Get-UserInformation $mailbox.LegacyDN)
$mailboxCount++
if($userObject.givenName) {
$userGivenName = $userObject.givenName.ToString()
} else {
$userGivenName = ""
}
if($userObject.sn) {
$userSn = $userObject.sn.ToString()
} else {
$userSn = ""
}
if($userObject.displayName) {
$userDisplayName = $userObject.displayName.ToString()
} else {
$userDisplayName = $mailbox.MailboxDisplayName
}
if($userObject -ne $null) {
$userDomain = (Get-ObjectADDomain $userObject.distinguishedName).Split(".")[0]
} else {
$userDomain = ""
}
if($userObject.sAMAccountName) {
$userSAMAccountName = $userObject.sAMAccountName.ToString()
} else {
$userSAMAccountName = ""
}
if($userObject.mail) {
$userMail = $userObject.mail.ToString()
} else {
$userMail = ""
}
$totalMessages += $mailbox.TotalItems
$totalStorage += $mailbox.Size
$userMailboxInformation = New-Object -typeName PSObject
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Mailbox Server" -value $exchangeServer.Name
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Storage Group" -value $mailbox.StorageGroupName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Store Name" -value $mailbox.StoreName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Display Name" -value $userDisplayName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "First Name" -value $userGivenName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Last Name" -value $userSn
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Domain" -value $userDomain
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "User ID" -value $userSAMAccountName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "E-Mail Address" -value $userMail
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Size (KB)" -value ("{0:N0}" -f $mailbox.Size)
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Messages" -value ("{0:N0}" -f $mailbox.TotalItems)
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Storage Limit" -value $storageLimitInfo
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Last Logon" -value $lastLogonTime
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Last User Access" -value $mailbox.LastLoggedOnUserAccount
$userMailboxes += $userMailboxInformation
if($error) {
Write-LogEntry ("[ERROR] Problems found scanning mailbox:" + $userDisplayName) $logFilename
foreach($errorEntry in $error) {
Write-LogEntry $errorEntry $logFilename
}
}
} else {
Write-LogEntry ("Skipping:" + $mailbox.LegacyDN) $logFilename
}
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Mailbox review" -status "Completed" -Completed }
}
} else {
if($exchangeServer.AdminDisplayVersion -match "Version 8.") {
Write-LogEntry "Exchange 2007 Server" $logFilename
} elseif($exchangeServer.AdminDisplayVersion -match "Version 14.") {
Write-LogEntry "Exchange 2010 Server" $logFilename
}
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status ("Loading Mailboxes on " + $exchangeServer.Fqdn) -percentComplete (($serversScanned / $exchangeServers.Count) * 100) }
$error.clear()
try {
$mailboxes = @(Get-MailboxStatistics -server $exchangeServer.Fqdn | Where-Object { $_.ObjectClass -ne "Mailbox, ExOleDbSystemMailbox" -and $_.DisconnectDate -eq $null } | Select-Object -property DisplayName,ItemCount,LegacyDN,LastLoggedOnUserAccount,LastLogoffTime,LastLogonTime,StorageLimitStatus,TotalDeletedItemSize,TotalItemSize,MailboxTableIdentifier,DatabaseName,OriginatingServer)
} catch {
Write-LogEntry ("[ERROR] Unable to obtain mailboxes on " + $exchangeServer.Fqdn)
foreach($errorEntry in $error) {
Write-LogEntry $errorEntry $logFilename
}
$mailboxes = @()
Add-Member -inputObject $exchangeServer -type NoteProperty -name "totalMessages" -value $totalMessages
Add-Member -inputObject $exchangeServer -type NoteProperty -name "totalStorage" -value $totalStorage
Add-Member -inputObject $exchangeServer -type NoteProperty -name "mailboxCount" -value $mailboxCount
Add-Member -inputObject $exchangeServer -type NoteProperty -name "issuedWarning" -value $issuedWarning
Add-Member -inputObject $exchangeServer -type NoteProperty -name "prohibitedSend" -value $prohibitedSend
Add-Member -inputObject $exchangeServer -type NoteProperty -name "mailboxDisabled" -value $mailboxDisabled
Add-Member -inputObject $exchangeServer -type NoteProperty -name "unlimitedMailboxes" -value $unlimitedMailboxes
Add-Member -inputObject $exchangeServer -type NoteProperty -name "orphanedMailboxes" -value $orphanedMailboxes
Add-Member -inputObject $exchangeServer -type NoteProperty -name "largeMailboxes" -value $largeMailboxes
Write-LogEntryLineBreak $logFilename
continue
}
Write-LogEntry ($mailboxes.Count.ToString() + " mailboxes found") $logFilename
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status ("Scanning Mailboxes: " + $exchangeServer.Fqdn) -percentComplete (($serversScanned / $exchangeServers.Count) * 100) }
foreach($mailbox in $mailboxes) {
$error.clear()
$mailboxesScanned++
if($mailbox.LegacyDN -and $mailbox.LegacyDN -notmatch "CN=MICROSOFT SYSTEM ATTENDANT" -and $mailbox.LegacyDN -notmatch "CN=SMTP" -and $mailbox.LegacyDN -notmatch "CN=SYSTEMMAILBOX") {
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity (($mailboxes.Count).ToString() + " Mailboxes to Review") -status ("Scanning Mailbox of " + $mailbox.DisplayName) -percentComplete (($mailboxesScanned / $mailboxes.Count) * 100) }
if($mailbox.LastLogonTime) {
$lastLogonTime = Get-Date $mailbox.LastLogonTime -format G
} else {
$lastLogonTime = ""
}
$storageLimitStatus, $issuedWarning, $prohibitedSend, $mailboxDisabled, $unlimitedMailboxes, $orphanedMailboxes = Get-StorageLimitInfo $mailbox.storageLimitStatus $issuedWarning $prohibitedSend $mailboxDisabled $unlimitedMailboxes $orphanedMailboxes
$userObject = Get-ActiveDirectoryObject (Get-UserInformation $mailbox.LegacyDN)
$mailboxCount++
if($userObject.givenName) {
$userGivenName = $userObject.givenName.ToString()
} else {
$userGivenName = ""
}
if($userObject.sn) {
$userSn = $userObject.sn.ToString()
} else {
$userSn = ""
}
if($userObject.displayName) {
$userDisplayName = $userObject.displayName.ToString()
} else {
$userDisplayName = $mailbox.DisplayName
}
if($userObject -ne $null) {
$userDomain = (Get-ObjectADDomain $userObject.distinguishedName).Split(".")[0]
} else {
$userDomain = ""
}
if($userObject.sAMAccountName) {
$userSAMAccountName = $userObject.sAMAccountName.ToString()
} else {
$userSAMAccountName = ""
}
if($userObject.mail) {
$userMail = $userObject.mail.ToString()
} else {
$userMail = ""
}
if($mailbox.TotalItemSize.Value.ToKB() -gt $largeMailboxSize) {
$largeMailboxes++
}
$totalMessages += $mailbox.ItemCount
$totalStorage += $mailbox.TotalItemSize.Value.ToKB()
$userMailboxInformation = New-Object -typeName PSObject
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Mailbox Server" -value $exchangeServer.Name
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Database Name" -value $mailbox.DatabaseName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Display Name" -value $userDisplayName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "First Name" -value $userGivenName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Last Name" -value $userSn
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Domain" -value $userDomain
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "User ID" -value $userSAMAccountName
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "E-Mail Address" -value $userMail
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Size (KB)" -value ("{0:N0}" -f $mailbox.TotalItemSize.Value.ToKB())
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Messages" -value ("{0:N0}" -f $mailbox.ItemCount)
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Storage Limit" -value $storageLimitStatus
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Last Logon" -value $lastLogonTime
Add-Member -inputObject $userMailboxInformation -type NoteProperty -name "Last User Access" -value $mailbox.LastLoggedOnUserAccount
$userMailboxes += $userMailboxInformation
if($error) {
Write-LogEntry ("[ERROR] Problems found scanning mailbox:" + $userDisplayName) $logFilename
foreach($errorEntry in $error) {
Write-LogEntry $errorEntry $logFilename
}
}
} else {
Write-LogEntry ("Skipping:" + $mailbox.LegacyDN) $logFilename
}
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Mailbox review" -status "Completed" -Completed }
}
}
Add-Member -inputObject $exchangeServer -type NoteProperty -name "totalMessages" -value $totalMessages
Add-Member -inputObject $exchangeServer -type NoteProperty -name "totalStorage" -value $totalStorage
Add-Member -inputObject $exchangeServer -type NoteProperty -name "mailboxCount" -value $mailboxCount
Add-Member -inputObject $exchangeServer -type NoteProperty -name "issuedWarning" -value $issuedWarning
Add-Member -inputObject $exchangeServer -type NoteProperty -name "prohibitedSend" -value $prohibitedSend
Add-Member -inputObject $exchangeServer -type NoteProperty -name "mailboxDisabled" -value $mailboxDisabled
Add-Member -inputObject $exchangeServer -type NoteProperty -name "unlimitedMailboxes" -value $unlimitedMailboxes
Add-Member -inputObject $exchangeServer -type NoteProperty -name "orphanedMailboxes" -value $orphanedMailboxes
Add-Member -inputObject $exchangeServer -type NoteProperty -name "largeMailboxes" -value $largeMailboxes
Write-LogEntry ("Total Messages:" + $totalMessages) $logFilename
Write-LogEntry ("Total Storage:" + $totalStorage) $logFilename
Write-LogEntry ("Mailboxes Reported count:" + $mailboxCount) $logFilename
Write-LogEntry ("Issued Warning count:" + $issuedWarning) $logFilename
Write-LogEntry ("Prohibited Send count:" + $prohibitedSend) $logFilename
Write-LogEntry ("Mailbox Disabled count:" + $mailboxDisabled) $logFilename
Write-LogEntry ("Unlimited Mailbox count:" + $unlimitedMailboxes) $logFilename
Write-LogEntry ("Orphaned Mailbox count:" + $orphanedMailboxes) $logFilename
Write-LogEntry ("Large Mailbox count:" + $largeMailboxes) $logFilename
$userMailboxes | Export-Csv -path ($workDirectory + "\" + $exchangeServer.Name + ".csv") -noTypeInformation
Write-LogEntryLineBreak $logFilename
}
Write-LogEntry "Saving Master Exchange Server List: $masterExchangeServerFilename" $logFilename
$exchangeServers | Export-Csv -path $masterExchangeServerFilename -noTypeInformation
Write-LogEntry ("Unloading Exchange SnapIn") $logFilename
Unload-ExchangeSnapin
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Build Excel Spreadsheet" -percentComplete "1" }
Write-LogEntry "Loading Master Exchange Server List: $masterExchangeServerFilename" $logFilename
$allExchangeServers = $exchangeServers #Import-Csv -path $masterExchangeServerFilename
$exchangeServers = @()
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Build Excel Spreadsheet" -percentComplete "20" }
$progressBar = 0
Write-LogEntry ("Filtering Exchange Servers") $logFilename
foreach($exchangeServer in $allExchangeServers) {
$progressBar++
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Filtering Exchange Servers" -status ($exchangeServer.Fqdn) -percentComplete (($progressBar / $allExchangeServers.Count) * 100) }
if($exchangeServer.mailboxCount -eq 0) {
Write-LogEntry ($exchangeServer.fqdn + " has no mailboxes. Skipping.") $logFilename
continue
}
$filteredExchangeServer = New-Object -typeName PSObject
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Name" -value $exchangeServer.Name
if($exchangeServer.AdminDisplayVersion -match "Version 14") {
$exchangeVersion = ("Exchange 2010 " + $exchangeServer.Edition)
} elseif($exchangeServer.AdminDisplayVersion -match "Version 8") {
$exchangeVersion = ("Exchange 2007 " + $exchangeServer.Edition)
} elseif($exchangeServer.AdminDisplayVersion -match "Version 6") {
$exchangeVersion = ("Exchange 2003 " + $exchangeServer.Edition)
} else {
$exchangeVersion = "Unknown"
}
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "IP Address" -value $exchangeServer.ipV4Address
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Exchange Version" -value $exchangeVersion
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Online Since" -value $exchangeServer.WhenCreated
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Total Mailboxes" -value ("{0:N0}" -f [int]$exchangeServer.mailboxCount)
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Total Messages" -value ("{0:N0}" -f [int]$exchangeServer.totalMessages)
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Total Storage (MB)" -value ("{0:N0}" -f ($exchangeServer.totalStorage / 1KB))
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Space Warning" -value ("{0:N0}" -f [int]$exchangeServer.issuedWarning)
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Prohibited Send" -value ("{0:N0}" -f [int]$exchangeServer.prohibitedSend)
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Disabled Mailboxes" -value ("{0:N0}" -f [int]$exchangeServer.mailboxDisabled)
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Unlimited Mailboxes" -value ("{0:N0}" -f [int]$exchangeServer.unlimitedMailboxes)
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Orphaned Mailboxes" -value ("{0:N0}" -f [int]$exchangeServer.orphanedMailboxes)
Add-Member -inputObject $filteredExchangeServer -type NoteProperty -name "Large Mailboxes" -value ("{0:N0}" -f [int]$exchangeServer.largeMailboxes)
$exchangeServers += $filteredExchangeServer
Write-LogEntry ($exchangeServer.fqdn + " will be added to the report.") $logFilename
}
Write-LogEntryLineBreak $logFilename
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Build Excel Spreadsheet" -percentComplete "40" }
Write-LogEntry ("Saving Filtered List: $filteredExchangeServerFilename") $logFilename
$exchangeServers | Export-Csv -path $filteredExchangeServerFilename -noTypeInformation
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Creating Worksheets" -status "Overview" -percentComplete "25" }
if(Test-Path -path $excelFilename) { Remove-Item -path $excelFilename }
$excelObject = New-Object -comObject Excel.Application
$excelObject.Visible = $false
$excelObject.DisplayAlerts = $false
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Creating Worksheets" -status "Overview" -percentComplete "50" }
Write-LogEntry "Injecting Filtered List, $filteredExchangeServerFilename, into first worksheet" $logFilename
$workbookObject = $excelObject.Workbooks.Open($filteredExchangeServerFilename)
$workbookObject.Title = ("Exchange Server Report for " + (Get-Date -Format D))
$workbookObject.Author = "Robert M. Toups, Jr."
$worksheetObject = $workbookObject.Worksheets.Item($sheetNumber)
$worksheetObject.UsedRange.Columns.Autofit() | Out-Null
$worksheetObject.Name = "Exchange Servers"
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Creating Worksheets" -status "Overview" -percentComplete "75" }
$listObject = $worksheetObject.ListObjects.Add([Microsoft.Office.Interop.Excel.XlListObjectSourceType]::xlSrcRange, $worksheetObject.UsedRange, $null,[Microsoft.Office.Interop.Excel.XlYesNoGuess]::xlYes,$null)
$listObject.Name = "Exchange Servers Table"
$excelCurrentTableStyle, $excelTableStyle = Get-ExcelTableStyle $excelCurrentTableStyle $excelTableStyles
$listObject.TableStyle = $excelTableStyle
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Creating Worksheets" -status "Overview" -percentComplete "100" }
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Build Excel Spreadsheet" -percentComplete "60" }
$progressBar = 0
foreach($exchangeServer in $exchangeServers) {
$progressBar++
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Creating Worksheets" -status ($exchangeServer.Name) -percentComplete (($progressBar / $exchangeServers.Count) * 100) }
$temporaryWorkFilename = ($workDirectory + "\" + $exchangeServer.Name + ".csv")
Write-LogEntry ("Loading $temporaryWorkFilename into a temporary Workbook") $logFilename
if(Test-Path -path $temporaryWorkFilename) {
Write-LogEntry ("I found $temporaryWorkFilename. That's a good thing.") $logFilename
} else {
Write-LogEntry ("Can't find $temporaryWorkFilename. That is not expected. I am skipping " + $exchangeServer.name) $logFilename
continue
}
$tempWorkbookObject = $excelObject.Workbooks.Open($temporaryWorkFilename)
$tempWorksheetObject = $tempWorkbookObject.Worksheets.Item(1)
Write-LogEntry "Copying all information to the clipboard" $logFilename
$tempWorksheetObject.UsedRange.Copy() | Out-Null
$sheetNumber++
Write-LogEntry "Adding Worksheet #$sheetnumber to $excelFilename" $logFilename
$workbookObject.WorkSheets.Add([System.Reflection.Missing]::Value, $workbookObject.Worksheets.Item($workbookObject.Sheets.Count)) | Out-Null
$worksheetObject = $workbookObject.Worksheets.Item($sheetNumber)
$worksheetObject.Activate()
Write-LogEntry ("Naming Worksheet #$sheetnumber to " + $exchangeServer.Name) $logFilename
$worksheetObject.Name = ($exchangeServer.Name)
Write-LogEntry "Pasting clipboard to newly added worksheet" $logFilename
$worksheetObject.Paste()
$lastRow = $worksheetObject.UsedRange.Rows.Count
$lastColumn = $worksheetObject.UsedRange.Columns.Count
Write-LogEntry "Worksheet last row: $lastRow" $logFilename
Write-LogEntry "Worksheet last column: $lastColumn" $logFilename
$worksheetObject.UsedRange.Columns.Autofit() | Out-Null
Write-LogEntry "Getting rid of the Select All from the paste" $logFilename
$worksheetObject.Cells.Item(1,1).Select() | Out-Null
$listObject = $worksheetObject.ListObjects.Add([Microsoft.Office.Interop.Excel.XlListObjectSourceType]::xlSrcRange, $worksheetObject.UsedRange, $null,[Microsoft.Office.Interop.Excel.XlYesNoGuess]::xlYes,$null)
$listObject.Name = ($exchangeServer.Name + " Table")
$excelCurrentTableStyle, $excelTableStyle = Get-ExcelTableStyle $excelCurrentTableStyle $excelTableStyles
Write-LogEntry "Using table style: $ExcelTableStyle ($excelCurrentTableStyle)" $logFilename
$listObject.TableStyle = $excelTableStyle
if($lastColumn -eq 14) {
Write-LogEntry "Adding totals to Columns J & K" $logFilename
$worksheetObject.Cells.Item($lastRow+1,10) = "=SUM(J2:J" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,11) = "=SUM(K2:K" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,10).NumberFormat = "#,##0"
$worksheetObject.Cells.Item($lastRow+1,11).NumberFormat = "#,##0"
} else {
Write-LogEntry "Adding totals to Columns I & J" $logFilename
$worksheetObject.Cells.Item($lastRow+1,9) = "=SUM(I2:I" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,10) = "=SUM(J2:J" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,9).NumberFormat = "#,##0"
$worksheetObject.Cells.Item($lastRow+1,10).NumberFormat = "#,##0"
}
Write-LogEntry "Destroying temporary workbook" $logFilename
$tempWorkbookObject.Close()
Write-LogEntryLineBreak $logFilename
}
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Build Excel Spreadsheet" -percentComplete "80" }
$worksheetObject = $workbookObject.Worksheets.Item(1)
$worksheetObject.Activate()
Write-LogEntry "Formatting Overview page" $logFilename
$lastRow = $worksheetObject.UsedRange.Rows.Count
Write-LogEntry "Last row: $lastRow" $logFilename
$worksheetObject.Cells.Item($lastRow+1,5) = "=SUM(E2:E" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,6) = "=SUM(F2:F" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,7) = "=SUM(G2:G" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,8) = "=SUM(H2:H" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,9) = "=SUM(I2:I" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,10) = "=SUM(J2:J" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,11) = "=SUM(K2:K" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,12) = "=SUM(L2:L" + $lastRow + ")"
$worksheetObject.Cells.Item($lastRow+1,13) = "=SUM(M2:M" + $lastRow + ")"
Write-LogEntry "Added total rows" $logFilename
for($y=5; $y -le 13; $y++) {
$worksheetObject.Cells.Item(($lastRow+1),$y).NumberFormat = "#,##0"
}
Write-LogEntry "Formatted Totals" $logFilename
foreach($exchangeServer in $exchangeServers) {
if(!$beQuiet) { Write-Progress -id 2 -parentId 1 -activity "Hyperlinking Worksheets" -status ($exchangeServer.Name) -percentComplete (($serverCount / $exchangeServers.Count) * 100) }
Write-LogEntry ("Hyperlinked " + $exchangeServer.name + " worksheet to Overview page") $logFilename
$serverCount++
$selectedRange = $worksheetObject.Range("A" + $serverCount)
$worksheetObject.Hyperlinks.Add($selectedRange, ("#`'" + $exchangeServer.Name + "`'!A1")) | Out-Null
$selectedRange.Font.Bold = $true
}
Write-LogEntryLineBreak $logFilename
Write-LogEntry "Saving $excelFilename" $logFilename
$workbookObject.SaveAs($excelFilename,51) # http://msdn.microsoft.com/en-us/library/bb241279.aspx
$workbookObject.Saved = $true
$workbookObject.Close()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbookObject) | Out-Null
Write-LogEntry "Quiting Excel" $logFilename
$excelObject.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excelObject) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
if(Test-Path -path $excelFilename) {
Write-LogEntry "Successfully saved $excelFilename"
} else {
Write-LogEntry "This is embarassing, I failed to save $excelFilename. You might want to look into that."
}
if(!$beQuiet) { Write-Progress -id 1 -activity "Exchange Server Report" -status "Build Excel Spreadsheet" -percentComplete "100" }
Write-LogEntryLineBreak $logFilename
if($cleanupWorkDirectory -eq $true) {
Write-LogEntry "Removing the work directory: $workDirectory" $logFilename
Remove-Item -literalPath $workDirectory -Recurse
if(Test-Path -path $workDirectory) {
Write-LogEntry "Failed to clean the work directory: $workDirectory" $logFilename
} else {
Write-LogEntry "Successfully cleaned the work directory: $workDirectory" $logFilename
}
} else {
Write-LogEntry "There still is work data located here: $workDirectory" $logFilename
}
Write-LogEntry ("Run Time: " + (Get-RunTime ((Get-Date) - $startTime))) $logFilename
Write-LogEntry "WHEW! I am done!" $logFilename
Write-LogEntryLineBreak $logFilename
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com2tag:blogger.com,1999:blog-4411406500801797400.post-48931898377145887522011-10-26T20:35:00.000-04:002011-10-26T20:35:00.786-04:00Clone NTFS Permissions Fast with PowerShellNeed to clone NFTS permissions between two folders or files? The <a href="http://technet.microsoft.com/en-us/library/dd347635.aspx">Get-Acl</a> & <a href="http://technet.microsoft.com/en-us/library/dd315261.aspx">Set-Acl</a> commandlets provide this capability when used in conjunction with each other. With the one-liner below, you can transfer complex permissions from source to destination in seconds.
<pre class="brush:powershell;toolbar:false">Set-Acl -path "\\fileserver.asia.ad.local\dept\target_directory" -aclObject (Get-Acl -path "\\fileserver.europe.ad.local\dept\source_directory")</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-59348975072968904252011-08-12T20:45:00.000-04:002011-08-16T09:25:19.808-04:00Nagios Check for Scheduled TSM Backups (Updated)I have cleaned the code from my previous post, <a href="http://activelydirect.blogspot.com/2011/06/nagios-check-for-scheduled-tsm-backups.html">Nagios Check for Scheduled TSM Backups</a>, that included some superfluous code derived from my post, <a href="http://activelydirect.blogspot.com/2011/02/audit-tivoli-storage-manager-backup.html">Audit Tivoli Storage Manager Backup Client Schedules on Windows Servers</a>. The updated code is more efficient and returns service status faster in the event of an extended period of "deafness" between the client and the TSM server and subsequent restart of the TSM service.
<pre class="brush:powershell;toolbar:false">param([string]$tsmFile)
#--------------------------------------------------------------------------------------------------#
Function Get-TSMInformation {
$tsmInformation = @()
$schedulerServices = "HKLM:\SOFTWARE\IBM\ADSM\CurrentVersion\BackupClient\Scheduler Service"
$windowsServivces = "HKLM:\SYSTEM\CurrentControlSet\Services"
if(Test-Path -path $schedulerServices) {
$tsmServices = @(Get-Item $schedulerServices | ForEach-Object { $_.Property })
foreach($windowsService in Get-ChildItem $windowsServivces) {
if($tsmServices -contains (Split-Path -leaf $windowsService.Name)) {
$tsmServiceName = (Split-Path -leaf $windowsService.Name)
$tsmServiceSubKey = Get-Item ($windowsServivces + "\" + (Split-Path -leaf $windowsService.Name))
$startValue = $tsmServiceSubKey.GetValue("Start")
$clientNodeName = ($tsmServiceSubKey.OpenSubKey("Parameters")).GetValue("clientNodeName")
$scheduleLog = ($tsmServiceSubKey.OpenSubKey("Parameters")).GetValue("scheduleLog")
$currentStatus = (Get-Service -name $tsmServiceName).Status
$clientNodeInformation = New-Object -typeName PSObject
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "tsmServiceName" -value $tsmServiceName
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "startValue" -value $startValue
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "clientNodeName" -value $clientNodeName
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "scheduleLog" -value $scheduleLog
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "status" -value $currentStatus
$tsmInformation += $clientNodeInformation
}
}
}
return $tsmInformation
}
#--------------------------------------------------------------------------------------------------#
Function Read-EndOfFileByByteChunk($fileName,$totalNumberOfLines,$byteChunk) {
if($totalNumberOfLines -lt 1) { $totalNumberOfLines = 1 }
if($byteChunk -le 0) { $byteChunk = 10240 }
$linesOfText = New-Object System.Collections.ArrayList
if([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$asciiEncoding = New-Object System.Text.ASCIIEncoding
$fileSize = $fileStream.Length
$byteOffset = $byteChunk
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$totalBytesProcessed = 0
$lastReadAttempt = $false
do {
if($byteOffset -ge $fileSize) {
$byteChunk = $fileSize - $totalBytesProcessed
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$byteOffset = $fileSize
$lastReadAttempt = $true
}
$fileStream.Seek((-$byteOffset), [System.IO.SeekOrigin]::End) | Out-Null
$fileStream.Read($bytesRead, 0, $byteChunk) | Out-Null
$chunkOfText = New-Object System.Collections.ArrayList
$chunkOfText.AddRange(([System.Text.RegularExpressions.Regex]::Split($asciiEncoding.GetString($bytesRead),"\r\n")))
$firstLineLength = ($chunkOfText[0].Length)
$byteOffset = ($byteOffset + $byteChunk) - ($firstLineLength)
if($lastReadAttempt -eq $false -and $chunkOfText.count -lt $totalNumberOfLines) {
$chunkOfText.RemoveAt(0)
}
$totalBytesProcessed += ($byteChunk - $firstLineLength)
$linesOfText.InsertRange(0, $chunkOfText)
} while($totalNumberOfLines -ge $linesOfText.count -and $lastReadAttempt -eq $false -and $totalBytesProcessed -lt $fileSize)
$fileStream.Close()
if($linesOfText.count -gt 1) {
$linesOfText.RemoveAt($linesOfText.count-1)
}
$deltaLines = ($linesOfText.count - $totalNumberOfLines)
if($deltaLines -gt 0) {
$linesOfText.RemoveRange(0, $deltaLines)
}
} else {
$linesOfText.Add("[ERROR] $fileName not found") | Out-Null
}
return $linesOfText
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name returnNormal -option Constant -value 0
Set-Variable -name returnWarning -option Constant -value 1
Set-Variable -name returnError -option Constant -value 2
Set-Variable -name returnUnknown -option Constant -value 3
Set-Variable -name computerFqdn -option Constant -value (([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()).HostName + "." + ([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()).DomainName)
Set-Variable -name backupWindow -option Constant -value 24 # in Hours
Set-Variable -name deafService -option Constant -value 36 # in Hours
Set-Variable -name enableRestarts -option Constant -value $true # Allow check to restart TSM if the service
Set-Variable -name lookBack -option Constant -value 250 # Number of lines to tail
Set-Variable -name maximumFailures -option Constant -value 5 # Your tolerance for failed files
Set-Variable -name successfulBackup -value $false
Set-Variable -name todaysBackupFound -value $false
Set-Variable -name backupStillRunning -value $false
Set-Variable -name completionTime -value $null
Set-Variable -name totalFailed -value 0
Set-Variable -name logEntries -value @()
Set-Variable -name exitMessage -value "Massive Script Failure"
Set-Variable -name exitValue -value $returnError
#--------------------------------------------------------------------------------------------------#
if($tsmFile -eq "$" -or (!$tsmFile)) {
$tsmInfo = @(Get-TSMInformation)
foreach($tsmInstance in $tsmInfo) {
if($tsmInstance.scheduleLog -match "\\dsmsched.log") {
$tsmLogFile = $tsmInstance.scheduleLog
break
}
}
} else {
$tsmLogFile = ($env:programfiles + "\Tivoli\TSM\baclient\$tsmFile")
}
if(Test-Path -path $tsmLogFile) {
$logEntries = Read-EndOfFileByByteChunk $tsmLogFile $lookBack 1280
foreach($logEntry in $logEntries) {
if($logEntry.Length -ge 19) {
$dateTest = $logEntry.SubString(0,19) -as [DateTime]
if($dateTest) {
if(((Get-Date) - (Get-Date $logEntry.SubString(0,19))).TotalHours -le $backupWindow) {
if($logEntry -match "Scheduled event '(.*?)' completed successfully.") {
$successfulBackup = $true
$completionTime = Get-Date $logEntry.SubString(0,19)
}
if($logEntry -match "Total number of objects failed:") {
[int]$totalFailed = ($logEntry -Replace "(.*)Total number of objects failed:", "").Trim()
}
$todaysBackupFound = $true
}
$lastLine = $logEntry
}
}
}
if($successfulBackup -eq $false -and $todaysBackupFound -eq $true) {
$lastLogTime = ((Get-Date) - (Get-Date $lastLine.SubString(0,19))).TotalMinutes
if($lastLogTime -le 15) {
$backupStillRunning = $true
}
}
if($todaysBackupFound -eq $false) {
if(((Get-Date) - (Get-Date $lastLine.SubString(0,19))).TotalHours -ge $deafService) {
$tsmInformation = @(Get-TSMInformation)
if($tsmInformation.Count -gt 0) {
foreach($tsmInstance in $tsmInformation) {
if($tsmInstance.scheduleLog -eq $tsmLogFile) {
if($tsmInstance.status -eq "Running") {
Restart-Service -name $tsmInstance.tsmServiceName
$exitMessage = ("TSM Scheduler `"" + $tsmInstance.tsmServiceName + "`" has not contacted the TSM server in $deafService hours. Restarting service.")
} else {
Start-Service -name $tsmInstance.tsmServiceName
$exitMessage = ("TSM Scheduler `"" + $tsmInstance.tsmServiceName + "`" was stopped and hasn't contacted the TSM server in $deafService hours. Starting service.")
}
$exitValue = $returnError
break
}
}
} else {
$exitMessage = ("Unable to determine which service is associated to $tsmLogFile")
$exitValue = $returnError
}
} else {
$exitMessage = ("Unable to find data in the last $backupWindow hours in $tsmLogFile. Last Backup log date: " + (Get-Date $lastLine.SubString(0,19)))
$exitValue = $returnError
}
} elseif($totalFailed -gt $maximumFailures) {
$exitMessage = "Backup completed with $totalFailed failed objects."
$exitValue = $returnWarning
} elseif($successfulBackup -eq $true) {
$exitMessage = "Backup completed successfully: $completionTime"
$exitValue = $returnNormal
} elseif($backupStillRunning -eq $true) {
$exitMessage = ("Backup still running! Please allow to complete. Current status: " + $lastLine -Replace "\\","/")
$exitValue = $returnWarning
} else {
$exitMessage = ("Unable to find a successful backup. Last status: " + $lastLine -Replace "\\","/")
$exitValue = $returnError
}
Write-Host $exitMessage
$Host.SetShouldExit($exitValue)</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com2tag:blogger.com,1999:blog-4411406500801797400.post-49889620309543365512011-07-28T20:40:00.000-04:002011-07-28T20:40:00.504-04:00Get the Creator of an Active Directory Object with PowerShell<pre class="brush:powershell;toolbar:false">$userObject = [ADSI]"LDAP://localdc.mydomain.ad.local/CN=Smith\, John,OU=Users,DC=mydomain,DC=ad,DC=local,DC=com"
$objectOwner = $userObject.PSBase.get_ObjectSecurity().GetOwner([System.Security.Principal.NTAccount]).Value</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-82803299458809504872011-06-24T20:20:00.002-04:002011-06-25T06:49:35.353-04:00Quick One-Time Scheduled TaskWhile you can set one-time Schedule Tasks, it is cumbersome whether you do it via the GUI interface or through a PowerShell script. Unless you might use it again at a future date, you will need to clean it up later. A quick way to obtain the same result is to use <a href="http://technet.microsoft.com/en-us/library/dd347683.aspx"><strong>Start-Sleep</strong></a>. Simply open a PowerShell session under the security context you want to execute the commandlet or script and use the syntax below. The semicolon separates commandlets allowing for this technique. The date math performed in the command returns a <a href="http://technet.microsoft.com/en-us/library/ee176916.aspx">time span</a> in total seconds that Start-Sleep uses. The example waits till 10:55 PM of the same day then restarts a TSM backup client service using <a href="http://technet.microsoft.com/en-us/library/dd347673.aspx"><strong>Restart-Service</strong></a>. This is a great method if you need to kick-off a commandlet on the weekend before you leave the office Friday night. Just adjust the date and time of the first <a href="http://technet.microsoft.com/en-us/library/dd347647.aspx"><strong>Get-Date</strong></a> commandlet.
<pre class="brush:powershell;toolbar:false;gutter:false">Start-Sleep -Seconds ((Get-Date "10:55 PM") - (Get-Date)).TotalSeconds;Restart-Service -DisplayName "TSM Client Scheduler"</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-17706874251381114672011-06-17T20:25:00.003-04:002011-06-22T17:40:00.277-04:00PowerShell Tail ImprovementsHere are the latest improvements I have made to my tail functions for PowerShell. I typically work in a heterogenous computing environment so I need to be able to handle the various text file encodings and <a href="http://en.wikipedia.org/wiki/Newline">new line delimiters</a> I encounter. The previous blogs posts (<a href="http://activelydirect.blogspot.com/2011/03/unix-tail-like-functionality-in.html">here</a>, <a href="http://activelydirect.blogspot.com/2011/03/unix-tail-like-functionality-in_14.html">here</a> & <a href="http://activelydirect.blogspot.com/2011/04/replicating-unix-f-in-powershell.html">here</a>) showed a steady improvement and functionality with the major feature I needed to complete being the detecting and reading of text files that were not were ASCII encoded with Windows end-of-line (CR+LF). This stops hard coding specific changes in my code to deal with each situation I encounter.<br />
<br />
I have added two functions to the code to handle these two items. Looking at the head of the file, I attempt to detect the <a href="http://en.wikipedia.org/wiki/Byte_order_mark">byte order mark</a> (BOM) to determine if the file is unicode encoded and its <a href="http://en.wikipedia.org/wiki/Endianess">endianess</a>. If I am unable to make that determination, revert to ASCII as the encoding. I work with the <a href="http://msdn.microsoft.com/en-us/library/system.text.encoding.aspx">System.Text.Encoding</a> class to assist in the decode of the unicode based text files. The second function detects the new line delimiter by searching the head for the match of Windows (CR+LF), UNIX (LF) or Classic Macintosh (CR) to assist in the breaking of the lines for initial tail read.<br />
<br />
In the code sample below, you will find these two new functions with the log file "tailed" being a system.log from a hypothetical Mac OS X server sharing out its "var" directory via <a href="http://www.samba.org/">SAMBA</a> so we can access the system.log file in the log subdirectory. This file is typically ASCII encoded with UNIX new lines.<br />
<br />
If you are running Microsoft Forefront Client Security and want to monitor the updates, virus detections and removals, you need to access the "MPLog-*.log" file which is Unicode-16 Little-Endian encoded. Swap out the inputFile variable to watch this file for updates. You can find that file here: <br />
<pre class="brush:powershell;toolbar:false;gutter:false">$env:ALLUSERSPROFILE\Application Data\Microsoft\Microsoft Forefront\Client Security\Client\Antimalware\Support</pre>These are good examples to demonstrate the capability of the added functions add flexibility to my prior attempts. <br />
<pre class="brush:powershell;toolbar:false">Function Get-FileEncoding($fileStream) {
$fileEncoding = $null
if($fileStream.CanSeek) {
[byte[]] $bytesToRead = New-Object byte[] 4
$fileStream.Read($bytesToRead, 0, 4) | Out-Null
if($bytesToRead[0] -eq 0x2B -and $bytesToRead[1] -eq 0x2F -and $bytesToRead[2] -eq 0x76) { # UTF-7
$encoding = "utf7"
} elseif($bytesToRead[0] -eq 0xFF -and $bytesToRead[1] -eq 0xFE) { # UTF-16 Little-Endian
$encoding = "unicode-le"
} elseif($bytesToRead[0] -eq 0xFE -and $bytesToRead[1] -eq 0xFF) { # UTF-16 Big-Endian
$encoding = "unicode-be"
} elseif($bytesToRead[0] -eq 0 -and $bytesToRead[1] -eq 0 -and $bytesToRead[2] -eq 0xFE -and $bytesToRead[3] -eq 0xFF) { # UTF-32 Big Endian
$encoding = "utf32-be"
} elseif($bytesToRead[0] -eq 0xFF -and $bytesToRead[1] -eq 0xFE -and $bytesToRead[2] -eq 0 -and $bytesToRead[3] -eq 0) { # UTF-32 Little Endian
$encoding = "utf32-le"
} elseif($bytesToRead[0] -eq 0xDD -and $bytesToRead[1] -eq 0x73 -and $bytesToRead[2] -eq 0x66 -and $bytesToRead[3] -eq 0x73) { # UTF-EBCDIC
$encoding = "unicode"
} elseif($bytesToRead[0] -eq 0xEF -and $bytesToRead[1] -eq 0xBB -and $bytesToRead[2] -eq 0xBF) { # UTF-8 with BOM
$encoding = "utf8"
} else { # ASCII Catch-All
$encoding = "ascii"
}
switch($encoding) {
"unicode-be" { $fileEncoding = New-Object System.Text.UnicodeEncoding($true, $true) }
"unicode-le" { $fileEncoding = New-Object System.Text.UnicodeEncoding($false, $true) }
"utf32-be" { $fileEncoding = New-Object System.Text.UTF32Encoding($true, $true) }
"utf32-le" { $fileEncoding = New-Object System.Text.UTF32Encoding($false, $true) }
"unicode" { $fileEncoding = New-Object System.Text.UnicodeEncoding($true, $true) }
"utf7" { $fileEncoding = New-Object System.Text.UTF7Encoding }
"utf8" { $fileEncoding = New-Object System.Text.UTF8Encoding }
"utf32" { $fileEncoding = New-Object System.Text.UTF32Encoding }
"ascii" { $fileEncoding = New-Object System.Text.AsciiEncoding }
}
}
return $fileEncoding
}
#--------------------------------------------------------------------------------------------------#
Function Get-NewLine($fileStream, $fileEncoding) {
$newLine = $null
$byteChunk = 512
if($fileStream.CanSeek) {
$fileSize = $fileStream.Length
if($fileSize -lt $byteChunk) { $byteChunk -eq $fileSize }
[byte[]] $bytesToRead = New-Object byte[] $byteChunk
$fileStream.Read($bytesToRead, 0, $byteChunk) | Out-Null
$testLines = $fileEncoding.GetString($bytesToRead)
if($testLines -match "\r\n") { # Windows
$newLine = "\r\n"
} elseif($testLines -match "\n") { # Unix
$newLine = "\n"
} elseif($testLines -match "\r") { # Classic Mac
$newLine = "\r"
} else { # When all else fails, Go Windows
$newLine = "\r\n"
}
}
return $newLine
}
#--------------------------------------------------------------------------------------------------#
Function Read-EndOfFileByByteChunk($fileName,$totalNumberOfLines,$byteChunk) {
if($totalNumberOfLines -lt 1) { $totalNumberOfLines = 1 }
if($byteChunk -le 0) { $byteChunk = 10240 }
$linesOfText = New-Object System.Collections.ArrayList
if([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$fileEncoding = Get-FileEncoding $fileStream
$newLine = Get-NewLine $fileStream $fileEncoding
$fileSize = $fileStream.Length
$byteOffset = $byteChunk
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$totalBytesProcessed = 0
$lastReadAttempt = $false
do {
if($byteOffset -ge $fileSize) {
$byteChunk = $fileSize - $totalBytesProcessed
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$byteOffset = $fileSize
$lastReadAttempt = $true
}
$fileStream.Seek((-$byteOffset), [System.IO.SeekOrigin]::End) | Out-Null
$fileStream.Read($bytesRead, 0, $byteChunk) | Out-Null
$chunkOfText = New-Object System.Collections.ArrayList
$chunkOfText.AddRange(([System.Text.RegularExpressions.Regex]::Split($fileEncoding.GetString($bytesRead),$newLine)))
$firstLineLength = ($chunkOfText[0].Length)
$byteOffset = ($byteOffset + $byteChunk) - ($firstLineLength)
if($lastReadAttempt -eq $false -and $chunkOfText.count -lt $totalNumberOfLines) {
$chunkOfText.RemoveAt(0)
}
$totalBytesProcessed += ($byteChunk - $firstLineLength)
$linesOfText.InsertRange(0, $chunkOfText)
} while($totalNumberOfLines -ge $linesOfText.count -and $lastReadAttempt -eq $false -and $totalBytesProcessed -lt $fileSize)
$fileStream.Close()
if($linesOfText.count -gt 1) {
$linesOfText.RemoveAt($linesOfText.count-1)
}
$deltaLines = ($linesOfText.count - $totalNumberOfLines)
if($deltaLines -gt 0) {
$linesOfText.RemoveRange(0, $deltaLines)
}
} else {
$linesOfText.Add("[ERROR] $fileName not found") | Out-Null
}
Write-Host $linesOfText.count
return $linesOfText
}
#--------------------------------------------------------------------------------------------------#
Function Read-FileUpdates($fileName,$startSize) {
if([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$fileEncoding = Get-FileEncoding $fileStream
$fileStream.Close()
while([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
if($fileStream.CanSeek) {
$currentFileSize = $fileStream.Length
if($currentFileSize -gt $startSize) {
$byteChunk = $currentFileSize - $startSize
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$fileStream.Seek((-$byteChunk), [System.IO.SeekOrigin]::End) | Out-Null
$fileStream.Read($bytesRead, 0, $byteChunk) | Out-Null
Write-Host ($fileEncoding.GetString($bytesRead)) -noNewLine
$startSize = $currentFileSize
}
}
$fileStream.Close()
Start-Sleep -milliseconds 250
}
}
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name inputFile -option Constant -value "\\macosx-server.mydomain.local\var\log\system.log"
#--------------------------------------------------------------------------------------------------#
if([System.IO.File]::Exists($inputFile)) {
Write-Host (Read-EndOfFileByByteChunk $inputFile 10 1280 | Out-String) -noNewLine
$fileStream = New-Object System.IO.FileStream($inputFile,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$fileSize = $fileStream.Length
$fileStream.Close()
Read-FileUpdates $inputFile $fileSize
} else {
Write-Host "Could not find $inputFile..." -foregroundColor Red
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-82669027824787934162011-06-14T20:40:00.000-04:002011-06-14T20:40:00.823-04:00Validate IPv4 Addresses in PowerShellHere is a simple function to validate if a <a href="https://secure.wikimedia.org/wikipedia/en/wiki/IPv4_address">IPv4 address</a> meets the <a href="http://tools.ietf.org/html/rfc2474">RFC</a>. It does not confirm if the host associated to the IP Address is accessible. To do that with PowerShell, you need to utilize the <a href="http://msdn.microsoft.com/en-us/library/system.net.networkinformation.ping.aspx">System.Net.NetworkInformation.Ping</a> class. The validation is performed by a fairly complex <a href="http://www.regular-expressions.info/regexbuddy/ipaccurate.html">Regular Expression</a> using '-match'.
<pre class="brush:powershell;toolbar:false">Function Test-IPv4Address($ipAddress) {
if($testAddress -match "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b") {
$addressValid = $true
} else {
$addressValid = $false
}
return $addressValid
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name testAddresses -option Constant -value @("192.168.1.1","546.23.10.12","8.8.8.8","127.0.999.26")
#--------------------------------------------------------------------------------------------------#
foreach($testAddress in $testAddresses) {
if((Test-IPv4Address $testAddress)) {
Write-Host "$testAddress is a validly formatted IPv4 Address" -foregroundColor Green
} else {
Write-Host "$testAddress is not a validly formatted IPv4 Address" -foregroundColor Red
}
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-29079713049246435352011-06-13T21:25:00.002-04:002011-08-12T23:41:24.900-04:00Nagios Check for Scheduled TSM BackupsGood backups are the best friends you have as a System Administrator. If you don't know if your backups are successful, your business is at risk. Being unable to recover data after a catastrophic failure on a business critical system is typically an <a href="https://duckduckgo.com/?q=%22resume%20generating%20event%22">RGE</a> and can put a company out of business. IBM's <a href="http://www-01.ibm.com/software/tivoli/solutions/backup/">Tivoli Storage Manager</a> system provides a robust backup and recovery and has decent reporting. I prefer to consolidate system health monitoring in one system, <a href="http://www.nagios.org/">Nagios</a>, so I do not need a separate monitoring console for every product in production. Monitoring backups successfully over hundreds or thousands of systems from a generated report is laborious. Nagios provides checks and balances for resolving system issues. In one view, you know your risk level. If you don't resolve a problem, it remains CRITICAL. Having a check for TSM backups simplifies the process and greatly reduces the labor required to monitor backups.<br />
<br />
Scheduled TSM backups write to a log defined in the schedule's .opt file; typically dsmsched.log. On large file servers with long dsmsched.log retention periods, this file can grow easily over 1 gigabyte. Reading the entire dsmsched.log file to determine success of the last backup will likely breach the timeout of the Nagios check. To compensate for this, we need to <a href="http://activelydirect.blogspot.com/2011/03/unix-tail-like-functionality-in_14.html">tail the log file</a> and retrieve the summary data from the last backup. In the check below, I do just that. If you pass the name of the schedule log file (you can run multiple schedules on a client; each with a different log file name), the check will look for it install directory in the "Program Files" stored in the environmental variable. If no log file name is provided to the check, it will search the registry to look for the default log filename. If you are running custom install locations, this will need to me modified.<br />
<br />
As you follow through the flow of the code, you will see what triggers are passed on to Nagios. They are fairly straightforward: <ul><li>If a backup has not completed in 24 hours, a critical alarm is generated</li>
<li>If a certain number of failed file backups are reported, a warning alarm is generated</li>
<li>If a backup is still running, a warning alarm is generated</li>
<li>If a successful backup is detected, a normal return is generated</li>
</ul>These are all customizable via the initial variables in the code. One special feature of this code (and is optional) is the ability to restart the TSM Client Scheduler if no communication between the client and server have occurred in the last 36 hours. Not a common problem but one that I have encountered enough times to make this a feature; saving time spent manually restarting the process. Restarting the TSM Client Scheduler service will re-initiate the communication.<br />
<br />
And remember, just because you have log files saying you have "good backups" that doesn't mean it's true. You need to test restores on a regular basis as a part of your disaster recovery practice. Ultimately, backups are only as good as their ability to be restored.
<br /><br />
<b><span class="Apple-style-span" style="color: red;">UPDATE: </span><span class="Apple-style-span" style="font-weight: normal;">I have made some improvements to this code <a href="http://activelydirect.blogspot.com/2011/08/nagios-check-for-scheduled-tsm-backups.html">here</a>.
<pre class="brush:powershell;toolbar:false">param([string]$tsmFile)
#--------------------------------------------------------------------------------------------------#
Function Get-TSMInfo($server, $tsmInfo) {
$key = "SOFTWARE"
$hKey = [Microsoft.Win32.RegistryHive]::LocalMachine
$baseKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($hKey, $server)
foreach($rootKeyValue in ($baseKey.OpenSubKey($key)).GetSubKeyNames()) {
if($rootKeyValue -eq "IBM" -and ($baseKey.OpenSubKey("$key\IBM\ADSM\CurrentVersion")).SubKeyCount -gt 2) {
$tsmVersion = ($baseKey.OpenSubKey("$key\IBM\ADSM\CurrentVersion\BackupClient")).GetValue("PtfLevel")
$tsmPath = ($baseKey.OpenSubKey("$key\IBM\ADSM\CurrentVersion\BackupClient")).GetValue("Path")
$key = "SYSTEM\CurrentControlSet\Services"
if($tsmVersion -ne "" -and $tsmPath -ne "") {
foreach($keyValue in ($baseKey.OpenSubKey($key)).GetSubKeyNames()) {
foreach($subKeyValue in ($baseKey.OpenSubKey("$key\$keyValue")).GetSubKeyNames()) {
$clientNodeName = ""
$errorLog = ""
$optionsFile = ""
$scheduleLog = ""
if(($baseKey.OpenSubKey("$key\$keyValue").GetValue("Start")) -eq "2") {
if($subKeyValue -eq "Parameters") {
foreach($value in ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValueNames()) {
if($value -eq "clientNodeName") {
$clientNodeName = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
} elseif($value -eq "errorLog") {
$errorLog = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
} elseif($value -eq "optionsFile") {
$optionsFile = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
} elseif($value -eq "scheduleLog") {
$scheduleLog = ($baseKey.OpenSubKey("$key\$keyValue\Parameters")).GetValue($value)
}
}
}
}
if($clientNodeName -ne "" -and $errorLog -ne "" -and $optionsFile -ne "" -and $scheduleLog -ne "") {
$optionsFileUncPath = ("\\$server\" + ($optionsFile.SubString(0,1) + "$" + $optionsFile.SubString(2)))
$tsmServer = "FAILED"
$tsmClientPort = "FAILED"
if(Test-Path -path $optionsFileUncPath) {
foreach($line in (Get-Content -path $optionsFileUncPath)){
if($line -match "TCPSERVERADDRESS") {
$tsmServer = ($line -replace "TCPSERVERADDRESS","").Trim()
}
if($line -match "TCPCLIENTPORT") {
$tsmClientPort = ($line -replace "TCPCLIENTPORT","").Trim()
}
}
}
$serviceStatus = $null
foreach($service in Get-Service) {
if($service.DisplayName -eq $keyValue) {
$serviceStatus = $service.Status
break
}
}
if($serviceStatus -eq "Running" -or $serviceStatus -eq "Stopped") {
$clientNodeInformation = New-Object -typeName PSObject
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "server" -value $server
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "tsmVersion" -value $tsmVersion
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "installPath" -value $tsmPath
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "tsmServer" -value $tsmServer
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "tsmClientPort" -value $tsmClientPort
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "scheduleName" -value $keyValue
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "clientNodeName" -value $clientNodeName
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "optionsFile" -value $optionsFile
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "scheduleLog" -value $scheduleLog
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "errorLog" -value $errorLog
Add-Member -inputObject $clientNodeInformation -type NoteProperty -name "status" -value $serviceStatus
$tsmInfo += $clientNodeInformation
}
}
}
}
}
}
}
return $tsmInfo
}
#--------------------------------------------------------------------------------------------------#
Function Read-EndOfFileByByteChunk($fileName,$totalNumberOfLines,$byteChunk) {
if($totalNumberOfLines -lt 1) { $totalNumberOfLines = 1 }
if($byteChunk -le 0) { $byteChunk = 10240 }
$linesOfText = New-Object System.Collections.ArrayList
if([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$asciiEncoding = New-Object System.Text.ASCIIEncoding
$fileSize = $fileStream.Length
$byteOffset = $byteChunk
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$totalBytesProcessed = 0
$lastReadAttempt = $false
do {
if($byteOffset -ge $fileSize) {
$byteChunk = $fileSize - $totalBytesProcessed
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$byteOffset = $fileSize
$lastReadAttempt = $true
}
$fileStream.Seek((-$byteOffset), [System.IO.SeekOrigin]::End) | Out-Null
$fileStream.Read($bytesRead, 0, $byteChunk) | Out-Null
$chunkOfText = New-Object System.Collections.ArrayList
$chunkOfText.AddRange(([System.Text.RegularExpressions.Regex]::Split($asciiEncoding.GetString($bytesRead),"\r\n")))
$firstLineLength = ($chunkOfText[0].Length)
$byteOffset = ($byteOffset + $byteChunk) - ($firstLineLength)
if($lastReadAttempt -eq $false -and $chunkOfText.count -lt $totalNumberOfLines) {
$chunkOfText.RemoveAt(0)
}
$totalBytesProcessed += ($byteChunk - $firstLineLength)
$linesOfText.InsertRange(0, $chunkOfText)
} while($totalNumberOfLines -ge $linesOfText.count -and $lastReadAttempt -eq $false -and $totalBytesProcessed -lt $fileSize)
$fileStream.Close()
if($linesOfText.count -gt 1) {
$linesOfText.RemoveAt($linesOfText.count-1)
}
$deltaLines = ($linesOfText.count - $totalNumberOfLines)
if($deltaLines -gt 0) {
$linesOfText.RemoveRange(0, $deltaLines)
}
} else {
$linesOfText.Add("[ERROR] $fileName not found") | Out-Null
}
return $linesOfText
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name returnNormal -option Constant -value 0
Set-Variable -name returnWarning -option Constant -value 1
Set-Variable -name returnError -option Constant -value 2
Set-Variable -name returnUnknown -option Constant -value 3
Set-Variable -name computerFqdn -option Constant -value (([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()).HostName + "." + ([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()).DomainName)
Set-Variable -name backupWindow -option Constant -value 24 # in Hours
Set-Variable -name deafService -option Constant -value 36 # in Hours
Set-Variable -name enableRestarts -option Constant -value $true # Allow check to restart TSM if the service
Set-Variable -name lookBack -option Constant -value 250 # Number of lines to tail
Set-Variable -name maximumFailures -option Constant -value 5 # Your tolerance for failed files
Set-Variable -name successfulBackup -value $false
Set-Variable -name todaysBackupFound -value $false
Set-Variable -name backupStillRunning -value $false
Set-Variable -name completionTime -value $null
Set-Variable -name totalFailed -value 0
Set-Variable -name logEntries -value @()
Set-Variable -name exitMessage -value "Massive Script Failure"
Set-Variable -name exitValue -value $returnError
#--------------------------------------------------------------------------------------------------#
if($tsmFile -eq "$" -or (!$tsmFile)) {
$tsmInfo = @(Get-TSMInfo $computerFqdn @())
foreach($tsmInstance in $tsmInfo) {
if($tsmInstance.scheduleLog -match $tsmFile) {
if($tsmInstance.scheduleLog -match "\\dsmsched.log") {
$tsmLogFile = $tsmInstance.scheduleLog
Write-Host $tsmLogFile
break
}
}
}
} else {
$tsmLogFile = ($env:programfiles + "\Tivoli\TSM\baclient\$tsmFile")
}
if(Test-Path -path $tsmLogFile) {
$logEntries = Read-EndOfFileByByteChunk $tsmLogFile $lookBack 1280
foreach($logEntry in $logEntries) {
if($logEntry.Length -ge 19) {
$dateTest = $logEntry.SubString(0,19) -as [DateTime]
if($dateTest) {
if(((Get-Date) - (Get-Date $logEntry.SubString(0,19))).TotalHours -le $backupWindow) {
if($logEntry -match "Scheduled event '(.*?)' completed successfully.") {
$successfulBackup = $true
$completionTime = Get-Date $logEntry.SubString(0,19)
}
if($logEntry -match "Total number of objects failed:") {
[int]$totalFailed = ($logEntry -Replace "(.*)Total number of objects failed:", "").Trim()
}
$todaysBackupFound = $true
}
$lastLine = $logEntry
}
}
}
if($successfulBackup -eq $false -and $todaysBackupFound -eq $true) {
$lastLogTime = ((Get-Date) - (Get-Date $lastLine.SubString(0,19))).TotalMinutes
if($lastLogTime -le 15) {
$backupStillRunning = $true
}
}
if($todaysBackupFound -eq $false) {
if(((Get-Date) - (Get-Date $lastLine.SubString(0,19))).TotalHours -ge $deafService -and $enableRestarts -eq $true) {
$tsmInfo = @(Get-TSMInfo $computerFqdn @())
$schedulerFound = $false
foreach($tsmInstance in $tsmInfo) {
if($tsmInstance.scheduleLog -match $tsmFile) {
if($tsmInstance.status -eq "Running") {
Restart-Service -name $tsmInstance.scheduleName
$exitMessage = ("TSM Scheduler `"" + $tsmInstance.scheduleName + "`" has not contacted the TSM server in $deafService hours. Restarting service.")
} else {
Start-Service -name $tsmInstance.scheduleName
$exitMessage = ("TSM Scheduler `"" + $tsmInstance.scheduleName + "`" was stopped and hasn't contacted the TSM server in $deafService hours. Starting service.")
}
$schedulerFound = $true
$exitValue = $returnError
break
}
}
if($schedulerFound -eq $false) {
$timeSinceLastContact = ((Get-Date) - (Get-Date $lastLine.SubString(0,19))).TotalHours
$exitMessage = ("Unable to find data in the last $backupWindow hours in $tsmLogFile and the client hasn't contacted the TSM Server in $timeSinceLastContact hours. Last Backup log date: " + (Get-Date $lastLine.SubString(0,19)))
$exitValue = $returnError
}
} else {
$exitMessage = ("Unable to find data in the last $backupWindow hours in $tsmLogFile. Last Backup log date: " + (Get-Date $lastLine.SubString(0,19)))
$exitValue = $returnError
}
} elseif($totalFailed -ge $maximumFailures) {
$exitMessage = "Backup completed with $totalFailed failed objects."
$exitValue = $returnWarning
} elseif($successfulBackup -eq $true) {
$exitMessage = "Backup completed successfully: $completionTime"
$exitValue = $returnNormal
} elseif($backupStillRunning -eq $true) {
$exitMessage = ("Backup still running! Please allow to complete. Current status: " + $lastLine -Replace "\\","/")
$exitValue = $returnWarning
} else {
$exitMessage = ("Unable to find a successful backup. Last status: " + $lastLine -Replace "\\","/")
$exitValue = $returnError
}
} else {
$exitMessage = "Unable to locate $tsmLogFile"
$exitValue = $returnError
}
Write-Host $exitMessage
$Host.SetShouldExit($exitValue)
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com1tag:blogger.com,1999:blog-4411406500801797400.post-36118902658770703492011-05-19T21:55:00.008-04:002012-02-28T14:42:23.283-05:00Exporting the Membership of a Large, Nested, Active Directory GroupOne of the most common requests I receive is to enumerate the membership of an Active Directory group and provide some basic information back about the members. Simple enough task if the group is flat and contains a limited number of members. Once you start dealing with nested groups and groups containing more than 1,500 members (1,000 members for an Active Directory 2000 Forest), you need to start planning how you output the information.<br />
<br />
With groups containing more than 1,500 members, performing the following will not provide the entire population. <br />
<pre class="brush:powershell;toolbar:false">$groupObject = New-Object System.DirectoryServices.DirectoryEntry("GC://CN=All Employees,OU=Distribution Lists,DC=ad,DC=mydomain,DC=local")
foreach($member in $groupObject.member) { Write-Output $member }</pre>You will only enumerate the first 1,500 distinguished names of the multivalued attribute 'member' and output them to the console. Big enough to convince you that you retrieved all the members on first glance but on further investigation you will realize you are missing some people. Hopefully, you caught that and not the person that requested the report with the resolved distinguished names. To overcome this limitation, you need to search within the group object itself and <a href="http://msdn.microsoft.com/en-us/library/aa367017(v=vs.85).aspx">return a range of values</a> within the 'member' attribute; looping until you have enumerated each and every distinguished name.<br />
<br />
Active Directory groups can contain user objects, computer objects, contact objects and other groups. To obtain the full membership of a Security Group to understand who has rights to an asset or a Distribution List to understand who will receive an e-mail, you must be aware of nested groups. If a group is a member is a member of the initial group, you will need to enumerate that group and any other groups that it may contain and so on. One of the issues that you can encounter with nested groups is the Administrator that nested a group that is member of a group that itself is patriline to that group. This leads to looping in reporting and must be accounted when recursively enumerating the membership. I tackle this problem by storing the nested group distinguished names that were enumerated in an array and at each recursion evaluating if that distinguished name is an element.<br />
<br />
The code sample below deals with these two issues related to group enumeration. I have tested this against code against a group that contained 32,000+ member objects with multiple layers of nesting genreating a 2.9 megabyte report. The only big deficit in the code is there is no provision to deal with objects that are of the <a href="http://activelydirect.blogspot.com/2011/01/dealing-with-foreignsecurityprincipal.html">foreignSecurityPrincipal</a> class. If you have cross-forest membership in groups, you can add that to the if-then statement and add your own formatting function for those objects. A nice feature of this script is that if you need to enumerate a large number of groups that have similar sAMAccountNames you can wild card them in the command line argument, such as "-group *AdSales*" or "-group DL_*". The code in the script is fairly portable. You can recombine these functions to enumerate the groups associated to <a href="http://activelydirect.blogspot.com/2011/02/file-system-rights-part-2.html">NTFS security</a> to provide an access report. You just need to update the functions that deal with the formatting of the objects to provide the desired information. <br />
<pre class="brush:powershell;toolbar:false">param([string]$domain,[string]$group,[switch]$verbose)
#-------------------------------------------------------------------------------------------------#
Function Find-GroupDistinguishedNamesBysAMAccountName($domain,$sAMAccountName) {
$groupDistinguishedNames = @()
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.SearchRoot = (New-Object System.DirectoryServices.DirectoryEntry(("LDAP://" + (Get-LocalDomainController $domain) + "/" + (Get-DomainDn $domain))))
$directorySearcher.Filter = "(&(objectClass=group)(objectCategory=group)(sAMAccountName=$sAMAccountName))"
$directorySearcher.PropertiesToLoad.Clear()
$directorySearcher.PropertiesToLoad.Add("distinguishedName") | Out-Null
$searchResults = $directorySearcher.FindAll()
if($searchResults -ne $null) {
foreach($searchResult in $searchResults) {
if($searchResult.Properties.Contains("distinguishedName")) {
$groupDistinguishedNames += $searchResult.Properties.Item("distinguishedName")[0]
}
}
} else {
$groupDistinguishedNames = $null
}
$directorySearcher.Dispose()
return $groupDistinguishedNames
}
#-------------------------------------------------------------------------------------------------#
Function Get-GroupType($groupType) {
if($groupType -eq -2147483646) {
$groupType = "Global Security Group"
} elseif($groupType -eq -2147483644) {
$groupType = "Domain Local Security Group"
} elseif($groupType -eq -2147483643) {
$groupType = "BuiltIn Group"
} elseif($groupType -eq -2147483640) {
$groupType = "Universal Security Group"
} elseif($groupType -eq 2) {
$groupType = "Global Distribution List"
} elseif($groupType -eq 4) {
$groupType = "Local Distribution List"
} elseif($groupType -eq 8) {
$groupType = "Universal Distribution List"
} else {
$groupType = "Unknown Group Type"
}
return $groupType
}
#-------------------------------------------------------------------------------------------------#
Function Get-GroupMembers($distinguishedName,$level,$groupsReported) {
$reportText = @()
$groupObject = New-Object System.DirectoryServices.DirectoryEntry("GC://" + ($distinguishedName -replace "/","\/"))
if($groupObject.groupType[0] -ne -2147483640 -or $groupObject.groupType[0] -ne 8) {
$groupObject = New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + (Get-LocalDomainController(Get-ObjectAdDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher($groupObject)
$lastQuery = $false
$quitLoop = $false
if($groupObject.member.Count -ge 1000) {
$rangeStep = 1000
} elseif($groupObject.member.Count -eq 0) {
$lastQuery = $true
$quitLoop = $true
} else {
$rangeStep = $groupObject.member.Count
}
$rangeLow = 0
$rangeHigh = $rangeLow + ($rangeStep - 1)
$level = $level + 2
while(!$quitLoop) {
if(!$lastQuery) {
$attributeWithRange = "member;range=$rangeLow-$rangeHigh"
} else {
$attributeWithRange = "member;range=$rangeLow-*" }
$directorySearcher.PropertiesToLoad.Clear()
$directorySearcher.PropertiesToLoad.Add($attributeWithRange) | Out-Null
$searchResult = $directorySearcher.FindOne()
$directorySearcher.Dispose()
if($searchResult.Properties.Contains($attributeWithRange)) {
foreach($member in $searchResult.Properties.Item($attributeWithRange)) {
$memberObject = Get-ActiveDirectoryObject $member
if($memberObject.objectClass -eq "group") {
$reportText += Format-Group $memberObject $level $groupsReported
} elseif ($memberObject.objectClass -eq "contact") {
$reportText += Format-Contact $memberObject $level
} elseif ($memberObject.objectClass -eq "computer") {
$reportText += Format-Computer $memberObject $level
} elseif ($memberObject.objectClass -eq "user") {
$reportText += Format-User $memberObject $level
} else {
Write-Warning "NOT SUPPORTED: $member"
}
}
if($lastQuery) { $quitLoop = $true }
} else {
$lastQuery = $true
}
if(!$lastQuery) {
$rangeLow = $rangeHigh + 1
$rangeHigh = $rangeLow + ($rangeStep - 1)
}
}
return $reportText
}
#-------------------------------------------------------------------------------------------------#
Function Format-User($userObject,$level) {
$reportText = @()
if($userObject.displayName) {
$identity = ((" " * $level) + $userObject.displayName.ToString() + " [" + (Get-ObjectNetBiosDomain $userObject.distinguishedName) + "\" + $userObject.sAMAccountName.ToString() + "]")
} else {
$identity = ((" " * $level) + $userObject.name.ToString() + " [" + (Get-ObjectNetBiosDomain $userObject.distinguishedName) + "\" + $userObject.sAMAccountName.ToString() + "]")
}
if($userObject.mail) {
$identity = ("$identity <" + $userObject.mail.ToString() + ">")
}
if($userObject.userAccountControl[0] -band 0x0002) {
$identity = "$identity (User Disabled)"
} else {
$identity = "$identity (User Enabled)"
}
$reportText += $identity
$description = ((" " * $level) + " Description: " + $userObject.description.ToString())
$reportText += $description
if($verbose) { Write-Host ($reportText | Out-String) }
return $reportText
}
Function Format-Contact($contactObject,$level) {
$reportText = @()
$identity = ((" " * $level) + $contactObject.displayName.ToString() + " [" + (Get-ObjectNetBiosDomain $contactObject.distinguishedName) + "] <" + $contactObject.mail.ToString() + "> (Contact)")
$description = ((" " * $level) + " Description: " + $contactObject.description.ToString())
$reportText += $identity
$reportText += $description
if($verbose) { Write-Host ($reportText | Out-String) }
return $reportText
}
Function Format-Computer($computerObject,$level) {
$reportText = @()
$identity = ((" " * $level) + $computerObject.name + " [" + (Get-ObjectNetBiosDomain $computerObject.distinguishedName) + "\" + $computerObject.sAMAccountName.ToString() + "] (Computer)")
$operatingSystem = ((" " * $level) + " OS: " + $computerObject.operatingSystem.ToString())
$reportText += $identity
if($computerObject.dNSHostName) {
$fqdn = ((" " * $level) + " FQDN: " + ($computerObject.dNSHostName.ToString()).ToLower())
$reportText += $fqdn
$reportText += $operatingSystem
} else {
$reportText += $operatingSystem
}
if($verbose) { Write-Host ($reportText | Out-String) }
return $reportText
}
Function Format-Group($groupObject,$level,$groupsReported) {
$reportText = @()
$identity = ((" " * $level) + $groupObject.name.ToString() + " [" + (Get-ObjectNetBIOSDomain $groupObject.distinguishedName) + "\" + $groupObject.sAMAccountName.ToString() + "]")
if($groupObject.mail) {
$identity = ("$identity <" + $groupObject.mail.ToString() + ">")
}
$identity = ("$identity (" + (Get-GroupType $groupObject.groupType) + ")")
$reportText += $identity
if($verbose) { Write-Host ($reportText | Out-String) }
if($groupsReported -notcontains $groupObject.distinguishedName) {
$groupsReported += $groupObject.distinguishedName
$reportText += Get-GroupMembers $groupObject.distinguishedName.ToString() $level $groupsReported
} else {
$reportText += ((" " * $level) + " [previously reported nested group]")
}
$reportText += ""
return $reportText
}
#-------------------------------------------------------------------------------------------------#
Function Get-AllForestDomains {
$domains = @()
$forestInfo = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
foreach($domain in $forestInfo.Domains) {
$domains += $domain.name
}
return $domains
}
Function Get-DomainDn($domain) {
return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domain/RootDSE")).defaultNamingContext).ToString()
}
Function Get-ObjectAdDomain($distinguishedName) {
return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
Function Get-ObjectNetBiosDomain($distinguishedName) {
return ((Get-ObjectAdDomain $distinguishedName).Split(".")[0]).ToUpper()
}
Function Get-LocalDomainController($domain) {
return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domain/RootDSE")).dnsHostName).ToString()
}
Function Get-ActiveDirectoryObject($distinguishedName) {
return New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + (Get-LocalDomainController (Get-ObjectAdDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#-------------------------------------------------------------------------------------------------#
Set-Variable -name reportsDirectory -option Constant -value "$pwd\Reports"
Set-Variable -name forestDomains -option Constant -value @(Get-AllForestDomains)
#-------------------------------------------------------------------------------------------------#
if(!([bool]$domain) -or !([bool]$group)) {
Write-Host (" Example: .\Export-GroupMembership.ps1 -domain " + $forestDomains[0] + " -group Administrators -verbose") -foregroundcolor Yellow
Write-Host ""
}
if(!([bool]$domain)) {
Write-Host " You are missing the `"-domain`" Switch" -Foregroundcolor Red
Write-Host ""
Write-Host " The domain of the group."
Write-Host " Valid Domains:"
foreach($forestDomain in $forestDomains) {
Write-Host (" $forestDomain [" + ($forestDomain.Split("."))[0] + "]")
}
Write-Host ""
Write-Host " Please enter the Domain below"
while(!([bool]$domain)) {
$domain = Read-Host -prompt "`tDomain"
}
Write-Host ""
}
if(!([bool]$group)) {
Write-Host " You are missing the `"-group`" Switch" -Foregroundcolor Red
Write-Host ""
Write-Host " Please enter the group name below"
while(!([bool]$group)) {
$group = Read-Host -prompt "`tGroup"
}
Write-Host ""
}
$validDomain = $false
foreach($forestDomain in $forestDomains) {
if($forestDomain -eq $domain) {
$validDomain = $true
break
}
if((($forestDomain.Split("."))[0]) -eq $domain) {
$validDomain = $true
break
}
}
if($validDomain -eq $false) {
Write-Host ""
Write-Host "$domain is not a valid domain in your current forest." -foregroundcolor Red
Write-Host ""
exit
}
if(!(Test-Path -path $reportsDirectory)) {
New-Item -path $reportsDirectory -type Directory | Out-Null
}
$groupDistinguishedNames = Find-GroupDistinguishedNamesBysAMAccountName $domain $group
if($groupDistinguishedNames -eq $null) {
Write-Host "Unable to locate $domain\$group. Exiting..." -foregroundColor Red
exit
}
foreach($groupDistinguishedName in $groupDistinguishedNames) {
$groupObject = Get-ActiveDirectoryObject $groupDistinguishedName
$groupName = $groupObject.name.ToString()
$groupsAMAccountName = $groupObject.sAMAccountName.ToString()
$groupDomain = Get-ObjectAdDomain $groupDistinguishedName
$groupNetBiosDomain = Get-ObjectNetBiosDomain $groupDistinguishedName
$groupType = Get-GroupType $groupObject.groupType
$outputFilename = ($groupDomain + "-" + $groupsAMAccountName + ".txt")
$reportText = @()
$reportText += ("#" + ("-" * 78) + "#")
$reportText += " Members of $groupName [$groupNetBiosDomain\$groupsAMAccountName]"
$reportText += (" Description: " + $groupObject.description.ToString())
if($groupObject.mail -and $groupType -match "Distribution") {
$reportText += " Distribution List E-Mail Address: " + $groupObject.mail.ToString()
} elseif($groupObject.mail) {
$reportText += " Security Group E-Mail Address: " + $groupObject.mail.ToString()
}
$reportText += " Group Type: $GroupType"
$reportText += (" Report generated on " + (Get-Date -format D) + " at " + (Get-Date -format T))
$reportText += ("#" + ("-" * 78) + "#")
if($verbose) { Write-Host ($reportText | Out-String) }
$reportText += Get-GroupMembers $groupDistinguishedName 0 @()
$reportText += ("#" + ("-" * 78) + "#")
if($verbose) { Write-Host ("#" + ("-" * 78) + "#") }
Set-Content -path "$reportsDirectory\$outputFilename" -value ($reportText | Out-String)
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com2tag:blogger.com,1999:blog-4411406500801797400.post-46640447584237392162011-05-18T17:32:00.000-04:002011-05-18T17:32:16.062-04:00Correct Order to Start and Stop BlackBerry Enterprise Server Services Using PowerShellA common activity I perform after patching Exchange Servers and Domain Controllers is to restart BlackBerry services on client BlackBerry servers. Failure to do so can lead to issues for BlackBerry users such as failed delivery, lookup failures and inability to send. RIM recommends a specific order for these services to be <a href="http://www.blackberry.com/btsc/search.do?cmd=displayKC&docType=kc&externalId=KB13718">stopped and started</a>. Since patching occurs typically occurs in the dead of night to avoid impacting end users, you want to get things done quickly but accurately so you can get some sleep. In the code samples below, I take advantage of <a href="http://technet.microsoft.com/en-us/library/dd347591.aspx">Get-Service</a>, <a href="http://technet.microsoft.com/en-us/library/dd347576.aspx">Stop-Service</a> and <a href="http://technet.microsoft.com/en-us/library/dd347637.aspx">Start-Service</a> to execute the correct order of the process. I store the required service order in an array and take advantage of the natural looping order of elements in "foreach" to manipulate the services. The remaining services are discovered by obtaining all services with the name BlackBerry and filtering out the services that required a specific order for stopping and starting. If there are any issues with stopping or stopping the services, the code immediately exits so you can take manual intervention, research and resolve the problem. <br />
<br />
Stopping BlackBerry Services: <br />
<pre class="brush:powershell;toolbar:false">#--------------------------------------------------------------------------------------------------#
Set-Variable -name selectedServices -option Constant -value @("BlackBerry Controller","BlackBerry Dispatcher","BlackBerry Router")
#--------------------------------------------------------------------------------------------------#
foreach($selectedService in $selectedServices) {
if((Get-Service -Name $selectedService).Status -ne "Stopped") {
Write-Output "Stopping $selectedService..."
Stop-Service -Name $selectedService
if((Get-Service -Name $selectedService).Status -ne "Stopped") {
Write-Output "$selectedService failed to stop. Exiting..."
exit
} else {
Write-Output "$selectedService successfully stopped."
}
} else {
Write-Output "$selectedService already stopped!"
}
}
foreach($blackberryService in (Get-Service -Name Blackberry*)) {
if($selectedServices -notcontains $blackberryService.Name) {
if($blackberryService.Status -ne "Stopped") {
Write-Output ("Stopping " + $blackberryService.Name + "...")
Stop-Service -Name $blackberryService.Name
if((Get-Service -Name $blackberryService.Name).Status -ne "Stopped") {
Write-Output ($blackberryService.Name + " failed to stop. Exiting...")
exit
} else {
Write-Output ($blackberryService.Name + " successfully stopped.")
}
} else {
Write-Output ($blackberryService.Name + " already stopped!")
}
}
}</pre>Starting BlackBerry Services: <br />
<pre class="brush:powershell;toolbar:false">#--------------------------------------------------------------------------------------------------#
Set-Variable -name selectedServices -option Constant -value @("BlackBerry Router","BlackBerry Dispatcher","BlackBerry Controller")
#--------------------------------------------------------------------------------------------------#
foreach($selectedService in $selectedServices) {
if((Get-Service -Name $selectedService).Status -ne "Running") {
Write-Output "Starting $selectedService..."
Start-Service -Name $selectedService
if((Get-Service -Name $selectedService).Status -ne "Running") {
Write-Output "$selectedService failed to start. Exiting..."
exit
} else {
Write-Output "$selectedService successfully started."
}
} else {
Write-Output "$selectedService already running!"
}
}
foreach($blackberryService in (Get-Service -Name Blackberry*)) {
if($selectedServices -notcontains $blackberryService.Name) {
if($blackberryService.Status -ne "Running") {
Write-Output ("Starting " + $blackberryService.Name + "...")
Start-Service -Name $blackberryService.Name
if((Get-Service -Name $blackberryService.Name).Status -ne "Running") {
Write-Output ($blackberryService.Name + " failed to start. Exiting...")
exit
} else {
Write-Output ($blackberryService.Name + " successfully started.")
}
} else {
Write-Output ($blackberryService.Name + " already running!")
}
}
}</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-19862279502985269072011-05-10T23:30:00.003-04:002011-05-18T17:33:04.686-04:00Build a Report of Unused BlackBerry DevicesA crucial element of BlackBerry management is reducing the cost of providing BlackBerry service. An unused BlackBerry is a wasted license, an unnecessary cell phone bill with a data plan and possibly a security issue. When thousands of handhelds are under management, dozens can be idle. In the current economic climate, every penny counts in your IT budget. Finding these idle devices can add up to thousands of dollars in savings annually.<br />
<br />
In the code sample below, I build upon a previous blog post, "<a href="http://activelydirect.blogspot.com/2011/02/blackberry-user-report.html">BlackBerry User Report</a>". The command text is updated to select information from the UserStats table where the connection data for the BlackBerry devices is stored. If no value is held in the row for connectivity, a <a href="http://msdn.microsoft.com/en-us/library/system.dbnull.aspx">DBNull</a> value will be returned. This is not the same as PowerShell's built-in "$null" and you will need to test against the DBNull class to determine this. The end result of this code example is a comma separated values text file with the employees neglecting their BlackBerry. Reach out to them and see if they no longer need their handheld or if they have moved on to an ActiveSync device as their primary mobile messaging device.<br />
<br />
In a change from previous blog posts, I am using <a href="http://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.aspx">System.DirectoryServices.DirectorySearcher</a> class to perform the user object lookup instead of <a href="http://msdn.microsoft.com/en-us/library/ms675532(v=vs.85).aspx">ActiveX Data Object (ADO)</a>. I have a tendency to use ADO as the methodology is the same in PowerShell, Perl and VBScript making the translation of ADSI queries between languages simple. <br />
<pre class="brush:powershell;toolbar:false">Function Get-ForestRootDn {
return ((New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")).rootDomainNamingContext).ToString()
}
Function Get-ObjectAdDomain($distinguishedName) {
return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
Function Find-ActiveDirectoryObjectByEMailAddress($smtpAddress) {
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.SearchRoot = (New-Object System.DirectoryServices.DirectoryEntry("GC://$forestRootDn"))
$directorySearcher.Filter = "(&(objectClass=user)(objectCategory=person)(proxyAddresses=smtp:$smtpAddress))"
$searchResult = $directorySearcher.FindOne()
if($searchResult -ne $null) {
$userObject = New-Object System.DirectoryServices.DirectoryEntry($searchResult.Path)
} else {
$userObject = $null
}
$directorySearcher.Dispose()
return $userObject
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -scope Script -value (Get-ForestRootDn)
Set-Variable -name dbNull -option Constant -value ([System.DBNull]::Value) # http://msdn.microsoft.com/en-us/library/system.dbnull.aspx
Set-Variable -name databases -option Constant -value @("mssql2008-east.ad.mydomain.local","mssql2008-west.ad.mydomain.local","mssql2008-europe.ad.mydomain.local","mssql2008-japan.ad.mydomain.local")
Set-Variable -name idleDays -option Constant -value 14
#--------------------------------------------------------------------------------------------------#
$blackberryUsers = @()
foreach($sqlServer in $databases) {
$sqlConnection = New-Object System.Data.SQLClient.SQLConnection
$sqlConnection.ConnectionString = "server=$sqlServer;database=BESMgmt;trusted_connection=true;"
$sqlConnection.Open()
$sqlCommand = New-Object System.Data.SQLClient.SQLCommand
$sqlCommand.Connection = $sqlConnection
$sqlCommand.CommandText = "SELECT u.DisplayName as DisplayName, u.MailboxSMTPAddr as MailboxSMTPAddr, u.PIN as PIN, d.phonenumber as phonenumber, s.MachineName as MachineName, t.LastFwdTime as LastFwdTime, t.LastSentTime as LastSentTime, t.MsgsPending as MsgsPending FROM UserConfig u, SyncDeviceMgmtSummary d, ServerConfig s, UserStats t WHERE u.id = d.userconfigid and u.ServerConfigId = s.id and u.id = t.userconfigid"
$sqlDataReader = $sqlCommand.ExecuteReader()
if($sqlDataReader.HasRows -eq $true) {
while($sqlDataReader.Read()) {
$displayName = $sqlDataReader.Item("DisplayName")
$proxyAddress = $sqlDataReader.Item("MailboxSMTPAddr")
$pin = $sqlDataReader.Item("PIN")
$phoneNumber = $sqlDataReader.Item("phonenumber")
$blackberryServer = $sqlDataReader.Item("MachineName")
$lastFwdTime = ($sqlDataReader.Item("LastFwdTime"))
$lastSentTime = $sqlDataReader.Item("LastSentTime")
$msgsPending = $sqlDataReader.Item("MsgsPending")
if($lastSentTime -eq $dbNull) { $lastSentTime = "No Contact" } else { $lastSentTime = (Get-Date $lastSentTime) }
if($lastFwdTime -eq $dbNull) { $lastFwdTime = "No Contact" } else { $lastFwdTime = (Get-Date $lastFwdTime) }
if(($lastSentTime -eq "No Contact" -or $lastFwdTime -eq "No Contact") -or ((Get-Date $lastSentTime) -le (Get-Date).AddDays(-$idleDays) -or (Get-Date $lastFwdTime) -le (Get-Date).AddDays(-$idleDays))) {
$userObject = Find-ActiveDirectoryObjectByEMailAddress $proxyAddress
if($userObject -eq $null) {
Write-Host "USER NOT FOUND: $proxyAddress" -foregroundColor Red
} else {
$blackberryUser = New-Object -typeName PSObject
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Domain" -value ((Get-ObjectAdDomain $userObject.distinguishedName.ToString()).Split(".")[0]).ToUpper()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "User ID" -value $userObject.sAMAccountName.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "First Name" -value $userObject.givenName.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Last Name" -value $userObject.sn.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Display Name" -value $userObject.displayName.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "E-Mail Address" -value $userObject.mail.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Last Message Forwarded" -value $lastFwdTime
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Last Message Sent" -value $lastSentTime
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Messages Pending" -value $msgsPending
Add-Member -inputObject $blackberryUser -type NoteProperty -name "PIN" -value $pin
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Cell Phone Number" -value $phoneNumber
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Desk Phone Number" -value $userObject.telephoneNumber.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Street Address" -value ($userObject.streetAddress.ToString() -replace "`r`n",", ")
Add-Member -inputObject $blackberryUser -type NoteProperty -name "City" -value $userObject.l.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "State" -value $userObject.st.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Zip Code" -value $userObject.postalCode.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Country" -value $userObject.c.ToString()
Add-Member -inputObject $blackberryUser -type NoteProperty -name "BlackBerry Server" -value $blackberryServer
$blackberryUsers += $blackberryUser
}
}
}
}
$sqlConnection.Close()
}
$blackberryUsers | Export-Csv -path "Stale Blackberry User Report.csv" -noTypeInformation
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-53285014391339848492011-04-18T23:00:00.009-04:002011-06-18T11:50:27.448-04:00Replicating UNIX "tail -f" in PowerShellBuilding upon my previous blog post, <a href="http://activelydirect.blogspot.com/2011/03/unix-tail-like-functionality-in_14.html">Unix Tail-like Functionality in PowerShell Revisited</a>, I have completed the next step in providing tail functionality in my PowerShell scripts; the ability to emulate the "-f" argument.<br />
<br />
From the <a href="http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man1/tail.1.html">TAIL(1)</a> man page: <br />
<blockquote>The -f option causes tail to not stop when end of file is reached, but rather to wait for additional data to be appended to the input.</blockquote>My approach to replicating this functionality in PowerShell is to once again take advantage of <a href="http://msdn.microsoft.com/en-us/library/system.io.filestream.aspx">System.IO.FileStream</a> Class and read from the end of file as I did in my previous tail emulations. Originally, I thought this was going to be a more difficult task to accomplish but since I learned so much from my prior attempts, it turned out to be fairly simple and a lot less code to implement. My solution focuses on the fact that the file size grows as more data is appended to the text file. If I know how large the file was in a prior reading than it is currently, I know how many bytes have been added to the file and from there, I know the number of bytes I need to read from the end of the file and return to the console. All I need is a looping routine to constantly check the file for changes. In my code sample below, I emulate "tail -f" to the console by first <a href="http://activelydirect.blogspot.com/2011/03/unix-tail-like-functionality-in_14.html">reading the last 10 lines</a> of a hypothetical BlackBerry Enterprise Server Management Agent log file (on an active server this file grows constantly) on a remote server as Unix "tail -f" would then start to monitor the log file for changes by comparing the file size waiting 100 milliseconds between each comparison. This process will continue until you send a <a href="http://en.wikipedia.org/wiki/Control-C">break</a> or the file is deleted.<br />
<br />
With this bit of starter code, you should be able to implement some unique tools to monitor and react to data in log files in real-time. <br />
<br />
<b><span class="Apple-style-span" style="color: red;">UPDATE: </span><span class="Apple-style-span" style="font-weight: normal;">I have made some improvements to this code <a href="http://activelydirect.blogspot.com/2011/06/powershell-tail-improvements.html">here</a>.</span></b><br />
<pre class="brush:powershell;toolbar:false">Function Read-EndOfFileByByteChunk($fileName,$totalNumberOfLines,$byteChunk) {
if($totalNumberOfLines -lt 1) { $totalNumberOfLines = 1 }
if($byteChunk -le 0) { $byteChunk = 10240 }
$linesOfText = New-Object System.Collections.ArrayList
if([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$asciiEncoding = New-Object System.Text.ASCIIEncoding
$fileSize = $fileStream.Length
$byteOffset = $byteChunk
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$totalBytesProcessed = 0
$lastReadAttempt = $false
do {
if($byteOffset -ge $fileSize) {
$byteChunk = $fileSize - $totalBytesProcessed
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$byteOffset = $fileSize
$lastReadAttempt = $true
}
$fileStream.Seek((-$byteOffset), [System.IO.SeekOrigin]::End) | Out-Null
$fileStream.Read($bytesRead, 0, $byteChunk) | Out-Null
$chunkOfText = New-Object System.Collections.ArrayList
$chunkOfText.AddRange(([System.Text.RegularExpressions.Regex]::Split($asciiEncoding.GetString($bytesRead),"\r\n")))
$firstLineLength = ($chunkOfText[0].Length)
$byteOffset = ($byteOffset + $byteChunk) - ($firstLineLength)
if($lastReadAttempt -eq $false -and $chunkOfText.count -lt $totalNumberOfLines) {
$chunkOfText.RemoveAt(0)
}
$totalBytesProcessed += ($byteChunk - $firstLineLength)
$linesOfText.InsertRange(0, $chunkOfText)
} while($totalNumberOfLines -ge $linesOfText.count -and $lastReadAttempt -eq $false -and $totalBytesProcessed -lt $fileSize)
$fileStream.Close()
if($linesOfText.count -gt 1) {
$linesOfText.RemoveAt($linesOfText.count-1)
}
$deltaLines = ($linesOfText.count - $totalNumberOfLines)
if($deltaLines -gt 0) {
$linesOfText.RemoveRange(0, $deltaLines)
}
} else {
$linesOfText.Add("[ERROR] $fileName not found") | Out-Null
}
return $linesOfText
}
#--------------------------------------------------------------------------------------------------#
Function Read-FileUpdates($fileName,$startSize) {
$asciiEncoding = New-Object System.Text.ASCIIEncoding
while([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$currentFileSize = $fileStream.Length
if($currentFileSize -gt $startSize) {
$byteChunk = $currentFileSize - $startSize
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$fileStream.Seek((-$byteChunk), [System.IO.SeekOrigin]::End) | Out-Null
$fileStream.Read($bytesRead, 0, $byteChunk) | Out-Null
Write-Host ($asciiEncoding.GetString($bytesRead)) -noNewLine
$startSize = $currentFileSize
}
$fileStream.Close()
Start-Sleep -milliseconds 100
}
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name inputFile -option Constant -value "\\japan-bes.ad.mydomain.local\E$\Program Files\Research In Motion\BlackBerry Enterprise Server\Logs\20110418\JAPAN-BES_MAGT_02_20110418_0001.txt"
#--------------------------------------------------------------------------------------------------#
if([System.IO.File]::Exists($inputFile)) {
Write-Host (@(Read-EndOfFileByByteChunk $inputFile 10 1280) | Out-String) -noNewLine
$fileStream = New-Object System.IO.FileStream($inputFile,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$fileSize = $fileStream.Length
$fileStream.Close()
Read-FileUpdates $inputFile $fileSize
} else {
Write-Host "Could not find $inputFile..." -foregroundColor Red
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-36995113162006872242011-04-11T21:45:00.001-04:002011-06-18T11:56:03.415-04:00Audit Group Policy ObjectsI needed to review a group of Group Policy Objects and determine if an Active Directory group is listed in the permissions for the GPO. If it was not, I needed to be aware of it. In the code sample, I take advantage of the Group Policy module to loop through all the GPOs in a domain that have "Workstation" in the display name, identify the ones that do not include the "ASIA\DesktopTeam" in the permission list and output them to a comma separated values text file. <pre class="brush:powershell;toolbar:false">#--------------------------------------------------------------------------------------------------#
Set-Variable -name accountDomain -option Constant -value "ASIA"
Set-Variable -name accountsAMAccountName -option Constant -value "DesktopTeam"
Set-Variable -name workingDomain -option Constant -value "asia.ad.mycompany.local"
Set-Variable -name matchFilter -option Constant -value "Workstation"
Set-Variable -name outputFile -option Constant -value ($workingDomain + "-" + $matchFilter + ".csv")
#--------------------------------------------------------------------------------------------------#
if((Get-Host).Version.Major -ne 2) {
Write-Host "You must have PowerShell V2 installed to use this script. Exiting!" -foregroundColor Red
exit
}
if(!(Get-Module -ListAvailable | Where-Object { $_.Name -eq "GroupPolicy" }).Name) {
Write-host "Unable to locate the GroupPolicy module. Exiting!" -foregroundColor Red
exit
}
Import-Module -Name GroupPolicy
$badGpos = @()
$gpoList = Get-GPO -All -Domain $workingDomain | Where-Object { $_.DisplayName -match $matchFilter }
foreach($gpo in $gpoList) {
$gpoPermissions = Get-GPPermissions -guid $gpo.Id -Domain $workingDomain -All
$found = $false
foreach($gpoPermission in $gpoPermissions) {
if($gpoPermission.Trustee.Domain -eq $domain -and $gpoPermission.Trustee.Name -eq $sAMAccountName) {
$found = $true
}
}
if($found -eq $false) {
$badGpos += $gpo
}
}
$badGpos | Export-Csv -path $outputFile -noTypeInformation
Remove-Module -Name GroupPolicy</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-85945782369940620282011-03-30T21:15:00.003-04:002011-11-04T16:45:18.359-04:00Creating Dynamic User Objects in Active DirectoryAccount management in Active Directory comes at a cost of time and effort for an Administrator. This is especially burdensome for objects that are only needed for a limited timeframe such as testing objects. Active Directory provides a low cost method for dealing with temporary objects through the <a href="http://msdn.microsoft.com/en-us/library/ms676291(v=vs.85).aspx">dynamic object class</a> which provides automatic garbage collection based on a <a href="http://msdn.microsoft.com/en-us/library/ms675669(v=vs.85).aspx">time-to-live</a> value in seconds. This benefit does come with several limitations:<br />
<ul><li>The maximum TTL of a dynamic object is 31,556,926 seconds (1 year).</li>
<li>A deleted dynamic object due to its TTL expiring does not leave a <a href="http://technet.microsoft.com/en-us/magazine/2007.09.tombstones.aspx">tombstone</a> behind.</li>
<li>All DCs holding replicas of dynamic objects must run on Windows Server 2003 or greater.</li>
<li>Dynamic entries with TTL values are supported in all partitions except the Configuration partition and Schema partition.</li>
<li>Active Directory Domain Services do not publish the optional dynamicSubtrees attribute, as described in the <a href="http://tools.ietf.org/html/rfc2589">RFC 2589</a>, in the <a href="http://msdn.microsoft.com/en-us/library/ms684291(v=vs.85).aspx">root DSE</a> object.</li>
<li>Dynamic entries are handled similar to non-dynamic entries when processing search, compare, add, delete, modify, and modifyDN operations.</li>
<li>There is no way to change a static entry into a dynamic entry and vice-versa.</li>
<li>A non-dynamic entry cannot be added subordinate to a dynamic entry.</li>
</ul>In the code sample below, I take the advantage of the dynamic object class to create 50 test accounts that will expire and delete themselves from the forest on May 1, 2011 at 2:30 pm. The accounts created for a hypothetical finance application testing are placed in a specific OU, provided a unique sAMAccountName (tested to ensure this) and a pseudo-random password created for them. All this information is placed in a comma separated values text file so you can pass it over to your Quality Assurance team.<br />
<br />
<span class="Apple-style-span" style="color: red;">And remember, this script creates objects in Active Directory! Use at your own risk! I might have the best of intentions but my skill may betray you. Test, test and further test before implementing this code in a production environment. </span><br />
<pre class="brush:powershell;toolbar:false">Function Get-LocalDomainController($objectDomain) {
return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
Function Get-ObjectADDomain($distinguishedName) {
return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
Function Get-ActiveDirectoryObject($distinguishedName) {
return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
Function Get-DomainDistinguishedName($domain) {
return ("dc=" + $domain.Replace(".",",dc="))
}
Function Test-sAMAccountName($sAMAccountName,$domain) {
$objectConnection = New-Object -comObject "ADODB.Connection"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand = New-Object -comObject "ADODB.Command"
$objectCommand.ActiveConnection = $objectConnection
$ldapBase = ("LDAP://" + (Get-LocalDomainController $domain) + "/" + (Get-DomainDistinguishedName $domain))
$ldapAttr = "sAMAccountName"
$ldapScope = "subtree"
$ldapFilter = "(&(objectClass=user)(sAMAccountName=$sAMAccountName))"
$ldapQuery= "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
$objectCommand.CommandText = $ldapQuery
$objectRecordSet = $objectCommand.Execute()
if($objectRecordSet.RecordCount -gt 0) {
$found = $true
} else {
$found = $false
}
return $found
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name destinationOu -option Constant -value "ou=Finance Testing,dc=americas,dc=ad,dc=mycompany,dc=local"
Set-Variable -name totalNumberOfAccounts -option Constant -value 50
Set-Variable -name givenName -option Constant -value "Test"
Set-Variable -name sn -option Constant -value "Account"
Set-Variable -name passwordSize -option Constant -value 12
#--------------------------------------------------------------------------------------------------#
$randomNumber = New-Object System.Random
$accounts = @()
$endDate = "05/01/2011 14:30:00"
$entryTtlSeconds = [int]((Get-Date $endDate) - (Get-Date)).TotalSeconds
#######################################################################
# Or you can do a time span instead for the account time to live i.e.,
# $entryTtlSeconds = [int](New-TimeSpan -days 90).TotalSeconds
# $entryTtlSeconds = [int](New-TimeSpan -minutes 60).TotalSeconds
#######################################################################
if($entryTtlSeconds -gt 31556926) {
Write-Host "Time-to-live greater than 1 year. Exiting!" -foregroundColor Red
exit
}
$destinationOuObject = Get-ActiveDirectoryObject $destinationOu
if($destinationOuObject.distinguishedName -ne $destinationOu) {
Write-Host "Unable to connect to $destinationOu. Exiting!" -foregroundColor Red
exit
}
for($i = 1;$i -le $totalNumberOfAccounts;$i++) {
$accountName = ($givenName + $sn + "$i")
Write-Host "Creating $accountName..."
if(Test-sAMAccountName $accountName (Get-ObjectADDomain $destinationOu)) {
Write-Host "$accountName already exists, skipping..." -foregroundColor Yellow
continue
}
$userObject = $destinationOuObject.Create("user","CN=$accountName")
$userObject.PutEx(2,"objectClass",@("dynamicObject","user"))
$userObject.Put("entryTTL",$entryTtlSeconds)
$userObject.Put("sAMAccountName", $accountName) # Mandatory attribute
$userObject.Put("givenName",$givenName)
$userObject.Put("sn",($sn + "$i"))
$userObject.Put("displayName",($givenName + " " + $sn + "$i"))
$userObject.Put("description","Account Used for Application Testing")
$userObject.Put("wWWHomePage","http://sharepointsite/projects/newfinancesystem")
$userObject.Put("userPrincipalName",($accountName + "@" + (Get-ObjectADDomain $destinationOu)))
$userObject.SetInfo()
$password = ""
for($x = 1;$x -le $passwordSize;$x++) {
$password += [char]($randomNumber.Next(33,126))
}
$userObject.SetPassword($password)
$userObject.Put("userAccountControl", 512)
$userObject.SetInfo()
$account = New-Object -typeName PSObject
Add-Member -inputObject $account -type NoteProperty -name "domain" -value (Get-ObjectADDomain $destinationOu).Split(".")[0]
Add-Member -inputObject $account -type NoteProperty -name "sAMAccountName" -value ($userObject.sAMAccountName).ToString()
Add-Member -inputObject $account -type NoteProperty -name "givenName" -value ($userObject.givenName).ToString()
Add-Member -inputObject $account -type NoteProperty -name "sn" -value ($userObject.sn).ToString()
Add-Member -inputObject $account -type NoteProperty -name "userPrincipalName" -value ($userObject.userPrincipalName).ToString()
Add-Member -inputObject $account -type NoteProperty -name "password" -value $password
$accounts += $account
}
$accounts | Export-Csv -path "$givenName $sn List.csv" -noTypeInformation
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-14803965908889567522011-03-29T22:05:00.000-04:002011-03-29T22:08:39.392-04:00Retrieve the Fully Qualified Domain Name of a Local Active Directory Bound ComputerHere is the method I use to obtain the fully qualified domain name of a local Active Directory bound computer in my scripts.
<pre class="brush:powershell;toolbar:false">
$computerFqdn = (([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()).HostName + "." + ([System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()).DomainName)
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-51855735241889085042011-03-22T20:20:00.000-04:002011-03-22T20:20:00.583-04:00Scheduling Tasks in PowerShell for Windows Server 2008 -- Part 3: Daily TasksIn my previous blog posts, "<a href="http://activelydirect.blogspot.com/2011/02/scheduling-tasks-in-powershell-for.html">Scheduling Tasks in PowerShell for Windows Server 2008 -- Part 1: Monthly Task</a>" and "<a href="http://activelydirect.blogspot.com/2011/02/scheduling-tasks-in-powershell-for_22.html">Scheduling Tasks in PowerShell for Windows Server 2008 -- Part 2: Weekly Tasks</a>", I explored the various settings required for scheduled tasks for those given date ranges. This post deals with the daily reoccurring tasks that administrators need to schedule, sometimes more than once a day. In the code sample below, I schedule a fictitious log cleanup PowerShell script to run three times a day; midnight, 8 am and 4 pm. For security reasons, after running this script, you should close the PowerShell command window as the password submitted for the Task Security Principal will remain in the command buffer allowing anyone with access to your keyboard to "up arrow" and reveal it.
<pre class="brush:powershell;toolbar:false"># Parameters to modify
$taskName = "Start-TriDailyLogCleanup"
$taskWorkingDirectory = "C:\PowerShell"
$taskPath = "%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe"
$taskArguments = "-command `"$taskWorkingDirectory\$taskName.ps1`""
$taskAuthor = "ad\myaccount"
$taskDescription = "The Tri-Daily Log Cleanup on the servers."
$taskSecurityPrincipal = "ad\maintenance"
$taskShedulerTaskFolder = "\MyTasks"
$startTimes = @((Get-Date "03/23/2011 00:00:00" -Format s),(Get-Date "03/23/2011 08:00:00" -Format s),(Get-Date "03/23/2011 16:00:00" -Format s))
# Would like to use -asSecureString but RegisterTaskDefinition does not accept it
# Look over your shoulder before typing
$password = Read-Host -prompt "$taskSecurityPrincipal Password"
# The meaty parts
$taskService = New-Object -ComObject Schedule.Service
$taskService.Connect()
$rootFolder = $taskService.GetFolder($taskShedulerTaskFolder)
$taskDefinition = $taskService.NewTask(0)
$registrationInformation = $taskDefinition.RegistrationInfo
$registrationInformation.Description = $taskDescription
$registrationInformation.Author = $taskAuthor
$taskPrincipal = $taskDefinition.Principal
$taskPrincipal.LogonType = 1
$taskPrincipal.UserID = $taskSecurityPrincipal
$taskPrincipal.RunLevel = 0
$taskSettings = $taskDefinition.Settings
$taskSettings.StartWhenAvailable = $true
$taskSettings.RunOnlyIfNetworkAvailable = $true
$taskSettings.Priority = 7
$taskSettings.ExecutionTimeLimit = "PT2H"
$taskTriggers = $taskDefinition.Triggers
foreach($startTime in $startTimes) {
$executionTrigger = $taskTriggers.Create(2)
$executionTrigger.StartBoundary = $startTime
}
$taskAction = $taskDefinition.Actions.Create(0)
$taskAction.Path = $taskPath
$taskAction.Arguments = $taskArguments
$taskAction.WorkingDirectory = $taskWorkingDirectory
# 6 == Task Create or Update
# 1 == Password must be supplied at registration
$rootFolder.RegisterTaskDefinition($taskName, $taskDefinition, 6, $taskSecurityPrincipal, $password, 1)
# Since we captured this in plain text I am going to nuke the value
# Not 100% security. Close the PowerShell command window to increase security.
Clear-Variable -name password
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-71723184811488138382011-03-21T21:30:00.007-04:002011-03-21T21:32:55.021-04:00MD5 Hash For PowerShellFor a project, I had a requirement to move selected files between two servers and provide proof that the copied matched exactly the original file. A common method to provide this proof is through a <a href="http://en.wikipedia.org/wiki/MD5">MD5 hash</a> for each of the files to create a fingerprint. A MD5 hash provides the ability to compare two files without having to go to the trouble and slowness of comparing the files byte by byte using the algorithm created by Ron Rivest in 1991 to create a 128-bit <a href="http://en.wikipedia.org/wiki/Hash_function">hash</a>. You can save the hash and use it for future reference to see if the file has changed.<br />
<br />
In the code sample below, I use the <a href="http://msdn.microsoft.com/en-us/library/system.security.cryptography.md5cryptoserviceprovider.aspx">System.Security.Cryptography.MD5CryptoServiceProvider</a> class to create the hash in bytes then convert it to a string using the <a href="http://msdn.microsoft.com/en-us/library/system.bitconverter.aspx">System.Bitconverter</a> class and removing all dashes to create a 32 character representation of that hash. If you require even more bits to be present in your hash, the <a href="http://msdn.microsoft.com/en-us/library/system.security.cryptography.aspx">System.Security.Cryptography</a> namespace provides up to 512 bits. To achieve this level of security, use <a href="http://msdn.microsoft.com/en-us/library/system.security.cryptography.sha512managed.aspx">System.Security.Cryptography.SHA512Managed</a> class instead of System.Security.Cryptography.MD5CryptoServiceProvider in the function.<br />
<br />
To test out this code, change out the file names below. Copy a file to a new location, run the function and see the results. Then open the copied file, make a minor edit and test again. It's interesting to see the changes in the returned hash.<br />
<pre class="brush:powershell;toolbar:false">Function Get-MD5Hash($fileName) {
if([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$MD5Hash = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
[byte[]]$fileByteChecksum = $MD5Hash.ComputeHash($fileStream)
$fileChecksum = ([System.Bitconverter]::ToString($fileByteChecksum)).Replace("-","")
$fileStream.Close()
} else {
$fileChecksum = "ERROR: $fileName Not Found"
}
return $fileChecksum
}
$fileName = "\\east-coast-fs.ad.mycompany.local\documents\Important Excel Spreadsheet.xlsx"
$fileChecksumOne = Get-MD5Hash $fileName
if($fileChecksumOne -match "^ERROR:") {
Write-Host $fileChecksumOne -foregroundColor Red
exit
}
$fileName = "\\west-coast-fs.ad.mycompany.local\documents\Important Excel Spreadsheet.xlsx"
$fileChecksumTwo = Get-MD5Hash $fileName
if($fileChecksumTwo -match "^ERROR:") {
Write-Host $fileChecksumTwo -foregroundColor Red
exit
}
if($fileChecksumOne -eq $fileChecksumTwo) {
Write-Host "Files match!"
Write-Host $fileChecksumOne -foregroundColor Green
Write-Host $fileChecksumTwo -foregroundColor Green
} else {
Write-Host "Files do not match!"
Write-Host $fileChecksumOne -foregroundColor Red
Write-Host $fileChecksumTwo -foregroundColor Green
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-9190173486172375782011-03-17T22:45:00.057-04:002011-12-13T18:18:48.541-05:00Add a New Mail Domain to Multiple UsersIn my work, it is not uncommon to have new domains purchased for business units and employees immediately needing e-mail addresses with the new domain. So after the MX record is created and Exchange is updated to accept mail for the new domain, its time to target the users that require the new mail domain to be included in the proxyAddresses attribute. The proxyAddresses attribute is multivalued so you must use treat it as an array and use PutEx to append to the attribute with the new address. In the code sample below, I take a file of e-mail addresses that users already use, split off the "<a href="http://tools.ietf.org/html/rfc2822">local-part</a>" from the domain using the at sign as the delimiter and append the $newMailDomain constant to the local-part to create the new e-mail address for the user. When inserting the new e-mail address into the proxyAddresses attribute with PutEx, you will need to send it as an array even if it only has 1 element. To be on the safe side, which you should also do when modifying production data, a backup of all the current elements of the user's proxyAddresses attribute are dumped into a subdirectory called "backup" in a file name that matches their mail attribute (with ".txt" appended) and the script is set to not write to the directory by default. You must alter the $writeEnabled constant in order to actually update the user's object. The script also checks to see if the e-mail address you are trying to assign to the employee's account is already present and skips if so. I am using <a href="http://technet.microsoft.com/en-us/library/dd315282.aspx">Write-Output</a> to document the scripts behavior so you can easily pipe it out to a text file for review.<br />
<br />
This script does not make the new address the employee's primary SMTP address. In order to do this, you would need to remove the current primary denoted by the "SMTP:" at the start of the proxyAddress and return it back with "smtp:" then add the new e-mail address with "SMTP:" at the start. I will provide a code sample in the future that will detail that swap out and how to do it safely.<br />
<br />
<span class="Apple-style-span" style="color: red;">And remember, this script modifies data! Use at your own risk! I might have the best of intentions but my skill may betray you. Test, test and further test before implementing this code in a production environment.</span><br />
<pre class="brush:powershell;toolbar:false">Function Get-LocalDomainController($objectDomain) {
return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
Function Get-ObjectADDomain($distinguishedName) {
return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
Function Get-ActiveDirectoryObject($distinguishedName) {
return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
Set-Variable -name adsPropertyAppend -option Constant -value 3
Set-Variable -name newMailDomain -option Constant -value "@newdomain.local"
Set-Variable -name inputFile -option Constant -value "userlist.txt"
Set-Variable -name writeEnabled -option Constant -value $false
#--------------------------------------------------------------------------------------------------#
if(Test-Path -path $inputFile) {
$userList = Get-Content -path $inputFile
} else {
Write-Host "Could not locate $inputFile. Exiting..." -foregroundColor Red
exit
}
if(!(Test-Path -path "backup")) {
New-Item -path "backup" -type directory | Out-Null
}
$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection
foreach($user in $userList) {
$ldapBase = "GC://$forestRootDn"
$ldapAttr = "distinguishedName"
$ldapScope = "subtree"
$ldapFilter = "(&(objectClass=user)(proxyAddresses=smtp:$user))"
$ldapQuery = "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
$objectCommand.CommandText = $ldapQuery
$objectRecordSet = $objectCommand.Execute()
if(!$objectRecordSet.EOF) {
while(!$objectRecordSet.EOF) {
$userObject = Get-ActiveDirectoryObject $objectRecordSet.Fields.Item('distinguishedName').Value
$newEmailAddress = ("smtp:" + ($user.Split("@")[0]).ToLower() + $newMailDomain)
Write-Output ($userObject.displayName).ToString()
Write-Output "New Address: $newEmailAddress"
$notFound = $true
Set-Content -path ("backup\" + ($userObject.mail).ToString() + ".txt") -value $userObject.proxyAddresses
foreach($proxyAddress in $userObject.proxyAddresses) {
if($proxyAddress -eq $newEmailAddress) {
$notFound = $false
}
}
if($notFound -eq $true) {
Write-Output "Adding $newEmailAddress"
if($writeEnabled -eq $true) {
$userObject.PutEx($adsPropertyAppend, "proxyAddresses", @($newEmailAddress))
$userObject.SetInfo()
}
} else {
Write-Output "Already has $newEmailAddress"
}
$objectRecordSet.MoveNext()
}
} else {
Write-Output "Could not locate $user in the forest."
}
Write-Output ("-" * 50)
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-51196585501630813332011-03-14T19:45:00.009-04:002011-06-18T11:53:23.095-04:00Unix Tail-like Functionality in PowerShell RevisitedMy first attempt to replicate tail for PowerShell, which I wrote about in "<a href="http://activelydirect.blogspot.com/2011/03/unix-tail-like-functionality-in.html">Unix Tail-like Functionality in PowerShell</a>", was horribly inefficient once you got past a couple dozen lines. This makes since given the method I was using -- a byte by byte reverse read of a text file, converting each byte at a time to ASCII. I knew the solution was to "<a href="http://thesaurus.com/browse/wolf+down">wolf down</a>" large byte chunks and process them as a whole. Using <a href="http://msdn.microsoft.com/en-us/library/744y86tc.aspx">System.Text.ASCIIEncoding.GetString</a>, I am doing that just after reading into memory multiple bytes using <a href="http://msdn.microsoft.com/en-us/library/system.io.filestream.read.aspx">System.IO.FileStream.Read</a>. With this change in methodology, I am getting to within 3% of the speed of tail in UNIX in my tests. The largest test I've performed was returning 1,000,000 lines from a 850MB log file. A Mac OS X 10.6.6 workstation performed the task in 16 seconds using tail and a Windows Server 2003 server returned in 17 seconds using this method. Good enough for me. Most of my needs are in the thousands of lines which I am able to return in hundreds of milliseconds which is perfect my monitoring scripts in <a href="http://www.nagios.org/">Nagios</a>. Compared to my previous attempt, this is a <a href="http://www.nasm.si.edu/collections/artifact.cfm?id=A19920072000">Lockheed SR-71</a> vs. a <a href="http://www.nasm.si.edu/exhibitions/gal100/wright1903.html">Wright Brothers Flyer</a>. A small 5,000 tail using the old code took 5 1/2 minutes to return while this code returned the same lines in 200 milliseconds. Huge difference!<br />
<br />
In the code sample below, I am using 10 kilobytes for that chunking. I found that number suited most of my needs. However, you can greatly increase that number for large number of lines to be returned (I used 4MB for my million line test). You can also do a little automatic tuning by altering the number of bytes using the number of lines you are seeking. One thing to be aware when passing files to this code, if you pass a file to System.IO.File/FileStream without a full path, it will not assume the file is located in the path of the executed script so Test-Path is not a valid test. Using <a href="http://msdn.microsoft.com/en-us/library/system.io.directory.getcurrentdirectory.aspx">System.IO.Directory.GetCurrentDirectory</a>, you can find this by running the following in PowerShell:<br />
<pre class="brush:powershell;toolbar:false">[System.IO.Directory]::GetCurrentDirectory()</pre>More than likely, it will point to the home directory of the profile the shell is executed under.<br />
<br />
Also be aware that this tail-like function does not handle unicode log files. The method I am using to decode the bytes is ASCII dependent. I am not using <a href="http://msdn.microsoft.com/en-us/library/system.text.unicodeencoding.aspx">System.Text.UnicodeEncoding</a> yet in the code. Currently ASCII meets all my needs for reading log files but I am still interested in adding compatibility to this function. I am also assuming that all log files denote the end of a line using carriage return & line feed (CHR 13 + CHR 10) which is how majority of text files are written in Windows. UNIX & old style Macintosh text files will not work properly with this code. You will need to modify line 23 to change the delimiter for the split for those text file formats.<br />
<br />
<span class="Apple-style-span" style="color: red;">UPDATE:</span> I have now finished an update that provides the "tail -f" functionality for continuously reading the updates to a text file. Read about it in my blog post, <a href="http://activelydirect.blogspot.com/2011/04/replicating-unix-f-in-powershell.html">Replicating UNIX "tail -f" in PowerShell.</a><br /><br />
<span class="Apple-style-span" style="color: red;">UPDATE:</span> I have updated the code to handle unicode text files and non-Windows new lines. You can review the code <a href="http://activelydirect.blogspot.com/2011/06/powershell-tail-improvements.html">here</a>.
<pre class="brush:powershell;toolbar:false">Function Read-EndOfFileByByteChunk($fileName,$totalNumberOfLines,$byteChunk) {
if($totalNumberOfLines -lt 1) { $totalNumberOfLines = 1 }
if($byteChunk -le 0) { $byteChunk = 10240 }
$linesOfText = New-Object System.Collections.ArrayList
if([System.IO.File]::Exists($fileName)) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$asciiEncoding = New-Object System.Text.ASCIIEncoding
$fileSize = $fileStream.Length
$byteOffset = $byteChunk
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$totalBytesProcessed = 0
$lastReadAttempt = $false
do {
if($byteOffset -ge $fileSize) {
$byteChunk = $fileSize - $totalBytesProcessed
[byte[]] $bytesRead = New-Object byte[] $byteChunk
$byteOffset = $fileSize
$lastReadAttempt = $true
}
$fileStream.Seek((-$byteOffset), [System.IO.SeekOrigin]::End) | Out-Null
$fileStream.Read($bytesRead, 0, $byteChunk) | Out-Null
$chunkOfText = New-Object System.Collections.ArrayList
$chunkOfText.AddRange(([System.Text.RegularExpressions.Regex]::Split($asciiEncoding.GetString($bytesRead),"\r\n")))
$firstLineLength = ($chunkOfText[0].Length)
$byteOffset = ($byteOffset + $byteChunk) - ($firstLineLength)
if($lastReadAttempt -eq $false -and $chunkOfText.count -lt $totalNumberOfLines) {
$chunkOfText.RemoveAt(0)
}
$totalBytesProcessed += ($byteChunk - $firstLineLength)
$linesOfText.InsertRange(0, $chunkOfText)
} while($totalNumberOfLines -ge $linesOfText.count -and $lastReadAttempt -eq $false -and $totalBytesProcessed -lt $fileSize)
$fileStream.Close()
if($linesOfText.count -gt 1) {
$linesOfText.RemoveAt($linesOfText.count-1)
}
$deltaLines = ($linesOfText.count - $totalNumberOfLines)
if($deltaLines -gt 0) {
$linesOfText.RemoveRange(0, $deltaLines)
}
} else {
$linesOfText.Add("[ERROR] $fileName not found") | Out-Null
}
return $linesOfText
}
#--------------------------------------------------------------------------------------------------#
$fileName = "C:\Logs\really-huge.log" # Your really big log file
$numberOfLines = 100 # Number of lines from the end of the really big log file to return
$byteChunk = 10240 # Size of bytes read per seek during the search for lines to return
####################################################################################################
## This is a possible self-tuning method you can use but will blow up memory on an enormous
## number of lines to return
## $byteChunk = $numberOfLines * 256
####################################################################################################
$lastLines = @()
$lastLines = Read-EndOfFileByByteChunk $fileName $numberOfLines $byteChunk
foreach($lineOfText in $lastLines) {
Write-Output $lineOfText
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-38189950113176156752011-03-10T21:15:00.031-05:002011-03-11T10:26:06.999-05:00Write Excel Spreadsheets Fast in PowerShellIf you try to populate an Excel spreadsheet cell by cell using PowerShell expect it to take a long time. As I showed in a previous post, "<a href="http://activelydirect.blogspot.com/2011/03/speed-up-reading-excel-files-in.html">Speed Up Reading Excel Files in PowerShell</a>", when dealing with Excel work with comma separated value text files. The population of the cells appears to be the huge bottleneck in Excel permformace but if you let Excel load a comma separated values file, you can quickly format and manipulate the prepopulated cells. <br />
<br />
One example from my own work involves a spreadsheet with ~3,500 rows. If I populate the rows directly through Excel it can take upwards of five minutes to complete the process. If I dump the data to a comma separated values text file then import it into Excel, it takes about 2 seconds -- a huge time savings. When you are running ad hoc reports, it pays off. If you want an excuse to head down to Starbucks from your office, it doesn't.<br />
<br />
To demonstrate this technique, I will take a previous post, "<a href="http://activelydirect.blogspot.com/2011/02/blackberry-user-report.html">BlackBerry User Report</a>", and change the report from a comma separated values text file to Excel spreadsheet. First to make the final result look nice, the NoteProperty names are cleaned up so they make attractive headers in the spreadsheet. Swap out the following lines to achieve this.<br />
<pre class="brush:powershell;toolbar:false;first-line:259"> Add-Member -inputObject $blackberryUser -type NoteProperty -name "Domain" -value $domain
Add-Member -inputObject $blackberryUser -type NoteProperty -name "User ID" -value $sAMAccountName
Add-Member -inputObject $blackberryUser -type NoteProperty -name "First Name" -value $firstName
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Last Name" -value $firstName
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Display Name" -value $displayName
Add-Member -inputObject $blackberryUser -type NoteProperty -name "E-Mail Address" -value $proxyAddress
Add-Member -inputObject $blackberryUser -type NoteProperty -name "PIN" -value $pin
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Cell Phone Number" -value $phoneNumber
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Desk Phone Number" -value $telephoneNumber
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Street Address" -value $streetAddress
Add-Member -inputObject $blackberryUser -type NoteProperty -name "City" -value $city
Add-Member -inputObject $blackberryUser -type NoteProperty -name "State" -value $state
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Zip Code" -value $zipCode
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Country" -value $country
Add-Member -inputObject $blackberryUser -type NoteProperty -name "BlackBerry Model" -value $modelName
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Carrier" -value $homeNetwork
Add-Member -inputObject $blackberryUser -type NoteProperty -name "IMEI" -value $imei
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Password Enabled" -value $passwordEnabled
Add-Member -inputObject $blackberryUser -type NoteProperty -name "Exchange Server" -value $exchangeServer
Add-Member -inputObject $blackberryUser -type NoteProperty -name "BlackBerry Server" -value $blackberryServer
</pre>Now that you have nice and pretty column headers in your NoteProperty, its time to write out that Excel spreadsheet. This code assumes that you have at least Excel 2007 installed on your scripting workstation/server. First the code writes out to the temp directory using a GUID (just to be fancy) for the comma separated values file's name. We create an Excel object and load that temporary file. At that point, we can save out a fairly boring Excel spreadsheet. That won't impress anyone. If you want to look "pro" when you distribute reports, having a good layout is key. You want to save out a nicely formatted spreadsheet. So you autofit all the columns to longest cell in each, populate the worksheet's name, update some of the metadata in the spreadsheet and then apply a table style. <a href="http://msdn.microsoft.com/fr-fr/library/documentformat.openxml.spreadsheet.tablestyle.aspx">Table styles</a> are a quick way to add color to your spreadsheet with minimal effort by creating a list object. I have found that if you want to apply your own custom style (a future blog post) there is no speed penalty as long as the data is prepopulated. And finally we perform a Save As, remind Excel that we are saved, quit Excel and do some garbage cleanup. If you just quit Excel programatically, you will find that Excel will still be in memory.<br />
<pre class="brush:powershell;toolbar:false;first-line:286">$blackberryUsers = $blackberryUsers | Sort-Object "BlackBerry Server", "Last Name", "First Name"
$excelFile = ("\\fileserver.ad.mydomain.local\it_reports\blackberry\" + (Get-Date -format yyyyMMdd) + "-BlackBerry User Report.xlsx")
$temporaryCsvFile = ($env:temp + "\" + ([System.Guid]::NewGuid()).ToString() + ".csv")
$blackberryUsers | Export-Csv -path $temporaryCsvFile -noTypeInformation
if(Test-Path -path $excelFile) { Remove-Item -path $excelFile }
$excelObject = New-Object -comObject Excel.Application
$excelObject.Visible = $false
$workbookObject = $excelObject.Workbooks.Open($temporaryCsvFile)
$workbookObject.Title = ("BlackBerry User List for " + (Get-Date -Format D))
$workbookObject.Author = "Robert M. Toups, Jr."
$worksheetObject = $workbookObject.Worksheets.Item(1)
$worksheetObject.UsedRange.Columns.Autofit() | Out-Null
$worksheetObject.Name = "BlackBerry Users"
$listObject = $worksheetObject.ListObjects.Add([Microsoft.Office.Interop.Excel.XlListObjectSourceType]::xlSrcRange, $worksheetObject.UsedRange, $null,[Microsoft.Office.Interop.Excel.XlYesNoGuess]::xlYes,$null)
$listObject.Name = "User Table"
$listObject.TableStyle = "TableStyleMedium4" # Style Cheat Sheet in French/English: http://msdn.microsoft.com/fr-fr/library/documentformat.openxml.spreadsheet.tablestyle.aspx
$workbookObject.SaveAs($excelFile,51) # http://msdn.microsoft.com/en-us/library/bb241279.aspx
$workbookObject.Saved = $true
$workbookObject.Close()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbookObject) | Out-Null
$excelObject.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excelObject) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
if(Test-Path -path $temporaryCsvFile) { Remove-Item -path $temporaryCsvFile }
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com1tag:blogger.com,1999:blog-4411406500801797400.post-67936085135439611522011-03-08T23:45:00.047-05:002011-03-09T19:45:37.843-05:00Speed Up Reading Excel Files in PowerShellOne of the most frustrating aspects of PowerShell for me is the extremely sluggish performance the ComObject is for Excel. I deal with spreadsheets on a daily basis and have processes to read them and compare information. I have spent hours upon hours trying to figure out how to optimize the process of reading cells. One day, I said to myself, "Why can't I read Excel documents at near the speed I can read a text file?" (in more colorful language than this). The proverbial lightbulb went off above my head. That one question lead to the code below. If I can't read Excel files at the speed I want and I can with text files, why not convert the Excel spreadsheet to a text based format? A comma separated value text file! <br />
<br />
Using PowerShell to load an Excel spreadsheet is quick. The slowdown is reading the cells/rows. So the solution is to load the Excel spreadsheet and immediately save it in the temp directory as a <a href="http://msdn.microsoft.com/en-us/library/bb241279.aspx">.csv file</a>. PowerShell has a wonderful commandlet called <a href="http://technet.microsoft.com/en-us/library/dd347665.aspx">Import-Csv</a> that will allow you to read in a comma separated value text file as PSObject and associate the first row as a NoteProperty for each column in the spreadsheet. As long as your Excel spreadsheet is straight forward, this works perfectly and blazing fast. I have some processes that are now 20 times faster using this method.<br />
<pre class="brush:powershell;toolbar:false">Function Remove-File($fileName) {
if(Test-Path -path $fileName) { Remove-Item -path $fileName }
}
#--------------------------------------------------------------------------------------------------#
$excelFile = "\\server.ad.mydomain.local\excelreports\myreport.xlsx"
if(Test-Path -path $excelFile) {
$csvFile = ($env:temp + "\" + ((Get-Item -path $excelFile).name).Replace(((Get-Item -path $excelFile).extension),".csv"))
Remove-File $csvFile
$excelObject = New-Object -ComObject Excel.Application
$excelObject.Visible = $false
$workbookObject = $excelObject.Workbooks.Open($excelFile)
$workbookObject.SaveAs($csvFile,6) # http://msdn.microsoft.com/en-us/library/bb241279.aspx
$workbookObject.Saved = $true
$workbookObject.Close()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbookObject) | Out-Null
$excelObject.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excelObject) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
$spreadsheetDataObject = Import-Csv -path $csvFile # Use the $spreadsheetDataObject for your analysis
Remove-File $csvFile
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com5tag:blogger.com,1999:blog-4411406500801797400.post-81948320344974619932011-03-07T22:10:00.022-05:002011-03-21T21:02:01.975-04:00Unix Tail-like Functionality in PowerShellA common tool I use in shell scripts on Unix/Linux/Mac OS X servers is <a href="http://en.wikipedia.org/wiki/Tail_(Unix)">tail</a>. While there are command-line <a href="http://tailforwin32.sourceforge.net/">tail conversions</a> <a href="http://www.microsoft.com/downloads/en/details.aspx?FamilyID=9d467a69-57ff-4ae7-96ee-b18c4790cffd&DisplayLang=en">for Windows</a>, I need something I can integrate into a script for reading the end of large log files, search for information and act on that result. I don't want to distribute third party software along with the script to accomplish the task. Get-Content and Select-Object are not suitable for large files.<br />
<br />
After researching the capabilities of File IO in .Net, I found that <a href="http://msdn.microsoft.com/en-us/library/system.io.filestream.aspx">System.IO.FileStream</a> class had just what I needed. Using this class, I read the target text file byte by byte from the end of the file until I reach a selected number of lines of text delimited by a carriage return. The amount of time it takes to obtain the data is related to the number of characters per line. It works very well in 500 lines or less in my typical log files (I tested up to 1 gigabyte) and much faster than using:<br />
<pre class="brush:powershell;toolbar:false">Get-Content "C:\Logs\really-huge.log" | Select-Object -last 100
</pre>The code meets 95% of my needs but I am sure I can optimize it so it comes close to matching the speed of tail from the Unix distributions I commonly use. It's my first stab at tackling the problem. One interesting part of the code is that I use <a href="http://msdn.microsoft.com/en-us/library/system.collections.arraylist.aspx">System.Collections.ArrayList</a> instead of a standard PowerShell array. The reason is since I am reading the file in reverse, I need to return the data back in the proper order. The ArrayList object allows me to insert into the first element so I don't have to re-write the array in the right order after collecting the data. Also I noticed that using <a href="http://msdn.microsoft.com/en-us/library/system.convert.aspx">System.Convert</a> to covert the bytes to a character instead of using PowerShell's native [char] was faster. Returning large number of lines, it was significant -- about .5 seconds per 100 lines.<br />
<br />
I will keep working on this to close that 5% and update this post with a link to an updated blog post in the future with the improvements. <br />
<br />
<b><span class="Apple-style-span" style="color: red;">UPDATE: </span><span class="Apple-style-span" style="font-weight: normal;">I have rewritten this function in a <a href="http://activelydirect.blogspot.com/2011/03/unix-tail-like-functionality-in_14.html">new blog post</a> and it is lightning fast. This code is deprecated and should only be used for amusement purposes.</span></b><br />
<pre class="brush:powershell;toolbar:false">Function Read-EndOfFile($fileName,$totalNumberOfLines) {
$fileStream = New-Object System.IO.FileStream($fileName,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::ReadWrite)
$linesOfText = New-Object System.Collections.ArrayList
$byteOffset = 1
$lineOfText = ""
do {
$fileStream.Seek((-$byteOffset), [System.IO.SeekOrigin]::End) | Out-Null
$byte = $fileStream.ReadByte()
if($byte -eq 13) {
} elseif($byte -eq 10) {
$linesOfText.Insert(0, $lineOfText)
$lineOfText = ""
} else {
$lineOfText = [System.Convert]::ToChar($byte) + $lineOfText
}
$byteOffset++
} while ($linesOfText.count -le $totalNumberOfLines)
$fileStream.Close()
return $linesOfText
}
#--------------------------------------------------------------------------------------------------#
$fileName = "C:\Logs\really-huge.log" # Your really big log file
$numberOfLines = 100 # Number of lines from the end of the really big log file to return
if([System.IO.File]::Exists($fileName) -and $numberOfLines -gt 0) {
$lastLines = Read-EndOfFile $fileName $numberOfLines
foreach($lineOfText in $lastLines) {
Write-Output $lineOfText
}
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-22383188922473256152011-03-07T20:45:00.017-05:002011-03-07T20:45:00.339-05:00SharePoint 2010 and Social Collaboration<div style="text-align: left;">Below is a <a href="http://www.webopedia.com/TERM/W/Webinar.html">webinar</a> that I attended hosted by <a href="http://www.gig-werks.com/">Gig Werks</a> focused on the Social Collaboration features in SharePoint 2010. I found it to be a good introduction to the built-in capabilities of the product.</div><br />
<center><object width="640" height="510"><param name="movie" value="http://www.youtube-nocookie.com/v/FrNJ6UYwfkE?fs=1&hl=en_US&hd=1"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube-nocookie.com/v/FrNJ6UYwfkE?fs=1&hl=en_US&hd=1" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="640" height="510"></embed></object></center>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-35966960091109538302011-03-03T21:00:00.012-05:002011-06-14T11:16:14.910-04:00Quick Organizational Chart from Active DirectoryBuilding on my blog post, <a href="http://activelydirect.blogspot.com/2011/02/export-managers-nested-direct-reports.html">Export Manager's Nested Direct Reports</a>, I quickly threw together a revision of the code that will create visual (ASCII at least) organizational chart based on the directReports attribute of an employee in Active Directory. The nesting method I am using could easy be converted to simulate the "tree" DOS command using Get-ChildObject on file systems or about any programming challenge that deals with nested objects. The one main fault of the code, is that it is not pretty when it gets to the last nested object of a parent. It leaves the "|" in front of the last nested subordinate object. A little more work and I could clean this presentation issue, but not today.<br />
<pre class="brush:powershell;toolbar:false">Function Get-DirectReports($distinguishedName,$level) {
$managerObject = Get-ActiveDirectoryObject $distinguishedName
if($managerObject.directReports.count -gt 0) {
foreach($directReport in $managerObject.directReports) {
$directReportObject = Get-ActiveDirectoryObject $directReport
Write-Output (("| " * $level) + "|")
Write-Output (("| " * $level) + "+-" + ($directReportObject.givenName).ToString() + " " + ($directReportObject.sn).ToString())
Get-DirectReports $directReport ($level + 1)
}
}
}
Function Get-LocalDomainController($objectDomain) {
return ([System.DirectoryServices.ActiveDirectory.ActiveDirectorySite]::GetComputerSite()).Servers | Where-Object { $_.Domain.Name -eq $objectDomain } | ForEach-Object { $_.Name } | Select-Object -first 1
}
Function Get-ObjectADDomain($distinguishedName) {
return ((($distinguishedName -replace "(.*?)DC=(.*)",'$2') -replace "DC=","") -replace ",",".")
}
Function Get-ActiveDirectoryObject($distinguishedName) {
return [ADSI]("LDAP://" + (Get-LocalDomainController (Get-ObjectADDomain $distinguishedName)) + "/" + ($distinguishedName -replace "/","\/"))
}
#--------------------------------------------------------------------------------------------------#
Set-Variable -name forestRootDn -option Constant -value ([ADSI]("LDAP://" + (([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()).name) + "/rootDSE")).defaultNamingContext
#--------------------------------------------------------------------------------------------------#
$objectConnection = New-Object -comObject "ADODB.Connection"
$objectCommand = New-Object -comObject "ADODB.Command"
$objectConnection.Open("Provider=ADsDSOObject;")
$objectCommand.ActiveConnection = $objectConnection
$manager = "the.ceo@mycompany.local"
$ldapBase = "GC://$forestRootDn"
$ldapAttr = "distinguishedName"
$ldapScope = "subtree"
$ldapFilter = "(&(objectClass=user)(proxyAddresses=smtp:$manager))"
$ldapQuery = "<$ldapBase>;$ldapFilter;$ldapAttr;$ldapScope"
$objectCommand.CommandText = $ldapQuery
$objectRecordSet = $objectCommand.Execute()
while(!$objectRecordSet.EOF) {
$firstLevelObject = Get-ActiveDirectoryObject $objectRecordSet.Fields.Item('distinguishedName').Value
$topLevelManager = (($firstLevelObject.givenName).ToString() + " " + ($firstLevelObject.sn).ToString())
Write-Output $topLevelManager
Get-DirectReports $firstLevelObject.distinguishedName 0
$objectRecordSet.MoveNext()
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0tag:blogger.com,1999:blog-4411406500801797400.post-79872905227997151112011-02-25T20:45:00.004-05:002011-02-25T20:45:00.122-05:00IP Address to BinaryWhen I was teaching myself subnetting some time ago, I wrote a perl script to assist in learning the conversion. There are many ways to do this but I wanted a method to help me learn to do the conversion in my head. This is why the subroutine is written out as collection of if than statements to represent the bits in the octet.<br />
<pre class="brush:perl;toolbar:false">#!/usr/bin/perl
# use: ./ip2binary.pl 192.168.1.1
use strict;
my $ip = $ARGV[0];
if($ip =~ /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/) {
my @binary;
my @octets = split(/\./,$ip);
foreach my $octet (@octets) {
push (@binary, return_binary($octet));
}
print join(".",@binary) . "\n";
} else {
print "$ip is not a valid IP\n";
}
exit;
sub return_binary {
my $decimal = $_[0];
my @binary;
if($decimal >= 128) {
$binary[0] = 1;
$decimal -= 128;
} else {
$binary[0] = 0;
}
if($decimal >= 64) {
$binary[1] = 1;
$decimal -= 64;
} else {
$binary[1] = 0;
}
if($decimal >= 32) {
$binary[2] = 1;
$decimal -= 32;
} else {
$binary[2] = 0;
}
if($decimal >= 16) {
$binary[3] = 1;
$decimal -= 16;
} else {
$binary[3] = 0;
}
if($decimal >= 8) {
$binary[4] = 1;
$decimal -= 8;
} else {
$binary[4] = 0;
}
if($decimal >= 4) {
$binary[5] = 1;
$decimal -= 4;
} else {
$binary[5] = 0;
}
if($decimal >= 2) {
$binary[6] = 1;
$decimal -= 2;
} else {
$binary[6] = 0;
}
if($decimal >= 1) {
$binary[7] = 1;
$decimal -= 1;
} else {
$binary[7] = 0;
}
return join("",@binary);
}
</pre>Robert M. Toups, Jr.http://www.blogger.com/profile/08899355162267631933noreply@blogger.com0