Getting the Contents of an Active Directory Integrated DNS Zone

Microsoft has long offered (where “long” means “since Windows Server 2003”) the “dnscmd.exe” program to control the actions of a DNS server. And, if you installed the adminpack (RSAT-Tools-DNS on Vista/Server 2008 and above) you could perform this control on a remote workstation/server as well.

However, the output of dnscmd leaves….something…. to be desired, especially when you want to format and manipulate that output.

Using a prior blog post of mine (Hex and Decimal Output In PowerShell), I more-or-less reverse-engineered the format of how Active Directory Integrated zones (DNS domains) are stored in Active Directory. For all of my personal testing, the program below was successful at displaying the output and decoding it properly. However, that doesn’t mean I decode everything!!! Just those things I could test.

A key desire of mine was to be able to get the contents of an ADI zone into a CSV (comma-separated-value) format; so of course that option is present. I also wanted proper timeout values for aging/scavenging, so of course that happens by default (which dnscmd can’t do at all).

If the script below doesn’t work for you, I’d be interested in hearing about it, and why it doesn’t work. Of course, I make no promises, but I’ll probably fix those issues. 🙂

Without further ado…

##
## dns-dump.ps1
##
## Michael B. Smith
## michael at smithcons dot com
## http://TheEssentialExchange.com/blogs/michael
## May/June, 2009
##
## Use as you wish, no warranties expressed, implied or explicit.
## Works for me, but it may not for you.
## If you use this, I would appreciate an attribution.
##

Param(
	[string]$zone,
	[string]$dc,
	[switch]$csv,
	[switch]$help
)

function dumpByteArray([System.Byte[]]$array, [int]$width = 9)
{
	## this is only used if we run into a record format
	## we don't understand.

	$hex = ""
	$chr = ""
	$int = ""

	$i = $array.Count
	"Array contains {0} elements" -f $i
	$index = 0
	$count = 0
	while ($i-- -gt 0)
	{
		$val = $array[$index++]

		$hex += ("{0} " -f $val.ToString("x2"))

		if ([char]::IsLetterOrDigit($val) -or 
		    [char]::IsPunctuation($val)   -or 
		   ([char]$val -eq " "))
		{
			$chr += [char]$val
		}
		else
		{
			$chr += "."
		}

		$int += "{0,4:N0}" -f $val

		$count++
		if ($count -ge $width)
		{
			"$hex $chr $int"
			$hex = ""
			$chr = ""
			$int = ""
			$count = 0
		}		
	}

	if ($count -gt 0)
	{
		if ($count -lt $width)
		{
			$hex += (" " * (3 * ($width - $count)))
			$chr += (" " * (1 * ($width - $count)))
			$int += (" " * (4 * ($width - $count)))
		}

		"$hex $chr $int"
	}
}

function dword([System.Byte[]]$arr, [int]$startIndex)
{
	## convert four consecutive bytes in $arr into a
	## 32-bit integer value... if I had bit-manipulation
	## primitives in PowerShell, I'd use them instead
	## of the multiply operator.

	$res = $arr[$startIndex]
	$res = ($res * 256) + $arr[$startIndex + 1]
	$res = ($res * 256) + $arr[$startIndex + 2]
	$res = ($res * 256) + $arr[$startIndex + 3]

	return $res
}

