April 2012 - Posts

In my last blog post, Determining the Exchange Version Without Using Get-ExchangeServer,I showed how to read the Exchange version from the registry. Shortly after posting that, the questions began to roll in about "how to make my PowerShell session behave like the Exchange Management Shell".

Well, it's pretty easy.

In the New-ExchangeConnection function, I use "dot-sourcing" to minimize the work. When you dot-source something, it means you are basically including the contents of the file into YOUR file, at that location. So, I use the same setup that EMS uses; just changed to work on the proper version of Exchange, plus the little extra magic that usually happens behind the scenes, but doesn't because it's happening in my script instead of in the "real" EMS.

So here it is!

###
### New-ExchangeConnection
###
### Create a connection to an Exchange server, in a version
### appropriate way.
###

function New-ExchangeConnection
{
	### Load the versioning function

	. .\Get-ExchangeVersion.ps1

	$exchangeVersion = Get-ExchangeVersion
	if( $exchangeVersion )
	{
		switch ( $exchangeVersion.Version )
		{
			'2007'
				{
					### This first segment is handled by a PSConsole file in EMS itself
					Get-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin -EA 0
					if ($?)
					{
						## snap-in already loaded, do nothing
					}
					else
					{
						Add-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin
					}

					### This line makes the rest of the magic happen
					. 'C:\Program Files\Microsoft\Exchange Server\bin\Exchange.ps1'


					return 1
				}
			'2010'
				{
					### with the advent of remote PowerShell in E14, almost all
					### of the magic gets hidden
					$credential = $null
					$global:remoteSession = $null
					. 'C:\Program Files\Microsoft\Exchange Server\V14\bin\RemoteExchange.ps1'
					Connect-ExchangeServer -auto

					return 1
				}
			default
				{
					write-error "Unsupported version $($exchangeVersion.Version)"
					return 0
				}
		}
	}

	write-error "This is not an Exchange server"
	return 0
}
Posted by michael | with no comments
Filed under: , ,

If you write lots of Exchange scripts (as I do), and have several different versions of Exchange on which you need to run those scripts (as I do); you soon see the need for being able to determine what version of Exchange a particular server is running - and you may need to do this outside of the Exchange Management Shell.

This may be necessary because of different behaviors that are required with the Exchange Management Shell depending on the version of Exchange. It also may be required because the version of Exchange pre-dates the Exchange Management Shell (i.e., Exchange 2003). As an additional rationale, your script may need to load the Exchange Management Shell and cannot do that properly without knowing the version of Exchange that is being targeted (the process differs between Exchange 2007 and Exchange 2010).

Thus, I've written a couple of functions that I wrap in a script to give me that information. Get-ExchangeServer, the function, returns a simple object containing the name of the server examined, plus the version of Exchange that is running on that server. That is:

struct result {

string Name;

string Version;

}

If the result of the function is $null, then the version could not be determined and the server targeted either does not exist or is (very likely) not running Exchange. If the server does not exist (or the firewall on the server prevents remote management via WMI) then an error is displayed.

The version information about Exchange is stored in the registry of each Exchange server. The script below shows some techniques for accessing the registry to obtain string (reg_sz) and short integer (reg_dword) values while using PowerShell. Note that PowerShell has multiple mechanisms for accessing this information, including the so-called Registry Provider. I personally find using the WMI functions to be a bit easier to handle.

You can include these functions directly into your PowerShell profile, or you can dot-source the function on an as-needed basis.

Without further ado, enjoy!

###
### Get-ExchangeVersion
###
### Return the version of the specified Exchange server
###

Set-StrictMode -Version 2.0

$HKCR = 2147483648
$HKCU = 2147483649
$HKLM = 2147483650

function RegRead
{
	Param(
		[string]$computer,
		[int64] $hive,
		[string]$keyName,
		[string]$valueName,
		[ref]   $value,
		[string]$type = 'reg_sz'
	)

	try {
		$wmi = [wmiclass]"\\$computer\root\default:StdRegProv"
		if( $wmi -eq $null )
		{
			return 1
		}
	}
	catch {
		##$error[0]
		write-error "Could not open WMI access to $computer"
		return 1
	}

	switch ( $type )
	{
		'reg_sz'
			{
				$r = $wmi.GetStringValue( $hive, $keyName, $valueName )
				$value.Value = $r.sValue
			}
		'reg_dword'
			{
				$r = $wmi.GetDWORDValue( $hive, $keyName, $valueName )
				$value.Value = $r.uValue
			}
		default
			{
				write-error "Unsupported type: $type"
			}
	}

	$wmi = $null

	return $r.ReturnValue
}

