Enumerating networks and building routes with PowerShell

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()

 

Enumerating IP Addresses on Network Adapters using PowerShell

Regardless of whether a server is for Exchange, for SQL, or for any other use; a key component of the server’s configuration is the set of IP addresses assigned to it.

Many organizations apply all addresses (even server addresses) using DHCP. For servers, this is often based on reservations of the MAC addresses on those servers. In most cases, this works well. However, if the DHCP server(s) should be unavailable (for whatever reason), the server will not receive the desired address, and instead will receive an APIPA address (that is, an IPv4 address starting with “169.” as the first octet of the IPv4 address).

Regardless of the result, for many servers it is important to know if the IP addresses should change. The first step in that process is to be able to enumerate (list) all of the IP addresses for all of the network interfaces on the server.

There are a number of mechanisms available for obtaining this information from Windows; including but not limited to “ipconfig.exe”, WMI, “wmic.exe”, “netsh.exe”, and others. The challenge for using most of the available interfaces is that they provide a significant amount of extraneous information. Extraneous information makes the output very difficult to parse and use within scripts.

This blog post provides a script that enumerates all of the network interfaces available on a particular computer and the associated IP addresses. The second script, which you could schedule to run every 15-30 minutes, compares and contrasts any changes in IP addresses on a server and reports the change via a SMTP server (such as Exchange Server).

The first script:

 

###
### IP-Address-Check.ps1
###
### This script lists all Ethernet adapters supporting IP on a computer and
### the associated IP addresses - both IPv4 and IPv6. A simple object is
### output containing the name of the interface, an array containing the IPv4
### addresses, and an array containing the IPv6 addresses.
###
### While this information is available from netsh.exe, the format of the
### output from netsh.exe is not conducive for easy re-use or parsing.
###
### Michael B. Smith
### michael at smithcons dot com
### http://essential.exchange
### February 28, 2012
###

Set-StrictMode -Version 2.0

$HKLM = 2147483650
$wmi  = [wmiclass]"\root\default:StdRegProv"

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

	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
			}
		'reg_qword'
			{
				$r = $wmi.GetQWORDValue( $hive, $keyName, $valueName )
				$value.Value = $r.uValue
			}
		'reg_binary'
			{
				$r = $wmi.GetBinaryValue( $hive, $keyName, $valueName )
				$value.Value = $r.uValue
			}
		default
			{
				write-error "ERROR: RegRead $hive $keyName $valueName"
				return 1
			}
	}

	return $r.ReturnValue
}

function EnumerateAdapters
{
	$nicSettings = gwmi Win32_NetworkAdapterSetting -EA 0
	foreach( $nicSetting in $nicSettings )
	{
		$nicElement = [wmi] $nicSetting.Element
		if( $nicElement.AdapterType -eq "Ethernet 802.3" )
		{
			$nicConfig = [wmi] $nicSetting.Setting ## is of type Win32_NetworkAdapterConfiguration
			$nicGUID = $nicConfig.SettingID
			if( $nicConfig.IPEnabled -eq $true )
			{
				$hive = $HKLM
				$keyName = "System\CurrentControlSet\Control\Network\" +
					"{4D36E972-E325-11CE-BFC1-08002BE10318}\" + 
					$nicGUID + 
					"\Connection"
				$valueName = "Name"

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

				$arrIPListPublic_v6 = @()
				$arrIPListPublic_v4 = @()

				foreach( $ip in $nicConfig.IPAddress )
				{
					if( $ip.IndexOf( ':' ) -ge 0 )
					{
						$arrIPListPublic_v6 += $ip
					}
					else
					{
						$arrIPListPublic_v4 += $ip
					}
				}

				$obj = "" | select Name, IPv4Addresses, IPv6Addresses
				$obj.Name = $name
				$obj.IPv4Addresses = $arrIPListPublic_v4
				$obj.IPv6Addresses = $arrIPListPublic_v6

				$obj  ### output the object to the pipeline

				$arrIPListPublic_v4 = $null
				$arrIPListPublic_v6 = $null
			}

			$nicConfig = $null
		}
		$nicElement = $null
	}
	$nicSettings = $null
}

###
### Main
###

EnumerateAdapters

 

The second script:

 

###
### IP-Check-Controller.ps1
###
### Check to see if any IP addresses on a computer have changed. This may 
### be relevant in many configurations based on DHCP.
###
### Michael B. Smith
### michael at smithcons dot com
### http://essential.exchange
### February 28, 2012
###

Param(
	[string]$to  = "ip-report@example.com",
	[string]$mx  = "mx.example.com",
	[string]$sub = "The IP address information has changed at $($env:ComputerName)",
	[string]$fr  = "ip-check-controller@example.local"
)

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

###
### Main
###

if( ( Test-Path "Addr-old.txt" -PathType Leaf ) )
{
	rm "Addr-old.txt"
}
if( ( Test-Path "Addr-new.txt" -PathType Leaf ) )
{
	mv "Addr-new.txt" "Addr-old.txt"
}

.\IP-Address-Check | out-file "Addr-new.txt"

$result = Compare-Object -ReferenceObject $(gc "Addr-old.txt") -DifferenceObject $(gc "Addr-new.txt") -PassThru
if( $result -eq $null )
{
	## no changes to file
	exit
}

### if we get here, the files are different

$text =
	"A change has occurred in the IP addresses or active adapters.$nl$nl"+
	"The changes found are:$nl"

foreach( $line in $result )
{
	$text += "`t" + $line + $nl
}

$text += $nl + "The new full configuration is:$nl"
	$lines = gc "Addr-New.txt"
	foreach( $line in $lines )
	{
		$text += "`t$line$nl"
	}

$text += $nl + "The old full configuration is:$nl"
	$lines = gc "Addr-Old.txt"
	foreach( $line in $lines )
	{
		$text += "`t$line$nl"
	}

Send-MailMessage -To $to -Subject $sub -From $fr -Body $text -SmtpServer $mx -Priority High

 

Typically, instead of scheduling the second PowerShell script, you would schedule a BAT script, as shown below:

 

@echo off
REM
REM IP-Check-Controller.bat
REM
REM Michael B. Smith
REM michael at smithcons dot com
REM http://essential.exchange
REM February 28, 2012
REM

powershell.exe -command ".\IP-Check-Controller.ps1"

 

Until next time…

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

 


Follow me on twitter: @EssentialExch

Sending an email to users whose password is about to expire, a PowerShell Rewrite

In September of 2005, I wrote a blog post named “Sending an e-mail to users whose password is about to expire“. Written in VBScript, it was one of my most popular blog posts of all time. I still have clients of mine that use it and I get occaisional email questions regarding it.

