This is actually two separate scripts. One is "ReportMetabase.ps1" which does the actual parsing of the metabase file. You call it as "ReportMetabase.ps1 [full\path\to\metabase.xml] [report.csv]" and it will generate a CSV of all websites, virtual folders, and their associated application pools, webroots, application names, serverbindings, and metabase IDs.
The second script is "ProcessMetabases.ps1" which will, based on the settings of its .config file, step through a directory tree and for each match of "metabase.xml", invoke "ReportMetabase.ps1" and capture its results into a compiled CSV of all systems.
One thing I added into this after making it is the ability to specify a list of server renames. Apparently, when you rename a server, the IIS Metabase keeps the "ServerComment" field with the old server's name. I discovered this after suddenly seeing servers I thought we decommissioned years ago show up on the report...
While I run this against a copy of all of our code, you can easily run this against a local server's metabase.xml even while IIS is running. It simply reads in the metabase.xml, doesn't actually open or lock the file, and does not even attempt to open it for writing.
Pre-Requisites:
- Windows 2003 (not tested on 2000 or 2008) and IIS 6.0
- Powershell 1.0
- A copy of all metabase.xml files in your environment and access to read them, OR, run ReportMetabase.ps1 against the local metabase of a machine.
What this script does:
- ProcesMetabases.ps1 reads in its .config file to determine what directory tree to traverse, what to use as a temporary folder, and where to save the file report.
- ProcesMetabase.ps1 then traverses a directory tree and calls ReportMetabase.ps1 with the location to the Metabase.xml and a temp.csv file name. Each iteration it adds the contents of temp.csv to a compiled list to be saved after all Metabase.xml files have been read
- ReportMetabase.ps1 reads in its .config file to determine a list of server renames to apply, and a list of virtual directories or websites to exclude from the report
- ReportMetabase.ps1 then imports the specified Metabase.xml and loops through each Website and VirtualDirectory key, saving each value to a custom object
- The custom object is then exported to the specified csv file.
Script Files:
Steps:
- Download the zipfile
- Extract it to a target folder
- Modify both .config files to contain the proper values
- Either call ReportMetabase.ps1 against a local server, or ProcessMetabases.ps1 against a directory tree of metabase.xml files.
For reference, here are the scripts:
ProcessMetabases.ps1:
$Version="v8.4.21 Aaron Dodd"
$Description="Wrapper for ReportMetabases.ps1"
#------------------------------------------------------------------------------
# Settings / Variables
#------------------------------------------------------------------------------
If (Test-Path "ProcessMetabases.config") {
$cfg=[xml](get-content "ProcessMetabases.config")
} Else {
Write-Host "!! ERROR !! - Config file not found"
Write-Host "A file with the same name as this script, ending in .config, must exist in the same directory as this script."
exit
}
$StateManagementRoot=$cfg.configuration.StateManagementRoot.name
$FinalReport=$cfg.configuration.FinalReport.name
$TempDir=$cfg.configuration.TempFolder.name
$TempReport=$TempDir + "\temp.csv"
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Process metabase files
#------------------------------------------------------------------------------
$Metabases=Get-ChildItem -path $StateManagementRoot -recurse -include Metabase.xml | select fullname
ForEach ($Metabase in $Metabases) {
./ReportMetabase.ps1 $Metabase.FullName $TempReport
$TempCsv += Import-Csv $TempReport
}
$TempCsv | Export-Csv $FinalReport -notype
#------------------------------------------------------------------------------
ReportMetabases.ps1:
$Version="v8.4.21 Aaron Dodd"
$Description="Processes a metabase.xml file and reports all websites and various important settings"
$Usage="ReportOnMetabase.ps1 [path\to\metabase.xml] [report.csv]"
#==============================================================================
# Verify we received arguments
#------------------------------------------------------------------------------
If ($args.count -lt 2) {
Write-Host $Version
Write-Host $Description
Write-Host $Usage
exit
}
#==============================================================================
# Settings / Variables
#------------------------------------------------------------------------------
# Import config file
If (Test-Path "ReportMetabase.config") {
$cfg=[xml](get-content "ReportMetabase.config")
} Else {
Write-Host "!! ERROR !! - Config file not found"
Write-Host "A file with the same name as this script, ending in .config, must exist in the same directory as this script."
exit
}
# Object to hold the contents of the metabase file
$mb=[xml](get-content $args[0])
# Report to save
$ReportFile=$args[1]
# Hash table of metabase location # and ServerComment values from IIsWebServer keys
# Used to map virtual directories and other settings to their root websites by name
$LocationCommentMapping = @{}
$LocationBindingMapping = @{}
#------------------------------------------------------------------------------
# Array of virtual directories to ignore
#------------------------------------------------------------------------------
# These would be the 5th element of the metabase "location" key, i.e.:
# /LM/W3SVC/#/ROOT/VirtualDirectory
$VirtualDirectoryIgnoreList = New-Object System.Collections.ArrayList
ForEach ($key in $cfg.configuration.Exclusions.VirtualDirectory.add) {
$VirtualDirectoryIgnoreList.add($key.name) > $Null
}
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Array of websites to ignore
#------------------------------------------------------------------------------
# This would be the value of the metabase "ServerComment" key
$WebSiteIgnoreList = New-Object System.Collections.ArrayList
ForEach ($key in $cfg.configuration.Exclusions.WebSite.add) {
$WebSiteIgnoreList.add($key.name) > $Null
}
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Array of metabase IDs to ingore
#------------------------------------------------------------------------------
# These would be the 3rd element of the metabase "location" key, i.e.:
# /LM/W3SVC/3
$LocationIndexIgnoreList = New-Object System.Collections.ArrayList
ForEach ($key in $cfg.configuration.Exclusions.LocationIndex.add) {
$LocationIndexIgnoreList.add($key.name) > $Null
}
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Hash table of server renames
#------------------------------------------------------------------------------
# We take the setting from the metabase for what the servername is. However,
# if a server is renamed, the metabase retains the original name. This is hash
# table of known renames, to replace
$ServerRenames = @{}
ForEach ($key in $cfg.configuration.ServerRenames.add) {
$ServerRenames.add($key.key,$key.value)
}
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Disconnected Recordset to hold all our info before writing to disk
#------------------------------------------------------------------------------
$adVarChar = 200 # field type to be set to variant in recordset
$adFldIsNullable = 32 # to make recordset field nullable
$MaxCharacters = 255 # max size of field
$MBRecordSet = New-Object -com "ADOR.RecordSet"
$MBRecordSet.Fields.Append("Location", $adVarChar, $MaxCharacters, $adFldIsNullable)
$MBRecordSet.Fields.Append("Name", $adVarChar, $MaxCharacters, $adFldIsNullable)
$MBRecordSet.Fields.Append("Path", $adVarChar, $MaxCharacters, $adFldIsNullable)
$MBRecordSet.Fields.Append("ServerBindings", $adVarChar, $MaxCharacters, $adFldIsNullable)
$MBRecordSet.Fields.Append("AppPoolId", $adVarChar, $MaxCharacters, $adFldIsNullable)
$MBRecordSet.Fields.Append("AppRoot", $adVarChar, $MaxCharacters, $adFldIsNullable)
$MBRecordSet.Open()
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Objects containing the metabase settings we'll query
#------------------------------------------------------------------------------
$WebSites = $mb.configuration.MBProperty.IIsWebServer | select ServerComment,ServerBindings,AppPoolId,Location
$WebVDirs = $mb.configuration.MBProperty.IIsWebVirtualDir | select AppFriendlyName,Path,AppPoolId,Location,AppRoot
#------------------------------------------------------------------------------
#==============================================================================
# Process Metabase
#------------------------------------------------------------------------------
# Determine ServerName
#------------------------------------------------------------------------------
If (!$mb.configuration.MBProperty.IIsWebService.ServerComment) {
$WebServerName = "[UNKNOWN]"
} Else {
$WebServerName = $mb.configuration.MBProperty.IIsWebService.ServerComment
}
If ($ServerRenames.ContainsKey($WebServerName)) {
$WebServerName = $ServerRenames.Get_Item($WebServerName)
}
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Process the "IIsWebServer" key first to enumerate all root websites
#------------------------------------------------------------------------------
ForEach ($Site in $WebSites) {
# Site.Location is /LM/W3SVC/number. We want the "number"
$ThisLocationIndex = $Site.Location.Split('/')[3]
# Proceed if this isn't a website location index to ignore
If ($LocationIndexIgnoreList -notcontains $ThisLocationIndex) {
# Populate the recordset
$MBRecordSet.AddNew()
$MBRecordSet.Fields.Item("Location") = $Site.Location
$MBRecordSet.Fields.Item("Name") = $Site.ServerComment
$MBRecordSet.Fields.Item("AppRoot") = $Site.ServerComment
# Multiple bindings are separated by linebreaks and tabs. Lets convert to semi-colon
$TempBindings = [regex]::Replace($Site.ServerBindings,"`n",";");
$TempBindings = [regex]::Replace($TempBindings,"`t","");
$MBRecordSet.Fields.Item("ServerBindings") = $TempBindings
# Populate hash tables
$LocationBindingMapping.Add($ThisLocationIndex,$TempBindings)
$LocationCommentMapping.Add($ThisLocationIndex,$Site.ServerComment)
}
}
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# Process the "IIsWebVirtualDir" key to enumerate all VDirs and root settings
#------------------------------------------------------------------------------
ForEach ($Site in $WebVDirs) {
# Site.Location is /LM/W3SVC/number/AppRoot/AppName
$ThisLocationIndex = $Site.Location.Split('/')[3]
$ThisAppName = $Site.Location.Split('/')[5]
# Proceed if this isn't a website location index to ignore
If ($LocationIndexIgnoreList -notcontains $ThisLocationIndex) {
# If there is no AppPoolId, it's the DefaultAppPool
If (!$Site.AppPoolId) {
$ThisAppPool="DefaultAppPool"
} Else {
$ThisAppPool=$Site.AppPoolId
}
If (!$ThisAppName) {
# Process /LM/W3SVC/#/ROOT sites, but not /ROOT/Vdir
# This updates existing root site properties from Bindings above
$MBRecordSet.MoveFirst();
do {
$MbLocationIndex = $MBRecordSet.Fields.Item("Location").Value.Split('/')[3]
If ($MbLocationIndex -eq $ThisLocationIndex) {
$MBRecordSet.Fields.Item("Path") = $Site.Path
$MBRecordSet.Fields.Item("AppPoolId") = $ThisAppPool
$MBRecordSet.Update()
}
$MBRecordSet.MoveNext()
} until ($MBRecordSet.EOF)
} Else {
# Process /LM/W3SVC/#/ROOT/Vdir sites
$ThisAppRoot = $LocationCommentMapping.Get_Item($ThisLocationIndex)
$ThisBinding = $LocationBindingMapping.Get_Item($ThisLocationIndex)
$MBRecordSet.AddNew()
$MBRecordSet.Fields.Item("Location") = $Site.Location
$MBRecordSet.Fields.Item("Path") = $Site.Path
$MBRecordSet.Fields.Item("Name") = $ThisAppName
$MBRecordSet.Fields.Item("AppRoot") = $ThisAppRoot
$MBRecordSet.Fields.Item("ServerBindings") = $ThisBinding
$MBRecordSet.Fields.Item("AppPoolId") = $ThisAppPool
}
}
}
#------------------------------------------------------------------------------
#==============================================================================
# Save the report
#------------------------------------------------------------------------------
# Start the CSV file, adding the headers
"IIS Website Name,Application Name,Server,Path,AppPoolId,ServerBindings,MetabaseLocationID" | out-file $ReportFile
# Loop through recordset and output results to CSV
$MBRecordSet.Sort = "AppRoot ASC"
$MBRecordSet.MoveFirst();
do {
$ThisLocationIndex = $MBRecordSet.Fields.Item("Location").Value.Split('/')[3]
$ThisWebSite = $MBRecordSet.Fields.Item("AppRoot").Value
$ThisVirtualDir = $MBRecordSet.Fields.Item("Name").Value
# Make sure we're not outputting items listed in the IgnoreList arrays
If (
$WebSiteIgnoreList -notcontains $ThisWebSite -and
$VirtualDirectoryIgnoreList -notcontains $ThisVirtualDir -and
$LocationIndexIgnoreList -notcontains $ThisLocationIndex
) {
$Output = $MBRecordSet.Fields.Item("AppRoot").Value.ToLower() + ","
$Output += $MBRecordSet.Fields.Item("Name").Value.ToLower() + ","
$Output += $WebServerName.ToLower() + ","
$Output += $MBRecordSet.Fields.Item("Path").Value.ToLower() + ","
$Output += $MBRecordSet.Fields.Item("AppPoolId").Value.ToLower() + ","
$Output += $MBRecordSet.Fields.Item("ServerBindings").Value.ToLower() + ","
$Output += $MBRecordSet.Fields.Item("Location").Value.ToLower()
$Output | out-file -append $ReportFile
}
$MBRecordSet.MoveNext()
} until ($MBRecordSet.EOF)
#==============================================================================