function Get-ExchangeVersion
{
	Param(
		[string]$computer = '.'
	)

	if( $computer -eq '.' )
	{
		$computer = $env:ComputerName
	}

	## Exchange vNext (assumption!)
	## HKLM\Software\Microsoft\ExchangeServer\v15\Setup
	## MsiProductMajor (DWORD 15)

	## Exchange 2010
	## HKLM\Software\Microsoft\ExchangeServer\v14\Setup
	## MsiProductMajor (DWORD 14)

	## Exchange 2007
	## HKLM\SOFTWARE\Microsoft\Exchange\Setup
	## MsiProductMajor (DWORD 8)

	## Exchange 2003
	## HKLM\SOFTWARE\Microsoft\Exchange\Setup
	## Services Version (DWORD 65)

	$v = 0

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v15\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 15 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = 'vNext'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v14\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 14 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2010'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\Exchange\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 8 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2007'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\Exchange\Setup' 'Services Version' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 65 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2003'
		return $obj
	}

	### almost certainly not an Exchange server

	return $null
}

One of the traditional issues associated with cleaning up an Active Directory Directory Services (AD DS) domain in DNS is to ensure that duplicate names in DNS are removed (this is typically an issue caused by not having DNS Scavenging enabled, or by having hosts forcefully removed from the domain and not properly cleaning up DNS). As a corollary, this can also lead to duplication of manually assigned IP addresses, regardless of whether those IP addresses are IPv4 or IPv6.

Duplications can cause issues for many different servers and services, including AD DS, Exchange, SharePoint, etc.

I've written a PowerShell script that can help you determine the duplicates in order to clean those up. See the script below!

## ## build-dns-objects.ps1 ## ## ## Michael B. Smith ## michael (at) TheEssentialExchange.com ## April, 2012 ## ## ## Primary functionality: ## ## Based on either an input file or the output of a default command: ## ## dnscmd ( $env:LogonServer ).SubString( 2 ) /enumrecords $env:UserDnsDomain "@" ## ## Create an array containing all of the DNS objects describing the input. ## ## ---- ## ## Secondary functionality: ## ## Find all the duplicate IP addresses and the duplicate names ## contained within either the file or the command output. ## ## By specifying the -skipRoot option, all records for the root of ## the domain are ignored. ## ## ## General record format returned by DNScmd.exe: ## ## name ## [aging:xxxxxxxx] ## TTL ## resource-record-type ## value ## [optional additional values] ## ## Fields may be separated by one-or-more spaces or one-or-more tabs ## [aging:xxxxxxxx] fields are optional ## Param( [string]$filename, [switch]$skipRoot ) function new-dns-object { return ( "" | Select Name, Aging, TTL, RRtype, Value ) } function tmpFileName { [string] $strFile = ( Join-Path $Env:Temp ( Get-Random ) ) + ".txt" if( ( Test-Path -Path $strFile -PathType Leaf ) ) { rm $strNetworkFile -EA 0 if( $? ) { ## write-output "...file was deleted" } else { ## write-output "...couldn't delete file, error: $($error[0].ToString())" } } return $strFile } if( $filename -and ( $filename.Length -gt 0 ) ) { $tmp = $filename } else { $tmp = tmpFileName dnscmd ( $env:LogonServer ).SubString( 2 ) /enumrecords $env:UserDnsDomain "@" >$tmp } $objects = @() $records = gc $tmp $script:zone = '' ## Primary functionality: foreach( $record in $records ) { ## write-output "Processing: $record" if( !$record ) { continue } if( $record -eq "Returned records:" ) { continue } if( $record -eq "Command completed successfully." ) { continue } $firstChar = $record.SubString( 0, 1 ) $record = $record.Trim() if( $record.Length -eq 0 ) { continue } $object = new-dns-object $index = 0 $record = $record.Replace( "`t", " " ) $array = $record.Split( ' ' ) if( ( $firstchar -eq " " ) -or ( $firstchar -eq "`t" ) ) { $object.Name = $script:Zone } else { $object.Name = $array[ 0 ] $script:Zone = $array[ 0 ] $index++ } if( ( $array[ $index ].Length –ge 3 ) –and ( $array[ $index ].SubString( 0, 3 ) –eq “[Ag” ) ) ## [Aging:3604987] { $object.Aging = $array[ $index ] $index++ } $object.TTL = $array[ $index ] $object.RRType = $array[ $index + 1 ] $object.Value = $array[ $index + 2 ] $objects += $object } ## Secondary functionality: ## There are more efficient ways to do this, but this is easy. ## search for duplicate names $hash = @{} foreach( $o in $objects ) { if( $o.RRtype -eq "A" ) { $name = $o.Name if( $skipRoot -and ( $name -eq "@" ) ) { continue } if( $hash.$name ) { "Duplicate name: $name, IP: $($o.Value), original IP: $($hash.$name)" } else { $hash.$name = $o.Value } } } $hash = $null ## search for duplicate IP addresses $hash = @{} foreach( $o in $objects ) { if( $o.RRtype -eq "A" ) { if( $skipRoot -and ( $o.Name -eq "@" ) ) { continue } $ip = $o.Value if( $hash.$ip ) { "Duplicate IP: $ip, name: $($o.Name), original name: $($hash.$ip)" } else { $hash.$ip = $o.Name } } } $hash = $null " " "Done"