However, it is certainly showing its age!

There are other solutions available now, for free. However, the other solutions don’t meet all of my needs. (As always, I encourage you to choose the solution that best meets your needs.)

In my case, I need to be able to support:

  • Fast and efficient searching of Active Directory
  • Support for Fine Grained Password Policies (FGPPs, also known as Password Settings Objects or PSOs)
  • Authenticated SMTP
  • SSL/TLS SMTP
  • Report to Administrative User only (via SMTP)
  • Report to Administrative User only (via console)
  • Report to end-user (via SMTP)

The script in this blog post meets all of those needs. And, it is now written in PowerShell instead of VBScript.

I also chose to use the .Net Framework (System.DirectoryServices) for access to Active Directory (as well as some ADSI), instead of using the Active Directory PowerShell cmdlets. This makes it possible to execute the script on pretty-much any domain-joined computer, instead of one that requires RSAT-ADDS to be installed. It also avoids some weirdness around certain values returned by the AD cmdlets not matching older cmdlets or the AD itself.

This script is designed to work with PowerShell v2.0. The only PowerShell v2.0 feature is use of the Send-MailMessage cmdlet and using splatting to call Send-MailMessage. If you need this on PowerShell v1.0, you must just write a replacement for Send-MailMessage (use System.Net.Mail – it’s not a big deal).

The script will work on any domain functional level (DFL). The DFL is relevant to whether Fine-Grained Password Policies (FGPP, also known as Password Settings Objects – PSOs) are in use or not. FGPPs can be used when the domain level is at Windows 2008 or higher.

Coming in at 767 lines, this is the longest single PowerShell script I believe I’ve posted. But it’s well documented and hopefully self-descriptive. There are some fairly advanced capabilities demonstrated in this script, so you may find it worthwhile to study it a bit. If you have questions, let me know.

###
### Send-MailToUsersWithExpiringPasswords
###
### The top third of the script is data acquisition (and well documented).
### The bottom two-thirds is simple email-sending and report writing.
###
### This is PowerShell v1 compatible EXCEPT for using Send-MailMessage. You can
### easily replace that using System.Net.Mail if you wish.
###
### Parameter information:
###	daysForEmail   - how many days before a password expires should a user receive warning emails
###	adminEmail     - the administrator's email address
###	adminEmailOnly - do not send email to users, only report to the administrator
###	SMTPfrom       - the From address for the SMTP message(s)
###	SMTPserver     - the server to be used for sending the SMTP message(s)
###	SMTPuser       - if credentials are required, the user for authenticating to the SMTP server
###	SMTPpassword   - if credentials are required, the password for the SMTPuser
###	anr            - instead of searching all users, only search for users matching the specified ANR string
###	SMTPuseSSL     - use an SSL/TLS connection, not a clear-text SMTP connection
###	Quiet          - if NOT set, a copy of the admin report is dumped to the pipeline as text
###	DontSendEmail  - Email is never sent to either users or admin
###

Param(
	[int]$daysForEmail = 14,
	[string]$adminEmail = "michael@smithcons.com",
	[switch]$adminEmailOnly,
	[string]$SMTPfrom,
	[string]$SMTPserver,
	[string]$SMTPuser,
	[string]$SMTPpassword,
	[string]$anr,
	[switch]$SMTPuseSSL,
	[switch]$Quiet,
	[switch]$DontSendEmail
)

### Using Set-StrictMode helps protect against wonky errors that get caught by the
### compiler in compiled languages. Specifically (from the helpfile for the cmdlet):
### -- Prohibits references to uninitialized variables (including uninitialized 
###    variables in strings).
### -- Prohibits references to non-existent properties of an object.
### -- Prohibits function calls that use the syntax for calling methods.
### -- Prohibits a variable without a name (${}).
###
### However, using strict mode means that extra care has to be taken when using
### hashtables and property value collections. You see that in this script every
### time you see the Item() accessor method being used.

Set-StrictMode -Version 2.0

### For information about ANR, see "Ambiguous Name Resolution" in
### http://technet.microsoft.com/en-us/library/cc978014.aspx

$domainObject = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$domainName   = $domainObject.Name
$domainRoot   = "LDAP://" + $domainName
$domainADSI   = [ADSI]$domainRoot
$domainMode   = $domainADSI.'msDS-Behavior-Version' ## Windows2008Domain is 3.

### domainMode is a PITA. System.DirectoryServices.ActiveDirectory.Domain.DomainMode and
### Microsoft.ActiveDirectory.Management.ADDomainMode have different values for the same
### enums!
###
### The first type is returned by System.DirectoryServices, the second type by the
### Get-ADDomain PowerShell cmdlet.
###
### That is why I ignore both of those potential access methods and use ADSI to access the
### value directly from the domain object. For specific information about those values, see:
### http://msdn.microsoft.com/en-us/library/cc223742(v=prot.10).aspx

[System.Int64]$Script:MaxPasswordAge = 0

function GetMaximumPasswordAge
{
	###
	### GetMaximumPasswordAge
	###
	### Retrieve the maximum password age that is set on the domain object. This is
	### normally set by the "Default Domain Policy".
	###

	if( $Script:MaxPasswordAge )
	{
		### Cache the value so that it only has to be retrieved once, converted
		### to an int64 once, and converted to days once. Win-win-win.

		return $Script:MaxPasswordAge
	}

	### Dealing with ADSI unfortunately also means dealing with COM objects.
	### Using ConvertLargeIntegerToInt64 takes the COM object and converts 
	### it into a native .Net type.

	[System.Int64]$Script:MaxPasswordAge = $domainADSI.ConvertLargeIntegerToInt64( $domainADSI.maxPwdAge.Value )

	### Convert to days
	### 	there are 86,400 seconds per day (24 * 60 * 60)
	### 	there are 10,000,000 nanoseconds per second

	[System.Int64]$Script:MaxPasswordAge = ( -$Script:MaxPasswordAge / ( 86400 * 10000000 ) )

	return [System.Int64]$Script:MaxPasswordAge
}

function newSecurePassword( [string]$password )
{
	###
	### newSecurePassword
	###
	### Take the normal string password provided and turn it into a 
	### secure string that can be used to set credentials.
	###
	### In PowerShell v2.0, this can be done with ConvertTo-SecureString.
	### That cmdlet isn't available in v1.0 though.
	###

	$secure = New-Object System.Security.SecureString

	$password.ToCharArray() |% { $secure.AppendChar( $_ ) }

	return $secure
}