function analyzeArray([System.Byte[]]$arr, [System.Object]$var)
{
	$nameArray = $var.distinguishedname.ToString().Split(",")
	$name = $nameArray[0].SubString(3)

	## AGE is stored backwards. The most-significant-byte comes
	## last, instead of first, unlike all the other 32-bit values.
	$age = $arr[23]
	$age = ($age * 256) + $arr[22]
	$age = ($age * 256) + $arr[21]
	$age = ($age * 256) + $arr[20]
	if ($age -ne 0)
	{
		## hours since January 1, 1601 (start of Windows epoch)
		## there is a long-and-dreary way to do this manually,
		## but get-date makes it trivial to do the conversion.
		$timestamp = (get-date -year 1601 -month 1 -day 1 -hour 0 -minute 0 -second 0).AddHours($age)
	}

	$ttl = dword $arr 12

	if ($arr[0] -eq 4 -and $arr[1] -eq 0)
	{
		# "A" record
		$ip = "{0}.{1}.{2}.{3}" -f $arr[24], $arr[25], $arr[26], $arr[27]

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "A", $ip
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "A", $ip
		}
	}
	elseif ((($arr[0] -eq 80) -or ($arr[0] -eq 82)) -and $arr[1] -eq 0)
	{
		# "SOA" record
		# "Start-Of-Authority"

		$nslen = $arr[44]
		$segments = $arr[45]
		$index = 46
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		$priserver = $nsname
		# "Primary server: $nsname"

		$index += 1
		$nslen = $arr[$index++]
		$segments = $arr[$index++]

		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		# "Responsible party: $nsname"
		$resparty = $nsname

		#"TTL: $ttl"
		# "Age: $age"

####		$unk1 = dword $arr 16
####		"Unknown1: $unk1"

		$serial = dword $arr 24
		# "Serial: $serial"

		$refresh = dword $arr 28
		# "Refresh: $refresh"

		$retry = dword $arr 32
		# "Retry: $retry"

		$expires = dword $arr 36
		# "Expires: $expires"

		$minttl = dword $arr 40
		# "Minimum TTL: $minttl"

		if ($age -eq 0)
		{
			$agestr = "[static]"
		}
		else
		{
			$agestr = "[" + $timestamp.ToString() + "]"
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10}"

			$formatstring -f $name, $agestr, $ttl, `
				"SOA", $priserver, $resparty, `
				$serial, $refresh, $retry, `
				$expires, $minttl
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}"

			$formatstring -f $name, $agestr, $ttl, "SOA"
			(" " * 32) + "Primary server: $priserver"
			(" " * 32) + "Responsible party: $resparty"
			(" " * 32) + "Serial: $serial"
			(" " * 32) + "TTL: $ttl"
			(" " * 32) + "Refresh: $refresh"
			(" " * 32) + "Retry: $retry"
			(" " * 32) + "Expires: $expires"
			(" " * 32) + "Minimum TTL (default): $minttl"
		}

		#### dumpByteArray $arr
	}
	elseif ((($arr[0] -eq 32) -or ($arr[0] -eq 30)) -and $arr[1] -eq 0)
	{
		# "NS" record
		$nslen = $arr[24]
		$segments = $arr[25]
		$index = 26
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "NS", $nsname
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "NS", $nsname
		}
	}
	elseif ((($arr[0] -eq 36) -or ($arr[0] -eq 38)) -and $arr[1] -eq 0)
	{
		# "SRV" record

		$port = $arr[28]
		$port = ($port * 256) + $arr[29]

		$weight = $arr[26]
		$weight = ($weight * 256) + $arr[27]

		$pri = $arr[24]
		$pri = ($pri * 256) + $arr[25]

		$nslen = $arr[30]
		$segments = $arr[31]
		$index = 32
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3} {4} {5}"
		}

		if ($age -eq 0)
		{
			$formatstring -f `
				$name, "[static]", `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}
		else
		{
			$formatstring -f `
				$name, ("[" + $timestamp.ToString() + "]"), `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}

	}
	else
	{
		$name
		$var.distinguishedname.ToString()
		dumpByteArray $arr
	}

}

function processAttribute([string]$attrName, [System.Object]$var)
{
	$array = $var.$attrName.Value
####	"{0} contains {1} rows of type {2} from {3}" -f $attrName, $array.Count, $array.GetType(), $var.distinguishedName.ToString()

	if ($array -is [System.Byte[]])
	{
####		dumpByteArray $array
####		" "
		analyzeArray $array $var
####		" "
	}
	else
	{
		for ($i = 0; $i -lt $array.Count; $i++)
		{
####			dumpByteArray $array[$i]
####			" "
			analyzeArray $array[$i] $var
####			" "
		}
	}
}

function usage
{
"
.\dns-dump -zone  [-dc ] [-csv] |
	   -help

dns-dump will dump, from Active Directory, a particular named zone. 
The zone named must be Active Directory integrated.

Zone contents can vary depending on domain controller (in regards
to replication and the serial number of the SOA record). By using
the -dc parameter, you can specify the desired DC to use. Otherwise,
dns-dump uses the default DC.

Usually, output is formatted for display on a workstation. If you
want CSV (comma-separated-value) output, specify the -csv parameter.
Use out-file in the pipeline to save the output to a file.

Finally, to produce this helpful output, you can specify the -help
parameter.

This command is basically equivalent to (but better than) the:

	dnscmd /zoneprint 
or
	dnscmd /enumrecords  '@'

commands.

Example 1:

	.\dns-dump -zone essential.local -dc win2008-dc-3

Example 2:

	.\dns-dump -help

Example 3:

	.\dns-dump -zone essential.local -csv |
            out-file essential.txt -encoding ascii

	Note: the '-encoding ascii' is important if you want to
	work with the file within the old cmd.exe shell. Otherwise,
	you can usually leave that off.
"
}

	##
	## Main
	##

	if ($help)
	{
		usage
		return
	}

	if ($args.Length -gt 0)
	{
		write-error "Invalid parameter specified"
		usage
		return
	}

	if (!$zone)
	{
		throw "must specify zone name"
		return
	}

	$root = [ADSI]"LDAP://RootDSE"
	$defaultNC = $root.defaultNamingContext

	$dn = "LDAP://"
	if ($dc) { $dn += $dc + "/" }
	$dn += "DC=" + $zone + ",CN=MicrosoftDNS,CN=System," + $defaultNC

	$obj = [ADSI]$dn
	if ($obj.name)
	{
		if ($csv)
		{
			"Name,Timestamp,TTL,RecordType,Param1,Param2"
		}

		#### dNSProperty has a different format than dNSRecord
		#### processAttribute "dNSProperty" $obj

		foreach ($record in $obj.psbase.Children)
		{
			####	if ($record.dNSProperty) { processAttribute "dNSProperty" $record }
			if ($record.dnsRecord)   { processAttribute "dNSRecord"   $record }
		}
	}
	else
	{
		write-error "Can't open $dn"
	}

	$obj = $null

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Hex and decimal output in PowerShell

In UNIX/Linux/whatever there is this little utility called ‘od’ which makes it trivial to dump the output of a file in multiple formats.

Yesterday, I was working on a project and needed something similar. I could’ve downloaded Cygwin and installed it, or UnixUtils from Sourceforge, but no – I decided to write a PowerShell routine to give me what I needed.

If you’ve never had the need to see hexadecimal, alphanumeric, and decimal information on a file all at once, well, move along now!

Otherwise, below is my solution for your benefit. A couple of interesting points are my use of System.Char methods to determine whether a character is printable (viewable) and my use of the format operator (-f) which uses System.Format.

Many attributes in Active Directory (including many Exchange and DNS related attributes) have a raw form that devolves into a System.Byte[] array (or an array of System.Byte[] arrays). You can get an arbitrary file as a byte array by using the Get-Content cmdlet with the “-Encoding Byte” parameter.

Here is example output (with a line of header information that isn’t from this routine):

dNSRecord contains 62 rows of type System.Byte[] from DC=_gc._tcp.E14-Site._sites,DC=essential.local,CN=MicrosoftDNS,CN=System,DC=essential,DC=local
Array contains 62 entries
26 00 21 00 05 f0 00 00 d7  &.!..ð...   38   0  33   0   5 240   0   0 215
07 00 00 00 00 02 58 00 00  ......X..    7   0   0   0   0   2  88   0   0
00 00 ac 9a 36 00 00 00 00  ....6....    0   0 172 154  54   0   0   0   0
64 0c c4 1e 03 0c 77 69 6e  d.Ä...win  100  12 196  30   3  12 119 105 110
32 30 30 38 2d 64 63 2d 33  2008-dc-3   50  48  48  56  45 100  99  45  51
09 65 73 73 65 6e 74 69 61  .essentia    9 101 115 115 101 110 116 105  97
6c 05 6c 6f 63 61 6c 00     l.local.   108   5 108 111  99  97 108   0    

And without further ado:

function dumpByteArray([System.Byte[]]$array, [int]$width = 9)
{
	$hex = ""
	$chr = ""
	$int = ""

	$i = $array.Count
	"Array contains {0} elements" -f $i
	$index = 0
	$count = 0
	while ($i-- -gt 0)
	{
		$val = $array[$index++]

		$hex += ("{0} " -f $val.ToString("x2"))

		if ([char]::IsLetterOrDigit($val) -or 
		    [char]::IsPunctuation($val)   -or 
		   ([char]$val -eq " "))
		{
			$chr += [char]$val
		}
		else
		{
			$chr += "."
		}

		$int += "{0,4:N0}" -f $val

		$count++
		if ($count -ge $width)
		{
			"$hex $chr $int"
			$hex = ""
			$chr = ""
			$int = ""
			$count = 0
		}		
	}

	if ($count -gt 0)
	{
		if ($count -lt $width)
		{
			$hex += (" " * (3 * ($width - $count)))
			$chr += (" " * (1 * ($width - $count)))
			$int += (" " * (4 * ($width - $count)))
		}

		"$hex $chr $int"
	}
}

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Hex and decimal output in PowerShell

In UNIX/Linux/whatever there is this little utility called ‘od’ which makes it trivial to dump the output of a file in multiple formats.

Yesterday, I was working on a project and needed something similar. I could’ve downloaded Cygwin and installed it, or UnixUtils from Sourceforge, but no – I decided to write a PowerShell routine to give me what I needed.

If you’ve never had the need to see hexadecimal, alphanumeric, and decimal information on a file all at once, well, move along now!

Otherwise, below is my solution for your benefit. A couple of interesting points are my use of System.Char methods to determine whether a character is printable (viewable) and my use of the format operator (-f) which uses System.Format.

Many attributes in Active Directory (including many Exchange and DNS related attributes) have a raw form that devolves into a System.Byte[] array (or an array of System.Byte[] arrays). You can get an arbitrary file as a byte array by using the Get-Content cmdlet with the “-Encoding Byte” parameter.

Here is example output (with a line of header information that isn’t from this routine):

dNSRecord contains 62 rows of type System.Byte[] from DC=_gc._tcp.E14-Site._sites,DC=essential.local,CN=MicrosoftDNS,CN=System,DC=essential,DC=local
Array contains 62 entries
26 00 21 00 05 f0 00 00 d7  &.!..ð...   38   0  33   0   5 240   0   0 215
07 00 00 00 00 02 58 00 00  ......X..    7   0   0   0   0   2  88   0   0
00 00 ac 9a 36 00 00 00 00  ....6....    0   0 172 154  54   0   0   0   0
64 0c c4 1e 03 0c 77 69 6e  d.Ä...win  100  12 196  30   3  12 119 105 110
32 30 30 38 2d 64 63 2d 33  2008-dc-3   50  48  48  56  45 100  99  45  51
09 65 73 73 65 6e 74 69 61  .essentia    9 101 115 115 101 110 116 105  97
6c 05 6c 6f 63 61 6c 00     l.local.   108   5 108 111  99  97 108   0    

And without further ado:

function dumpByteArray([System.Byte[]]$array, [int]$width = 9)
{
	$hex = ""
	$chr = ""
	$int = ""

	$i = $array.Count
	"Array contains {0} elements" -f $i
	$index = 0
	$count = 0
	while ($i-- -gt 0)
	{
		$val = $array[$index++]

		$hex += ("{0} " -f $val.ToString("x2"))

		if ([char]::IsLetterOrDigit($val) -or 
		    [char]::IsPunctuation($val)   -or 
		   ([char]$val -eq " "))
		{
			$chr += [char]$val
		}
		else
		{
			$chr += "."
		}

		$int += "{0,4:N0}" -f $val

		$count++
		if ($count -ge $width)
		{
			"$hex $chr $int"
			$hex = ""
			$chr = ""
			$int = ""
			$count = 0
		}		
	}

	if ($count -gt 0)
	{
		if ($count -lt $width)
		{
			$hex += (" " * (3 * ($width - $count)))
			$chr += (" " * (1 * ($width - $count)))
			$int += (" " * (4 * ($width - $count)))
		}

		"$hex $chr $int"
	}
}

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Handling the userPrincipalName in PowerShell

I dealt with the importance of the userPrincipalName in one of my very early blog postings, dated May 26, 2004: The User Principal Name and You.

While my company has changed, the basic information contained within that post has not changed at all.

I would add one fact: the current Active Directory forest domain is an implied userPrincipalName domain and is not reflected in the list of userPrincipalName domains (also called uPNSuffixes) that are presented in such tools as Active Directory Domains and Trusts. The Active Directory Users and Computer tool is aware of this, and will present it as an option when configuring new user objects.

This PowerShell code handles the addition, reporting, and elimination of UPN suffixes:

#
# userPrincipalName processing routines
#
# Michael B. Smith
# michael@smithcons.com
# April 6, 2009
#
[int] $ADS_PROPERTY_CLEAR	= 1
[int] $ADS_PROPERTY_UPDATE	= 2
[int] $ADS_PROPERTY_APPEND	= 3
[int] $ADS_PROPERTY_DELETE	= 4

function get-ConfigurationPartition
{
	$rootdse  = [ADSI]"LDAP://RootDSE"
	$configNC = $rootdse.ConfigurationNamingContext
	$rootdse  = $null

	$partition = [ADSI]("LDAP://CN=Partitions," + $configNC)

	return $partition
}

function get-UPN
{
	$config = get-ConfigurationPartition
	$suffix = $config.uPNSuffixes
	$config = $null

	return $suffix
}

function test-UPN([string]$upn)
{
	$suffixes = get-UPN

	if (!$suffixes)
	{
		return $false
	}

	if (($suffixes -is [System.String]) -and ($suffixes -eq $upn))
	{
		return $true
	}

	foreach ($suffix in $suffixes)
	{
		if ($suffix -eq $upn)
		{
			return $true
		}
	}

	return $false
}

function new-UPN([string]$upn)
{
	if (test-UPN $upn)
	{
		write-error "$upn already exists"
	}
	else
	{
		$config = get-ConfigurationPartition
		$config.PutEx($ADS_PROPERTY_APPEND, "uPNSuffixes", @($upn))
		$config.SetInfo()
		$config = $null
	}
}

function remove-UPN([string]$upn)
{
	if (test-UPN $upn)
	{
		$config = get-ConfigurationPartition
		$config.PutEx($ADS_PROPERTY_DELETE, "uPNSuffixes", @($upn))
		$config.SetInfo()
		$config = $null
	}
	else
	{
		write-error "$upn doesn't exist"
	}
}

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Monitoring Exchange Server 2007 with System Center Operations Manager 2007

Just a few days ago, my new book (of the subject title!) started shipping from Amazon. If it hasn’t already, it should start appearing in your local bookstores this week.

If you want to order from Amazon, you can get it here.

Eight months in the making, this book was a labor of love. Including information on installing and configuring OpsMgr 2007, it also includes much information about operating a reliable Exchange environment. Even if you don’t have OpsMgr in your organization, you can learn from this book the key areas of Exchange that need monitoring and how to do so.

A strong PowerShell component is provided in several chapters to assist you in the generation of synthetic transactions for testing your Exchange environment.

While the book is written toward a key audience of Exchange Server 2007 administrators, much material is also provided for the Exchange Server 2003 administrator. The book uses a virtualized environment to describe a test roll-out of an OpsMgr 2007 and Exchange 2003/2007 mixed environment.

Since Exchange Server depends on the health of Windows Server, Active Directory, DNS, and IIS; tracking the health and well-being of these key services is also covered.

Go buy it. You’ll like it. 🙂

The chapter titles are:

  1. An Evolution of Server Management
  2. Monitoring Exchange Server 2007
  3. Installing and Configuring OpsMgr 2007
  4. Deplying OpsMgr 2007
  5. The First Management Pack: WIndows Server
  6. The Active Directory Management Pack
  7. The Domain Name System (DNS) Management Pack
  8. The Internet Information Services Management Pack
  9. SQL Server: An Ancillary Management Pack
  10. Exchange Server 2003
  11. Exchange Server 2007
  12. Exchange Server 2007 Redundancy
  13. Exchange Server Operations
  14. Tracking Mail Flow

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter: @EssentialExch

Monitoring Exchange Server 2007 with System Center Operations Manager 2007

Just a few days ago, my new book (of the subject title!) started shipping from Amazon. If it hasn’t already, it should start appearing in your local bookstores this week.

If you want to order from Amazon, you can get it here.

Eight months in the making, this book was a labor of love. Including information on installing and configuring OpsMgr 2007, it also includes much information about operating a reliable Exchange environment. Even if you don’t have OpsMgr in your organization, you can learn from this book the key areas of Exchange that need monitoring and how to do so.

A strong PowerShell component is provided in several chapters to assist you in the generation of synthetic transactions for testing your Exchange environment.

While the book is written toward a key audience of Exchange Server 2007 administrators, much material is also provided for the Exchange Server 2003 administrator. The book uses a virtualized environment to describe a test roll-out of an OpsMgr 2007 and Exchange 2003/2007 mixed environment.

Since Exchange Server depends on the health of Windows Server, Active Directory, DNS, and IIS; tracking the health and well-being of these key services is also covered.

Go buy it. You’ll like it. 🙂

The chapter titles are:

  1. An Evolution of Server Management
  2. Monitoring Exchange Server 2007
  3. Installing and Configuring OpsMgr 2007
  4. Deplying OpsMgr 2007
  5. The First Management Pack: WIndows Server
  6. The Active Directory Management Pack
  7. The Domain Name System (DNS) Management Pack
  8. The Internet Information Services Management Pack
  9. SQL Server: An Ancillary Management Pack
  10. Exchange Server 2003
  11. Exchange Server 2007
  12. Exchange Server 2007 Redundancy
  13. Exchange Server Operations
  14. Tracking Mail Flow

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter: @EssentialExch

Exchange 2007 and Windows 2008: Offline Exchange Backup

In my article Getting a List of Stores in a PowerShell Script you learned how to obtain a list of all the files involved for the Exchange database stores on a particular Exchange server. In the preceding article, Getting a List of Storage Groups in a PowerShell Script, you learned how to obtain a list of all the files unique to the Exchange storage groups on a particular Exchange server.

As a part of both of those articles, you learned how to create a list of the volumes used by the files in the storage groups and in the database stores.

Now that we have that information, what can we do with it?

Easy! We can generate a script that can create an offline backup of our Exchange databases. In future articles, you’ll learn how to turn this offline backup into an online backup, using VSS (the Volume Shadow Copy Service).

As a quick reminder, the following global objects are important and were introduced in the earlier articles of this series:

$volumes – a hash array containing the disk volumes used by the Exchange storage groups and database stores

$pathPattern – a hash array containing a list of all the regular-expression patterns required to back up all the files involved for all Exchange storage groups and database stores

getStores – a function populating $volumes and $pathPattern for the files used by Exchange database stores

getStorageGroups – a function populating $volumes and $pathPattern for the files used by Exchange storage groups

validateArrays – a function verifying that the $volumes and $pathPattern arrays are not empty; the function returns zero if the main program should proceed.

A utility that we have not previously discussed is robocopy. Robocopy was introduced as a part of the Windows 2000 Server Resource Kit. Among other features, it copies large files as quickly as possible, much more quickly than the cmd.exe copy and xcopy. Robocopy is a standard utility in Windows Vista and Windows Server 2008.

Two additional global variables need to be introduced:

$destination – the directory below which backups will be stored; in the case of Exchange backups, the directory structure is reproduced identically. For example, if $destination is “C:\Backups” and the file “C:\Program Files\Microsoft\Exchange Server\Mailbox\First Storage Group\E00.CHK” is a file to be backed up, then the destination file name will be “C:\Backups\Program Files\Microsoft\Exchange Server\Mailbox\First Storage Group\E00.CHK”.

$nl – a DOS newline

So, after all the preparation we’ve already done, an offline backup script actually turns out to be quite simple. The PowerShell script below will generate a DOS script to be executed by cmd.exe. Robocopy will copy all relevant files to the backup location specified by $destination. If all copies succeed then the script succeeds. If any copy fails, the script aborts.

A couple of things to be careful of – this script actually executes the backup! To do that, in an offline mode, the Microsoft Exchange Information Store service must be stopped. When that service is stopped, Exchange is basically down. So…don’t run this on your production system without commenting out the line that starts with cmd.exe (unless you are actually doing the offline backup).

Secondly, offline backups do not purge transaction logfiles. We’ll need to learn how to do an online backup before we can make that happen.

Finally, the out-file cmdlet uses an unusual parameter: “-encoding ascii”. This is because cmd.exe does not understand Unicode files (which is the default for out-file). Something to remember for your own scripts!

  
    $destination = "C:\backups"

    $nl = "`r`n"

    function buildRobocopyString($collection)
    {

        $str = ""

        foreach ($filepath in $collection)
        {
            $file = split-path $filepath -leaf
            $path = split-path $filepath -parent
            #
            # the destination path is the source path appended to
            # the backup folder location.
            #
            $destpath = join-path $destination $path.SubString(3, $path.Length - 3)

            $str += "echo Copying " + $file + "..." + $nl
            $str += "robocopy " + '"' + $path + '" "' + $destpath + 
                '" "' + $file + '" /copyall /ZB >nul' + $nl
            $str += "if not errorlevel 0 goto :abort" + $nl
        }

        return $str
    }

    function buildCMD
    {
        $script = "@echo off" + $nl

        $script += 'net stop "Microsoft Exchange Information Store" /y' + $nl

        $script += buildRobocopyString $pathPattern.keys
	$script += $nl
        $script += 'net start "Microsoft Exchange Information Store"' + $nl
        $script += "exit 0" + $nl
        $script += ":abort" + $nl
        $script += "exit 1" + $nl

        $script | out-file (join-path (gc env:temp) "offline-backup.cmd") -encoding ascii
        cmd.exe /c (join-path (gc env:temp) "offline-backup.cmd")
    }

    #
    # Main
    #

    if ((getStorageGroups) -eq 0)
    {
        getStores
        if ((validateArrays) -eq 0)
        {
            buildCMD
        }
    }

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter: @EssentialExch

Getting a List of Stores in a PowerShell Script

In my last post, Getting a List of Storage Groups in a PowerShell Script, you saw how to use the information from the Get-StorageGroup cmdlet to discover the particular disk volumes used by a storage group and to build a list of the files that were used by the storage group.

Now, that list of files did not contain the databases contained within the storage group. Instead, it simply contained the system file (Exx.CHK) for each storage group and the log files (Exx*.LOG) for each storage group. This is because the Get-StorageGroup cmdlet does not return us that information. Instead, we use the Get-MailboxDatabase and Get-PublicFolderDatabase cmdlets to find out the name of our databases. It’s unfortunate, but there is not a single Get-ExchangeDatabase cmdlet that combines the functionality of both,

Note that in Exchange Server 2007, each database consists of a single file, with an extension of EDB. In Exchange Server 2003, there was also a second file per database called the streaming file with an extension of STM.

To remind you of the global variables being used:

  • $computername is a string that contains the name of the computer on which the script is being executed.
  • $volumes is a hash array that contains a list of all the disk volumes so far detected.
  • $pathpattern is a hash array that contains a list of all the fully-qualified paths to all files so far discovered.

A store is always a simple filename. However, it’s important to remember that all of the file names that are used for naming stores and storage groups in Exchange Server may contain spaces and parentheses. That can lead to a requirement for special handling of file names.

When interrogating the list of stores on a particular server, any stores present in the Recovery Storage Group are also listed. These should be ignored.

Each cmdlet we use will return a collection. Get-MailboxDatabase will return a collection of all mailbox databases present on the given server. This collection could be empty even if storage groups are present. Get-PublicFolderDatabase will return a collection of all public folder databases present on the given server. This collection could also be empty (and, in fact, is more likely to be empty). The PowerShell code needs to be prepared to handle those eventualities.

In the PowerShell code below, the getStores function obtains all the stores on the server and adds the store filenames to the $pathpattern array and adds the disk volumes used by the stores to the $volumes array. Following getStores is the validateArrays function. If either the $volumes or the $pathpattern array is empty, it returns a value of 1 and displays a message on the PowerShell host. If both have contents, validateArrays returns a value of 0 and displays the contents of those arrays.

Without further ado:

function getStores
{
	## locate the databases, both mailbox and public folder

	$colMB = get-MailboxDatabase -server $computername
	$colPF = get-PublicFolderDatabase -server $computername

	## parse them for volumes too

	foreach ($mdb in $colMB)
	{
		if ($mdb.Recovery)
		{
			write-host ("Skipping RECOVERY MDB " + $mdb.Name)
			continue
		}
		write-host ($mdb.Name + "`t " + $mdb.Guid)
		write-host ("`t" + $mdb.EdbFilePath)
		write-host " "

		$pathPattern.($mdb.EdbFilePath) = 1

		$vol = $mdb.EdbFilePath.ToString().SubString(0, 1)
		$volumes.$vol += 1
	}

	foreach ($mdb in $colPF)
	{
		## a PF db can never be in a recovery storage group
		## which is why the Recovery check isn't done here

		write-host ($mdb.Name + "`t " + $mdb.Guid)
		write-host ("`t" + $mdb.EdbFilePath)
		write-host " "

		$pathPattern.($mdb.EdbFilePath) = 1

		$vol = $mdb.EdbFilePath.ToString().SubString(0, 1)
		$volumes.$vol += 1
	}

	return
}