I've been working with a company that is in the process of setting up a remote datacenter for disaster recovery. They brought me in to help design their Exchange cross-site resilience, and I've been helping them with a few other things too.

Primary connectivity between the primary datacenter and the remote datacenter is via an L2TP tunnel, nailed up and encrypted, across the public Internet, using RRAS.

With over 300 servers in the primary datacenter and over 100 servers targeted in the remote datacenter, we needed a way to set up the proper routes for each server farm to access the other (in both cases, the default gateway is a TMG array).

I decided that I could do that in a few lines of PowerShell. Truth be told, the basic functionality is pretty simple - you iterate through the computers you are examining, look at the wired network interfaces on those computers, see those that have a particular source IP address, and give them a route to the destination network.

However - and as always, the devil is in the details - It Ain't That Simple (IATS - HAH!).

First of all, Active Directory was used as the source of computers. That's great! However, there are computers in AD that aren't in DNS. I have no clue how that happened, but one can surmise that the computers were cut off, their IP addresses reassigned, and the computer object never removed from AD. That's one condition we have to deal with.

Secondly, as a corollary to the above, workgroup computers aren't found at all. With the solution presented here, there were already DNS entries for these computers (and there were only a handful of them), so we temporarily created computer objects for them in AD.

Third, we found some computers have bad IP addresses (specifically 0.0.0.0). Again, I have no clue how that happened, but those have to be filtered out.

Fourth, group policies in this organization aren't applied very logically. That's not what they called me in for, so I had little say. Regardless, a number of computer did not have remote management enabled, thus preventing remote examination of their configuration and remote changes to their configuration. Detection of computers without remote management thus became something the script had to deal with.

Fifth, we had first decided to use 'ping' to determine the accessibility of the computers as part of the discovery process. Well, fewer than half of the computers allowed 'ping' through their firewalls, so that solution had to be summarily discarded. In order to determine accessibility, we actually had to attempt to access the computers.

Sixth, and this is actually a downstream symptom of the issue above, on some servers remote management generates an error. Those servers need to be detected and repaired.

Seventh, and finally, some servers have multiple IP addresses. This may mean that they are already being used as RRAS servers and they need human intelligence to determine whether a route change should be applied.

I didn't use "route add" because in some situations "route add" is known to fail, and "netsh interface ipv4 add route" is preferred.

The resulting utility script is not particularly pretty. But it's very useful and the techniques illustrated are likely to be useful for you in your scripting needs too. So, I present it for your pleasure. :-)

[string] $nl = "`r`n"