function newPSCredential( [string]$username, [string]$password )
{
	###
	### newPSCredential
	###
	### Create a new PSCredential object containing the provided
	### username and plain-text password.
	###

	$pass = newSecurePassword $password

	$cred = New-Object System.Management.Automation.PSCredential( $username, $pass )
	$pass = $null

	return $cred
}

###
### We need to find all those user's who:
###	Are normal users                       0x00000200 ADS_UF_NORMAL_ACCOUNT
###	Are not disabled                       0x00000002 ADS_UF_ACCOUNTDISABLE
###	Do not have "password never expires"   0x00010000 ADS_UF_DONT_EXPIRE_PASSWD
###	Do not have "no password required"     0x00000020 ADS_UF_PASSWD_NOTREQD
###
###	Once we have the users, determine whether the user has a PSO
###	by examining msDS-PSOApplied (if the domain mode of the executing domain
###	is at Windows2008Domain or higher).
###
###	If the user has a PSO, load the PSO (and store it to a hashtable)
###	and evaluate the user's password against the PSO.
###
###	If the user does not have a PSO, evalute the user's password
###	against the Default Domain Policy Maximum Password Age (which
###	is found by the function GetMaximumPasswordAge above).
###
### The most efficient way to find users is to do an LDAP search. But building the
### proper search isn't that easy. We need a number of filters:
###
###	objectCategory=Person
###	(userAccountControl & ADS_UF_NORMAL_ACCOUNT)     <> 0
###	(userAccountControl & ADS_UF_ACCOUNTDISABLE)     == 0
###	(userAccountControl & ADS_UF_DONT_EXPIRE_PASSWD) == 0
###	(userAccountControl & ADS_UF_PASSWD_NOTREQD)     == 0
###
### Since userAccountControl is a BIT-FLAG attribute (meaning that individuals bits
### of the value control these options) then we need to be able to do a bit-wise
### LDAP search. It's important to realize that with a bit-wise search, the result
### of a particular filter is either not-one (!1, which is false) or not-zero (!0,
### which is true).
###
### So the next step in building our query is to use the LDAP bit-wise AND filter:
###
###	1.2.840.113556.1.4.803
###
### This is used to do a bit-wise AND of the attribute on the left to the value
### on the right. For example:
###
###	attribute:1.2.840.113556.1.4.803:=1024
###
### This means a bit-wise AND is done of the value of the attribute to the value
### 1024 (which is 0x400 in hexadecimal). If the result of that bit-wise AND is
### zero, then the value of the filter is false. If the result is non-zero, then
### the value of the filter is true. Putting a "!" in front of a false result
### makes the result true. Putting a "!" in front of a true result, makes it false.
###
### A combination of these two techniques makes it possible to scan for zero and
### non-zero bits (that is, those which are set to one and those which are set to
### zero).
###
### LDAP also supports a bit-wise OR filter, using the special value of:
###
###	1.2.840.113556.1.4.804
###
### Given the presence of AND and OR filters, it is possible to build very complex
### combination filters.
###
### A combination filter is built up of individual filters combined with either a
### logical OR ("|") or a logical AND ("&") and surrounded by parentheses.
###
###	(&
###		(objectCategory=Person)
###		(userAccountControl:1.2.840.113556.1.4.803:=512)
###		(!userAccountControl:1.2.840.113556.1.4.803:=2)
###		(!userAccountControl:1.2.840.113556.1.4.803:=65536)
###		(!userAccountControl:1.2.840.113556.1.4.803:=32)
###	)
###
### So, in pseudo-C code this is:
###
###	if( ( objectCategory == Person ) AND
###	    ( ( userAccountControl & ADS_UF_NORMAL_ACCOUNT     ) != 0 ) AND
###	    ( ( userAccountControl & ADS_UF_ACCOUNTDISABLE     ) == 0 ) AND
###	    ( ( userAccountControl & ADS_UF_DONT_EXPIRE_PASSWD ) == 0 ) AND
###	    ( ( userAccountControl & ADS_UF_PASSWD_NOTREQD     ) == 0 ) )
###	{
###		### we've got a matching user!
###	}
###
$ldapFilter =   "(&"							      +
			"(objectCategory=Person)"                             +
			"(userAccountControl:1.2.840.113556.1.4.803:=512)"    +
			"(!userAccountControl:1.2.840.113556.1.4.803:=2)"     +
			"(!userAccountControl:1.2.840.113556.1.4.803:=65536)" +
			"(!userAccountControl:1.2.840.113556.1.4.803:=32)"

if( $anr )
{
	###
	### using an ANR subquery allows us to reduce the result set from the LDAP query
	###
	$ldapFilter += "(anr=$anr)"
}

$ldapFilter += ")"

###
### build the LDAP search
###

$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.PageSize    = 1000
$directorySearcher.SearchRoot  = $domainRoot
$directorySearcher.SearchScope = "subtree"
$directorySearcher.Filter      = $ldapFilter

###
### load the properties we want
###

$directorySearcher.PropertiesToLoad.Add( "displayName"        ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "mail"               ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "pwdLastSet"         ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "sAMAccountName"     ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "userAccountControl" ) | Out-Null

if( $domainMode -ge 3 )
{
	### this attribute is only valid on Windows2008Domain and above
	$directorySearcher.PropertiesToLoad.Add( "msDS-PSOApplied" ) | Out-Null
}

$users = $directorySearcher.FindAll()

###
### All the data is in $users (or will be paged into $users).
### Build the necessary reports and emails.
###

$crnl = "`r`n"
$script:adminReport = ""

function line
{
	foreach( $arg in $args )
	{
		$script:adminReport += $arg + $crnl
	}
}

$now = Get-Date
$maximumPasswordAge = GetMaximumPasswordAge

line ( "Admin Report - Send Mail to Users with Expiring Passwords - Run Date/Time: " + $now.ToString() )
line " "
line "Parameters:"
line "        Days warning for sending email: $daysForEmail"
line "        Administrator email: $adminEmail"

if( $DontSendEmail )
{
	line "        Email will not be sent to either the administrator email or to user's email"
}
elseif( $adminEmailOnly )
{
	line "        Only the administrator will be sent email"
}

if( ![System.String]::IsNullOrEmpty( $SMTPfrom ) )
{
	line "        SMTP From address: $SMTPfrom"
}

if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
{
	line "        SMTP server: $SMTPserver"
}

if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
{
	line "        SMTP user: $SMTPuser"
}

if( ![System.String]::IsNullOrEmpty( $SMTPpassword ) )
{
	line "        SMTP password: $SMTPpassword"
}

if( $SMTPuseSSL )
{
	line "        UseSSL with SMTP is set"
}