function validateArrays
{
	$drives = $volumes.keys
	if ($drives.Count -lt 1)
	{
		write-host "No disk volumes were found. Aborting."
		return 1
	}

	write-host ("There were " + $drives.Count.ToString() + " disk volumes for Exchange server $computername. They are:")
	foreach ($drive in $drives)
	{
		write-host "`t$drive"
	}

	write-host " "

        $paths = $pathPattern.keys
        if ($paths.Count -lt 1)
        {
                write-host "No paths were found. Aborting."
                return 1
        }

	write-host ("There are " + $pathPattern.Count.ToString() + " directories to be backed up. They are:")
	foreach ($directory in $pathPattern.keys)
	{
		write-host "`t$directory"
	}
	write-host " "

	return 0
}

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter: @EssentialExch

Getting a List of Storagegroups in a PowerShell Script

In Getting Our Computername in a PowerShell Script, you learned how to do just that – store the running computer name (the short NetBIOS name) into a PowerShell variable. At the same time, you learned WHY and HOW it worked. From this point forward, I’ll presume that you’ve executed this particular PowerShell statement:

$global:computername = (gc env:computername)

Note that this causes the storage scope of the $computername variable to be global. It can be interrogated from any function as just $computername (as long as there are no variables that have the same name in a closer scope [such as local scope or function scope]), but for the global value to be updated requires the inclusion of the “global:” specifier.