$HKCR = 2147483648
$HKCU = 2147483649
$HKLM = 2147483650

$script:badIP         = @()
$script:commands      = @()
$script:cannotping    = @()
$script:cannotresolve = @()
$script:cannotmanage  = @()
$script:warningmsg    = @()

function msg
{
	$str = ''

	foreach( $arg in $args )
	{
		$str += $arg + ' '
	}
	Write-Host $str
}

function RegRead
{
	Param(
		[string]$computer,
		[int64] $hive,
		[string]$keyName,
		[string]$valueName,
		[ref]   $value,
		[string]$type = "REG_SZ"
	)

	$wmi = [wmiclass]"\\$computer\root\default:StdRegProv"
	if( $wmi -eq $null )
	{
		$script:cannotmanage += $computer
		return 1
	}

	$r = $wmi.GetStringValue( $hive, $keyName, $valueName )
	$value.Value = $r.sValue

	$wmi = $null

	return $r.ReturnValue
}


function test-ping( [string]$server )
{
	[string]$routine = "test-ping:"

	trap 
	{
		# we should only get here if the New-Object fails.

		write-error "$routine Cannot create System.Net.NetworkInformation.Ping for $server."
		return $false
	}

	$ping = New-Object System.Net.NetworkInformation.Ping
	if( $ping )
	{
		trap [System.Management.Automation.MethodInvocationException] 
		{
			###write-error "$routine Invalid hostname specified (cannot resolve $server)."
			msg "...cannot resolve $server in DNS"
			$script:cannotresolve += $server
			return $false
		}

		for( $i = 1; $i -le 2; $i++ )
		{
			$rslt = $ping.Send( $server )
			if( $rslt -and ( $rslt.Status -eq [System.Net.NetworkInformation.IPStatus]::Success ) )
			{
				### msg "$routine Can ping $server. Successful on attempt $i."
				$ping = $null
				return $true
			}
			sleep -seconds 1
		}
		$ping = $null
	}

	###write-error "$routine Cannot ping $server. Failed after 5 attempts."
	msg "...cannot ping $server"
	$script:cannotping += $server

	return $false
}

### 
### Main
###

$start = (get-date).DateTime.ToString()
msg 'Begin' $start

