Wednesday, April 30, 2008

Powershell script to report on all IIS servers in our domain

There are many scripts out there to query IIS via WMI or ADSI and enumerate the settings and report on them. However, we back up all of our server's code to a central share every night, which includes grabbing the IIS metabase.xml file, so I decided to avoid yet another call to the remote servers (and having to deal with possible authentication / connection / etc issues) and instead use Powershell's ability to read XML files to parse each of these and report on the servers' IIS settings.

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:
  1. 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.
  2. 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
  3. 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
  4. ReportMetabase.ps1 then imports the specified Metabase.xml and loops through each Website and VirtualDirectory key, saving each value to a custom object
  5. The custom object is then exported to the specified csv file.

Script Files:

Steps:
  1. Download the zipfile
  2. Extract it to a target folder
  3. Modify both .config files to contain the proper values
  4. 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)
#==============================================================================

Powershell script to monitor MSMQ queues

Part of one of the products I support communicates with other products via Microsoft Message Queues (MSMQ). While we can monitor all of the various servers' CPU, memory, disk, network, etc for performance issues, one of the best determining factors is to see if a queue has suddenly grown. Since all queues should be either near zero or at a steady amount of messages depending on the time of day, any queue that shows an upward trend or sudden spike usually means there is an issue somewhere.

I usually run this at peak times of the day with a polling interval of ten seconds, whereas our monitoring system polls once every few minutes. This allows me to quickly see trends that may need attention. I also run it after performing maintenance just to verify the traffic patterns.

This script will not only query and display the most populated queues, but will also keep a count of the previous seven iterations to show an upward or downward trend. See below:
(Queue names were changed post-screenshot, sorry for the sloppiness)

Pre-Requisites:
  • Windows 2003 (not tested on 2000 or 2008)
  • Powershell 1.0
  • Rights to perform perfmon queries against the MSMQ server
  • Rights to enumerate the messages in all of the MSMQ queues
  • This has only been tested against "Private Queues"

What this script does:
  1. It reads in its .config files to determine the MSMQ server to contact, the important queues (if any) to flag, the top number of queues to poll, and the polling interval
  2. It then connects to the target server and queries for the largest X queues (defined in the config file)
  3. It steps through each queue and if its name matches an "important queue" it flags it with an asterisk
  4. Assigns the queue's name and count to a custom object
  5. Prints the custom object in table form
  6. On the next iteration, it sets the previous values to the custom object's "PriorN" column, keeping the previous 7 total.

Script files:

Steps:
  1. Download the code
  2. Extract it to a target folder
  3. Edit the .config file to set the proper values
  4. Execute it

For reference, here is the script code:
$Version="v8.4.28 Aaron Dodd"
$Description="Query MSMQ server for top X queues"

#------------------------------------------------------------------------------
# Settings / Variables
#------------------------------------------------------------------------------

$cfg=[xml](get-content QueryQueues.config)

$Computer = $cfg.configuration.QueueServer.name
$ImportantQueues = $cfg.configuration.ImportantQueues
$MaxQueues = $cfg.configuration.MaxQueues.value
$PollInterval = $cfg.configuration.PollInterval.value

# Hashes to store the values (queuename, messagescount)
$Current=@{}
$Previous1=@{}
$Previous2=@{}
$Previous3=@{}
$Previous4=@{}
$Previous5=@{}
$Previous6=@{}
$Previous7=@{}