Thankfully, when we install the Exchange Tools on a workstation or server, one of the Exchange cmdlets is get-storagegroup. To see a list of all the storage groups on all the (Exchange 2007+) servers in your Exchange organization, you just enter get-storagegroup at the prompt in an Exchange Management Shell (EMS).

So, are we done? Nope.

While that list is probably all we need if we are working from a command prompt, if we are writing a script, we are probably interested in more than just the displayed information. For example, consider these requirements of the things we may want to know:

  • where the system files for the storage groups are placed
  • where the log files for the storage groups are placed
  • the log file prefix for the storage groups
  • a list of all the disk volumes used by the storage groups

Using get-storagegroup plus a feature of PowerShell makes all of this pretty easy. The feature we will use is called an associative array. Also known as a hash array, for those of you with a Perl background. An associative array is basically a two-dimensional array that is mapped to a single dimensional array based on the index element. Unlike a normal array, an associative array can be indexed by almost anything – a string, a regular expression, an integer – anything you may want to use. In order to pull off this “magic”, an associative array actually consists of two parallel arrays – the keys array and the values array. Now, these arrays get indexed as you are used to – by integers, starting at zero. However, their contents may consist of any object.

Note: in Perl, associative arrays aren’t quite as flexible. In PowerShell, you can literally have an object as a key and an object as a value. This allows you the flexibility of storing arbitrarily complex values into an associative array. However, the search through the keys array is linear. Thus, you should probably keep the arrays fairly small.