line "        Maximum Password Age in Default Domain Policy = $maximumPasswordAge"
line "        Domain Mode $($domainObject.DomainMode.ToString())"
line ( "        User count = " + $users.Count.ToString() )

if( [System.String]::IsNullOrEmpty( $SMTPserver ) -and [System.String]::IsNullOrEmpty( $PSEmailServer ) -and !$DontSendEmail )
{
	Write-Error "No email server was specified via the SMTPserver parameter or the `$PSEmailServer environment variable."
	exit
}

if( ![System.String]::IsNullOrEmpty( $SMTPuser ) -and [System.String]::IsNullOrEmpty( $SMTPpassword ) )
{
	Write-Error "No SMTPpassword was specified."
	exit
}

if( ![System.String]::IsNullOrEmpty( $SMTPpassword ) -and [System.String]::IsNullOrEmpty( $SMTPuser ) )
{
	Write-Error "No SMTPuser was specified."
	exit
}

if( [System.String]::IsNullOrEmpty( $SMTPfrom ) )
{
	$SMTPfrom = "Password Administrator "
}

###
### some of the bitflags attached to the userAccountControl attribute
###

$ADS_UF_NORMAL_ACCOUNT     = 0x00200
$ADS_UF_ACCOUNTDISABLE     = 0x00002
$ADS_UF_DONT_EXPIRE_PASSWD = 0x10000
$ADS_UF_PASSWD_NOTREQD     = 0x00020

$psoCache = @{}

foreach( $user in $users )
{
	###
	### we spend some time being pretty careful dealing with the properties. this
	### allows us to verify we get the attributes we want, and for those that were
	### not present, we can establish reasonable defaults.
	###

	line " "

	$propertyBag = $user.properties
	if( !$propertybag )
	{
		line "error! null propertybag!"
		continue
	}

	$mail = ""
	$disp = ""
	$sam  = ""
	$pls  = 0
	$pso  = 0
	$uac  = 0
	$pwdX = $null

	$dispObj = $propertyBag.Item( 'displayname' )
	if( $dispObj -and ( $dispObj.Count -gt 0 ) )
	{
		$disp = $dispObj.Item( 0 )
		if( !$disp ) { $disp = "" }
	}
	$dispObj = $null
	### line "displayname = $disp"

	$samObj = $propertyBag.Item( 'samaccountname' )
	if( $samObj -and ( $samObj.Count -gt 0 ) )
	{
		$sam = $samObj.Item( 0 )
		if( !$sam ) { $sam = "" }
	}
	else
	{
		$sam = ""
	}
	$samObj = $null
	### line "sam = $sam"

	$uacObj = $propertyBag.Item( 'useraccountcontrol' )
	if( $uacObj -and ( $uacObj.Count -gt 0 ) )
	{
		$uac = $uacObj.Item( 0 )
	}
	else
	{
		line "no uac for $sam, assumed 0x200"
		$uac = $ADS_UF_NORMAL_ACCOUNT
	}
	$uacObj = $null
	### line "uac = $uac"

	$plsObj = $propertyBag.Item( 'pwdlastset' )
	if( $plsObj -and ( $plsObj.Count -gt 0 ) )
	{
		$pls = $plsObj.Item( 0 )
	}
	else
	{
		### this can be a normal occurence if the password has never been set
		$pls = 0
	}
	$plsObj = $null
	### line "pls = $pls"

	line ( $sam.PadRight( 21 ) + $pls.ToString().PadRight( 21 ) + $uac.ToString().PadRight( 8 ) + '0x' + $uac.ToString('x').PadRight( 8 ) )

	if( $pls -eq 0 )
	{
		line ( " " * 8 + "The password has never been set for this user, skipped" )
		continue
	}

	[System.Int64]$localMaxAge = $maximumPasswordAge

	if( $domainMode -ge 3 )
	{
		###
		### PSOs can be created on Server 2003, but they don't work properly until
		### the domain mode is Windows2008Domain or higher.
		###
		### So if we find a PSO and the domain mode is Windows2008Domain or higher,
		### we first determine whether we've seen this PSO before. If we have seen
		### the PSO before, then we've stored the msDS-MaximumPasswordAge value for
		### the PSO into a hash table for quick retrieval. If we have not seen the
		### PSO before, then we use ADSI to load the PSO and retrieve the value for
		### msDS-MaximumPasswordAge and then cache the value for future access. By
		### using a cache, we only have to access Active Directory to obtain values
		### once per PSO, leading to a significant performance improvement compared
		### to using the native cmdlets or S.DS.
		###

		$psoObj = $propertyBag.Item( 'msds-psoapplied' )
		if( $psoObj -and ( $psoObj.Count -gt 0 ) )
		{
			$pso = $psoObj.Item( 0 )
			### line ( "PSO object/name" + $pso )

			if( $psoCache.Item( $pso ) )
			{
				[System.Int64]$localMaxAge = $psoCache.Item( $pso )
				### line ( " " * 8 + "Accessed PSO from cache = " + $pso )
			}
			else
			{
				$psoADSI = [ADSI]( "LDAP://" + $pso )
				$ageOBJ = $psoADSI.'msDS-MaximumPasswordAge'

				[System.Int64]$localMaxAge = $psoADSI.ConvertLargeIntegerToInt64( $ageOBJ.Value )

				### Convert to days
				### 	there are 86,400 seconds per day (24 * 60 * 60)
				### 	there are 10,000,000 nanoseconds per second
				$localMaxAge = ( -$localMaxAge / ( 86400 * 10000000 ) )

				$psoCache.$pso = $localMaxAge
				### line ( " " * 8 + "Stored PSO to cache = " + $pso )

				$ageOBJ  = $null
				$psoADSI = $null

				### line "localMaxAge = $localMaxAge"
			}
		}
		else
		{
			### completely normal to not have a PSO, in that case, use the maxPwdAge
			### from the default domain policy.
			$pso = $null
			### line "pso is null"
		}
		$psoObj = $null
	}

	### In an Exchange environment, the 'mail' attribute contains the primary SMTP
	### address for a user. In a non-Exchange environment, that should also be true,
	### but no system process validates it. We do presume that the mail address is
	### valid, if present.

	$mailObj = $propertyBag.Item( 'mail' )
	if( $mailObj -and ( $mailObj.Count -gt 0 ) )
	{
		$mail = $mailObj.Item( 0 )
	}
	else
	{
		$mail = ''
	}

	if( 0 )
	{
		### The conditions reported on below cannot occur based on our LDAP filter
		### But they helped me develop and test the LDAP filter. :-)

		if( $uac -band $ADS_UF_NORMAL_ACCOUNT )
		{
			"`t`tNormal account"
		}
		else
		{
			"`t`tNot a normal account"
		}
		if( $uac -band $ADS_UF_ACCOUNTDISABLE )
		{
			"`t`tAccount disabled"
		}
		else
		{
			"`t`tAccount enabled"
		}
		if( $uac -band $ADS_UF_DONT_EXPIRE_PASSWD )
		{
			"`t`tPassword doesn't expire"
		}
		else
		{
			"`t`tPassword expires"
		}
		if( $uac -band $ADS_UF_PASSWD_NOTREQD )
		{
			"`t`tPassword not required"
		}
		else
		{
			"`t`tPassword required"
		}
	}

	### If we get here, $pls is non-zero.
	###
	### $pls comes to us in FileTime format (the number of 100 nansecond ticks 
	### since Jan 1, 1601). So it must be converted to DateTime and adjusted 
	### for the normal clock in order for us to do our arithmetic on it. For
	### more information about FileTime, see:
	### msdn.microsoft.com/en-us/library/windows/desktop/ms724290(v=vs.85).aspx

	$date = [DateTime]$pls
	$passwordLastSet = $date.AddYears( 1600 ).ToLocalTime()
	$passwordExpires = $passwordLastSet.AddDays( $localMaxAge )

	line ( " " * 8 + "The password was last set on " + $passwordLastSet.ToString() )

	if( $now -gt $passwordExpires )
	{
		line ( " " * 8 + "The password already expired on $($passwordExpires.ToString()). No email will be sent.")
		continue
	}
	else
	{
		line ( " " * 8 + "The password will expire on "  + $passwordExpires.ToString() )
	}

	$difference = $passwordExpires - $now
 	$days       = $difference.Days.ToString()
	$hours      = $difference.Hours.ToString()
	$minutes    = $difference.Minutes.ToString()

	line ( " " * 8 + "(This is in $days days, $hours hours, $minutes minutes)" )

	if( $difference.Days -le $daysForEmail )
	{
		if( [System.String]::IsNullOrEmpty( $mail ) -and !$adminEmailOnly )
		{
			line ( " " * 8 + "Oops - user doesn't have an email address." )
			continue
		}

		if( $DontSendEmail )
		{
			line ( " " * 8 + "Oops - we aren't supposed to send an email." )
			continue
		}

		line ( " " * 8 + "This user will be sent an email for password change." )

		$hash = @{}
		$hash.To       = $adminEmail
		$hash.Priority = 'High'
		$hash.From     = $SMTPfrom

		if( ![System.String]::IsNullOrEmpty( $disp ) )
		{
			$hash.Subject = "Warning! The network password for $disp ($sam) is about to expire."
		}
		else
		{
			$hash.Subject  = "Warning! The network password for $sam is about to expire."
		}

		if( !$adminEmailOnly -and ![System.String]::IsNullOrEmpty( $mail ) )
		{
			$hash.CC = $mail
		}

		if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
		{
			$hash.SmtpServer = $SMTPserver
		}
		elseif( ![System.String]::IsNullOrEmpty( $PSEmailServer ) )
		{
			$hash.SmtpServer = $PSEmailServer
		}

		###
		### Send-MailMessage will default to using $PSEmailServer when no other SMTP server is specified.
		### We checked earlier to ensure that at least one of those was specified.
		###

		if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
		{
			###
			### If SMTPuser is specified then SMTPpassword is also specified.
			### We checked earlier to make certain that if one was specified,
			### then both were specified.
			###

			$hash.Credential = newPSCredential $SMTPuser $SMTPpassword
		}

		if( $SMTPuseSSL )
		{
			$hash.UseSSL = $true
		}

		$bodyHeader = @"
WARNING!

"@
		$bodyHeader += "`r`nFor network user id: "
		if( ![System.String]::IsNullOrEmpty( $disp ) )
		{
			$bodyHeader += $disp + " (" + $sam + ")"
		}
		else
		{
			$bodyHeader += $sam
		}

		$hash.Body = @"
$bodyHeader

Your password is about to expire in $days days, $hours hours, $minutes minutes. 

Please change it now!

Thank you,
Your System Administrator
"@

		###
		### This is V2.0 function. I should replace it with something v1.0 compatible.
		### (splatting is also V2.0 only, which is why I'm lazy and didn't replace it.)
		###

		Send-MailMessage @hash

		$hash = $null
	}
}