ipmo ActiveDirectory
$computers = Get-AdComputer -Filter * -SearchBase "OU=Servers,DC=example,DC=com" -ResultSetSize $null | Select DnsHostName
msg "There are $($computers.Count) computers to check"
$computerCount = 0
foreach( $computer in $computers)
{
	$computerName = $computer.DnsHostName
	$computerCount++
	msg "Checking $computername... ($computerCount of $($computers.Count))"

#	if( -not ( test-ping $computerName ) )
#	{
#		###msg '...cannot resolve or ping'
#		msg ' '
#		continue
#	}

	### server IP addresses - I only care about IPv4
	$serverIPv4   = @()
	$serverIPname = @()

	$nicSettings = gwmi Win32_NetworkAdapterSetting -EA 0 -ComputerName $computerName
	if( $nicSettings -eq $null )
	{
		msg "...cannot access $computername"
		msg " "
		$script:cannotresolve += $computername
		continue
	}

	foreach( $nicSetting in $nicSettings )
	{
		### msg "Element=" $nicSetting.Element ## is of type Win32_NetworkAdapter
		if( $nicSetting.Element -eq $null )
		{
			msg "...netSetting.Element is null"
			continue
		}

		$nicElement = [wmi] $nicSetting.Element
		if( $nicElement -eq $null )
		{
			msg "...nicElement is null"
			continue
		}

		if( $nicElement.AdapterType -eq "Ethernet 802.3" )
		{
			### msg "Setting=" $nicSetting.Setting
			$nicConfig = [wmi] $nicSetting.Setting ## is of type Win32_NetworkAdapterConfiguration
			$nicGUID = $nicConfig.SettingID
			### msg "NicGUID=" $nicGUID
			### msg "NIC IPEnabled=" $nicConfig.IPEnabled.ToString()
			if( $nicConfig.IPEnabled -eq $true )
			{
				$returnValue = $true ## there is at least one valid NIC

				$hive = $HKLM
				$keyName = "System\CurrentControlSet\Control\Network\" +
					"{4D36E972-E325-11CE-BFC1-08002BE10318}\" + 
					$nicGUID + 
					"\Connection"
				$valueName = "Name"

				$name = ''
				$result = RegRead $computerName $hive $keyName $valueName ( [ref] $name ) 'reg_sz'
				if( $result -ne 0 )
				{
					$name = ""
				}

				###msg "NIC name:" $name

				$script:arrNicName += $name
				foreach( $ip in $nicConfig.IPAddress )
				{
					if( $ip.IndexOf( ':' ) -ge 0 )
					{
						$script:arrIPListPublic_v6 += $ip
						###msg "IPv6 address " $ip
					}
					else
					{
						$script:arrIPListPublic_v4 += $ip
						$serverIPv4   += $ip
						$serverIPname += $name
						###msg "IPv4 address:" $ip
					}
				}
			}
			$nicConfig = $null
		}
		$nicElement = $null
	}
	$nicSettings = $null

	$limit = $serverIPv4.Count - 1

	$allowedAddresses = 0
	for( $i = 0; $i -le $limit; $i++ )
	{
		$ip = $serverIPv4[ $i ]
		$name = $serverIPname[ $i ]

		if( $ip.Length -lt 10 )
		{
			$script:badIP += "$computerName $name $ip"
		}
		elseif( $ip.SubString( 0, 10 ) -eq "10.129.59." )
		{
			msg "*** YES - $computerName is on the server room secure network using NIC '$name' ***"
			$cmd = 'netsh -r ' + $computerName + 
				' interface ipv4 add route 10.129.68.0/22 "' + $name + '" 10.129.59.104'
			$script:commands += $cmd
			$allowedAddresses++
		}
		elseif( $ip.SubString( 0, 10 ) -eq "10.129.68." )
		{
			msg "*** YES - $computerName is on the datacenter network using NIC '$name' ***"
			$cmd = 'netsh -r ' + $computerName + 
				' interface ipv4 add route 10.129.59.0/20 "' + $name +'" 10.129.68.7'
			$script:commands += $cmd
			$allowedAddresses++
		}
	}
	if( $allowedaddresses -gt 1 )
	{
		msg "*** WARNING ***"
		$m = "$computerName has more than one IPv4 address. It may require extra configuration."
		msg $m
		$script:warningmsg += $m
		msg "*** WARNING ***"
	}

	if( $allowedaddresses -eq 0 )
	{
		msg "...no 10.129.59.0/24 or 10.129.68.0/24 IP addresses found on this computer (out of $($serverIPv4.Count))"
	}

	msg " "
}

msg "List of computers and NICs with bad IP addresses ($($script:badIP.Count))"
$script:badIP
' '

msg "List of computers in AD who cannot be accessed ($($script:cannotresolve.Count))"
$script:cannotresolve
' '

#msg "List of computers in AD who cannot be pinged ($($script:cannotping.Count))"
#$script:cannotping
#' '

msg "List of computer in AD who cannot be managed ($($script:cannotmanage.Count))"
$script:cannotmanage
' '

msg "Warning messages ($($script:warningmsg.Count))"
$script:warningmsg
' '

msg "Full list of routing commands ($($script:commands.Count))"
$script:commands
' '

msg 'Began at' $start
msg 'Done at' (get-date).DateTime.ToString()

The System Center 2012 suite released to web (RTW'ed) today.

The suite will be officially announced at the Microsoft Management Summit on April 17th.

This includes Operations Manager, Configuration Manager, Service Manager, Data Protection Manager, Orchestrator, App Controller, and Virtual Machine Manager.

As some of you are aware, in additional to my Exchange and Active Directory work; I also develop and deliver customer System Center classes for customers all over the world. I just finished my first System Center 2012 Configuration Manager course last week (the last week of March 2012) and began my first System Center 2012 Operations Manager course today (April 2, 2012).

The new versions are evolutionary steps forward and include some greatly improved ease of use and integration features.

If you have access to MSDN, TechNet, Volume Licensing, or Action Pack; the System Center suite should be available for you to download and evaluate. Go check them out!

Posted by michael | with no comments