While in the EMS, you may get the impression that get-storagegroup returns text, that would be incorrect. Instead, get-storagegroup returns a collection (which is a fancy word for an enumerable array) of objects of type Microsoft.Exchange.Data.Directory.SystemConfiguration.StorageGroup. That’s a mouthful. Let’s just say that it returns an array containing all of the storage group objects.

So, to process that array, you would do something like this:

$collection = get-storagegroup
foreach ($entry in $collection)
{
        ... do something with $entry ...
}

However, you are more likely to want to look at the storage groups on a particular server. So, that leads you to a construct like this:

$collection = get-storagegroup -server $computername
foreach ($entry in $collection)
{
        ... do something with $entry ...
}

Note the tie-in back to $computername! This will access the global $computername variable which has the name of the running computer stored within it.

Now, what does a storage group consist of? It really has only two things:

  • log files
  • system files

And each of those two things also has a prefix associated with them (which allows multiple storage groups to share a single directory). All log files have a particular format:

.LOG

All system files (there is only a single system file per storage group) have a particular format:

.CHK

A prefix follows this format:

[E ] | [R00]

In words, a prefix is either R00, which is exclusively for the Recovery Storage Group, of which there may be only one per server; or the letter ‘E’ followed by a two-digit number. The number represents the index of the storage group on the server, where the first storage group created is ’00’ the second is ’01’, etc. So the first storage group created on a server will have a prefix of E00.