###
### Invidual report emails complete.
### Now send summaries to the console and/or to the administrator email address.
###

line " "

if( !$quiet )
{
	###
	### If $quiet is NOT set, then dump the report to the console, as well
	### as sending the email to the administrator.
	###
	$script:adminReport
}

if( !$DontSendEmail )
{
	$hash = @{}
	$hash.To       = $adminEmail
	$hash.Priority = 'High'
	$hash.From     = $SMTPfrom
	$hash.Subject  = "Admin Report - Send Mail to Users with Expiring Passwords - Run Date/Time: " + $now.ToString()
	$hash.Body     = $adminReport

	###
	### Send-MailMessage will default to using $PSEmailServer when no other SMTP server is specified.
	### We checked earlier to ensure that at least one of those was specified.
	###

	if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
	{
		$hash.SmtpServer = $SMTPserver
	}
	elseif( ![System.String]::IsNullOrEmpty( $PSEmailServer ) )
	{
		$hash.SmtpServer = $PSEmailServer
	}

	if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
	{
		###
		### If SMTPuser is specified then SMTPpassword is also specified.
		### We checked earlier to make certain that if one was specified,
		### then both were specified.
		###

		$hash.Credential = newPSCredential $SMTPuser $SMTPpassword
	}

	if( $SMTPuseSSL )
	{
		$hash.UseSSL = $true
	}

	Send-MailMessage @hash

	$hash = $null
}

###
### Clean up a bit.
###

$now   = $null
$users = $null

$psoCache        = $null
$directorySearch = $null
$domainADSI      = $null
$domainObject    = $null

$script:adminreport = $null

### Done.

Until next time…

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


Follow me on twitter!  – @EssentialExch

Creating Explicit Credentials in PowerShell for WMI, Exchange, Lync, Remoting, etc.

When creating PowerShell cmdlets for any Microsoft technology – WMI, Exchange, Lync, etc. – it is common to need to provide credentials that are different from the default credentials. This can be even more important when you are using PowerShell remoting to connect to a remote computer.

However, using the built-in cmdlet Get-Credential causes a dialog box to be opened on the console! (And it will simply fail in some cases, when the internal PowerShell $host.UI.PromptForCredential interface has not been implemented.) This is certainly not something that you want to happen when your PowerShell script is being called with remote PowerShell or from a service, or in many other scenarios.