# Make an array of important queues
$ImpQueues=@()
ForEach ($Queue in $ImportantQueues.Queue) {
$ImpQueues+=$Queue.name
}
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# Continously poll the MSMQ server, report results
#------------------------------------------------------------------------------
For () {

clear

$Queues= Get-WmiObject Win32_PerfFormattedData_MSMQ_MSMQQueue -computer $computer | Sort-Object -property "MessagesinQueue" -descending | Select-Object -first $MaxQueues

# Update the historical queue hashes, clear the current hash
$Previous7=$Previous6
$Previous6=$Previous5
$Previous5=$Previous4
$Previous4=$Previous3
$Previous3=$Previous2
$Previous2=$Previous1
$Previous1=$Current
$Current=@{}

# Repeat for "important queues"

#Collection to store the object of values to report on
$ColReport = @()

ForEach ($Queue in $Queues) {
$QueueName = $Queue.Name.Split('\')[2]
# If the $QueueName is an "important" queue, we'll prepend it with "* " for readability
If ($ImpQueues -contains $QueueName) {$QueueName = "* " + $QueueName}
$Current.Add($QueueName,$Queue.MessagesInQueue)
}
ForEach ($Entry in $Current.GetEnumerator()) {
$Name = $Entry.Name
$Curr = $Entry.Value

If ($Previous1.ContainsKey($Name)) {
$Prior1 = $Previous1.Get_Item($Name)
} Else {
$Prior1 = "-"
}
If ($Previous2.ContainsKey($Name)) {
$Prior2 = $Previous2.Get_Item($Name)
} Else {
$Prior2 = "-"
}
If ($Previous3.ContainsKey($Name)) {
$Prior3 = $Previous3.Get_Item($Name)
} Else {
$Prior3 = "-"
}
If ($Previous4.ContainsKey($Name)) {
$Prior4 = $Previous4.Get_Item($Name)
} Else {
$Prior4 = "-"
}
If ($Previous5.ContainsKey($Name)) {
$Prior5 = $Previous5.Get_Item($Name)
} Else {
$Prior5 = "-"
}
If ($Previous6.ContainsKey($Name)) {
$Prior6 = $Previous6.Get_Item($Name)
} Else {
$Prior6 = "-"
}
If ($Previous7.ContainsKey($Name)) {
$Prior7 = $Previous7.Get_Item($Name)
} Else {
$Prior7 = "-"
}

$ObjReport = New-Object System.Object
$ObjReport | Add-Member -type NoteProperty -name Queue -value $Name
$ObjReport | Add-Member -type NoteProperty -name Curr -value $Curr
$ObjReport | Add-Member -type NoteProperty -name Prior1 -value $Prior1
$ObjReport | Add-Member -type NoteProperty -name Prior2 -value $Prior2
$ObjReport | Add-Member -type NoteProperty -name Prior3 -value $Prior3
$ObjReport | Add-Member -type NoteProperty -name Prior4 -value $Prior4
$ObjReport | Add-Member -type NoteProperty -name Prior5 -value $Prior5
$ObjReport | Add-Member -type NoteProperty -name Prior6 -value $Prior6
$ObjReport | Add-Member -type NoteProperty -name Prior7 -value $Prior7
$ColReport += $ObjReport

}

$ColReport | Sort-Object Curr -descending | Format-Table -auto

$timestamp = Get-Date
Write-Host Last Updated: $timestamp
Start-Sleep -seconds $PollInterval
}
#------------------------------------------------------------------------------


Future enhancements:
  • If someone can help me out with how, I'd like to have the "important queues" show up with a different background color instead of simply renaming it to prepend an asterisk
  • I'd love to incorporate the ability to press a key to force a new loop instead of having to wait the polling interval, for times when I want to quickly see queue changes

Powershell script to report on all scheduled tasks in our domain

One of the annoying things about Windows management is that you cannot centrally manage scheduled tasks across your environment. You're either spending a lot of money on a third party system to do this for you, or your trying to script this.

I'll skip going into a rant about what I think of most of the available task management products out there (yes, package management and gui automation are nice, but I really just want to manage the native Windows Scheduled Tasks, not replace it). I had previously played with WINAT and command line scripts to utilize AT.EXE. Neither were sufficient as they both worked with the old-style NT4 tasks, which meant you couldn't see them via Scheduled Tasks nor could you run them under their own identities.

Windows 2000 introduced a method to query Scheduled Task info via WMI but there was no way to actually set the information.

By pure accident I found "schtasks.exe", the exact utility I'd been looking for. With SCHTASKS.EXE you can query, create, delete, start, stop, etc any scheduled task on any server you have administrative rights, both locally and remotely. Whats even nicer is SCHTASKS.EXE supports exporting query results in .CSV format.

Below is a script that will take a list of servers and generate a master CSV of all of their tasks info.

For more information on using SCHTASKS.EXE, just run "SCHTASKS.EXE /?" from the command line.

Pre-Requisites:
  • Windows 2003 (not tested on 2000 or 2008)
  • Powershell 1.0

What this script does:
  1. Reads in values from a .config file for where to find a CSV file of server names, where to save the report, and what to use as its temp folder
  2. Loops through the specified list of servers and calls SCHTASKS against it with the option to save a verbose .csv file of all task info
  3. After each loop interation, reads the temporary .csv file into a CSV object of all prior iterations
  4. After all servers are queried, it saves the final compiled report
Script files:
Steps:
  1. Download the code
  2. Extract it wherever you like
  3. Modify the .config file to update the values
  4. Modify the servers.csv file to have the names of the servers to query
For reference, here is the script code:
$Version="v8.4.28 Aaron Dodd"
$Description="Generate CSV of scheduled tasks in the environment"

#------------------------------------------------------------------------------
# Settings / Variables
#------------------------------------------------------------------------------
If (Test-Path "QueryScheduledTasks.config") {
$cfg=[xml](get-content "QueryScheduledTasks.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
}

$ServerList = Import-Csv $cfg.configuration.ServerList.name
$FinalReport=$cfg.configuration.FinalReport.name
$TempDir=$cfg.configuration.TempFolder.name
$TempReport=$TempDir + "\temp.csv"
$ErrorActionPreference=$cfg.configuration.ErrorAction.value
#------------------------------------------------------------------------------


#------------------------------------------------------------------------------
# Process tasks
#------------------------------------------------------------------------------
ForEach ($Server in $ServerList) {
schtasks /QUERY /S $Server.Name /FO CSV /V > $TempReport
$TempCsv += Import-Csv $TempReport
}
Remove-Item $TempReport
$TempCsv | Export-Csv $FinalReport -notype
#------------------------------------------------------------------------------

Friday, April 11, 2008

CMD Script to automate subversion backups

This is nothing fancy, just a .cmd script to back up my production Subversion repository on a nightly basis. There are some good ones via Google results but none that really fit my needs (too complex or too simple).

Pre-Requisites:
  1. Windows 2003 (not tested on 2000 or 2008)
  2. Subversion installed locally (tools and repository) with the \bin folder in the path

What this script does:
  1. If I call it with "FULL" it will:
    1. delete all previous subversion dumps
    2. create a new dump, using deltas, named "FULL_REV-#_YYYY-MM-DD.svndump" where # is the current revision number
  2. If I call it with "INC" it will:
    1. create a new dump incremental from the previous revision number, named "INC_REV-#_YYYY-MM-DD.svndump"
    My future enhancements will be:
    1. Store the revision number of the last successful dump, and then perform the next incremental off that instead of assuming just N-1 (in case previous night's job failed)
    2. Utilize the "ERR_FLAG" to send errors to our monitoring system.
Script Files:
Steps:
  1. Download the code
  2. Extract it anywhere you want to run it
  3. Modify the file to replace the values of "RepoPath" and "DumpPath" with the proper locations for your environment
  4. Execute either as "svn_dump INC" or "svn_dump FULL" (see above for what each does).

For reference, here's the script code:
@ECHO OFF
SETLOCAL
SET VERSION=v8.4.11 Aaron Dodd
SET USAGE=svn_dump.cmd [FULL -or- INC]

:: Settings
::===========================================================================

SET RepoPath=D:\state_management\repository
SET DumpPath=D:\state_management\repository_dumps

:: Date/Time formatting
::---------------------
FOR /F "Tokens=2" %%I in ( " %date% " ) Do Set StrDate=%%I
FOR /F "Tokens=1 delims=/ " %%M in ( " %StrDate% " ) Do Set Month=%%M
FOR /F "Tokens=2 delims=/ " %%D in ( " %StrDate% " ) Do Set Day=%%D
FOR /F "Tokens=3 delims=/ " %%L in ( " %StrDate% " ) Do Set Year=%%L
FOR /F "Tokens=1 delims=. " %%I in ( " %time% " ) Do Set StrTime=%%I
FOR /F "Tokens=1 delims=: " %%H in ( " %StrTime% " ) Do Set Hour=%%H
FOR /F "Tokens=2 delims=: " %%M in ( " %StrTime% " ) Do Set Minute=%%M
FOR /F "Tokens=3 delims=: " %%S in ( " %StrTime% " ) Do Set Second=%%S

:: Process Arguments
::------------------
IF '%1' == '' GOTO USAGE

IF "%1" EQU "FULL" SET DumpType=FULL
IF "%1" EQU "full" SET DumpType=FULL
IF "%1" EQU "INC" SET DumpType=INC
IF "%1" EQU "inc" SET DumpType=INC

IF '%DumpType%' == '' GOTO USAGE

:: Verify Dependencies
IF NOT EXIST "%RepoPath%" CALL :ERROR_PATH REPOSITORY
IF NOT EXIST "%DumpPath%" CALL :ERROR_PATH DUMP

SVNADMIN 2> NUL
IF %ErrorLevel% EQU 9009 CALL :ERROR_DEP SVNADMIN

SVNLOOK 2> NUL
IF %ErrorLevel% EQU 9009 CALL :ERROR_DEP SVNLOOK


:: MAIN
::===========================================================================
::Determine latest subversion revision number
::-------------------------------------------
FOR /F %%Y IN ('SVNLOOK youngest "%RepoPath%"') DO SET RepoVer=%%Y
SET /A RepoPriorVer=%RepoVer% - 1 >NUL

SET DumpFile=%DumpPath%\%DumpType%_REV-%RepoVer%_%Year%-%Month%-%Day%.svndump

:: Clean up old svn dumps
::-----------------------
IF "%DumpType%" EQU "FULL" DEL /Q "%DumpPath%\*.svndump" >NUL

:: Perform backup
::---------------
IF "%DumpType%" EQU "FULL" SVNADMIN dump "%RepoPath%" --deltas > %DumpFile%
IF "%DumpType%" EQU "INC" SVNADMIN dump "%RepoPath%" -r %RepoPriorVer%:%RepoVer% --incremental > %DumpFile%

GOTO END

:: Subroutines
::===========================================================================
:USAGE
ECHO %VERSION%
ECHO %USAGE%
ECHO Edit this script to change where the repository path is, and where to save the dump
GOTO END

:ERROR_DEP
ECHO !!ERROR!! - Dependency not found
IF "%1" EQU "SVNADMIN" ECHO svnadmin.exe cannot be found.
IF "%1" EQU "SVNLOOK" ECHO svnlook.exe cannot be found.
ECHO Verify Subversion is installed and the \bin folder is in the system-wide PATH environment variable.
SET ERR_FLAG=1
GOTO END

:ERROR_PATH
ECHO !!ERROR!!
IF "%1" EQU "REPOSITORY" ECHO Cannot find %RepoPath%
IF "%1" EQU "DUMP" ECHO Cannot find %DumpPath%
SET ERR_FLAG=1
GOTO END

:END

ENDLOCAL

Thursday, April 10, 2008

Quick and dirty way to mass update Windows 2003 DNS entries

I recently came across an issue where I wanted to add in a few hundred DNS entries into our Windows 2003 DNS server. I wanted to script this, as I have no intention of do a mass update manually and I want to re-use this in the future.

Most of the Google results I find say the easiest methods are to either convert the DNS zone to "standard primary" from "active directory integrated" and then manually editing the resulting text file created, or to use a script by Dean Wells called "dnsdump.cmd"

The problem with converting a domain to standard is, aside from being a convoluted hack, I'm updating our core AD dns zone, which obviously cannot be changed to standard without causing issues.

The problem with "dnsdump.cmd" is it requires being run from the DNS server itself and is more focused on migrating DNS information than simple updates like this.

I found VBScript examples using WMI, but in true VBScript fashion, while it works it takes 20 lines of script to do one line's worth of work.

Reading through the source code of dnsdump.cmd, I realized it was calling "dnscmd.exe", which is one of the Windows Server support tools. After installing the support tools (from "SUPPORT\TOOLS" on my Windows Server 2003 CD-ROM) I had this very nice utility. You can get its syntax by just running it from the command line. Below is a solution to my problem that only requires a text file and one call to dnscmd.exe to complete (well, I guess technically many calls, as this is a "for" loop, but you know what I mean ;-) )

My solution:
  1. Create a text file that only contains two space-separated values: the server name and the IP address. For this example its called "addresses.txt".
  2. Call dnscmd as follows:
for /f "tokens=1,2" %a in (addresses.txt) do dnscmd.exe dnsserver.mydomain.com /RecordAdd mydomain.com %a A %b

Simple and sweet :)

Note: you need to be an admin of "dnsserver.mydomain.com" as specified above. Also, for PTR records, you need to change "A" to "PTR". Just look at the output of "dnscmd.exe" by itself for help. Also, "dnscmd.exe /EnumZones" is useful for getting the exact spelling to use for the "mydomain.com" portion above.

Update: to answer a question Linkposed to me: "tokens=1,2" tells the "for" operator to return the first and second values of the delimited line in "addresses.txt". By default, spaces are a delimiter, so if the text file contains "server1.mydomain.com 1.2.3.4" then %a is "server1.mydomain.com" and %b is "1.2.3.4". You can actually use any delimiter (like a comma) and then specify such as:
for /f "tokens=1,2 delims=," %a ....
Type "for /?" from a command prompt for more details.

Update2: A nice article on dnscmd.exe is here: Scripting DNS