For that same first storage group, the log files have a format of E00*.log and the system file is named E00.chk.

Note: the number of digits contained in went from five hexidecimal digits in Exchange 2003 (and all earlier versions) to eight hexidecimal digits in Exchange 2007+. Even with the change in log file size from 5 MB to 1 MB, this means that you get 80+ times as many log files before rollover in Exchange 2007+ as you did in earlier versions of Exchange Server.

Let’s see…what else? Oh yes – you want to ignore recovery storage groups. Except for recoveries from backup, you are not supposed to touch them.

Now, given all those above details, what can we do with them? This!

## $volumes will contain the volume letters used by all named
## files and directories.

$global:volumes = @{}

## any storage group will contain:
## a] a system file directory
## b] a log file directory
## c] a filename for each database within the SG
##
## $pathPattern contains the dos patterns of files in the storage group

$global:pathpattern = @{}		### Exx.chk, Exx*.log, *.edb

function getStorageGroups
{
        $count = 0
	#
	# locate the storage groups and their log files and system files
	#
	$colSG = get-StorageGroup -server $computername
	if ($colSG.Count -lt 1)
	{
		write-host "No storage groups found on server $computername"
		return 1
	}

	## parse the pathnames for each SG to determine what
	## volumes it stores data upon and what directories are used

	foreach ($sg in $colSG)
	{
		if ($sg.Recovery)
		{
			write-host ("Skipping RECOVERY STORAGE GROUP " + $sg.Name)
			continue
		}

                $count++

		$prefix  = $sg.LogFilePrefix
		$logpath = $sg.LogFolderPath.ToString()
		$syspath = $sg.SystemFolderPath.ToString()

		write-host $sg.Name.ToString() "`t" $sg.Guid.ToString()
		write-host "`tLog prefix:      $prefix"
		write-host "`tLog file path:   $logpath"
		write-host "`tSystem path:     $syspath"

		## E00*.log
		$pathpattern.(join-path $logpath ($prefix + "*.log")) = 1

		$vol = $logpath.SubString(0, 1)
		$volumes.$vol += 1

		## E00.chk
		$pathpattern.(join-path $syspath ($prefix + ".chk")) = 1

		$vol = $syspath.SubString(0, 1)
		$volumes.$vol += 1

		write-host " "
	}

	if ($count -lt 1)
	{
		write-host "No storage groups found on server $computername"
		return 1
	}

	return 0
}