The solution is to pass in the full credential, already containing the secure password and the user names and (optionally) the domain or a user principal name. This is a bit challenging, as the constructor for a secure string doesn’t provide you an option for passing in an entire password. Therefore, you must build the secure string one character at a time.

The two functions below make the process easy.

Note: the $username parameter to newPSCredential can be in several formats: a plain username, a domain\username, or username@domain.com, or computername\username (for a local user).

Note 2: some functions want a NetworkCredential instead of a PSCredential. Creating one of those is as simple as changing System.Management.Automation.PSCredential to System.Net.NetworkCredential.

Note 3: as a security best practice, after you call the newPSCredential function, you should ensure that the plain text password is no longer available in the calling routine.

Enjoy!

function newSecurePassword( [string]$password )
{
        ###
        ### newSecurePassword
        ###
        ### Take the normal string password provided and turn it into a 
        ### secure string that can be used to set credentials.
        ###

        $secure = new-object System.Security.SecureString

        $password.ToCharArray() |% { $secure.AppendChar( $_ ) }

        return $secure
}

function newPSCredential( [string]$username, [string]$password )
{
	###
	### newPSCredential
	###
	### Create a new PSCredential object containing the provided
	### username and plain-text password.
	###
        $pass = newSecurePassword $password

        $cred = new-object System.Management.Automation.PSCredential( $username, $pass )

        $cred
}

 

Until next time…

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


Follow me on twitter! : @EssentialExch

Generating a report on Distribution Groups and their Membership, v2

In August of 2010, I posted Generating a report on Distribution Groups and their Membership. For most people, that script worked just fine.

However, it had some issues:

  • Large groups would cause PowerShell to generate an error about concurrent pipelines
  • The script generated string output instead of object output

This new version fixes those issues and adds ‘help’ content for the script. The script is below, but the information about the reasoning is copied directly from the original post, modified appropriately.

A common request is to get a list of all distribution groups and the members contained in that distribution group.

Note: a distribution group may also be a security group. From an Exchange Server perspective, the important thing is whether the group is mail-enabled or not.

You can take this report and pipe it to out-file in order to save the output to a disk file. Then you can inspect the file later, email it, copy-n-paste it, whatever you want. Export-Csv and Export-CliXml are also good options for exporting this data, especially since there is the potential for the contents of several arrays to be output.

Here is the script:

##
## Report-DistributionGroupsAndMembers.ps1
## v2.0
##
## Michael B. Smith
## http://TheEssentialExchange.com
## August, 2010
## July, 2011
##
## Requires the Exchange Management Shell
## As of version 2.0, requires PowerShell 2.0
## Tested on both Exchange 2007 and Exchange 2010
##

#requires -Version 2.0

if( $args )
{
	if( ( $args.Length -eq 1 ) -and
	    ( ( $args[0] -eq "-?" ) -or
	      ( $args[0] -eq "-help" ) ) )
	{
		@"

NAME
	Report-DistributionGroupsAndMembers

SYNOPSIS
	This script outputs information about all distribution groups. The 
	information includes:

	GroupName    - The name of the distribution group
	Identity     - The unique identity of the group (an X400 identifier)
	ManagerNames - An array containing the names of the managers for the
		group. On Exchange 2007, this will contain a maximum of one
		entry. On Exchange 2010, there may be many entries. It is
		also possible (and quite likely in some environments), for
		this array to be empty.
	ManagerCanonicalNames - An array containing the canonical names
		(also known as the relative distinguished name) for the
		managers of this group. It will match, index by index, the
		contents of ManagerNames.
	Members      - An array containing the names of all the members of
		this group. It is also possible for this array to be empty.

	This script must be executed from within an Exchange Management Shell. 

SYNTAX
	Report-DistributionGroupsAndMembers [-help]

"@ 		| out-default

		return
	}

	throw "No parameters are allowed for this script"
}

$distributionGroups = Get-DistributionGroup -ResultSize Unlimited

foreach( $distributionGroup in $distributionGroups )
{
	## not all of these temporaries are necessary, but it
	## simplifies understanding the PowerShell code

	$groupName  = $distributionGroup.Name
	$groupID    = $distributionGroup.Identity

	$managedBy  = $distributionGroup.ManagedBy
	$mgrName    = @()
	$mgrCName   = @()

	if( $managedBy -is [Microsoft.Exchange.Data.Directory.ADObjectId] )
	{
		$mgrName  += $managedBy.Name
		$mgrCName += $managedBy.RDN.ToString().SubString(3)
	}
	elseif( $managedBy.Count -gt 0 )
	{
		foreach( $manager in $managedBy ) 
		{
			$mgrName  += $manager.Name
			$mgrCName += $manager.RDN.ToString().SubString(3)
		}
	}

	$membersArray = @()

	$members = Get-DistributionGroupMember -Identity $groupID -ResultSize Unlimited
	foreach( $member in $members )
	{
		$membersArray += $member.Name
	}

	$members = $null

	$hash = @{
		GroupName		= $groupName
		Identity		= $groupID
		ManagerNames		= $mgrName
		ManagerCanonicalNames	= $mgrCName
		Members			= $membersArray
	}

	New-Object PSObject -Property $hash	## inject to the pipeline
}

$distributionGroups = $null

 

Until next time…

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


Follow me on twitter! : @EssentialExch

Finding Disk Space Used By Exchange, v2

Long ago and far away (way back in 2006) I wrote an article for finding the disk space used by Exchange 2000 and Exchange 2003, Finding disk space used by Exchange. While this worked through a few Exchange 2007 betas, by Exchange 2007 RTM, the STM file had been removed, and the original script would bomb, making the script only useful for Exchange 2000 and Exchange 2003.

Astute VBscript users would determine how to comment out the few lines causing the issue, but for most folks, that was too much.

So, I finally updated that script for Exchange 2007 and Exchange 2010 (tested on both) and updated it to PowerShell.

This is the script:

function build-object( [string]$server, [string]$edbfilepath, [string]$displayName )
{
	write-debug "build-object enter"
	write-debug "build-object server: $server"
	write-debug "build-object edbfilepath: $edbfilepath type $($edbfilepath.gettype().ToString())"
	write-debug "build-object displayName: $displayName"
	write-debug " "

	$o = "" | Select Name, Display, Length
	$o.display = $displayName

#	if( $server -eq $env:ComputerName )
	if( 0 )
	{
		$r = dir -ea 0 $EdbFilePath
	}
	else
	{
		$filename = "\\" + $server + "\" + $edbfilepath.SubString( 0, 1 ) + 
			"$" + $edbfilepath.SubString( 2 )
		write-debug "Calculated filename: $filename"
		$r = dir -ea 0 $filename
	}

	$o.Name = $r.Name
	$o.Length = $r.Length

	write-debug "build-object exit"
	return $o
}

