InstallExchange.psm1
<#
Original credit for the majority of the logic in this module goes to: Michel de Rooij michel@eightwone.com http://eightwone.com And the AWS CloudFormation kickstarter for Exchange #> #region Constants $script:LogPath = "$env:SystemDrive\InstallExchange.log" $script:MajorOSVersion = Get-WmiObject Win32_OperatingSystem | Select-Object -Property @{Name = "Major"; Expression = {$_.Version.Split(".")[0] + "." +$_.Version.Split(".")[1]}} | Select-Object -ExpandProperty Major $script:MinorOSVersion = Get-WmiObject Win32_OperatingSystem | Select-Object -Property @{Name = "Minor"; Expression = {$_.Version.Split(".")[2]}} | Select-Object -ExpandProperty Minor $script:InstallExchangeTaskName = "InstallExchange" $script:RunOnceTaskName = "InstallExchangeMonitor" $script:FilterPacks = @( @{ "PackageId" = "{95140000-2000-0409-1000-0000000FF1CE}"; "PackageName" = "Microsoft Office 2010 Filter Pack"; "Url" = "http://download.microsoft.com/download/0/A/2/0A28BBFA-CBFA-4C03-A739-30CCA5E21659/FilterPack64bit.exe"; "Arguments" = @("/q", "/norestart") }, @{ "PackageId" = "00004159000290400100000000F01FEC\Patches\2B24AAAA46EAEB942BF5566A6B1DE170"; "PackageName" = "Microsoft Office 2010 Filter Pack SP1"; "Url" = "http://download.microsoft.com/download/A/A/3/AA345161-18B8-45AE-8DC8-DA6387264CB9/filterpack2010sp1-kb2460041-x64-fullfile-en-us.exe"; "Arguments" = @("/q", "/norestart") } ) $script:WS2008R2Prereqs = @( @{ "PackageId" = "KB974405"; "PackageName" = "KB974405: Windows Identity Foundation"; "Url" = "http://download.microsoft.com/download/D/7/2/D72FD747-69B6-40B7-875B-C2B40A6B2BDD/Windows6.1-KB974405-x64.msu"; "Arguments" = @("/quiet", "/norestart") }, @{ "PackageId" = "KB2619234"; "PackageName" = "KB2619234: Enable Association Cookie/GUID used by RPC/HTTP to also be used at RPC layer"; "Url" = "http://hotfixv4.microsoft.com/Windows 7/Windows Server2008 R2 SP1/sp2/Fix381274/7600/free/437879_intl_x64_zip.exe"; "Arguments" = @("/quiet", "/norestart") }, @{ "PackageId" = "KB2758857"; "PackageName" = "KB2758857: Insecure library loading could allow remote code execution (supersedes KB2533623)"; "Url" = "http://download.microsoft.com/download/A/9/1/A91A39EA-9BD8-422F-A018-44CD62CA7485/Windows6.1-KB2758857-x64.msu"; "Arguments" = @("/quiet", "/norestart") } ) $script:WS2012Prereqs = @( @{ "PackageId" = "KB2985459"; "PackageName" = "KB2985459: The W3wp.exe process has high CPU usage when you run PowerShell commands for Exchange"; "Url" = "http://hotfixv4.microsoft.com/Windows 8/Windows Server 2012 RTM/nosp/Fix512067/9200/free/477081_intl_x64_zip.exe"; "Arguments" = @("/quiet", "/norestart") }, @{ "PackageId" = "KB2884597"; "PackageName" = "KB2884597: Virtual Disk Service or applications that use the Virtual Disk Service crash or freeze in Windows Server 2012"; "Url" = "http://hotfixv4.microsoft.com/Windows 8 RTM/nosp/Fix469260/9200/free/467323_intl_x64_zip.exe"; "Arguments" = @("/quiet", "/norestart") }, @{ "PackageId" = "KB2894875"; "PackageName" = "KB2894875: Windows 8-based or Windows Server 2012-based computer freezes when you run the 'dir' command on an ReFS volume"; "Url" = "http://hotfixv4.microsoft.com/Windows 8 RTM/nosp/Fix473391/9200/free/468889_intl_x64_zip.exe"; "Arguments" = @("/quiet", "/norestart") } ) $script:WS2012R2Prereqs = @() $script:WS2016Prereqs = @() #Error Messages $ERR_OK = 0 $ERR_PROBLEMADPREPARE = 1001 $ERR_UNEXPECTEDOS = 1002 $ERR_UNEXPTECTEDPHASE = 1003 $ERR_PROBLEMADDINGFEATURE = 1004 $ERR_NOTDOMAINJOINED = 1005 $ERR_NOFIXEDIPADDRESS = 1006 $ERR_CANTCREATETEMPFOLDER = 1007 $ERR_UNKNOWNROLESSPECIFIED = 1008 $ERR_NOACCOUNTSPECIFIED = 1009 $ERR_RUNNINGNONADMINMODE = 1010 $ERR_AUTOPILOTNOSTATEFILE = 1011 $ERR_ADMIXEDMODE = 1012 $ERR_ADFORESTLEVEL = 1013 $ERR_INVALIDCREDENTIALS = 1014 $ERR_CANTLOADSERVERMANAGER = 1015 $ERR_MDBDBLOGPATH = 1016 $ERR_MISSINGORGANIZATIONNAME = 1017 $ERR_ORGANIZATIONNAMEMISMATCH = 1018 $ERR_PROBLEMPACKAGEDL = 1120 $ERR_PROBLEMPACKAGESETUP = 1121 $ERR_PROBLEMPACKAGEEXTRACT = 1122 $ERR_PROBLEMFILTERPACKDL = 1131 $ERR_PROBLEMFILTERPACKSETUP = 1132 $ERR_PROBLEMFILTERPACKSP1DL = 1133 $ERR_PROBLEMFILTERPACKSP1SETUP = 1134 $ERR_BADFORESTLEVEL = 1151 $ERR_BADDOMAINLEVEL = 1152 $ERR_NOTSUPPORTED = 1153 $ERR_MISSINGEXCHANGESETUP = 1201 $ERR_PROBLEMEXCHANGESETUP = 1202 $ERR_PROBLEMSAVECONIFG = 1203 $COUNTDOWN_TIMER = 10 $DOMAIN_MIXEDMODE = 0 $FOREST_LEVEL2003 = 2 # Minimum FFL/DFL levels $EX2013_MINFORESTLEVEL = 15137 $EX2013_MINDOMAINLEVEL = 13236 $EX2016_MINFORESTLEVEL = 15317 $EX2016_MINDOMAINLEVEL = 13236 # Supported Exchange versions $EX2013STOREEXE_RTM = "15.00.0516.032" $EX2013STOREEXE_CU1 = "15.00.0620.029" $EX2013STOREEXE_CU2 = "15.00.0712.024" $EX2013STOREEXE_CU3 = "15.00.0775.038" $EX2013STOREEXE_SP1 = "15.00.0847.032" $EX2013STOREEXE_CU5 = "15.00.0913.022" $EX2013STOREEXE_CU6 = "15.00.0995.029" $EX2013STOREEXE_CU7 = "15.00.1044.025" $EX2013STOREEXE_CU8 = "15.00.1076.009" $EX2013STOREEXE_CU9 = "15.00.1104.005" $EX2013STOREEXE_CU10 = "15.00.1130.007" $EX2013STOREEXE_CU11 = "15.00.1156.006" $EX2013STOREEXE_CU12 = "15.00.1178.004" #$EX2013STOREEXE_CU13 = "15.00.1210.003" $EX2013STOREEXE_CU13 = "15.00.1210.000" #This matches the installer version $EX2016STOREEXE_PRE = "15.01.0225.016" #$EX2016STOREEXE_RTM = "15.01.0225.042" $EX2016STOREEXE_CU1 = "15.01.0396.030" $EX2016STOREEXE_CU2 = "15.01.0466.034" $EX2016STOREEXE_RTM = "15.01.0225.037" #This matches the installer version $Versions= @{ $EX2013STOREEXE_RTM = "Exchange Server 2013 RTM"; $EX2013STOREEXE_CU1 = "Exchange Server 2013 Cumulative Update 1"; $EX2013STOREEXE_CU2 = "Exchange Server 2013 Cumulative Update 2"; $EX2013STOREEXE_CU3 = "Exchange Server 2013 Cumulative Update 3"; $EX2013STOREEXE_SP1 = "Exchange Server 2013 Service Pack 1"; $EX2013STOREEXE_CU5 = "Exchange Server 2013 Cumulative Update 5"; $EX2013STOREEXE_CU6 = "Exchange Server 2013 Cumulative Update 6"; $EX2013STOREEXE_CU7 = "Exchange Server 2013 Cumulative Update 7"; $EX2013STOREEXE_CU8 = "Exchange Server 2013 Cumulative Update 8"; $EX2013STOREEXE_CU9 = "Exchange Server 2013 Cumulative Update 9"; $EX2013STOREEXE_CU10 = "Exchange Server 2013 Cumulative Update 10"; $EX2013STOREEXE_CU11 = "Exchange Server 2013 Cumulative Update 11"; $EX2013STOREEXE_CU12 = "Exchange Server 2013 Cumulative Update 12"; $EX2013STOREEXE_CU13 = "Exchange Server 2013 Cumulative Update 13"; $EX2016STOREEXE_PRE = "Exchange Server 2016 Preview"; $EX2016STOREEXE_RTM = "Exchange Server 2016 RTM"; $EX2016STOREEXE_CU1 = "Exchange Server 2016 Cumulative Update 1"; $EX2016STOREEXE_CU2 = "Exchange Server 2016 Cumulative Update 2" } # Supported Operating Systems $WS2008R2_MAJOR = "6.1" $WS2012_MAJOR = "6.2" $WS2012R2_MAJOR = "6.3" $WS2016_MAJOR = "10.0" # .NET Versions $script:NET45 = 378389 $script:NET451 = 378675 $script:NET452 = 379893 $script:NET46 = 393297 $script:NET461 = 394271 # Exchange ISO Locations $script:EX2016CU2_ISO = "https://download.microsoft.com/download/C/6/C/C6C10C1B-EFD8-4AE7-AEE1-C04F45869F5D/ExchangeServer2016-x64-CU2.iso" $script:EX2016CU1_ISO = "https://download.microsoft.com/download/6/4/8/648EB83C-00F9-49B2-806D-E46033DA4AE6/ExchangeServer2016-CU1.iso" $script:EX2016RTM_EXE = "https://download.microsoft.com/download/3/9/B/39B8DDA8-509C-4B9E-BCE9-4CD8CDC9A7DA/Exchange2016-x64.exe" $script:EX2013CU13_EXE = "https://download.microsoft.com/download/7/4/9/74981C3B-0D3C-4068-8272-22358F78305F/Exchange2013-x64-cu13.exe" $script:EX2013CU12_EXE = "https://download.microsoft.com/download/2/C/1/2C151059-9B2A-466B-8220-5AE8B829489B/Exchange2013-x64-cu12.exe" #endregion Function Write-Log { <# .SYNOPSIS Writes to a log file and echoes the message to the console. .DESCRIPTION The cmdlet writes text or a PowerShell ErrorRecord to a log file and displays the log message to the console at the specified logging level. .PARAMETER Message The message to write to the log file. .PARAMETER ErrorRecord Optionally specify a PowerShell ErrorRecord object to include with the message. .PARAMETER Level The level of the log message, this is either INFO, WARNING, ERROR, DEBUG, or VERBOSE. This defaults to INFO. .PARAMETER Path The path to the log file. This defaults to $script:LogPath which is "$env:SystemDrive\InstallExchange.log". .PARAMETER NoInfo Specify to not add the timestamp and log level to the message being written. .INPUTS System.String The log message can be piped to Write-Log .OUTPUTS None .EXAMPLE try { $Err = 10 / 0 } catch [Exception] { Write-Log -Message $_.Exception.Message -ErrorRecord $_ -Level ERROR } Writes an ERROR log about dividing by 0 to the default log path. .EXAMPLE Write-Log -Message "The script is starting" Writes an INFO log to the default log path. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Position = 2)] [ValidateSet("INFO", "WARNING", "ERROR", "DEBUG", "VERBOSE")] [System.String]$Level = "INFO", [Parameter(Mandatory=$true, Position = 0, ValueFromPipeline = $true)] [System.String]$Message, [Parameter(Position = 1)] [System.Management.Automation.ErrorRecord]$ErrorRecord, [Parameter()] [System.String]$Path, [Parameter()] [switch]$NoInfo ) Begin { if ([System.String]::IsNullOrEmpty($Path)) { $Path = $script:LogPath } } Process { if ($ErrorRecord -ne $null) { $Message += "`r`n" $Message += ("Exception: `n" + ($ErrorRecord.Exception | Select-Object -Property * | Format-List | Out-String) + "`n") $Message += ("Category: " + ($ErrorRecord.CategoryInfo.Category.ToString()) + "`n") $Message += ("Stack Trace: `n" + ($ErrorRecord.ScriptStackTrace | Format-List | Out-String) + "`n") $Message += ("Invocation Info: `n" + ($ErrorRecord.InvocationInfo | Format-List | Out-String)) } if ($NoInfo) { $Content = $Message } else { $Content = "$(Get-Date) : [$Level] $Message" } Add-Content -Path $Path -Value $Content switch ($Level) { "INFO" { Write-Host $Content break } "WARNING" { Write-Warning -Message $Content break } "ERROR" { Write-Error -Message $Content break } "DEBUG" { Write-Debug -Message $Content break } "VERBOSE" { Write-Verbose -Message $Content break } default { Write-Warning -Message "Could not determine log level to write." Write-Host $Content break } } } End { } } Function Get-ExchangeInstallationMedia { <# .SYNOPSIS Downloads the specified Exchange installation media. .DESCRIPTION The cmdlet retrieves the installation media either from the internet or a specified AWS S3 bucket. The contents of the ISO or EXE are also automatically extracted to the destination directory. .PARAMETER Destination The location the ISO or EXE is downloaded to, should be a directory path. This defaults to "$env:SystemDrive\ExchangeSource". The contents of the ISO or EXE will also be extracted to this directory. .PARAMETER Source The URL to the installation media you want to download. .PARAMETER Version Specify the version of the installation media to download which uses preconfigured sources. .PARAMETER BucketName The AWS S3 bucket containing the installation media. .PARAMETER Key The S3 key of the installation media object. .INPUTS None .OUTPUTS None .EXAMPLE Get-ExchangeInstallationMedia -Version 2016_CU2 Retrieves the installation media for Exchange 2016 CU2 and downloads it from the internet to the default destination. .EXAMPLE Get-ExchangeInstallationMedia -BucketName MyISOs -Key "Exchange/ExchangeServer2016-x64-CU2.iso" Downloads the ISO file from the MyISOs bucket, with a folder called Exchange in the bucket containing the ISO file. This method is intended to be used by an EC2 instance that is running with an IAM role that allows the file to download without credentials or from a public S3 bucket. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter()] [System.String]$Destination = "$env:SystemDrive\ExchangeSource", [Parameter(ParameterSetName="Source",Mandatory=$true)] [AllowEmptyString()] [System.String]$Source, [Parameter(ParameterSetName="Version", Mandatory=$true)] [ValidateSet("2013_CU12", "2013_CU13", "2016_RTM","2016_CU1","2016_CU2")] [System.String]$Version, [Parameter(ParameterSetName="AWS",Mandatory=$true)] [System.String]$BucketName, [Parameter(ParameterSetName="AWS",Mandatory=$true)] [System.String]$Key ) Begin {} Process { if (!(Test-Path -Path $Destination)) { Write-Log -Message "Creating download destination at $Destination." New-Item -Path $Destination -ItemType Directory -Force -Confirm:$false | Out-Null } if ($PSCmdlet.ParameterSetName -eq "Version") { switch ($Version) { "2013_CU12" { $Source = $script:EX2013CU12_EXE break } "2013_CU13" { $Source = $script:EX2013CU13_EXE break } "2016_RTM" { $Source = $script:EX2016RTM_EXE break } "2016_CU1" { $Source = $script:EX2016CU1_ISO break } "2016_CU2" { $Source = $script:EX2016CU2_ISO break } default { $Source = "" break } } } if ($PSCmdlet.ParameterSetName -eq "AWS") { Write-Log -Message "Downloading $Key from AWS S3 Bucket $BucketName." $Parts = $Key.Split("/") $FileName = $Parts[$Parts.Length - 1] $DownloadDestination = Join-Path -Path $Destination -ChildPath $FileName Import-Module -Name AWSPowerShell -ErrorAction Stop try { Copy-S3Object -BucketName $BucketName -Key $Key -LocalFile "$DownloadDestination" Write-Log -Message "Successfully downloaded file from S3." } catch [Exception] { if ($_.Exception.Message -eq "Access Denied") { Write-Log -Message "Received a 403 response from S3, this could be because the object $Key doesn't exist or the EC2 Instance doesn't have an IAM role with permission." -Level ERROR -ErrorRecord $_ } else { Write-Log -Message "Error downloading object from S3." -Level ERROR -ErrorRecord $_ } } } else { $WebClient = New-Object -TypeName System.Net.WebClient $Uri = New-Object -TypeName System.Uri($Source) $FileName = $Uri.Segments.Get($Uri.Segments.Count - 1) $Index = $Source.LastIndexOf("/") $BaseUrl = $Source.Substring(0, $Index) $DownloadDestination = Join-Path -Path $Destination -ChildPath $FileName try { Register-ObjectEvent -InputObject $WebClient -EventName DownloadFileCompleted -SourceIdentifier Web.DownloadFileCompleted -Action { $Global:DownloadComplete = $true } | Out-Null Register-ObjectEvent -InputObject $WebClient -EventName DownloadProgressChanged -SourceIdentifier Web.DownloadProgressChanged -Action { $Global:Event = $event } | Out-Null Write-Log -Message "Downloading $FileName from $BaseUrl" $WebClient.DownloadFileAsync("$Source", "$DownloadDestination") $Counter = 0 while (!$Global:DownloadComplete) { $Percent = $Global:Event.SourceArgs.ProgressPercentage $TotalBytes = $Global:Event.SourceArgs.TotalBytesToReceive $ReceivedBytes = $Global:Event.SourceArgs.BytesReceived if ($Percent -ne $null) { Write-Progress -Activity "Downloading $FileName from $BaseUrl" -Status "$ReceivedBytes bytes \ $TotalBytes bytes" -PercentComplete $Percent if ($Counter % 30 -eq 0) { Write-Log -Message "Downloaded $ReceivedBytes bytes \ $TotalBytes bytes - $Percent%" -Level VERBOSE } } Start-Sleep -Seconds 1 $Counter++ } Write-Progress -Activity "Downloading $FileName from $BaseUrl" -Status "$ReceivedBytes bytes \ $TotalBytes bytes" -Completed Write-Log -Message "Successfully completed download." } finally { $WebClient.Dispose() } } $FileInfo = New-Object -TypeName System.IO.FileInfo("$DownloadDestination") if ($FileInfo.Extension.ToLower() -eq ".iso") { Write-Log -Message "Mounting ISO file." $Result = Mount-DiskImage -ImagePath "$DownloadDestination" -StorageType ISO -PassThru $Drive = $Result | Get-Volume | Select-Object -ExpandProperty DriveLetter Write-Log -Message "ISO mounted at drive $Drive`:\." #Use a job because the current PowerShell instance may not be able to access the mounted ISO drive $Job = Start-Job -ScriptBlock { $Counter = 0 while (!(Test-Path -Path "$($args[0]):") -and $Counter -lt 60) { Start-Sleep -Seconds 1 $Counter++ if ($Counter -eq 60) { Write-Log -Message "Error waiting for mounted ISO to become available." -Level ERROR throw "Error waiting for mounted ISO to become available." } } Write-Log -Message "Copying contents to $($args[1])." Copy-Item -Path "$($args[0]):\*" -Destination "$($args[1])" -Recurse Write-Log -Message "Copy completed." } -ArgumentList ($Drive, $Destination) Write-Log -Message "Waiting for extraction to complete..." Wait-Job -Job $Job | Out-Null if ($Job.State -eq [System.Management.Automation.JobState]::Failed) { Write-Log -Message "Job to copy ISO contents failed with error: $($Job.ChildJobs[0].Error)" -Level ERROR Exit 1 } Write-Log -Message "Unmounting ISO." Dismount-DiskImage -InputObject $Result Write-Log -Message "Deleting ISO." Remove-Item -Path "$DownloadDestination" -Confirm:$false -Force } $DirectoryInfo = New-Object -TypeName System.IO.DirectoryInfo("$Destination") if ($DirectoryInfo.GetFiles().Length -eq 1) { Write-Log -Message "Only 1 file was downloaded or extracted, going to unpack the single file." $Path = Get-ChildItem -Path $Destination -Filter "*.exe" | Select-Object -First 1 -ExpandProperty FullName Write-Log -Message "Extracting $Path" Start-Process -FilePath $Path -ArgumentList @("/a","/q","/x:`"$Destination`"") -Wait Write-Log -Message "Successfully extracted files." Write-Log -Message "Deleting self extracting cab file at $Path." Remove-Item -Path $Path -Confirm:$false -Force } } End { } } Function Get-TextVersion { <# .SYNOPSIS Retrieves the text based version of Exchange based on the numeric version of the installer file. .DESCRIPTION Performs a lookup of the numeric version of the installer file to match it to the text based version. .PARAMETER FileVersion The version of the installer file to match against. .INPUTS System.String The version number as a string can be piped to the cmdlet. .OUTPUTS System.String .EXAMPLE Get-TextVersion -FileVersion "15.01.0466.034" This returns "Exchange Server 2016 Cumulative Update 2" .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] [System.String]$FileVersion ) Begin { } Process { if ($script:Versions[$FileVersion]) { $Result = "$FileVersion ($($Versions[$FileVersion]))" } Else { $Result = "$FileVersion (Unknown Version)" } } End { Write-Output $Result } } Function Get-PSExecutionPolicy { <# .SYNOPSIS Gets the current PowerShell script execution policy for the computer. .DESCRIPTION Retrieves the execution policy from HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell. .INPUTS None .OUTPUTS System.String .EXAMPLE Get-PSExecutionPolicy This might return "Unrestricted" or "Bypass". .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { $PSPolicy= Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell" -Name ExecutionPolicy -ErrorAction SilentlyContinue if (![System.String]::IsNullOrEmpty($PSPolicy)) { Write-Log -Message "PowerShell Execution Policy is set to $($PSPolicy.ExecutionPolicy) through GPO" -Level "WARNING" } Else { Write-Log -Message "PowerShell Execution Policy not configured through GPO" -Level "VERBOSE" } } End { Write-Output $PSPolicy } } Function Test-LocalAdmin { <# .SYNOPSIS Tests for local admin credentials of the current user. .DESCRIPTION Tests for membership in the local administrators group. .INPUTS None .OUTPUTS System.Boolean .EXAMPLE Test-LocalAdmin This returns true if the user is a local administrator or false otherwise. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin { } Process { $CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent()) Write-Output ($CurrentPrincipal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) } End { } } Function Test-PendingReboots { <# .SYNOPSIS Determines if there are any pending reboot operations. .DESCRIPTION This cmdlet checks pending reboots from Windows Update, File Rename Operations, Computer Renaming, SCCM, and Component Based Servicing. .INPUTS None .OUTPUTS System.Boolean .EXAMPLE Test-PendingReboots Returns true if there are pending reboots or false otherwise. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin { $CbsReboot = $false $SccmReboot = $false } Process { $OSBuild = Get-WmiObject -Class Win32_OperatingSystem -Property BuildNumber -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BuildNumber $WindowsUpdateReboot = Test-Path -Path "HKLM:\\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" #if OS is Vista/2008 or greater if ([Int32]$OSBuild -ge 6001) { $CbsReboot = (Get-ChildItem -Path "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing" | Select-Object -ExpandProperty Name | Where-Object {$_ -contains "RebootPending"}) -ne $null } $FileRename = Get-ItemProperty -Path "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager" -Name "PendingFileRenameOperations" -ErrorAction SilentlyContinue $FileNameReboot = ($FileName -ne $null) $ComputerRenameReboot = (Get-ItemProperty -Path "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ActiveComputerName" -Name ComputerName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ComputerName) -ne (Get-ItemProperty -Path "HKLM:\\SYSTEM\\CurrentControlSet\\Control\\ComputerName\\ComputerName" -Name ComputerName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ComputerName) try { $SccmClientSDK = Invoke-WmiMethod -Class CCM_ClientUtilities -Name "DetermineIfRebootPending" -Namespace "ROOT\\ccm\\ClientSDK" -ErrorAction Stop $SccmReboot = ($SccmClientSDK.IsHardRebootPending -or $SccmClientSDK.RebootPending) } catch {} $Reboots = @{"Component Based Servicing" = $CbsReboot; "File Rename" = $FileNameReboot; "Computer Rename" = $ComputerRenameReboot; "Windows Update" = $WindowsUpdateReboot; "SCCM" = $SccmReboot} } End { $Reboots.GetEnumerator() | Where-Object {$_.Value -eq $true} | ForEach-Object { Write-Log -Message "Pending reboot for $($_.Name)." -Level "VERBOSE" } Write-Output ($Reboots.ContainsValue($true)) } } Function Set-UAC { <# .SYNOPSIS Sets the User Account Control to enabled or disabled. .DESCRIPTION This cmdlet sets the User Account Control to enabled or disabled. .PARAMETER Enabled Specify whether UAC should be enabled or disabled. .INPUTS Systyem.Boolean .OUTPUTS None .EXAMPLE Set-UAC -Enabled $false Disables UAC. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true, ValueFromPipeline = $true, Position = 0)] [System.Boolean]$Enabled ) Begin { Write-Log -Message "Setting User Account Control to Enabled = $Enabled." -Level VERBOSE } Process { New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name EnableLUA -Value ([Int32]$Enabled) -ErrorAction SilentlyContinue| out-null } End {} } Function Set-IEESC { <# .SYNOPSIS Sets Internet Explorer Enhanced Security Configuration to enabled or disabled. .DESCRIPTION This cmdlet sets Internet Explorer Enhanced Security Configuration to enabled or disabled. .PARAMETER Enabled Specify whether IEESC should be enabled or disabled. .INPUTS Systyem.Boolean .OUTPUTS None .EXAMPLE Set-IEESC -Enabled $false Disables IEESC. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [System.Boolean]$Enabled ) Begin {} Process { Write-Log "Setting IE Enhanced Security Configuration to Enabled = $Enabled." $AdminKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A7-37EF-4b3f-8CFC-4F3A74704073}" $UserKey = "HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\{A509B1A8-37EF-4b3f-8CFC-4F3A74704073}" Set-ItemProperty -Path $AdminKey -Name "IsInstalled" -Value ([Int32]$Enabled) Set-ItemProperty -Path $UserKey -Name "IsInstalled" -Value ([Int32]$Enabled) if ((Get-Process -Name Explorer -ErrorAction SilentlyContinue) -ne $null) { Stop-Process -Name Explorer } } End {} } Function Test-Credentials { <# .SYNOPSIS Validates a set of credentials. .DESCRIPTION This cmdlet takes a set of credentials and validates them against Active Directory. .PARAMETER EncryptedPassword An encrypted string representing the password. This string should be encrypted using the ConvertFrom-SecureString cmdlet under the current user's context. .PARAMETER UserName The name of the user account. This can be specified as either DOMAIN\UserName or just as UserName and the domain will default to the current user domain. .PARAMETER Password An unencrypted string. .PARAMETER Credential A PSCredential object of the credentials to validate. .INPUTS None .OUTPUTS System.Boolean .EXAMPLE Test-Credentials -UserName administrator -Password MyP@$$w0rD Validates the provided credentials using the current user domain. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding(DefaultParameterSetName="Encrypted")] Param( [Parameter(Mandatory=$true, ParameterSetName="Encrypted")] [System.String]$EncryptedPassword, [Parameter(Mandatory=$true, ParameterSetName="PlainText")] [System.String]$Password, [Parameter(Mandatory=$true, ParameterSetName="Encrypted")] [Parameter(Mandatory=$true, ParameterSetName="PlainText")] [System.String]$UserName, [Parameter(Mandatory=$true, ParameterSetName="Credential")] [PSCredential]$Credential ) Begin { $Result = $false } Process { switch ($PSCmdlet.ParameterSetName) { "Encrypted" { $PlainTextPassword = Convert-SecureStringToString -SecureString (ConvertTo-SecureString -String $EncryptedPassword) break } "PlainText" { break } "Credential" { $UserName = $Credential.UserName $PlainTextPassword = Convert-SecureStringToString -SecureString $Credential.Password break } } if($UserName.Contains("\")) { $Parts= $UserName.Split("\") $Domain = $Parts[0] $UserName= $Parts[1] } else { $Domain = $env:USERDOMAIN } Write-Log -Message "Testing credentials for user $UserName in domain $Domain." try { [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.AccountManagement") | Out-Null [System.DirectoryServices.AccountManagement.PrincipalContext]$Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $Domain) $Result = $Context.ValidateCredentials($UserName, $PlainTextPassword) Write-Log -Message "Provided credentials are valid : $Result" } catch [Exception] { Write-Log -Message "Error checking credentials." Write-Log -ErrorRecord $_ -Level WARNING } } End { Write-Output $Result } } Function Set-OpenFileSecurityWarning { <# .SYNOPSIS Enables or disables file security warnings from items downloaded from the internet. .DESCRIPTION This cmdlet enables or disables file security warnings from items downloaded from the internet. .PARAMETER Enable Specify to enable the security warnings. .PARAMETER Disable Specify to disable the security warnings. .INPUTS None .OUTPUTS None .EXAMPLE Set-OpenFileSecurityWarning -Disable Disables the security warning when opening files from the internet. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true, ParameterSetName="Enable")] [switch]$Enable, [Parameter(Mandatory=$true, ParameterSetName="Disable")] [switch]$Disable ) Begin {} Process { if ($Enable) { Write-Log -Message "Enabling File Security Warning dialog." -Level VERBOSE Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -ErrorAction SilentlyContinue Remove-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -ErrorAction SilentlyContinue Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -ErrorAction SilentlyContinue Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -ErrorAction SilentlyContinue } elseif ($Disable) { Write-Log -Message "Disabling File Security Warning dialog." -Level VERBOSE New-Item -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -ErrorAction SilentlyContinue | Out-Null New-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -Value ".exe;.msp;.msu;.msi" -ErrorAction SilentlyContinue | Out-Null New-Item -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -ErrorAction SilentlyContinue | Out-Null New-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -Value 1 -ErrorAction SilentlyContinue | Out-Null Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Associations" -Name "LowRiskFileTypes" -ErrorAction SilentlyContinue Remove-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments" -Name "SaveZoneInformation" -ErrorAction SilentlyContinue } } End {} } Function Get-ForestRootNC { <# .SYNOPSIS Gets the Active Directory Forest Root Naming Context. .DESCRIPTION This cmdlet gets the Active Directory Forest Root Naming Context. .INPUTS None .OUTPUTS System.String .EXAMPLE Get-ForestRootNC For the contoso.com forest root, this returns "DC=Contsos,DC=com". .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { try { Write-Log -Message "Getting forest root naming context." -Level VERBOSE [System.DirectoryServices.ActiveDirectory.Forest]$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() $NamingContext = "DC=$($Forest.Name.Replace(".",",DC="))" Write-Log -Message "Naming context is $NamingContext." -Level VERBOSE Write-Output $NamingContext } catch [Exception] { Write-Log -Message "Could not retrieve the forest root naming context." -ErrorRecord $_ -Level ERROR } } End {} } Function Get-DomainNC { <# .SYNOPSIS Gets the Active Directory Domain Naming Context. .DESCRIPTION This cmdlet gets the Active Directory Domain Naming Context for the computer's current domain. .INPUTS None .OUTPUTS System.String .EXAMPLE Get-DomainNC For the tailspintoys.com domain, this returns "DC=Tailspintoys,DC=com". .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin { } Process { try { Write-Log -Message "Getting domain root naming context." -Level VERBOSE [System.DirectoryServices.ActiveDirectory.Domain]$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() $NamingContext = "DC=$($Domain.Name.Replace(".",",DC="))" Write-Log -Message "Naming context is $NamingContext." -Level VERBOSE Write-Output $NamingContext } catch [Exception] { Write-Log -Message "Could not retrieve the domain root naming context." -ErrorRecord $_ -Level ERROR } } End {} } Function Get-ForestFunctionalLevel { <# .SYNOPSIS Gets the Active Directory Forest functional level. .DESCRIPTION This cmdlet gets the Active Directory Forest functional level. .INPUTS None .OUTPUTS System.Int .EXAMPLE Get-ForestFunctionalLevel Returns the integer value representing the current forest functional level. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { try { [System.DirectoryServices.ActiveDirectory.Forest]$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() Write-Output $Forest.ForestModeLevel } catch [Exception] { Write-Log "Could not retrieve forest functional level." -ErrorRecord $_ -Level ERROR } } End{} } Function Test-DomainNativeMode { <# .SYNOPSIS For a Windows 2000 Active Directory environment, tests if the Domain is running in native mode. .DESCRIPTION This cmdlet tests for Windows 2000 Domain native mode of the current domain. .INPUTS None .OUTPUTS System.Boolean .EXAMPLE Test-DomainNativeMode Returns true if the domain is not running Windows 2000 Mixed Mode. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { [System.DirectoryServices.ActiveDirectory.Domain]$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() Write-Output ($Domain.DomainMode -ne [System.DirectoryServices.ActiveDirectory.DomainMode]::Windows2000MixedDomain) } End {} } Function Get-ExchangeOrganization { <# .SYNOPSIS Gets the Exchange Organization. .DESCRIPTION Retrieves the msExchOrganizationContainer object name from Active Directory. The cmdlet returns null of the object does not exist. .INPUTS None .OUTPUTS System.String or Null .EXAMPLE Get-ExchangeOrganization Returns the Exchange Organization name. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin { Write-Log -Message "Getting Exchange Organization name from the msExchOrganizationContainer object class." } Process { try { $NC = Get-ForestRootNC $Path = "LDAP://CN=Microsoft Exchange,CN=Services,CN=Configuration,$NC" if ([System.DirectoryServices.DirectoryEntry]::Exists($Path) -eq $true) { $ExOrgContainer = [ADSI]$Path try { $Result = $ExOrgContainer.PSBase.Children | Where-Object { $_.objectClass -eq 'msExchOrganizationContainer' } | Select-Object -ExpandProperty Name } catch [Exception] { $Result = $null } } else { Write-Log -Message "Can't find Exchange Organization object" -Level VERBOSE $Result = $null } } catch [Exception] { Write-Log -Message "Can't find Exchange Organization object" -ErrorRecord $_ -Level VERBOSE $Result = $null } } End { Write-Output $Result } } Function Test-ExchangeOrganization { <# .SYNOPSIS Tests for the existence of a specific Exchange Organization. .DESCRIPTION This cmdlet tests for the existence of the specified Exchange Organization. .PARAMETER Organization The organization name to test the existence of. .INPUTS System.String .OUTPUTS System.Boolean .EXAMPLE Test-ExchangeOrganization -Organization "contoso" Returns true if the contoso Exchange organization exists in Active Directory. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,Position=0,ValueFromPipeline = $true)] [System.String]$Organization ) Begin { } Process { $NC= Get-ForestRootNC $Path = "LDAP://CN=$Organization,CN=Microsoft Exchange,CN=Services,CN=Configuration,$NC" $Result = [System.DirectoryServices.DirectoryEntry]::Exists($Path) } End { Write-Output $Result } } Function Get-ExchangeForestLevel { <# .SYNOPSIS Gets the current Exchange Forest level. .DESCRIPTION This cmdlet reads the ms-Exch-Schema-Version upperRange attribute. .INPUTS None .OUTPUTS System.String .EXAMPLE Get-ExchangeForestLevel Returns the current Exchange environment forest level. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { try { $NC= Get-ForestRootNC $Path = "LDAP://CN=ms-Exch-Schema-Version-Pt,CN=Schema,CN=Configuration,$NC" if ([System.DirectoryServices.DirectoryEntry]::Exists($Path) -eq $true) { $Result = [ADSI]$Path | Select-Object -ExpandProperty rangeUpper } else { $Result = $null } } catch [Exception] { Write-Log -Message "Could not retrieve Exchange Forest Level." -ErrorRecord $_ -Level VERBOSE $Result = $null } } End { Write-Output $Result } } Function Get-ExchangeDomainLevel { <# .SYNOPSIS Gets the current Exchange Domain level. .DESCRIPTION This cmdlet reads the Microsoft Exchange System Objects objectVersion attribute. .INPUTS None .OUTPUTS System.String .EXAMPLE Get-ExchangeDomainLevel Returns the current Exchange environment domain level. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { try { $NC = Get-DomainNC $Path = "LDAP://CN=Microsoft Exchange System Objects,$NC" if ([System.DirectoryServices.DirectoryEntry]::Exists($Path) -eq $true) { $Result = [ADSI]$Path | Select-Object -ExpandProperty objectVersion } else { $Result = $null } } catch [Exception] { Write-Log -Message "Could not retrieve Exchange Domain Level." -ErrorRecord $_ -Level VERBOSE $Result = $null } } End { Write-Output $Result } } Function Remove-AutodiscoverServiceConnectionPoint { <# .SYNOPSIS Removes the Autodiscover Service Connection Point from Active Directory. .DESCRIPTION This cmdlet removes the serviceConnectionPoint object from Active Directory for Exchange Autodiscover. .PARAMETER Name The name of the service connection point. .INPUTS None .OUTPUTS None .EXAMPLE Remove-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME Removes the autodiscover service connection point for the current server. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] [System.String]$Name ) Begin {} Process { $NC= Get-ForestRootNC $LDAPSearch= New-Object System.DirectoryServices.DirectorySearcher $LDAPSearch.SearchRoot= "LDAP://CN=Configuration,$NC" $LDAPSearch.Filter= "(&(cn=$Name)(objectClass=serviceConnectionPoint)(serviceClassName=ms-Exchange-AutoDiscover-Service)(|(keywords=67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68)(keywords=77378F46-2C66-4aa9-A6A6-3E7A48B19596)))" $LDAPSearch.FindAll() | ForEach-Object { Write-Log "Removing object $($_.Path)" -Level VERBOSE ([ADSI]($_.Path)).DeleteTree() } } End {} } Function Add-AutodiscoverServiceConnectionPoint { <# .SYNOPSIS Adds an Autodiscover Service Connection Point in Active Directory. .DESCRIPTION This cmdlet adds the serviceConnectionPoint object in Active Directory for Exchange Autodiscover. .PARAMETER Name The name of the service connection point. .PARAMETER ServiceBinding The FQDN of the Client Access Server. .INPUTS None .OUTPUTS None .EXAMPLE Add-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME -ServiceBinding "https://$($ENV:COMPUTERNAME).contoso.com/autodiscover/autodiscover.xml" Adds the autodiscover service connection point for the current server. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true, Position = 0)] [System.String]$Name, [Parameter(Mandatory=$true, Position = 1)] [System.String]$ServiceBinding ) Begin {} Process { $NC= Get-ForestRootNC $LDAPSearch= New-Object System.DirectoryServices.DirectorySearcher $LDAPSearch.SearchRoot= "LDAP://CN=Configuration,$NC" $LDAPSearch.Filter= "(&(cn=$Name)(objectClass=serviceConnectionPoint)(serviceClassName=ms-Exchange-AutoDiscover-Service)(|(keywords=67661d7F-8FC4-4fa7-BFAC-E1D7794C1F68)(keywords=77378F46-2C66-4aa9-A6A6-3E7A48B19596)))" $LDAPSearch.FindAll() | ForEach-Object { Write-Log "Setting serviceBindingInformation on $($_.Path) to $ServiceBinding." -Level VERBOSE try { $SCPObj= $_.GetDirectoryEntry() [void]$SCPObj.Put('serviceBindingInformation', $ServiceBinding) $SCPObj.SetInfo() } catch [Exception] { Write-Log "Problem setting serviceBindingInformation property." -Level ERROR -ErrorRecord $_ } } } End {} } Function Get-LocalFQDNHostname { <# .SYNOPSIS Gets the FQDN of the local host. .DESCRIPTION This cmdlet get the FQDN of the local host from DNS. .INPUTS None .OUTPUTS System.String .EXAMPLE Get-LocalFQDNHostname Returns the local computer's FQDN. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { Write-Output ([System.Net.Dns]::GetHostByName($env:COMPUTERNAME)).HostName } End {} } Function Set-RunOnceScript { <# .SYNOPSIS Adds a RunOnce script that launches a PowerShell script at user logon. .DESCRIPTION This cmdlet adds a RunOnce script that launches on user logon. If the specified Name already exists as a RunOnce entry, it is removed first. .PARAMETER Command The command to run. This can be the path to a script file, or native PowerShell commands. .PARAMETER StoreAsPlainText Stores the commands as plain text instead of Base64. .PARAMTER RunFile Specifies that the command parameter was the path to a script file and not native PowerShell commands. .PARAMETER Name The name of the RunOnce entry in the registry. This defaults to $script:RunOnceTaskName which is InstallExchangeMonitor. .INPUTS None .OUTPUTS None .EXAMPLE Set-RunOnceScript -Command "c:\test.ps1" -RunFile Configures the RunOnce setting to run the c:\test.ps1 file when a user logs on. .EXAMPLE Set-RunOnceScript -Command "Get-Service" -Name "ListServices" Configures the RunOnce setting to run the Get-Service cmdlet when a user logs on. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/26/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [System.String]$Command, [Parameter(ParameterSetName="Text")] [switch]$StoreAsPlainText, [Parameter(ParameterSetName="File")] [switch]$RunFile, [Parameter()] [System.String]$Name = $script:RunOnceTaskName ) Begin { } Process { $Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" if ([System.String]::IsNullOrEmpty($Name)) { $Name = $script:RunOnceTaskName } Remove-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue $RunOnce = "$PSHome\PowerShell.exe -NoProfile -NoLogo -NoExit -ExecutionPolicy Unrestricted" if ($StoreAsPlainText) { $Command = $Command.Replace("`"", "\`"").Replace("`n","").Replace("`r","").Replace("`t","") #$RunOnce += " -Command `"& {$Command}`"" $RunOnce += " -Command `"$Command`"" } elseif ($RunFile) { $RunOnce += " -File `"$Command`"" } else { $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command) $EncodedCommand = [System.Convert]::ToBase64String($Bytes) $RunOnce += " -EncodedCommand $EncodedCommand" } if (!(Test-Path -Path $Path)) { New-Item -Path $Path | Out-Null } Write-Log -Message "Setting RunOnce: $RunOnce" -Level VERBOSE New-ItemProperty -Path $Path -Name $Name -Value "$RunOnce" -PropertyType String | Out-Null Write-Log -Message "Successfully set RunOnce." -Level VERBOSE } End { } } Function Set-Pagefile { <# .SYNOPSIS Configures the size of the page file for optimal use by Exchange. .DESCRIPTION This cmdlet sets the page file to the size of the computer system's RAM plus 10MB up to a size of 32GB + 10MB as both the initial and maximum size. .INPUTS None .OUTPUTS None .EXAMPLE Set-Pagefile Sets the page file size manually. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param() Begin {} Process { Write-Log -Message "Checking Pagefile Configuration" -Level VERBOSE $CS = Get-WmiObject -Class Win32_ComputerSystem -EnableAllPrivileges if ($CS.AutomaticManagedPagefile) { Write-Log -Message "System configured to use Automatic Managed Pagefile, reconfiguring" try { $CS.AutomaticManagedPagefile = $false # RAM + 10 MB, with maximum of 32GB + 10MB $InstalledMem = $CS.TotalPhysicalMemory $DesiredSize = (($InstalledMem + 10MB), (32GB+10MB) | Measure-Object -Minimum).Minimum / 1MB $tmp = $CS.Put() $CPF = Get-WmiObject -Class Win32_PageFileSetting $CPF.InitialSize= $DesiredSize $CPF.MaximumSize= $DesiredSize $tmp = $CPF.Put() } catch [Exception] { Write-Log -Message "Problem reconfiguring pagefile." -ErrorRecord $_ -Level WARNING } $CPF= Get-WmiObject -Class Win32_PageFileSetting Write-Log -Message "Pagefile set to manual, initial/maximum size: $($CPF.InitialSize)MB / $($CPF.MaximumSize)MB." } else { Write-Log -Message "Manually configured page file, skipping configuration" -Level VERBOSE } } End { } } Function Set-HighPerformancePowerPlan { <# .SYNOPSIS Enables the high performance power plan on the computer. .DESCRIPTION This cmdlet sets the active power plan to the High Performance setting. .INPUTS None .OUTPUTS None .EXAMPLE Set-HighPerformancePowerPlan Sets the High Performance plan to active. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { Write-Log -Message "Configuring Power Plan" -Level VERBOSE $PowerPlan = Get-CimInstance -Name root\cimv2\power -Class Win32_PowerPlan -Filter "ElementName = 'High Performance'" $Temp = Invoke-CimMethod -InputObject $PowerPlan -MethodName Activate $CurrentPlan = Get-WmiObject -Namespace root\cimv2\power -Class Win32_PowerPlan | Where-Object { $_.IsActive } Write-Log -Message "Power Plan active: $($CurrentPlan.ElementName)" } End {} } Function Get-NETVersion { <# .SYNOPSIS Gets the current version of .NET version 4 installed. .DESCRIPTION This cmdlet gets the current version of .NET version 4 installed from the registry at HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full. .INPUTS None .OUTPUTS System.Int .EXAMPLE Get-NETVersion Retrieves the .NET version 4 specific version. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { $NetVersion = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -ErrorAction SilentlyContinue).Release Write-Log -Message ".NET version installed is $NetVersion." -Level VERBOSE } End { Write-Output ([int]$NetVersion) } } Function Set-NET461InstallBlock { <# .SYNOPSIS Sets a temporary installation block for .NET version 4.6.1 (KB3133990). .DESCRIPTION This cmdlet sets a temporary installation block for .NET 4.6.1 which is sometimes needed is a program is not compatible with this version and you don't want it to be accidentally installed through automatic updates. .INPUTS None .OUTPUTS None .EXAMPLE Set-NET461InstallBlock Blocks the installation of .NET 4.6.1 .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { Write-Log -Message "Set temporary installation block for .NET Framework 4.6.1 (KB3133990)." $RegKey = "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\WU" $RegName= "BlockNetFramework461" if (!(Test-Path -Path $RegKey)) { New-Item -Path "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP" -Name "WU" | Out-Null } if ((Get-ItemProperty -Path $RegKey -Name $RegName -ErrorAction SilentlyContinue) -eq $null) { New-ItemProperty -Path $RegKey -Name $RegName -Value 1 -PropertyType DWORD | Out-Null } if ((Get-ItemProperty -Path $RegKey -Name $RegName -ErrorAction SilentlyContinue) -eq $null) { Write-Log -Message "Unable to set registry key $RegKey\$RegName." -Level WARNING } } End {} } Function Start-ProcessWait { <# .SYNOPSIS Starts a new process and waits for it to complete. .DESCRIPTION This cmdlet starts a new process using .NET System.Diagnostics.Process and waits for it to complete. It optionally writes the standard out of the process to the log file. .PARAMETER FilePath The path to the executable, script, msi, msu, etc to be executed. .PARAMETER ArgumentList An array of arguments to run with the file being executed. This defaults to an empty array. .PARAMTER EnableLogging Specify to write standard output or standard errors to the log file. .INPUTS None .OUTPUTS None .EXAMPLE Start-ProcessWait -FilePath "c:\installer.msi" -EnableLogging -ArgumentList @("/qn") Launches a quiet installation from installer.msi with a no restart option. Logging is also enabled. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [System.String]$FilePath, [Parameter()] [System.String[]]$ArgumentList = @(), [Parameter()] [switch]$EnableLogging ) Begin { if ($ArgumentList -eq $null) { $ArgumentList = @() } } Process { Write-Log -Message "Executing $FilePath $($ArgumentList -Join " ")" -Level VERBOSE if (Test-Path -Path "$FilePath") { $FileInfo = New-Object -TypeName System.IO.FileInfo($FilePath) [System.Diagnostics.Process]$Process = New-Object -TypeName System.Diagnostics.Process $Process.StartInfo.RedirectStandardOutput = $true $Process.StartInfo.UseShellExecute = $false $Process.StartInfo.CreateNoWindow = $true $Process.StartInfo.RedirectStandardError = $true switch($FileInfo.Extension.ToUpper()) { ".MSU" { $ArgumentList += "$FilePath" $Process.StartInfo.Filename = "$env:SystemRoot\System32\WUSA.EXE" $Process.StartInfo.Arguments = $ArgumentList break } ".MSP" { $ArgumentList += "$FilePath" $ArgumentList += "/update" $Process.StartInfo.Filename = "MSIEXEC.EXE" $Process.StartInfo.Arguments = $ArgumentList break } ".MSI" { $ArgumentList += "$FilePath" $Process.StartInfo.Filename = "MSIEXEC.EXE" $Process.StartInfo.Arguments = $ArgumentList break } default { $Process.StartInfo.Filename = "$FilePath" $Process.StartInfo.Arguments = $ArgumentList break } } $Process.Start() | Out-Null if ($EnableLogging) { while (!$Process.HasExited) { while (![System.String]::IsNullOrEmpty(($Line = $Process.StandardOutput.ReadLine()))) { Write-Log -Message $Line -NoInfo } Start-Sleep -Milliseconds 100 } if ($Process.ExitCode -ne 0) { $Line = $Process.StandardError.ReadToEnd() if (![System.String]::IsNullOrEmpty($Line)) { Write-Log -Message $Line -Level ERROR -NoInfo } } else { $Line = $Process.StandardOutput.ReadToEnd() if (![System.String]::IsNullOrEmpty($Line)) { Write-Log -Message $Line -NoInfo } } } else { $Process.WaitForExit() } } else { Write-Log -Message "$FilePath not found." -Level WARNING } } End {} } Function Enable-IFilters { <# .SYNOPSIS Enables OneNote and Publisher IFilters in Exchange. .DESCRIPTION Enables OneNote and Publisher IFilters in Exchange. .INPUTS None .OUTPUTS None .EXAMPLE Enable-IFilters Enables the OneNote and Publisher IFilters. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { # Note: Requires restarting "Microsoft Exchange Transport" and "Microsoft Filtering Management Service", but reboot will take care of that Write-Log -Message "Enabling OneNote and Publisher filtering" -Level VERBOSE $iFilterDirName = "$env:CommonProgramFiles\Microsoft Shared\Filters\" $KeyParent = "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\HubTransportRole" $CLSIDKey = "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\HubTransportRole\CLSID" $FiltersKey = "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\HubTransportRole\filters" $ONEFilterLocation = $iFilterDirName + "\ONIFilter.dll" $PUBFilterLocation = $iFilterDirName + "\PUBFILT.dll" $ONEGuid ="{B8D12492-CE0F-40AD-83EA-099A03D493F1}" $PUBGuid ="{A7FD8AC9-7ABF-46FC-B70B-6A5E5EC9859A}" New-Item -Path $KeyParent -Name CLSID -ErrorAction SilentlyContinue -Force| Out-Null New-Item -Path $KeyParent -Name filters -ErrorAction SilentlyContinue -Force | Out-Null New-Item -Path $CLSIDKey -Name $ONEGuid -Value $ONEFilterLocation -Type String -Force| Out-Null New-Item -Path $CLSIDKey -Name $PUBGuid -Value $PUBFilterLocation -Type String -Force| Out-Null New-ItemProperty -Path "$CLSIDKey\$ONEGuid" -Name "ThreadingModel" -Value "Both" -Type String -Force| Out-Null New-ItemProperty -Path "$CLSIDKey\$PUBGuid" -Name "ThreadingModel" -Value "Both" -Type String -Force| Out-Null New-ItemProperty -Path "$CLSIDKey\$ONEGuid" -Name "Flags" -Value "1" -Type Dword -Force| Out-Null New-ItemProperty -Path "$CLSIDKey\$PUBGuid" -Name "Flags" -Value "1" -Type Dword -Force| Out-Null New-Item -Path $FiltersKey -Name ".one" -Value $ONEGuid -Type String -Force| Out-Null New-Item -Path $FiltersKey -Name ".pub" -Value $PUBGuid -Type String -Force| Out-Null $Acl = Get-Acl -Path $KeyParent $Rule = New-Object System.Security.AccessControl.RegistryAccessRule ("NETWORK SERVICE","ReadKey","Allow") $Acl.SetAccessRule($Rule) $Acl | Set-Acl -Path $KeyParent } End {} } Function Test-PackageInstallation { <# .SYNOPSIS Tests for the installation of the specified software or update. .DESCRIPTION This cmdlet evaluates Win32_QuickFixEngineering, HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall, HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall, and HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products for a matching product. .PARAMETER PackageId The Id of the installed package, software, or update. .INPUTS None .OUTPUTS System.Boolean .EXAMPLE Test-PackageInstallation -PackageId KB2803757 Tests for the installation of KB2803757. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [System.String]$PackageId ) Begin {} Process { $PresenceKey = $null $PresenceKey = Get-WmiObject -Class Win32_quickfixengineering -ErrorAction SilentlyContinue | Where-Object { $_.HotfixID -eq $PackageId } | Select-Object -ExpandProperty HotfixID if ([System.String]::IsNullOrEmpty($PresenceKey)) { $Result = Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$PackageId" if ($Result -eq $false) { # Alternative (seen KB2803754, 2802063 register here) $Result = Test-Path -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$PackageId" if ($Result -eq $false) { # Alternative (Office2010FilterPack SP1) $Result = Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\$PackageId" } } } else { $Result = $true } } End { Write-Output $Result } } Function Get-Package { <# .SYNOPSIS Retrieves a specified package from the internet. .DESCRIPTION This cmdlet tests for the presence of the desired package name, and if it is not present, downloads it from the given Url. Hotfixes that download with a _zip in the filename, but have a .exe extension, will be automatically expanded to a true zip file. .PARAMETER PackageName The name of the package. .PARAMETER Destination Where the package should be downloaded to. .PARAMETER Url The source to download the package from. .INPUTS None .OUTPUTS None .EXAMPLE Get-Package -PackageName "Test App" -Url "http://contoso.com/testapp.zip" -Destination "c:\testapp.zip" Enables the OneNote and Publisher IFilters. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter()] [System.String]$PackageName, [Parameter()] [System.String]$Url, [Parameter(Mandatory=$true)] [System.String]$Destination ) Begin { $Result = @() if (![System.String]::IsNullOrEmpty($PackageName)) { Write-Log -Message "Processing package $PackageName." } } Process { if (!(Test-Path -Path $Destination)) { if (![System.String]::IsNullOrEmpty($Url)) { Write-Log "$Destination not present, downloading from $Url." try { $WebClient = New-Object -TypeName System.Net.WebClient $WebClient.DownloadFile($Url, $Destination) $FileInfo = New-Object -TypeName System.IO.FileInfo($Destination) if ($FileInfo.Name.Contains("_zip")) { try { Write-Log -Message "Expanding Hotfix $($FileInfo.Name)." if (!$Destination.EndsWith(".zip")) { $Destination = Rename-Item -Path $Destination -NewName "$Destination.zip" -PassThru | Select-Object -ExpandProperty FullName } [System.IO.Compression.ZipArchive]$Zip = [System.IO.Compression.ZipFile]::OpenRead($Path) $Contents = $Zip.Entries | Select-Object -Property @{Name = "Path"; Expression = {"$($FileInfo.DirectoryName)\$($_.FullName)"}} | Select-Object -ExpandProperty Path $Zip.Dispose() [System.IO.Compression.ZipFile]::ExtractToDirectory($Path, $FileInfo.DirectoryName) Write-Log -Message "Successfully expanded files $($Contents -join `",`")" $Result = $Contents } catch [Exception] { Write-Log -Message "Error expanding zip file $Destination." -Level WARNING -ErrorRecord $_ } } else { $Result = @($Destination) } } catch [Exception] { Write-Log -Message "Problem downloading file from $Url using BITS." -Level WARNING -ErrorRecord $_ } } else { Write-Log -Message "$Destination not present and no URL provided, not downloading." -Level WARNING $Result = @() } } else { Write-Log -Message "$Destination is present, no need to download." -Level VERBOSE $Result = @($Destination) } } End { Write-Output $Result } } Function Get-FileVersion { <# .SYNOPSIS Gets the version of a specific file or file running a Windows service from its metadata. .DESCRIPTION This cmdlet gets the FileVersion data from a specified file or file running a service. .PARAMETER Path The path to the file. .PARAMETER Service The name of the service. .INPUTS None .OUTPUTS None .EXAMPLE Get-FileVersion -Path "c:\installer.exe" Gets the file version of installer.exe. .EXAMPLE Get-FileVersion -Service lmhosts Gets the file version of the svchost.exe running the lmhosts service. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,ParameterSetName="File",ValueFromPipeline = $true, Position = 0)] [ValidateScript({Test-Path -Path $_})] [System.String]$Path ) DynamicParam { [System.Management.Automation.RuntimeDefinedParameterDictionary]$ParamDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary $Services = Get-Service | Select-Object -ExpandProperty Name $ValidateSet = New-Object -TypeName System.Management.Automation.ValidateSetAttribute($Services) [System.Management.Automation.ParameterAttribute]$Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute $Attributes.ParameterSetName = "Service" $Attributes.Mandatory = $true $Attributes.ValueFromPipeline = $true $Attributes.Position = 0 $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] $AttributeCollection.Add($Attributes) $AttributeCollection.Add($ValidateSet) [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("ServiceName", [System.String], $AttributeCollection) $ParamDictionary.Add("ServiceName", $DynParam) return $ParamDictionary } Begin {} Process { switch ($PSCmdlet.ParameterSetName) { "File" { break } "Service" { $Path = (Get-WmiObject -Class Win32_Service -Filter "Name = `"$($PSBoundParameters.ServiceName)`"" | Select-Object -ExpandProperty PathName).Trim("`"") break } default { throw "Could not determine parameter set name from given parameters." } } $Version = New-Object -TypeName System.IO.FileInfo($Path) | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion } End { Write-Output $Version } } Function Start-PackageInstallation { <# .SYNOPSIS Installs the specified package. .DESCRIPTION This cmdlet launches the installation of a specified package. .PARAMETER PackageId The PackageId of the package to test if the package is already installed. .PARAMETER PackageName The name of the package to install, can be any text you want to identify the package in the logs. .PARAMETER Destination The location to download the installation files to. .PARAMETER Url The source path to download the installation files from. .PARAMETER Arguments The arguments to be used with the installation file. .INPUTS None .OUTPUTS None .EXAMPLE Start-PackageInstallation -PackageId "KB123456" -PackageName "Another update" -Destination "c:\kb123456.msu" -Url "http://contoso.com/kb123456.msu" -ArgumentList @("\qn") Installs the specified KB. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [System.String]$PackageId, [Parameter(Mandatory=$true)] [System.String]$PackageName, [Parameter(Mandatory=$true)] [System.String]$Destination, [Parameter(Mandatory=$true)] [System.String]$Url, [Parameter()] [System.String[]]$Arguments = @() ) Begin {} Process { Write-Log -Message "Processing $Package ($PackageId)" if (!(Test-PackageInstallation -PackageId $PackageId)) { Write-Log -Message "Package not detected, installing." $Contents = @() if (!(Test-Path -Path $Destination)) { # Download & Extract $Contents = Get-Package -Package $PackageName -Url $Url -Destination $Destination if ($Contents.Count -eq 0) { Write-Log -Message "Problem downloading/accessing $PackageName" -Level ERROR throw "Problem downloading/accessing $PackageName" } } else { $Contents += $Destination } Write-Log -Message "Installing $PackageName" foreach ($Item in $Contents) { $Lower = $Item.ToLower() if ($Lower.EndsWith(".exe") -or $Lower.EndsWith(".msi") -or $Lower.EndsWith(".msu") -or $Lower.EndsWith(".msp")) { Start-ProcessWait -FilePath $Item -ArgumentList $Arguments -EnableLogging } } if (!(Test-PackageInstallation -PackageId $PackageId)) { Write-Log -Message "Problem installing $PackageName after the install steps were run, did not find the package Id $PackageId." -Level ERROR throw "Problem installing $PackageName after the install steps were run, did not find the package Id $PackageId." } else { Write-Log -Message "$PackageName successfully installed." } } else { Write-Log -Message "$PackageName already installed" -Level VERBOSE } } End {} } Function Disable-SSLv3 { <# .SYNOPSIS Completely disables the use of SSLv3. .DESCRIPTION This cmdlet disables SSLv3 by use of both the client and server components. .INPUTS None .OUTPUTS None .EXAMPLE Disable-SSLv3 Disables SSLv3 on the system. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin { #Disable SSLv3 to protect against POODLE scan $ServerRegKey = "HKLM:\System\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server" $ClientRegKey = "HKLM:\System\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client" $ServerRegName = "Enabled" $ClientRegName = "DisabledByDefault" } Process { Write-Log -Message "Disabling SSLv3 protocol." if (!(Test-Path -Path $ServerRegKey)) { New-Item -Path $ServerRegKey | Out-Null } New-ItemProperty -Path $ServerRegKey -Name $ServerRegName -Value 0 -PropertyType DWORD | Out-Null if (!(Test-Path -Path $ClientRegKey)) { New-Item -Path $ClientRegKey | Out-Null } New-ItemProperty -Path $ClientRegKey -Name $ServerRegName -Value 0 -PropertyType DWORD | Out-Null New-ItemProperty -Path $ClientRegKey -Name $ClientRegName -Value 1 -PropertyType DWORD | Out-Null } End { Write-Log -Message "Successfully disabled SSLv3." } } Function Set-DisableSharedCacheServiceProbe { <# .SYNOPSIS Runs the contents of KB2971467 to disable the shared cache service probe. .DESCRIPTION Taken from DisableSharedCacheServiceProbe.ps1. Copyright (c) Microsoft Corporation. All rights reserved. .INPUTS None .OUTPUTS None .EXAMPLE Set-DisableSharedCacheServiceProbe Disables the shared cache service probe. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( ) Begin {} Process { Write-Log -Message "Applying DisableSharedCacheServiceProbe (KB2971467, 'Shared Cache Service Restart' Probe Fix)" $ExchangeInstallPath = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup" -ErrorAction SilentlyContinue -Name MsInstallPath | Select-Object -ExpandProperty MsiInstallPath if (![System.String]::IsNullOrEmpty($ExchangeInstallPath) -and (Test-Path -Path "$ExchangeInstallPath")) { $ProbeConfigFile= Join-Path -Path "$ExchangeInstallPath" -ChildPath "Bin\Monitoring\Config\SharedCacheServiceTest.xml" if (Test-Path $ProbeConfigFile) { $Date = Get-Date -Format s $Ext = ".orig_" + $Date.Replace(':', '-'); $Backup = $ProbeConfigFile + $Ext $XmlBackup = [XML](Get-Content -Path $ProbeConfigFile) $XmlBackup.Save($Backup) $XmlDoc = [XML](Get-Content -Path $ProbeConfigFile) $Definition = $XmlDoc.Definition.MaintenanceDefinition if ($Definition -eq $null) { Write-Log -Message "KB2971467: Expected XML node Definition.MaintenanceDefinition.ExtensionAttributes not found. Skipping." -Level WARNING } else { $Modified = $false if ($Definition.Enabled -ne $null -and $Definition.Enabled -ne "false") { $Definition.Enabled = "false" $Modified = $true } if($Modified -eq $true) { $XmlDoc.Save($ProbeConfigFile) Write-Log -Message "Finished KB2971467, Saved $ProbeConfigFile." } else { Write-Log -Message "Finished KB2971467, No values modified." } } } Else { Write-Log -Message "KB2971467: Did not find file in expected location, skipping $ProbeConfigFile." -Level WARNING } } Else { Write-Log -Message "KB2971467: Unable to locate Exchange install path" -Level WARNING } } End {} } Function Start-ExchangeCleanup { <# .SYNOPSIS Performs the necessary cleanup tasks after installing Exchange with this module. .DESCRIPTION This cmdlet removes any unneeded Windows Features, files, scheduled tasks, and RunOnce scripts after the Exchange installation. .PARAMETER WindowsFeatures The Windows features to uninstall. .PARAMETER Paths The list of files or folders to delete. .PARAMETER TaskName The name of the scheduled task for an unattended installation to remove. This defaults to $script:InstallExchangeTaskName which is InstallExchange. .PARAMETER RunOnceTaskName The name of the RunOnce script to remove. This defaults to $script:RunOnceTaskName which is InstallExchangeMonitor. .INPUTS None .OUTPUTS None .EXAMPLE Start-ExchangeCleanup -Paths @("c:\exchangetemp") Runs the cleanup tasks and deletes the folder c:\exchangetemp. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/26/2016 #> [CmdletBinding()] Param( [Parameter()] [System.String[]]$WindowsFeatures, [Parameter()] [System.String[]]$Paths, [Parameter()] [System.String]$TaskName = $script:InstallExchangeTaskName, [Parameter()] [System.String]$RunOnceTaskName = $script:RunOnceTaskName ) Begin { if ([System.String]::IsNullOrEmpty($TaskName)) { $TaskName = $script:InstallExchangeTaskName } if ([System.String]::IsNullOrEmpty($RunOnceTaskName)) { $RunOnceTaskName = $script:RunOnceTaskName } } Process { Write-Log -Message "Cleaning up..." foreach ($Item in $WindowsFeatures) { if (Get-WindowsFeature -Name $Item -ErrorAction SilentlyContinue) { try { Write-Log -Message "Removing Windows Feature: $Item." Remove-WindowsFeature -Name $Item -Confirm:$false } catch [Exception] { Write-Log -Message "Error removing $Item" -Level ERROR -ErrorRecord $_ } } } foreach ($Item in $Paths) { Write-Log -Message "Removing $Item" -Level VERBOSE Remove-Item -Path $Item -Force -Confirm:$false -ErrorAction SilentlyContinue -Recurse } Write-Log -Message "Removing scheduled task $TaskName." if ((Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) -ne $null) { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false Write-Log -Message "Successfully removed scheduled task." } else { Write-Log -Message "No scheduled task matching $TaskName present." } try { Write-Log -Message "Removing RunOnce scripts." $Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" Remove-ItemProperty -Path $Path -Name $RunOnceTaskName -ErrorAction SilentlyContinue Write-Log -Message "Successfully removed RunOnce command." } catch [Exception] { Write-Log -Message $_.Exception.Message -ErrorRecord $_ -Level WARNING } } End { Write-Log -Message "Successfully finished cleanup." } } Function Start-ExchangeFixIt { <# .SYNOPSIS Launches Microsoft published Exchange FixIt scripts. .DESCRIPTION This cmdlet runs the specified Exchange FixIt script contents. Taken from Exchange2013-KB2938053-FixIt.ps1 Parts taken from Exchange2013-KB2997355-FixIt.ps1 Copyright (c) Microsoft Corporation. All rights reserved. .PARAMETER KB The KB # of the FixIt to run. .INPUTS System.String .OUTPUTS None .EXAMPLE Start-ExchangeFixIt -KB KB2997355 Runs the KB2997355 FixIt script. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/26/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] [ValidateSet("KB2938053", "KB2997355")] [System.String]$KB ) Begin {} Process { switch ($KB) { "KB2938053" { Write-Log -Message "Applying Exchange2013-KB2938053-FixIt (KB2938053, Transport Agent Fix)" $BaseDirectory = "$env:windir\Microsoft.NET\assembly\GAC_MSIL" $PolicyDirectories = @{ "policy.14.0.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.0.cfg"; "policy.14.0.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.0.cfg"; "policy.14.1.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.1.cfg"; "policy.14.1.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.1.cfg"; "policy.14.2.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.2.cfg"; "policy.14.2.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.2.cfg"; "policy.14.3.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.3.cfg"; "policy.14.3.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.3.cfg"; "policy.14.4.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy14.4.cfg"; "policy.14.4.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy14.4.cfg"; "policy.15.0.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy15.0.cfg"; "policy.15.0.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy15.0.cfg"; "policy.8.0.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy.cfg"; "policy.8.0.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy.cfg"; "policy.8.1.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy8.1.cfg"; "policy.8.1.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy8.1.cfg"; "policy.8.2.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy8.2.cfg"; "policy.8.2.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy8.2.cfg"; "policy.8.3.Microsoft.Exchange.Data.Common" = "Microsoft.Exchange.Data.Common.VersionPolicy8.3.cfg"; "policy.8.3.Microsoft.Exchange.Data.Transport" = "Microsoft.Exchange.Data.Transport.VersionPolicy8.3.cfg"; } $Configs = @() foreach ($Key in $PolicyDirectories.Keys) { $Configs += Get-ChildItem -Path (Join-Path -Path $BaseDirectory -ChildPath $Key) -Recurse -Filter $PolicyDirectories[$Key] | Select-Object -ExpandProperty FullName } $Count = 0; foreach ($File in $Configs) { Write-Log -Message "Fixing $File..." -Level VERBOSE $Content = Get-Content -Path $File $Content -replace "[-\d+\.]*-->","-->" | Out-File $File -Force -Confirm:$false $Count++ } Write-Log -Message "Exchange2013-KB2938053-FixIt fixed $Count files." break } "KB2997355" { Write-Log -Message "Applying Exchange2013-KB2997355-FixIt (KB2997355, Exchange Online Mailbox Management Fix)." $ExchangeInstallPath = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup" -ErrorAction SilentlyContinue -Name MsiInstallPath | Select-Object -ExpandProperty MsiInstallPath if (![System.String]::IsNullOrEmpty($ExchangeInstallPath) -and (Test-Path -Path $ExchangeInstallPath)) { $XConfigFile = Join-Path -Path (Join-Path -Path $ExchangeInstallPath -ChildPath "ClientAccess\ecp\DDI") -ChildPath "RemoteDomains.xaml" Write-Log -Message "KB2997355: Updating XAML file $XConfigFile..." $Content = Get-Content -Path "$XConfigFile" $Content = $Content -Replace '<Variable DataObjectName="RemoteDomain" Name="DomainName" Type="{x:Type s:String}" />','<Variable DataObjectName="RemoteDomain" Name="DomainName" Type="{x:Type s:String}" /> <Variable DataObjectName="RemoteDomain" Name="TargetDeliveryDomain" Type="{x:Type s:Boolean}" />' $Content = $Content -Replace '<GetListWorkflow Output="Identity, Name, DomainName">','<GetListWorkflow Output="Identity, Name, DomainName, TargetDeliveryDomain">' $Content = $Content -Replace '<GetObjectWorkflow Output="Identity,Name, DomainName, AllowedOOFType, AutoReplyEnabled,AutoForwardEnabled,DeliveryReportEnabled, NDREnabled, TNEFEnabled, MeetingForwardNotificationEnabled, CharacterSet, NonMimeCharacterSet">','<GetObjectWorkflow Output="Identity, Name, DomainName, TargetDeliveryDomain, AllowedOOFType, AutoReplyEnabled, AutoForwardEnabled, DeliveryReportEnabled, NDREnabled, TNEFEnabled, MeetingForwardNotificationEnabled, CharacterSet, NonMimeCharacterSet">' $Content | Out-File "$XConfigFile" -Force -Confirm:$false # IISReset not required at this stage Write-Log -Message "KB2997355: Fixed XAML files" } else { Write-Log -Message 'KB2997355: Unable to locate Exchange install path' -Level WARNING } break } default { throw "Could not determine the selected KB to run the fix it." break } } } End { } } Function Test-ExchangeReadiness { <# .SYNOPSIS Tests the readiness of the server and Active Directory for the Exchange installation. .DESCRIPTION This cmdlet ensures all of the prerequisites for installing Exchange are in place. This includes: -Ensuring the temp directory is available for installation files -Verifying the OS version -Ensuring admin credentials -Access to the setup.exe for Exchange -Domain membership -Credential validation -Required components of the config file -Domain and forest functional levels .PARAMETER Config The generated config object with all of the specified parameters to run this module's installation cmdlet. .PARAMETER Credential The credentials to be used during an unattended installation. .INPUTS System.Object The config object can be piped to this cmdlet. .OUTPUTS None .EXAMPLE Test-ExchangeReadiness -Config $Config -Credential $Credential Tests the environment's and config's readiness to deploy exchange. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,ValueFromPipeline = $true, Position = 0)] [System.Object]$Config, [Parameter()] [PSCredential]$Credential ) Begin { $MajorSetupVersion = [System.Decimal]"$($Config.SetupVersion.Split(".")[0]).$($Config.SetupVersion.Split(".")[1])" } Process { Write-Log -Message "Performing sanity checks." Write-Log -Message "Checking temporary installation folder." if (!(Test-Path -Path "$($Config.TempDirectory)")) { try { New-Item -Path "$($Config.TempDirectory)" -ItemType Directory | Out-Null } catch [Exception] { Write-Log -Message "Error creating Temporary Installation Directory at $($Config.TempDirectory)." -Level ERROR Exit $ERR_CANTCREATETEMPFOLDER } } Write-Log -Message "Checking Operating System $($MajorOSVersion).$($MinorOSVersion)" if (($MajorOSVersion -ne $WS2012R2_MAJOR) -and ($MajorOSVersion -ne $WS2012_MAJOR) -and ($MajorOSVersion -eq $WS2008R2_MAJOR -and $MinorOSVersion -lt 7601)) { Write-Log -Message "Windows Server 2008 R2 SP1, Windows Server 2012 or Windows Server 2012 R2 is required, but not detected" -Level ERROR Exit $ERR_UNEXPECTEDOS } Write-Log -Message "Checking privilege elevation..." if (!(Test-LocalAdmin)) { Write-Log -Message "Script requires running with elevated privileges." -Level ERROR Exit $ERR_RUNNINGNONADMINMODE } else { Write-Log -Message "Credentials appear to be running with local administrator rights." } Write-Log -Message "Checking for access to Exchange setup.exe file." if (!(Test-Path -Path "$($Config.SourceDirectory)\setup.exe")) { Write-Log -Message "Can't find Exchange setup at $($Config.SourceDirectory)\setup.exe." Exit $ERR_MISSINGEXCHANGESETUP } else { Write-Log -Message "Exchange Setup Version: $(Get-TextVersion $Config.SetupVersion)." Write-Log -Message "Checking roles to install." if ($MajorSetupVersion -ge 15.01) { if (!$Config.InstallMailbox) { Write-Log -Message "No roles specified to install" -Level ERROR Exit $ERR_UNKNOWNROLESSPECIFIED } if ($Config.InstallCAS) { Write-Log -Message "Exchange 2016 setup detected, will ignore deprecated InstallCAS parameter." -Level WARNING } } else { if (!$Config.InstallMailbox -and !$Config.InstallCAS) { Write-Log -Message "No roles specified to install" -Level ERROR Exit $ERR_UNKNOWNROLESSPECIFIED } } } Write-Log -Message "Checking domain membership status..." if((Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain -eq $false) { Write-Log -Message "System is not domain-joined" -Level ERROR Exit $ERR_NOTDOMAINJOINED } Write-Log -Message "Checking NIC configuration..." if ((Get-WmiObject Win32_NetworkAdapterConfiguration -Filter {IPEnabled=True and DHCPEnabled=False}) -eq $null) { Write-Log -Message "System doesn't have a static IP address configured." -Level WARNING } if ($Config.TargetDirectory) { $Drive = Split-Path $Config.TargetDirectory -Qualifier Write-Log -Message "Checking installation target directory..." if(!(Test-Path -Path $Drive)) { Write-Log -Message "Target directory drive unavailable: ($Drive)" -Level ERROR Exit $ERR_MDBDBLOGPATH } } if ($Config.InstallMDBLogPath) { $Drive = Split-Path $Config.InstallMDBLogPath -Qualifier Write-Log -Message "Checking MDB log path..." if(!(Test-Path -Path $Drive)) { Write-Log -Message "MDB log drive unavailable: ($Drive)" -Level ERROR Exit $ERR_MDBDBLOGPATH } } if ($Config.InstallMDBDBPath) { $Drive = Split-Path $Config.InstallMDBDBPath -Qualifier Write-Log -Message "Checking MDB database path..." if(!(Test-Path -Path $Drive)) { Write-Log -Message "MDB database drive unavailable: ($Drive)" -Level ERROR Exit $ERR_MDBDBLOGPATH } } $ExOrg = Get-ExchangeOrganization if (![System.String]::IsNullOrEmpty($ExOrg)) { if(![System.String]::IsNullOrEmpty($Config.Organization)) { if($ExOrg -ne $Config.Organization) { Write-Log -Message "OrganizationName ($($Config.Organization)) mismatches with discovered Exchange Organization name ($ExOrg)." -Level ERROR Exit $ERR_ORGANIZATIONNAMEMISMATCH } } Write-Log -Message "Exchange Organization is: $ExOrg" } else { if(![System.String]::IsNullOrEmpty($Config.Organization)) { Write-Log -Message "Exchange Organization will be: $($Config.Organization)." } else { Write-Log -Message "Organization not specified and no Exchange Organization discovered." -Level ERROR Exit $ERR_MISSINGORGANIZATIONNAME } } Write-Log -Message "Checking Exchange Forest Schema Version" if($MajorSetupVersion -ge 15.01) { $MinForestLevel = $EX2016_MINFORESTLEVEL $MinDomainLevel = $EX2016_MINDOMAINLEVEL } else { $MinForestLevel = $EX2013_MINFORESTLEVEL $MinDomainLevel = $EX2013_MINDOMAINLEVEL } $ExchangeForestLevel = Get-ExchangeForestLevel if ($ExchangeForestLevel -ne $null) { Write-Log -Message "Exchange Forest Schema Version is $ExchangeForestLevel." if ($Config.Phase -eq 4 -and $ExchangeForestLevel -lt $MinForestLevel) { # Only check before starting setup Write-Log -Message "Minimum required Forest Functional Level version is $MinForestLevel, aborting." -Level ERROR Exit $ERR_BADFORESTLEVEL } } else { Write-Log -Message "Active Directory is not prepared" -Level WARNING } Write-Log -Message "Checking Exchange Domain Version" $ExchangeDomainLevel = Get-ExchangeDomainLevel if($ExchangeDomainLevel -ne $null) { Write-Log -Message "Exchange Domain Version is $ExchangeDomainLevel." if ($Config.Phase -eq 4 -and $ExchangeDomainLevel -lt $MinDomainLevel) { # Only check before starting setup Write-Log -Message "Minimum required Domain Functional Level version is $MinDomainLevel, aborting." -Level ERROR Exit $ERR_BADDOMAINLEVEL } } Write-Log -Message "Checking domain mode" if ((Test-DomainNativeMode) -eq $false) { Write-Log -Message "Domain is in mixed mode, native mode is required" -Level ERROR Exit $ERR_ADMIXEDMODE } else { Write-Log -Message "Domain is in native mode" } Write-Log -Message "Checking Forest Functional Level" if ((Get-ForestFunctionalLevel) -lt $FOREST_LEVEL2003) { Write-Log -Message "Forest is not Functional Level 2003 or later" -Level ERROR Exit $ERR_ADFORESTLEVEL } Else { Write-Log -Message "Forest Functional Level is 2003 or later" } if ((Get-PSExecutionPolicy) -ne $null) { # Referring to http://support.microsoft.com/kb/2810617/en Write-Log -Message "PowerShell Execution Policy is configured through GPO and may prohibit Exchange Setup. Clearing entry." -Level ERROR Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell" -Name ExecutionPolicy -Value "" -Force } if ($Config.Unattended) { Write-Log -Message "Checking provided credentials" if ($Credential -ne $null -and $Credential -ne [PSCredential]::Empty) { $Result = Test-Credentials -Credential $Credential } else { Write-Log -Message "Unattended specified, but no credentials provided." -Level ERROR $Result = $false } if ($Result -ne $true) { Write-Log -Message "Provided credentials don't seem to be valid." -Level ERROR Exit $ERR_INVALIDCREDENTIALS } } } End { Write-Output $true } } Function Start-ExchangeADPrep { <# .SYNOPSIS Runs the Active Directory preparation for Exchange. .DESCRIPTION This cmdlet tests and then prepares Active Directory using the standard Exchange installer. .PARAMETER Organization The Exchange Organization name that is being installed. .PARAMETER SetupFilePath The path to the setup.exe file used to install Exchange. .INPUTS None .OUTPUTS None .EXAMPLE Start-ExchangeADPrep -Organization "contoso" -SetupFilePath "c:\exchangefiles\setup.exe" Runs the Exchange AD prep. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [System.String]$Organization, [Parameter(Mandatory=$true)] [ValidateScript({Test-Path -Path $_})] [System.String]$SetupFilePath ) Begin {} Process { Write-Log -Message "Preparing Active Directory." $Params = @() Write-Log -Message "Checking Exchange Organization existence." if ((Test-ExchangeOrganization -Organization $Organization) -eq $false) { $Params += "/PrepareAD" $Params += "/OrganizationName:`"$Organization`"" } else { Write-Log -Message "Organization $Organization exists, checking Exchange Forest Schema and Domain versions." $ForestLevel = Get-ExchangeForestLevel $DomainLevel = Get-ExchangeDomainLevel Write-Log -Message "Exchange Forest Schema version: $ForestLevel, Domain: $DomainLevel." $Version = New-Object -TypeName System.IO.FileInfo("$SetupFilePath") | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion $MajorSetupVersion = [System.Decimal]"$($Version.Split(".")[0]).$($Version.Split(".")[1])" if ($MajorSetupVersion -ge 15.01) { $MinForestLevel = $EX2016_MINFORESTLEVEL $MinDomainLevel = $EX2016_MINDOMAINLEVEL } else { $MinForestLevel = $EX2013_MINFORESTLEVEL $MinDomainLevel = $EX2013_MINDOMAINLEVEL } if ($ForestLevel -lt $MinForestLevel -or $DomainLevel -lt $MinDomainLevel) { Write-Log -Message "Exchange Forest Schema or Domain needs updating. Required: Forest($MinForestLevel) / Domain($MinDomainLevel)." -Level WARNING $Params += "/PrepareAD" } else { Write-Log -Message "Active Directory is up to date." } } if ($Params.Count -gt 0) { Write-Log -Message "Preparing Active Directory, Exchange Organization is $Organization." $Params += "/IAcceptExchangeServerLicenseTerms" Start-ProcessWait -FilePath $SetupFilePath -ArgumentList $Params -EnableLogging if (!(Test-ExchangeOrganization -Organization $Organization) -or (Get-ExchangeForestLevel) -lt $MinForestLevel -or (Get-ExchangeDomainLevel) -lt $MinDomainLevel) { Write-Log -Message "Problem updating schema, domain, or Exchange organization." -Level ERROR Exit $ERR_PROBLEMADPREPARE } else { Write-Log -Message "Active Directory has been successfully prepared for Exchange." } } else { Write-Log -Message "Exchange organization $Organization already exists, skipping this step." } } End { } } Function Start-ExchangeInstallation { <# .SYNOPSIS Initiates the installation of a new Exchange environment. .DESCRIPTION This cmdlet runs the installation of Exchange using the setup.exe Exchange installer. .PARAMETER InstallMailbox Specify to install the mailbox role. .PARAMETER InstallCAS Specify to install the CAS role, this is ignored for Exchange 2016. .PARAMETER MDBName The name of the database file to be created, do not include an extension. .PARAMETER MDBDBPath The folder location to store the database file. The MDBName parameter is required to use this parameter. .PARAMETER MDBLogPath The folder location to store the database log files. .PARAMETER TargetDirectory The target directory for installation. This will default to the Exchange default. .PARAMETER SetupFilePath The path to the Exchange setup.exe file. .INPUTS None .OUTPUTS None .EXAMPLE Start-ExchangeInstallation -InstallMailbox ` -MDBName MyDB ` -MDBDBPath "c:\exchange\db" ` -MDBLogPath "c:\exchange\logs" ` -SetupFilePath "c:\exchangefiles\setup.exe" Runs the exchange installation. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/24/2016 #> [CmdletBinding()] Param( [Parameter()] [switch]$InstallMailbox, [Parameter()] [switch]$InstallCAS, [Parameter()] [System.String]$MDBName, [Parameter()] [System.String]$MDBDBPath, [Parameter()] [System.String]$MDBLogPath, [Parameter()] [System.String]$TargetDirectory, [Parameter(Mandatory=$true)] [ValidateScript({Test-Path -Path $_})] [System.String]$SetupFilePath ) Begin {} Process { $Version = New-Object -TypeName System.IO.FileInfo("$SetupFilePath") | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion $MajorSetupVersion = [System.Decimal]"$($Version.Split(".")[0]).$($Version.Split(".")[1])" Write-Log -Message "Installing Microsoft Exchange Server ($Version)." if ($MajorSetupVersion -ge 15.01) { $PresenceKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{CD981244-E9B8-405A-9026-6AEB9DCEF1F1}" } else { $PresenceKey= "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{4934D1EA-BE46-48B1-8847-F1AF20E892C1}" } $Roles = @() if ($InstallMailbox) { $Roles += "Mailbox" } if ($InstallCAS) { if ($MajorSetupVersion -ge 15.01) { Write-Log -Message "Ignoring specified InstallCAS option for Exchange 2016." -Level WARNING } else { $Roles += "ClientAccess" } } $Roles = $Roles -join "," $Params = @("/Mode:Install", "/Roles:`"$Roles`"", "/IAcceptExchangeServerLicenseTerms","/InstallWindowsComponents") if ($InstallMailbox) { if (![System.String]::IsNullOrEmpty($MDBName)) { $Params += "/MdbName:`"$MDBName`"" if (![System.String]::IsNullOrEmpty($MDBDBPath)) { $Params += "/DBFilePath:`"$MDBDBPath\$MDBName.edb`"" } } if (![System.String]::IsNullOrEmpty($MDBLogPath)) { $Params += "/LogFolderPath:`"$MDBLogPath`"" } } if (![System.String]::IsNullOrEmpty($TargetDirectory)) { $Params += "/TargetDir:`"$TargetDirectory`"" } $Params += "/DoNotStartTransport" Start-ProcessWait -FilePath $SetupFilePath -ArgumentList $Params -EnableLogging if ((Get-Item -Path $PresenceKey -ErrorAction SilentlyContinue) -eq $null) { Write-Log -Message "Error encountered installing Exchange" -Level ERROR Exit $ERR_PROBLEMEXCHANGESETUP } else { $LocalAdmins = Get-LocalGroupMembers -LocalGroup "Administrators" Write-Log -Message "Current local admins: `n$($LocalAdmins -join "`n")" -Level VERBOSE } } End { } } Function Get-LocalGroupMembers { <# .SYNOPSIS Gets the members of a local group .DESCRIPTION This cmdlet gets the members of a local group on the local or a remote system. The values are returned as DirectoryEntry values in the format WinNT://Domain/Name. .PARAMETER LocalGroup The local group on the computer to enumerate. .PARAMETER ComputerName The name of the computer to query. This defaults to the local computer. .INPUTS None .OUTPUTS System.String[] .EXAMPLE Get-LocalGroupMembers -LocalGroup Administrators Gets the membership of the local administrators group on the local machine. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/25/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,Position=0)] [System.String]$LocalGroup, [Parameter(Position=1)] [System.String]$ComputerName = $env:COMPUTERNAME ) Begin { } Process { if ([System.String]::IsNullOrEmpty($ComputerName)) { $ComputerName = $env:COMPUTERNAME } $Group = [ADSI]"WinNT://$ComputerName/$LocalGroup,group" $Members = $Group.Invoke("Members", $null) | Select-Object @{Name = "Name"; Expression = {$_[0].GetType().InvokeMember("ADSPath", "GetProperty", $null, $_, $null)}} | Select-Object -ExpandProperty Name } End { Write-Output $Members } } Function Add-DomainMemberToLocalGroup { <# .SYNOPSIS Adds a domain user or group to a local group. .DESCRIPTION This cmdlet adds a domain user or group to a local group on a specified computer. The cmdlet returns true if the member is added or is already a member of the group. The cmdlet uses the current computer domain to identify the domain member. .PARAMETER LocalGroup The local group on the computer that will have a member added. .PARAMETER Member The domain user or group to add. .PARAMETER MemberType The type of the domain member, User or Group. This defaults to User. .PARAMETER ComputerName The name of the computer on which to add the local group member. This defaults to the local computer. .INPUTS None .OUTPUTS System.Boolean .EXAMPLE Add-DomainMemberToLocalGroup -LocalGroup Administrators -Member "Exchange Trusted Subsystem" -MemberType Group Adds the domain group to the local administrators group on the local machine. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/25/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true,Position=0)] [System.String]$LocalGroup, [Parameter(Mandatory=$true,Position=1)] [System.String]$Member, [Parameter(Position=2)] [ValidateSet("Group", "User")] [System.String]$MemberType = "User", [Parameter(Position=3)] [System.String]$ComputerName = $env:COMPUTERNAME ) Begin { Write-Log -Message "Adding $Member to $LocalGroup." $Success = $false } Process { if ([System.String]::IsNullOrEmpty($ComputerName)) { $ComputerName = $env:COMPUTERNAME } $Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() | Select-Object -ExpandProperty Name $Group = [ADSI]"WinNT://$ComputerName/$LocalGroup,group" $Members = $Group.Invoke("Members", $null) | Select-Object @{Name = "Name"; Expression = {$_[0].GetType().InvokeMember("ADSPath", "GetProperty", $null, $_, $null)}} | Select-Object -ExpandProperty Name $NewMember = [ADSI]"WinNT://$Domain/$Member,$MemberType" if ($Members -inotcontains $NewMember.Path) { try { $Group.Add($NewMember.Path) Write-Log -Message "Successfully added membership." $Success = $true } catch [Exception] { Write-Log -Message $_.Exception.Message -ErrorRecord $_ -Level ERROR } } else { Write-Log -Message "$($NewMember.Name) already a member of $($Group.Name)." $Success = $true } } End { Write-Output $Success } } Function Install-Exchange { <# .SYNOPSIS Runs the complete testing, preparation, installation, and cleanup for an Exchange installation. .DESCRIPTION This cmdlet performs all steps necessary to install Exhange 2013/2016. The installation runs in phases with a reboot after each phase. If the installation is being run unattended, scheduled tasks are used to continue the installation process. A single configuration file is generated from the parameters that is used to persist the config and also identify which phase is being executed by this cmdlet. .PARAMETER Organization The Exchange Organization name that is being installed. .PARAMETER InstallMailbox Specify to install the mailbox role. .PARAMETER InstallCAS Specify to install the CAS role, this is ignored for Exchange 2016. .PARAMETER MDBName The name of the database file to be created, do not include an extension. .PARAMETER MDBDBPath The folder location to store the database file. The MDBName parameter is required to use this parameter. .PARAMETER MDBLogPath The folder location to store the database log files. .PARAMETER TargetDirectory The target directory for installation. This will default to the Exchange default. .PARAMETER TempDirectory The location to temporarily store downloaded setup files. This defaults to the specified SourceDirectory containing the Exchange setup files. .PARAMETER Unattended Specify that this will be an unattended installation and will perform all necessary reboots and use Windows Task Scheduler to continue installation after reboot. .PARAMETER UnattendedTaskName The name of the scheduled task that will be used to conduct the unattended installation. This defaults to $script:InstallExchangeTaskName which is InstallExchange. .PARAMETER SourceDirectory The path to the folder containing the Exchange setup files. The setup.exe file should be at the root of this directory. The Exchange installation media should already be extracted from the ISO or exe in this directory. .PARAMETER InstallFilterPack Specify to install the Office filter pack. .PARAMETER IncludeFixes Specify to install and/or run all applicable KBs or FixIts for this version of Exchange. .PARAMETER Phase Indicate if you wish to start on a phase of the install other than 1. The 6 phases are 1) Install OS Prerequisites 2) Install Exchange Prequisites 3) Install UCMA and prepare AD 4) Install Exchange 5) Run post configuration tasks 6) Complete setup actions, add server to DAG, perform cleanup .PARAMETER NoSetup This switch specifies that only the prerequisite steps are performed and no installation is conducted. This is why the PrepareAD step is broken out into a separate phase from the Install Exchange phase. .PARAMETER TargetDirectory The directory that Exchange will be installed into. This defaults to the Exchange setup default. .PARAMETER DAGName Specify the name of the DAG if this Exchange server should either setup a new DAG or join a DAG with the specified name. The cmdlet will determine which action to perform. Leave blank to run a standalone installation. .PARAMETER ProductKey Specify the product key to use if the installation media you are using does not have an embedded license key. .PARAMETER Credential The credential to use to execute an unattended installation and the credential that will be used to modify Active Directory. .PARAMETER ConfigFilePath The path to the existing configuration file. This is used by the unattended setup or can be used to run each phase manually without re-entering parameters. .INPUTS None .OUTPUTS None .EXAMPLE Install-Exchange -Organization "Contoso" ` -InstallMailbox ` -MDBDBPath "c:\Exchange\DB" ` -MDBName "MDB1" ` -MDBLogPath "c:\Exchange\Logs" ` -Unattended ` -SourceDirectory "c:\ExchangeSetup" ` -InstallFilterPack ` -IncludeFixes ` -Credential (Get-Credential) Launches an unattended Exchange installation for standalone instance. .EXAMPLE Install-Exchange -Organization "Contoso" ` -InstallMailbox ` -MDBDBPath "c:\Exchange\DB" ` -MDBName "MDB1" ` -MDBLogPath "c:\Exchange\Logs" ` -Unattended ` -SourceDirectory "c:\ExchangeSetup" ` -InstallFilterPack ` -IncludeFixes ` -Credential (Get-Credential) ` -DAGName "DAG1" Launches an unattended Exchange installation for a DAG configuration. .EXAMPLE Install-Exchange -Organization "Contoso" ` -NoSetup ` -Unattended ` -SourceDirectory "c:\ExchangeSetup" ` -Credential (Get-Credential) Launches an unattended Exchange prerequisite installation, but does not install Exchange. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/26/2016 #> [CmdletBinding()] Param( [Parameter(ParameterSetName="Parameters")] [System.String]$Organization, [Parameter(ParameterSetName="Parameters")] [switch]$InstallMailbox, [Parameter(ParameterSetName="Parameters")] [switch]$InstallCAS, [Parameter(ParameterSetName="Parameters")] [System.String]$MDBDBPath, [Parameter(ParameterSetName="Parameters")] [System.String]$MDBLogPath, [Parameter(ParameterSetName="Parameters")] [System.String]$MDBName, [Parameter(ParameterSetName="Parameters")] [System.String]$TempDirectory, [Parameter(ParameterSetName="Parameters")] [switch]$Unattended, [Parameter(ParameterSetName="Parameters")] [System.String]$UnattendedTaskName = $script:InstallExchangeTaskName, [Parameter(ParameterSetName="Parameters", Mandatory=$true)] [ValidateScript({Test-Path -Path $_})] [System.String]$SourceDirectory, [Parameter(ParameterSetName="Parameters")] [switch]$InstallFilterPack, [Parameter(ParameterSetName="Parameters")] [switch]$IncludeFixes, [Parameter(ParameterSetName="Parameters")] [ValidateRange(1,6)] [System.Int32]$Phase = 1, [Parameter(ParameterSetName="Parameters")] [switch]$NoSetup, [Parameter(ParameterSetName="Parameters")] [System.String]$TargetDirectory, [Parameter(ParameterSetName="Parameters")] [ValidateScript({ if (![System.String]::IsNullOrEmpty($_)) { $_.Length -le 15 } else { return true } })] [System.String]$DAGName = [System.String]::Empty, [Parameter(ParameterSetName="Parameters")] [System.String]$ProductKey, [Parameter()] [PSCredential]$Credential = [PSCredential]::Empty, [Parameter(ParameterSetName="ConfigFile", Mandatory=$true)] [ValidateScript({Test-Path -Path $_})] [System.String]$ConfigFilePath ) DynamicParam { [System.Management.Automation.RuntimeDefinedParameterDictionary]$ParamDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary $ValidateScript = New-Object -TypeName System.Management.Automation.ValidateScriptAttribute([System.Management.Automation.ScriptBlock]::Create("if (![System.String]::IsNullOrEmpty(`$_)) { Test-Path -Path `$_ } else { return `$true }")) if (![System.String]::IsNullOrEmpty($DAGName)) { [System.Management.Automation.ParameterAttribute]$Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute $Attributes.ParameterSetName = "Parameters" $Attributes.Mandatory = $false $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute] $AttributeCollection.Add($Attributes) $AttributeCollection.Add($ValidateScript) [System.Management.Automation.RuntimeDefinedParameter]$DynParam = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter("WitnessServer", [System.String], $AttributeCollection) $DynParam.Value = [System.String]::Empty $ParamDictionary.Add("WitnessServer", $DynParam) } return $ParamDictionary } Begin { $ScriptFullName = $MyInvocation.MyCommand.Path $ParameterString = $PSBoundParameters.GetEnumerator() -join " " if ([System.String]::IsNullOrEmpty($TempDirectory)) { $TempDirectory = $SourceDirectory } if ($Credential -eq $null) { $Credential = [PSCredential]::Empty } if ([System.String]::IsNullOrEmpty($UnattendedTaskName)) { $UnattendedTaskName = $script:InstallExchangeTaskName } if ($PSCmdlet.ParameterSetName -ne "ConfigFile") { $ConfigFile = "$TempDirectory\InstallExchange_config.json" } else { $ConfigFile = $ConfigFilePath } } Process { Write-Log -Message "Cmdlet called using $ParameterString." Write-Log -Message "Running on OS Build $script:MajorOSVersion.$script:MinorOSVersion." Write-Log -Message "Logging to $script:LogPath." -Level VERBOSE Write-Log -Message "Config file path: $ConfigFile." -Level VERBOSE if ($PSCmdlet.ParameterSetName -eq "ConfigFile") { $Config = ConvertFrom-Json -InputObject (Get-Content -Path "$ConfigFile" -Raw) } else { #No config file, initialize from parameters if ($Unattended -eq $true -and $Credential -eq [PSCredential]::Empty) { try { Write-Log -Message "Credentials not specified, prompting..." $Credential = Get-Credential } catch [Exception] { Write-Log -Message "Unattended specified, but no or improper credentials provided." -Level ERROR Exit $ERR_NOACCOUNTSPECIFIED } } $Config = @{} $Config.InstallMailbox = [bool]$InstallMailbox $Config.InstallCAS = [bool]$InstallCAS $Config.InstallMDBDBPath = $MDBDBPath $Config.InstallMDBLogPath = $MDBLogPath $Config.InstallMDBName = $MDBName $Config.TempDirectory = $TempDirectory $Config.PreviousPhase = ($Phase - 1) $Config.Phase = $Phase $Config.Organization = $Organization $Config.SourceDirectory = $SourceDirectory $Config.SetupVersion = New-Object -TypeName System.IO.FileInfo("$SourceDirectory\setup.exe") | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion $Config.TargetDirectory = $TargetDirectory $Config.Unattended = [bool]$Unattended $Config.IncludeFixes = [bool]$IncludeFixes $Config.InstallFilterPack = [bool]$InstallFilterPack $Config.NoSetup = [bool]$NoSetup $Config.SCP = $SCP $Config.Verbose = [int]$VerbosePreference $Config.FirstRun = $true $Config.DAGName = $DAGName $Config.WitnessServer = $PSBoundParameters.WitnessServer $Config.ProductKey = $ProductKey $Config.TaskName = $UnattendedTaskName if(![System.String]::IsNullOrEmpty($Config.DAGName) -and [System.String]::IsNullOrEmpty($Config.WitnessServer)) { Write-Log -Message "No witness server defined, using a domain controller." -Level WARNING $Ctx = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext([System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain) $Server = [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($Ctx) | Select-Object -ExpandProperty Name $Config.WitnessServer = $Server Write-Log -Message "Selected $Server for the witness server." } Set-Content -Path "$ConfigFile" -Value (ConvertTo-Json -InputObject $Config) -Force -Confirm:$false } if ($Config.Unattended -eq $true) { if ($Config.FirstRun -eq $true) { $Command = @" try { Set-RunOnceScript -Command "`$env:SystemDrive\MonitorLog.ps1" -RunFile -Name "$script:RunOnceTaskName" Get-Content -Path "$script:LogPath" -Wait } catch [Exception] { Write-Log -Message "Error running get-content for RunOnce command." -Level ERROR -ErrorRecord `$_ } "@ Set-Content -Path "$env:SystemDrive\MonitorLog.ps1" -Value $Command -Force -Confirm:$false } Set-RunOnceScript -Command "$env:SystemDrive\MonitorLog.ps1" -RunFile } if ($Config.Lock -eq $true) { } if ($Config.NoSetup -eq $true) { $MAX_PHASE = 3 } else { $MAX_PHASE = 6 } $VerbosePreference = $Config.Verbose if ($Config.Unattended -eq $true -and $Config.Phase -gt 1) { Write-Log -Message "Will continue unattended installation of Exchange." } if ($Config.FirstRun -eq $true -or $PSBoundParameters.ContainsKey("Phase")) { Write-Log -Message "Enabling Task Scheduler History." -Level VERBOSE $LogName = 'Microsoft-Windows-TaskScheduler/Operational' $EventLog = New-Object System.Diagnostics.Eventing.Reader.EventLogConfiguration $LogName $EventLog.IsEnabled = $true $EventLog.SaveChanges() Write-Log -Message "Performing sanity checks for Exchange readiness on first run." -Level VERBOSE $Result = Test-ExchangeReadiness -Config $Config -Credential $Credential } else { Write-Log -Message "The install phase is $($Config.Phase), skipping sanity checks." } Set-OpenFileSecurityWarning -Disable Write-Log -Message "Checking for pending reboot..." if (Test-PendingReboots) { if ($Config.Unattended) { Write-Log -Message "Reboot pending, will reboot the system and rerun this phase, $($Config.Phase)." } else { Write-Log -Message "Reboot pending, please reboot the system and restart the script. The parameters will be saved in the config file at $ConfigFile." -Level WARNING } } else { if ($Config.PreviousPhase -eq $Config.Phase) { Write-Log -Message "Caught the installation running in a loop. The previous phase $($Config.PreviousPhase) is the same as the current phase." -Level ERROR Write-Log -Message "Removing scheduled task $($Config.TaskName) and ending installation." if ((Get-ScheduledTask -TaskName "$($Config.TaskName)" -ErrorAction SilentlyContinue) -ne $null) { Unregister-ScheduledTask -TaskName "$($Config.TaskName)" -Confirm:$false Write-Log -Message "Successfully removed scheduled task." } else { Write-Log -Message "No scheduled task matching $($Config.TaskName) present." } Exit 1 } else { Write-Log -Message "Current phase is $($Config.Phase) of $MAX_PHASE." $Config.PreviousPhase = $Config.Phase Set-Content -Path "$ConfigFile" -Value (ConvertTo-Json -InputObject $Config) -Force -Confirm:$false switch ($Config.Phase) { 1 { Write-Log -Message "*** PHASE 1 *** : Installing Operating System prerequisites." $Features = @("Desktop-Experience", "RSAT-ADDS", "RSAT-Clustering-CmdInterface") if ($MajorOSVersion -eq $WS2008R2_MAJOR) { $Features += "NET-Framework" } else { $Features += "Server-Media-Foundation" } if (![System.String]::IsNullOrEmpty($Config.DAGName)) { $Features += "Failover-Clustering" } try { Import-Module -Name ServerManager -ErrorAction Stop Add-WindowsFeature -Name $Features -ErrorAction Stop | Out-Null foreach ($Feature in $Features) { if ((Get-WindowsFeature -Name "$Feature" -ErrorAction SilentlyContinue) -eq $null) { Write-Log -Message "Feature $Feature appears not to be installed after attempting installation." -Level ERROR Exit $ERR_PROBLEMADDINGFEATURE } } } catch [Exception] { Write-Log -Message "Error installing windows features." -Level ERROR -ErrorRecord $_ Exit 1 } Write-Log -Message "Completed Operating System prerequisites." $Config.Phase++ break } 2 { Write-Log -Message "*** PHASE 2 *** : Installing Exchange prerequisites." if ($Config.InstallFilterPack -eq $true) { foreach ($Item in $script:FilterPacks) { [System.Uri]$Uri = New-Object -TypeName System.Uri("$($Item.Url)") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] Start-PackageInstallation -PackageId "$($Item.PackageId)" -PackageName "$($Item.PackageName)" -Url "$($Item.Url)" -Destination "$($Config.TempDirectory)\$FileName" -Arguments $Item.Arguments } } #Check if .NET 4.5.2 or later installed $NETVersion = Get-NETVersion if ($NETVersion -lt $script:NET452) { if ($Config.SetupVersion -ge $EX2013STOREEXE_CU7) { if ($MajorOSVersion -eq $WS2008R2_MAJOR) { $PackageId = "{26784146-6E05-3FF9-9335-786C7C0FB5BE}" } else { $PackageId = "KB2934520" } $Url = "http://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe" [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] Start-PackageInstallation -PackageId $PackageId -PackageName "Microsoft .NET Framework 4.5.2" -Url $Url -Destination "$($Config.TempDirectory)\$FileName" -Arguments @("/q", "/norestart") } } else { Write-Log -Message ".NET Framework 4.5.2 or later already installed." } $Minimum2013Version = [System.Decimal]$EX2013STOREEXE_CU13.Replace(".","").Insert(4, ".") $Minimum2016Version = [System.Decimal]$EX2016STOREEXE_CU2.Replace(".","").Insert(4, ".") $CurrentVersion = [System.Decimal]$Config.SetupVersion.Replace(".","").Insert(4, ".") Write-Log -Message "Current Version $CurrentVersion, Minimum 2013 Version for .NET 4.6.1 $Minimum2013Version, Minimum 2016 Version for .NET 4.6.1 $Minimum2016Version." if ((($CurrentVersion -lt $Minimum2016Version) -and ($CurrentVersion -ge 1501)) -or (($CurrentVersion -lt $Minimum2013Version) -and ($CurrentVersion -lt 1501))) { Write-Log -Message "Blocking .NET 4.6.1 installation for all installations below Exchange 2016 CU2 or Exchange 2013 CU13." Set-NET461InstallBlock } else { if ($NETVersion -ge $script:NET46) { Write-Log -Message "Installing Exchange 2016 CU2 or Exchange 2013 CU13 or greater and at least .NET 4.6 installed, installing .NET 4.6.1 hotfix rollups." switch ($MajorOSVersion) { $WS2016_MAJOR { $Url = "" $PackageId = "" $Arguments = @() break } $WS2012R2_MAJOR { $Url = "http://download.microsoft.com/download/E/F/1/EF1FB34B-58CB-4568-85EC-FA359387E328/Windows8.1-KB3146715-x64.msu" $PackageId = "KB3146715" $Arguments = @("/install", "/quiet", "/norestart") break } $WS2012_MAJOR { $Url = "http://download.microsoft.com/download/E/F/1/EF1FB34B-58CB-4568-85EC-FA359387E328/Windows8-RT-KB3146714-x64.msu" $PackageId = "KB3146714" $Arguments = @("/install", "/quiet", "/norestart") break } $WS2008R2_MAJOR { $Url = "http://download.microsoft.com/download/E/F/1/EF1FB34B-58CB-4568-85EC-FA359387E328/NDP461-KB3146716-x86-x64-ENU.exe" $PackageId = "KB3146716" $Arguments = @("/q", "/norestart") break } default { Write-Log -Message "Unknown OS version $MajorOSVersion." -Level ERROR $Url = [System.String]::Empty break } } if (![System.String]::IsNullOrEmpty($Url)) { [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] try { Start-PackageInstallation -PackageId $PackageId -Url $Url -PackageName "Hotfix rollup for the .NET Framework 4.6 and 4.6.1 in Windows" -Destination "$($Config.TempDirectory)\$FileName" -Arguments $Arguments } catch [Exception] { Write-Log -Message "Error installing .NET 4.6 and 4.6.1 hotfix rollup." -Level ERROR -ErrorRecord $_ } } } } if ($PSVersionTable.PSVersion.Major -lt 5) { Write-Log -Message "WMF 5 is not installed, installing now." switch ($MajorOSVersion) { $WS2016_MAJOR { $Url = [System.String]::Empty $PackageId = [System.String]::Empty break } $WS2012R2_MAJOR { $Url = "https://download.microsoft.com/download/2/C/6/2C6E1B4A-EBE5-48A6-B225-2D2058A9CEFB/Win8.1AndW2K12R2-KB3134758-x64.msu" $PackageId = "KB3134758" break } $WS2012_MAJOR { $Url = "https://download.microsoft.com/download/2/C/6/2C6E1B4A-EBE5-48A6-B225-2D2058A9CEFB/W2K12-KB3134759-x64.msu" $PackageId = "KB3134759" break } $WS2008R2_MAJOR { $Url = "https://download.microsoft.com/download/2/C/6/2C6E1B4A-EBE5-48A6-B225-2D2058A9CEFB/Win7AndW2K8R2-KB3134760-x64.msu" $PackageId = "KB3134760" break } default { Write-Log -Message "Cannot match current Major OS Version for WMF installation." -Level ERROR $Url = [System.String]::Empty break } } if (![System.String]::IsNullOrEmpty($Url)) { [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] Start-PackageInstallation -PackageId $PackageId -PackageName "Windows Management Framework 5.0" -Url $Url -Destination "$($Config.TempDirectory)\$FileName" -Arguments @("/install", "/quiet", "/norestart") } } else { Write-Log -Message "PowerShell version $($PSVersionTable.PSVersion.Major) detected." } switch ($MajorOSVersion) { $WS2016_MAJOR { $PrereqPackages = $script:WS2016Prereqs break } $WS2012R2_MAJOR { $PrereqPackages = $script:WS2012R2Prereqs break } $WS2012_MAJOR { $PrereqPackages = $script:WS2012Prereqs break } $WS2008R2_MAJOR { $PrereqPackages = $script:WS2008R2Prereqs break } default { Write-Log -Message "Cannot match current Major OS Version for prereq installation." -Level ERROR $PrereqPackages = @() break } } foreach ($Item in $PrereqPackages) { [System.Uri]$Uri = New-Object -TypeName System.Uri("$($Item.Url)") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] Start-PackageInstallation -PackageId "$($Item.PackageId)" -PackageName "$($Item.PackageName)" -Url "$($Item.Url)" -Destination "$($Config.TempDirectory)\$FileName" -Arguments $Item.Arguments } $Config.Phase++ break } 3 { Write-Log -Message "*** PHASE 3 *** : Installing Exchange prerequisites (continued)." $Url = "http://download.microsoft.com/download/2/C/4/2C47A5C1-A1F3-4843-B9FE-84C0032C61EC/UcmaRuntimeSetup.exe" [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] Start-PackageInstallation -PackageId "{41D635FE-4F9D-47F7-8230-9B29D6D42D31}" -PackageName "Unified Communications Managed API 4.0 Runtime" -Url "$Url" -Destination "$($Config.TempDirectory)\$FileName" -Arguments @("/q", "/norestart") if (![System.String]::IsNullOrEmpty($Config.Organization)) { Write-Log -Message "Checking/Preparing Active Directory." Start-ExchangeADPrep -Organization "$($Config.Organization)" -SetupFilePath "$($Config.SourceDirectory)\setup.exe" } Write-Log -Message "Completed installing Exchange prerequisites." $Config.Phase++ break } 4 { Write-Log -Message "*** PHASE 4 *** : Installing Exchange." $MajorSetupVersion = "$($Config.SetupVersion.Split(".")[0]).$($Config.SetupVersion.Split(".")[1])" Start-ExchangeInstallation -InstallMailbox:$Config.InstallMailbox ` -InstallCAS:$Config.InstallCAS ` -MDBName $Config.InstallMDBName ` -MDBDBPath $Config.InstallMDBDBPath ` -MDBLogPath $Config.InstallMDBLogPath ` -TargetDirectory "$($Config.TargetDirectory)" ` -SetupFilePath "$($Config.SourceDirectory)\setup.exe" if (Get-Service -Name MSExchangeTransport -ErrorAction SilentlyContinue) { Write-Log -Message "Configuring MSExchangeTransport startup to Manual." Set-Service -Name MSExchangeTransport -StartupType Manual } if(Get-Service -Name MSExchangeFrontEndTransport -ErrorAction SilentlyContinue) { Write-Log -Message "Configuring MSExchangeFrontEndTransport startup to Manual." Set-Service -Name MSExchangeFrontEndTransport -StartupType Manual } switch($Config.SCP) { "" { # Do nothing break } $null { Write-Log -Message "Removing Service Connection Point record" Remove-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME break } default { Write-Log -Message "Configuring Service Connection Point record as $($Config.SCP)" Add-AutodiscoverServiceConnectionPoint -Name $ENV:COMPUTERNAME -ServiceBinding $Config.SCP break } } Write-Log -Message "Completed Exchange installation step." $Config.Phase++ break } 5 { Write-Log -Message "*** PHASE 5 *** : Post configuration tasks." Set-HighPerformancePowerPlan Set-Pagefile Disable-SSLv3 if ($Config.InstallMailbox) { if ($Config.InstallFilterPack) { Write-Log -Message "Enabling IFilters." Enable-IFilters } # Insert other Mailbox Server specifics here } if($Config.InstallCAS) { # Insert Client Access Server specifics here } if($Config.IncludeFixes) { Write-Log -Message "Installing applicable recommended hotfixes and security updates." $Version = Get-FileVersion -ServiceName "MSExchangeServiceHost" Write-Log -Message "Installed Exchange MSExchangeIS version is $(Get-TextVersion -FileVersion $Version)" -Level VERBOSE switch($Version) { $EX2013STOREEXE_CU2 { $Url = "http://download.microsoft.com/download/3/D/A/3DA5AC0D-4B94-479E-957F-C7C66DE1B30F/Exchange2013-KB2880833-x64-en.msp" [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] Start-PackageInstallation -PackageId "KB2880833" -PackageName "Security Update For Exchange Server 2013 CU2" -Destination "$($Config.SourceDirectory)\$FileName" -Url $Url -Arguments @("/q", "/norestart") break } $EX2013STOREEXE_CU3 { $Url = "http://download.microsoft.com/download/0/E/3/0E3FFD83-FE6A-48B7-85F2-3EF92155EFBE/Exchange2013-KB2880833-x64-en.msp" [System.Uri]$Uri = New-Object -TypeName System.Uri("$Url") $FileName = $Uri.Segments[$Uri.Segments.Count - 1] Start-PackageInstallation -PackageId "KB2880833" -PackageName "Security Update For Exchange Server 2013 CU3" -Destination "$($Config.SourceDirectory)\$FileName" -Url $Url -Arguments @("/q", "/norestart") break } $EX2013STOREEXE_SP1 { Start-ExchangeFixIt -KB KB2938053 break } $EX2013STOREEXE_CU5 { Set-DisableSharedCacheServiceProbe break } $EX2013STOREEXE_CU6 { Start-ExchangeFixIt -KB KB2997355 break } default { Write-Log -Message "No updates to install for Exchange." break } } } if (![System.String]::IsNullOrEmpty($Config.ProductKey)) { Write-Log -Message "Setting product key $($Config.ProductKey)." Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.SnapIn Get-ExchangeServer | Where-Object {$_.IsE15OrLater} | ForEach-Object { Set-ExchangeServer -Identity $_ -ProductKey $Config.ProductKey } Write-Log -Message "Successfully set the prodcut key, restarting the Information Store service." Restart-Service -Name MSExchangeIS -Force -Confirm:$false } if (![System.String]::IsNullOrEmpty($Config.DAGName) -and ![System.String]::IsNullOrEmpty($Config.WitnessServer)) { Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.SnapIn if ((Get-DatabaseAvailabilityGroup -Identity $Config.DAGName -ErrorAction SilentlyContinue) -eq $null) { Write-Log -Message "Adding Exchange Trusted Subsystem to Local Administrators on $($Config.WitnessServer) in preparation for DAG creation." $Success = Add-DomainMemberToLocalGroup -LocalGroup "Administrators" -Member "Exchange Trusted Subsystem" -MemberType Group -ComputerName $Config.WitnessServer if (!$Success) { Write-Log -Message "Could not add the Exchange Trusted Subsystem to the witness server local administrators group. Cannot complete DAG setup." -Level ERROR Exit 1 } } } Write-Log -Message "Completed post-configuration tasks." $Config.Phase++ break } 6 { Write-Log -Message "*** PHASE 6 *** Completing setup actions." if (Get-Service -Name MSExchangeTransport -ErrorAction SilentlyContinue) { Write-Log -Message "Configuring MSExchangeTransport startup to Automatic." Set-Service MSExchangeTransport -StartupType Automatic try { Start-Service -Name MSExchangeTransport } catch [Exception] { Write-Log -Message "Error starting MSExchangeTransport." -Level ERROR -ErrorRecord $_ } } if (Get-Service -Name MSExchangeFrontEndTransport -ErrorAction SilentlyContinue) { Write-Log -Message "Configuring MSExchangeFrontEndTransport startup to Automatic." Set-Service MSExchangeFrontEndTransport -StartupType Automatic try { Start-Service -Name MSExchangeFrontEndTransport } catch [Exception] { Write-Log -Message "Error starting MSExchangeFrontEndTransport." -Level ERROR -ErrorRecord $_ } } Set-UAC -Enabled $true Set-IEESC -Enabled $true if (![System.String]::IsNullOrEmpty($Config.DAGName) -and ![System.String]::IsNullOrEmpty($Config.WitnessServer)) { try { Write-Log -Message "Adding this server to DAG $($Config.DAGName)." Add-PSSnapin -Name Microsoft.Exchange.Management.PowerShell.SnapIn $Exists = $false if ((Get-DatabaseAvailabilityGroup -Identity $Config.DAGName -ErrorAction SilentlyContinue) -eq $null) { Write-Log -Message "Creating a DAG with name $($Config.DAGName)." New-DatabaseAvailabilityGroup -Name $Config.DAGName -WitnessServer $Config.WitnessServer -DatabaseAvailabilityGroupIPAddress ([System.Net.IPAddress]::None) Write-Log -Message "Successfully created DAG." } else { $Exists = $true } try { Write-Log -Message "Adding server to DAG." Write-Log -Message "Running a gpupdate." & gpupdate.exe /force Write-Log -Message "Adding Exchange Trusted Subsystem to Local Administrators in preparation for DAG join." $Success = Add-DomainMemberToLocalGroup -LocalGroup "Administrators" -Member "Exchange Trusted Subsystem" -MemberType Group if ($Success) { Add-DatabaseAvailabilityGroupServer -Identity $Config.DAGName -MailboxServer $ENV:COMPUTERNAME -ErrorAction Stop Write-Log -Message "Successfully added server to DAG." if ($Exists -eq $true) { Write-Log -Message "Since the DAG already exists, go ahead and setup copies of all the available databases." Get-MailboxDatabase | Where-Object {$_.MasterServerOrAvailabilityGroup -eq $Config.DAGName -and $_.Servers -notcontains $ENV:COMPUTERNAME} | ForEach-Object { try { Write-Log -Message "Creating a copy of $($_.Name) on local computer $env:COMPUTERNAME." Add-MailboxDatabaseCopy -Identity $_.Name -MailboxServer $env:COMPUTERNAME } catch [Exception] { Write-Log -Message "Error creating database copy." -Level ERROR -ErrorRecord $_ } } Write-Log -Message "Setting up database copies on the other servers." Get-DatabaseAvailabilityGroup -Identity $Config.DAGName | Select-Object -ExpandProperty Servers | Where-Object {$_ -notcontains $ENV:COMPUTERNAME} | ForEach-Object { Write-Log -Message "Setting up database copies on $_." $Server = $_ Get-MailboxDatabase | Where-Object {$_.MasterServerOrAvailabilityGroup -eq $Config.DAGName -and $_.Servers -notcontains $Server} | ForEach-Object { try { Write-Log -Message "Creating a copy of $($_.Name) on remote computer $Server." Add-MailboxDatabaseCopy -Identity $_.Name -MailboxServer $Server } catch [Exception] { Write-Log -Message "Error creating database copy." -Level ERROR -ErrorRecord $_ } } } Write-Log -Message (Get-MailboxDatabaseCopyStatus | Format-List | Out-String) } } else { Write-Log -Message "The Exchange Trusted Subsystem could not be added to the local administrators group." -Level ERROR Exit 1 } } catch [Exception] { Write-Log -Message "Error adding server to DAG." -Level ERROR -ErrorRecord $_ } } catch [Exception] { Write-Log -Message "Error creating DAG $($Config.DAGName)." -Level ERROR -ErrorRecord $_ } } Write-Log -Message "Setup finished." $Config.Phase++ break } default { Write-Log -Message "Unknown phase $($Config.Phase)." -Level ERROR break } } Set-OpenFileSecurityWarning -Enable if ($Config.Unattended -eq $true ) { #Use less than or equal since the phase is incremented before this check if ($Config.Phase -le $MAX_PHASE) { Write-Log -Message "Preparing system for the next phase." -Level VERBOSE Set-UAC -Enabled $false Set-IEESC -Enabled $false if ($Config.FirstRun -eq $true) { if ($Credential -ne $null -and $Credential -ne [PSCredential]::Empty) { <# Shouldn't need to use Invoke-Command $Task = Invoke-Command -ComputerName $ENV:COMPUTERNAME -ScriptBlock { Write-Output (New-InstallExchangeScheduledTask -Credential (New-Object -TypeName System.Management.Automation.PSCredential($args[0], $args[1])) -ConfigFilePath $args[2] -TaskName "$($args[3])") } -Credential $Credential -ArgumentList @($Credential.UserName, $Credential.Password, $ConfigFile) #> $Task = New-InstallExchangeScheduledTask -Credential $Credential -ConfigFilePath "$ConfigFile" -TaskName "$($Config.TaskName)" } else { Write-Log -Message "Unattended specified, this is a first run, but no Credential value was provided." -Level ERROR Exit 1 } $Config.FirstRun = $false } try { Write-Log -Message "Saving updated configuration file to $ConfigFile." Set-Content -Path "$ConfigFile" -Value (ConvertTo-Json -InputObject $Config) -Force -Confirm:$false } catch [Exception] { Write-Log -Message "Error saving configuration file." -Level ERROR -ErrorRecord $_ Exit $ERR_PROBLEMSAVECONIFG } Write-Log -Message "Rebooting in $COUNTDOWN_TIMER seconds..." Start-Sleep -Seconds $COUNTDOWN_TIMER Set-RunOnceScript -Command "$env:SystemDrive\MonitorLog.ps1" -RunFile Restart-Computer -Force } else { $Paths = @("$($Config.TempDirectory)", "$env:SystemDrive\MonitorLog.ps1") if($Config.NoSetup -eq $false) { $Paths += "$($Config.SourceDirectory)" } Start-ExchangeCleanup -Paths ($Paths | Select-Object -Unique) Write-Log -Message "Unattended setup complete." } } } } } End { Exit $ERR_OK } } #region Scheduled Tasks Function New-InstallExchangeScheduledTask { <# .SYNOPSIS Creates the scheduled task that is used for unattended Exchange installations. .DESCRIPTION This cmdlet creates a scheduled task that will run under the context of the provided credentials. .PARAMETER Credential The credential that the scheduled task will use to run. .PARAMETER ConfigFilePath The path to the configuration file that the scheduled task will use to continue running the Exchange installation. .PARAMETER TaskName The name to use for the scheduled task. This defaults to $script:InstallExchangeTaskName which is InstallExchange. .INPUTS None .OUTPUTS None .EXAMPLE New-InstallExchangeScheduledTask -Credential (Get-Credential) -ConfigFilePath "c:\exchangesource\config.json" -TaskName InstallExchange Creates the scheduled task for the unattended Exchange installation. .NOTES AUTHOR: Michael Haken LAST UPDATE: 8/26/2016 #> [CmdletBinding()] Param( [Parameter(Mandatory=$true)] [PSCredential]$Credential, [Parameter(Mandatory=$true)] [System.String]$ConfigFilePath, [Parameter()] [System.String]$TaskName = $script:InstallExchangeTaskName ) Begin { if ([System.String]::IsNullOrEmpty($TaskName)) { $TaskName = $script:InstallExchangeTaskName } if ((Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) -ne $null) { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false } } Process { $Command = "try {Install-Exchange -ConfigFilePath `"$ConfigFilePath`"} catch [Exception] {Write-Log -Message `"Error running Install-Exchange from scheduled task.`" -ErrorRecord `$_ -Level ERROR}" $Bytes = [System.Text.Encoding]::Unicode.GetBytes($Command) $EncodedCommand = [Convert]::ToBase64String($Bytes) $STParams = "-NonInteractive -WindowStyle Hidden -NoProfile -NoLogo -EncodedCommand $EncodedCommand" $STSource = "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe" $STAction = New-ScheduledTaskAction -Execute $STSource -Argument $STParams $STSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -MultipleInstances IgnoreNew $ScheduledTask = Register-ScheduledTask -TaskName $TaskName ` -Action $STAction ` -User "$($Credential.UserName)" ` -Password (Convert-SecureStringToString -SecureString $Credential.Password) ` -Trigger (New-ScheduledTaskTrigger -AtStartup -RandomDelay ([System.Timespan]::FromSeconds(30))) ` -Settings $STSettings ` -ErrorAction Stop ` -RunLevel Highest } End { Write-Output $ScheduledTask } } Function Convert-SecureStringToString { <# .SYNOPSIS The cmdlet converts a secure string to standard string. .DESCRIPTION The cmdlet converts a secure string to standard string. .PARAMETER SecureString The secure string to convert to a standard string .INPUTS System.Security.SecureString .OUTPUTS System.String .EXAMPLE Convert-SecureStringToString -SecureString (ConvertTo-SecureString -String "test" -AsPlainText -Force) Converts the secure string created from the text "test" back to plain text. .NOTES None #> [CmdletBinding()] Param( [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)] [SecureString]$SecureString ) Begin {} Process { $Marshal = [System.Runtime.InteropServices.Marshal] $Password = [System.String]::Empty try { $IntPtr = $Marshal::SecureStringToBSTR($SecureString) $Password = $Marshal::PtrToStringAuto($IntPtr) } finally { if ($IntPtr) { $Marshal::ZeroFreeBSTR($IntPtr) } } } End { Write-Output $Password } } #endregion |