This routine stores, for each storage group, the files that are contained within that storage group. It also stores away the disk volumes used by that storage group. For the write-host output of the function, you could surround the blocks by $debug conditional statements to minimize the output of the routine (or just remove them entirely).

So, what is contained within storage groups? Databases! In our next post in this series, you’ll learn how to deal with them programatically too.

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter: @EssentialExch

userAccountControl manipulation

The userAccountControl attribute, which resides on each user and computer object in an Active Directory forest, is responsible for, well, controlling lots of things about those accounts. For example it controls whether an account is locked out, or whether an account is disabled, or whether the password for the account expires.

The feature named User Account Control, introduced in Windows Vista, has nothing to do with the userAccountControl attribute. The naming collision is unfortunate.

The userAccountControl attribute is a bit-field attribute. This means that while many things are controlled by a single attribute value, each unique value can have an impact on an account. For information about all the possible values that the attribute can take, see KB 305144, How to use the UserAccountControl flags to manipulate user account properties.

In a forum I spend time with, a poster wanted to disable the “password never expires” flag on all the user accounts contained within an OU. Of course, you can do this manually, but that is subject to error and is very tedious. So, I provided them a PowerShell script to accomplish their objective. See below, and be aware that you can use the same techniques shown in this script to modify any bit-wise value.