Get-Command Get-ExchangeServer -ea 0 | out-null
if( ! $? )
{
	write-error "You must run this script within an Exchange Management Shell"
	return
}

$legacy = Get-ExchangeServer $env:Computername -ea 0
if( ! $? )
{
	write-error "You must run this script on an Exchange server"
	return
}

$org = $legacy.ExchangeLegacyDN.SubString( 3 )
$org = $org.SubString( 0, $org.IndexOf( '/' ))
"Exchange Organization Name: $org"

$default = ( Get-AcceptedDomain |? { $_.Default -eq $true } ).DomainName
"Default SMTP Domain: $default"

$forest = $legacy.DistinguishedName.SubString( $legacy.DistinguishedName.IndexOf( 'DC=' ) )
"Active Directory Forest: $forest"
" "
"All Exchange Servers in forest"
$servers = Get-ExchangeServer | select Name
foreach( $server in $servers )
{
	"`tName: $($server.Name)"
}
" "
"All Mailbox Servers in forest"
$mailbox = Get-ExchangeServer |? { $_.ServerRole -match "Mailbox" } | Select Name, IsMemberOfCluster
foreach( $server in $mailbox )
{
	"`tName: $($server.Name)"
}
" "

"Acquiring size of databases..."
" "
[int64]$totalSize = 0
foreach( $server in $mailbox )
{
	"Server name: $($server.Name)"

	[int64]$serverSize = 0
	$serverArray = @()

	if( $server.IsMemberOfCluster -eq 'Yes' )
	{
		$mailboxServer = Get-MailboxServer $server.Name -ea 0
		$myServer = $mailboxServer.RedundantMachines[0]

		$serverArray += (get-mailboxdatabase      -server $server.Name -ea 0) |% {
			build-object $myServer $_.EdbFilePath $_.AdminDisplayName;
		}

	}
	else
	{
		$serverArray += (get-mailboxdatabase      -server $server.Name -ea 0) |% {
			build-object $_.Server $_.EdbFilePath $_.AdminDisplayName;
		}
	}

	$serverArray += (get-publicfolderdatabase -server $server.Name -ea 0) |% {
		build-object $_.Server $_.EdbFilePath $_.AdminDisplayName;
	}

	$serverArray |% { $serverSize += $_.Length }

	foreach( $element in $serverArray )
	{
		if( $element )
		{
			"`tDatabase name: $($element.Display)"
			"`t`tEDB File: $($element.Name)"
			"`t`tEDB size: {0} bytes, {1} GB" -f $element.Length.ToString("N0"), ($element.Length / 1GB).ToString("N3")
			#$element
		}
	}
	"Total size of databases on server {0} bytes, {1} GB" -f  $serverSize.ToString("N0"), ($serverSize / 1GB).ToString("N3")
	" "
	$totalSize += $serverSize
}

"Total size of all databases in organization {0} bytes, {1} GB" -f  $totalSize.ToString("N0"), ($totalSize / 1GB).ToString("N3")

 

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

Moving a Mailbox Database Path

In Exchange 2007 and Exchange 2010, added functionality in Move-Mailbox (and New-MoveRequest), along with the support of online mailbox moves, have reduced the requirement of moving entire databases around very much (it’s easier to just move all the mailboxes in the mailbox database to a new mailbox database in the proper location).

But sometimes – you just need to do it. Even with the downtime it may cause.

You CAN do it from the Exchange Management Console or from the Exchange Management Shell. However, the provided status information is worthless. You have no idea how far along the data movement is. What I recommend instead is this process:

  1. Dismount the mailbox database
  2. Use ROBOCOPY to copy the database file from the old location to the new location
  3. Use Move-DatabasePath -ConfigurationOnly to update Exchange with the new location of the database
  4. Mount the mailbox database

In pseudo-PowerShell:

  1. dismount-database <dbname>
  2. robocopy <src-dir> <dest-dir> <dbname>.edb
  3. move-DatabasePath -Id <dbname> -ConfigurationOnly -EDBFilePath <new-full-path-to-edb>
  4. mount-database <dbname>

ROBOCOPY provides excellent information as to the status of a filecopy.

Also note that “ESEUTIL /y” is a good solution for copying large files as well. It provides respectable filecopy status information.

Until next time…

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

Edit November 22, 2010 – I’m extremely embarassed. In the original version of this post I used “unmount-database” (which doesn’t exist, of course) instead of “dismount-database”. Thankfully, only a few dozen people pointed out my error to me.


Follow me on twitter! : @EssentialExch

Changing Security Groups to Distribution Groups

In Exchange Server 2010, you cannot create a new Distribution Group that is not a Universal group. If your Exchange 2010 installation was upgraded from a prior release of Exchange Server, you may find Distribution Groups created in those earlier versions of Exchange that are not Universal. As I’ve discussed in Exchange Server 2007 and Universal Groups, Exchange simply Works Better ™ with Universal Groups.

Just kidding on the ™ – but it does work better.

So, I’ve got customers who have a large investment in vbscript automation. Of course, I feel that PowerShell is a better solution, but ripping-and-replacing thousands of lines of vbscript takes quite a while. One of the customer’s vbscript applications created distribution lists in Exchange 2003 (as you may or may not be aware, as long as a few key attributes were set on an object, in Exchange 2003 the Recipient Update Service would process that object automatically – such is not true in Exchange 2007). But that application does not work properly in Exchange 2007 or Exchange 2010.

Instead of re-writing a thousand-line vbscript, with all the testing and debugging that that would’ve entailed, it seemed prudent to simply “fix the problem”. In this case, there were two problems:

[1] Sometimes, a global group is created instead of a universal group, and

[2] The group needs to be mail-enabled.

This can be done, in true PowerShell fashion, in a “one-liner”. But it’s a LONG one-liner, and much easier to read when split up. This is a technique that may come in handy for you.

##
## Fix-Security-groups.ps1
## Michael B. Smith
## September 30, 2010
##
## Examine each group present in the Groups OU. If the group is a security
## group, then ensure that it is a Universal group and mail-enable it to become
## a distribution group.
##
get-group -organizationalUnit sub.example.com/Accounts/Groups |?  `
        {$_.RecipientType -eq 'Group'} |% { `
        if( $_.GroupType -match 'Global' ) `
        { `
                set-group $_.Identity -Universal; `
        } `
        Enable-DistributionGroup $_.Identity; `
}

Until next time…

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


Follow me on twitter! : @EssentialExch

Generating a report on Distribution Groups and their Membership

A common request is to get a list of all distribution groups and the members contained in that distribution group. The question came up on a mailing list I frequent today, and my initial response was a PowerShell “one-liner” (actually five lines, but all a single PowerShell statement).

The one-liner worked, but it had a couple of limitations: it wasn’t very pretty (that is, the output was formatted poorly) and if there were than one user set to manage the distribution group, that would be reportedly incorrectly. It was also slower than it had to be.

Note: a distribution group may also be a security group. From an Exchange Server perspective, the important thing is whether the group is mail-enabled or not.

So, I took that five-liner, cleaned it up, fixed the ManagedBy reporting bug, and sped it up (by using an embedded pipeline to report on the members contained in a distribution group). That turned it into a 50+ line script (which includes 10 lines of comments!). As I said on the mailing list – you can generate quick results with PowerShell. That’s good enough for most admins. But if you want it pretty and “production quality” then it’s going to take a little more time.

You can take this report and pipe it to out-string in order to save the output to a disk file. Then you can inspect the file later, email it, copy-n-paste it, whatever you want.

Here is the script:

 

##
## Report-DistributionGroupsAndMembers.ps1
## v1.1

##
## Michael B. Smith
## http://TheEssentialExchange.com
## August, 2010
##
## Requires Exchange Management Shell
## Should work with either PowerShell v1 or v2
## Tested on both Exchange 2007 and Exchange 2010

##

function formatManager($formatstring, $manager)
{
	$formatstring -f $manager.Name, ($manager.Parent.ToString() + "/" + $manager.RDN.ToString().SubString(3))
}

Get-DistributionGroup -ResultSize Unlimited |% {
	$group = $_
	"Group Name & Identity: {0}, {1}" -f $group.Name, $group.Identity
	$managedBy = $group.ManagedBy
	if( $managedBy -is [Microsoft.Exchange.Data.Directory.ADObjectId] )
	{
		formatManager "Group manager: {0}, {1}" $managedBy
	}
	elseif( $managedBy.Count -gt 1 )
	{
		[bool]$first = $true
		foreach( $manager in $managedBy ) 
		{
			if( $first ) 
			{
				formatManager "Group managers: {0}, {1}" $manager
				$first = $false
			}
			else
			{
				formatManager "                {0}, {1}" $manager
			}
		}
	}
	elseif( $managedBy.Count -gt 0 )
	{
		formatManager "Group manager: {0}, {1}" $managedBy[0]
	}

	"Members:"
	Get-DistributionGroupMember -Identity $group.Identity -ResultSize Unlimited |% {
		foreach( $member in $_ )
		{
			"`t$($member.Name)"
		}
	}
	"---"
}

 

Until next time…

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

–Edit on August 20, 2010
The original script would not display ManagedBy on Exchange 2007 (I’m running Exchange 2010). This is because ManagedBy in Exchange 2010 always returns an array-type object (it can contain multiple users). In Exchange 2007, the ManagedBy value is always a singleton. The required changes to the script are in red.

Michael B.


Follow me on twitter: @EssentialExch

Using PowerShell to determine your elevation status (UAC)

On a mailing list recently, SBS author and PowerShell MVP Charlie Russel posted how he used PowerShell to check whether a given PowerShell session was elevated. He also used that information to change the background color of the session (elevated shells are dangerous things!).

I took Charlie’s code and expanded it a bit and “made it mine”. I often need to know whether I’m running as an administrator, a server operator, and/or a backup operator. This is because I write lots of Exchange PowerShell scripts (which often require server operator or local administrator privileges) and backup PowerShell scripts (which require the user running the script to be a backup operator). The same technique Charlie used can also be used to determine those things. The key element here is the IsInRole() method of System.Security.Principal.WindowsPrincipal. For detailed information about that .Net class, google/bing for System.Security.Principal.WindowsPrincipal.

The IsInRole() method operates against a WindowsIdentity object. This is obtained from the current process.

The script is pretty self-explanatory. It is designed to be dot-sourced so the functions can be used within your current script. I’ve also included Charlie’s functionality for changing the background of an elevated shell session.

Without further ado….

##
## IsProtectedRole.ps1
##
## Contains functions for identifying protected roles the current user has tokens for.
##
## Intended for dot-sourcing.
##
## based on code from Charlie Russell (www.scribes.com).
##

$identity  = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object System.Security.Principal.WindowsPrincipal( $identity )

##
## Starting with Vista/Server2008, if UAC is enabled, then a user who has either direct
## or indirect membership in the BuiltIn\Administrators group is assigned not one but
## TWO security tokens. One of those tokens has the administrator privilege, and one
## does not. In order for you to have administrator privilege in PowerShell, you must
## start the PowerShell session from: Angel another elevated shell (either PowerShell or
## cmd.exe), or Beer elevate the session when you start the shell (i.e., "Run As Administrator").
##

function IsAdministrator
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::Administrator )
}

function IsUser
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::User )
}

function IsPowerUser
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::PowerUser )
}

function IsGuest
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::Guest )
}

function IsAccountOperator
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::AccountOperator )
}

function IsSystemOperator
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::SystemOperator )
}

function IsPrintOperator
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::PrintOperator )
}

function IsBackupOperator
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::BackupOperator )
}

function IsReplicator
{
	$principal.IsInRole( [System.Security.Principal.WindowsBuiltInRole]::Replicator )
}

function MarkAdministratorShell
{
	If (IsAdministrator)
	{
		$script:effectivename = "Administrator"
		$host.UI.RawUI.Backgroundcolor = "DarkRed"
		$host.UI.RawUI.Foregroundcolor = "White"
	}
	else
	{
		$script:effectivename = $identity.name
		$host.UI.RawUI.Backgroundcolor = "White"
		$host.UI.RawUI.Foregroundcolor = "DarkBlue"
	}

	clear-host
}

# write-host 'Administrator' (IsAdministrator)
# write-host 'User' (IsUser)
# write-host 'PowerUser' (IsPowerUser)
# write-host 'Guest' (IsGuest)
# write-host 'AccountOperator' (IsAccountOperator)
# write-host 'SystemOperator' (IsSystemOperator)
# write-host 'PrintOperator' (IsPrintOperator)
# write-host 'BackupOperator' (IsBackupOperator)
# write-host 'Replicator' (IsReplicator)

Until next time…

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

 


Follow me on twitter: @EssentialExch