You’ll note the use of “-band” and “-bxor” in the PowerShell script. These stand for “bit-wise AND” and similarly “bit-wise XOR”, respectively. The bit-wise operators ensure that each bit of a value is calculated against each corresponding bit in the paired value.

	$ou = "LDAP://cn=Users,dc=essential,dc=local"

	$ADS_UF_DONT_EXPIRE_PASSWD = 0x010000

	$objDomain = New-Object System.DirectoryServices.DirectoryEntry( $ou )
	$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
	$objSearcher.SearchRoot = $objDomain
	$objSearcher.Filter = "(&(objectCategory=person)(objectClass=user))"
	$results = $objSearcher.FindAll()

	foreach( $result in $results )
	{
		$user = [adsi] $result.Path
		$value = $user.userAccountControl.Item( 0 )

		( $user.Name.item( 0 ) + ' ' + $value.ToString() )

		if( ( $value -band $ADS_UF_DONT_EXPIRE_PASSWD ) -ne 0 )
		{
			$value = $value -bxor $ADS_UF_DONT_EXPIRE_PASSWD
			$user.userAccountControl = $value
			$user.SetInfo()
			( "`t" + $user.name + ' updated to $value' )
		}
	}

Until next time…

As always, if there are items you would like me to talk about, please drop me a line and let me know!


Follow me on twitter: @EssentialExch