Building my Orion Server [Scripting Edition] – Step 3.1

Let’s forget all about my previous step 3. For the sake of argument, step 3 is dead, long live step 3.1!  Any script should be a living, breathing document which gets updated as needed.  In this case, it was needed.  Why would I burn such a good script to the ground?  Well, to be blunt, I’ve found a better way to do this, with better ultimate performance.

If you need a refresher on the previous steps, I have Step 1 for Hyper-V, Step 1 for VMware, and Step 2.

Let start with the basics again – variable declaration.

Variable Declaration

The only variables that I’m defining here are the drives on which the various parts of the system lives.  If you’ve followed my step 1 & 2, then you are using these drive designations.  If not, then you should change this for your own needs.

$ProgramsDrive = "E:\"
$WebDrive      = "F:\"
$LogDrive      = "G:\"

Near the end of the previous post, I redirected a bunch of folders to the other drives, but I decided that I wanted to do the same thing for IIS before I installed it.

Redirect IIS

This is a big block, but basically it says that if the IIS folder doesn’t already exist (it shouldn’t since you didn’t install it yet, right?), then create it and create one on the “Web” drive as well, then redirect the C:\ drive to the “Web” drive. This was originally based on the mklink executable, but I am now using the New-Item cmdlet.

if ( -not ( Test-Path -Path "C:\inetpub" -ErrorAction SilentlyContinue ) )
{
    $Redirections = @()
    $Redirections += New-Object -TypeName PSObject -Property ( [ordered]@{ Order = [int]6; SourcePath = "C:\inetpub"; TargetDrive = $WebDrive } )
    $Redirections | Add-Member -MemberType ScriptProperty -Name TargetPath -Value { $this.SourcePath.Replace("C:\", $this.TargetDrive ) } -Force
    ForEach ( $Redirection in $Redirections | Sort-Object -Property Order )
    {
        # Check to see if the target path exists - if not, create the target path
        if ( -not ( Test-Path -Path $Redirection.TargetPath -ErrorAction SilentlyContinue ) )
        {
            Write-Host "Creating Path for Redirection [$( $Redirection.TargetPath )]" -ForegroundColor Yellow
            New-Item -ItemType Directory -Path $Redirection.TargetPath | Out-Null
        }
        # Execute it
        Write-Host "Creating Junction point from $( $Redirection.SourcePath ) --> $( $Redirection.TargetPath ) " -NoNewline
        New-Item -ItemType Junction -Path $Redirection.SourcePath -Value $Redirection.TargetPath | Out-Null
        Write-Host "[COMPLETED]" -ForegroundColor Green
    }
}

Install the missing Windows Features

This is pretty cut and dry.  We need these features, so instead of letting the Orion installer do it for me, I do it in advance.  The Add-WindowsFeature is one of my favorite script commands.

If you are installing the Orion Platform products version 2020.2.6 or later, then you don’t need the MSMQ (Message Queue) features. If that’s your situation, run the below.

# this is a list of the Windows Features that we'll need (omitting MSMQ for Orion Platform 2020.2.6+)
# it's being filtered for those which are not already installed
$Features = Get-WindowsFeature -Name FileAndStorage-Services, File-Services, FS-FileServer, FS-Data-Deduplication, Storage-Services, Web-Server, Web-WebServer, Web-Common-Http, Web-Default-Doc, Web-Dir-Browsing, Web-Http-Errors, Web-Static-Content, Web-Health, Web-Http-Logging, Web-Log-Libraries, Web-Request-Monitor, Web-Performance, Web-Stat-Compression, Web-Dyn-Compression, Web-Security, Web-Filtering, Web-Windows-Auth, Web-App-Dev, Web-Net-Ext45, Web-Asp-Net45, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Mgmt-Tools, Web-Mgmt-Console, Web-Mgmt-Compat, Web-Metabase, NET-Framework-45-Features, NET-Framework-45-Core, NET-Framework-45-ASPNET, NET-WCF-Services45, NET-WCF-TCP-PortSharing45, FS-SMB1, Windows-Defender-Features, Windows-Defender, Windows-Defender-Gui, PowerShellRoot, PowerShell, PowerShell-ISE, WoW64-Support, XPS-Viewer | Where-Object { -not $_.Installed }
$Features | Add-WindowsFeature

If you are installing Orion 2020.2.5 or earlier, then you’ll need the MSMQ (Message Queue) features, so run this script instead.

# this is a list of the Windows Features that we'll need
# it's being filtered for those which are not already installed
$Features = Get-WindowsFeature -Name FileAndStorage-Services, File-Services, FS-FileServer, FS-Data-Deduplication, Storage-Services, Web-Server, Web-WebServer, Web-Common-Http, Web-Default-Doc, Web-Dir-Browsing, Web-Http-Errors, Web-Static-Content, Web-Health, Web-Http-Logging, Web-Log-Libraries, Web-Request-Monitor, Web-Performance, Web-Stat-Compression, Web-Dyn-Compression, Web-Security, Web-Filtering, Web-Windows-Auth, Web-App-Dev, Web-Net-Ext45, Web-Asp-Net45, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Mgmt-Tools, Web-Mgmt-Console, Web-Mgmt-Compat, Web-Metabase, NET-Framework-45-Features, NET-Framework-45-Core, NET-Framework-45-ASPNET, NET-WCF-Services45, NET-WCF-TCP-PortSharing45, MSMQ, MSMQ-Services, MSMQ-Server, FS-SMB1, Windows-Defender-Features, Windows-Defender, Windows-Defender-Gui, PowerShellRoot, PowerShell, PowerShell-ISE, WoW64-Support, XPS-Viewer | Where-Object { -not $_.Installed }
$Features | Add-WindowsFeature

Enabling Disk Performance Metrics in Task Manager

Yeah, yeah, I know – this is a crutch for me, but I like having the Disk Metrics in the Task Manager.

Start-Process -FilePath "C:\Windows\System32\diskperf.exe" -ArgumentList "-Y" -Wait

Deduplicating the Logs

Deduplicating the log files saves a bunch of disk space.  If you use a back-end storage solution that deduplicates, then you don’t need to do this, but it doesn’t really hurt to do it here as well AFAIK.

if ( Get-WindowsFeature -Name FS-Data-Deduplication | Where-Object { $_.Installed } )
{
    Enable-DedupVolume -Volume ( $LogDrive.Replace("\", "") ) 
    Get-DedupVolume -Volume ( $LogDrive.Replace("\", "") ) | Set-DedupVolume -OptimizeInUseFiles -OptimizePartialFiles -MinimumFileAgeDays 0 -Verify $true
}

Clean up Default IIS

Out of the box, when you install IIS, several things get created.  I see no problem with this, but I like to free up as much memory and processor as possible for other uses, so I delete them.

Get-WebSite -Name "Default Web Site" | Remove-WebSite -Confirm:$false
Remove-WebAppPool -Name ".NET v2.0" -Confirm:$false -ErrorAction SilentlyContinue
Remove-WebAppPool -Name ".NET v2.0 Classic" -Confirm:$false -ErrorAction SilentlyContinue
Remove-WebAppPool -Name ".NET v4.5" -Confirm:$false -ErrorAction SilentlyContinue
Remove-WebAppPool -Name ".NET v4.5 Classic" -Confirm:$false -ErrorAction SilentlyContinue
Remove-WebAppPool -Name "Classic .NET AppPool" -Confirm:$false -ErrorAction SilentlyContinue
Remove-WebAppPool -Name "DefaultAppPool" -Confirm:$false -ErrorAction SilentlyContinue

Changing IIS Configuration Files

This is where it gets hairy.  There are many settings that I want to change.  Specifically, I want to change the logging location, type, fields, and period.  I also change the compressed file location and two disk space settings.  Before I touch any of this though, I make a backup copy of the config file.

# XML Object that will be used for processing
$ConfigFile = New-Object -TypeName System.Xml.XmlDocument

# Change the Application Host settings
$ConfigFilePath = "C:\Windows\System32\inetsrv\config\applicationHost.config"

# Load the Configuration File
$ConfigFile.Load($ConfigFilePath)
# Save a backup if one doesn't already exist
if ( -not ( Test-Path -Path "$ConfigFilePath.orig" -ErrorAction SilentlyContinue ) )
{
    Write-Host "Making Backup of $ConfigFilePath with '.orig' extension added" -ForegroundColor Yellow
    $ConfigFile.Save("$ConfigFilePath.orig")
}
# change the settings (create if missing, update if existing)
$ConfigFile.configuration.'system.applicationHost'.log.centralBinaryLogFile.SetAttribute("directory", [string]( Join-Path -Path $LogDrive -ChildPath "inetpub\logs\LogFiles" ) )
$ConfigFile.configuration.'system.applicationHost'.log.centralW3CLogFile.SetAttribute("directory", [string]( Join-Path -Path $LogDrive -ChildPath "inetpub\logs\LogFiles" ) )
$ConfigFile.configuration.'system.applicationHost'.sites.siteDefaults.logfile.SetAttribute("directory", [string]( Join-Path -Path $LogDrive -ChildPath "inetpub\logs\LogFiles" ) )
$ConfigFile.configuration.'system.applicationHost'.sites.siteDefaults.logfile.SetAttribute("logFormat", "W3C" )
$ConfigFile.configuration.'system.applicationHost'.sites.siteDefaults.logfile.SetAttribute("logExtFileFlags", "Date, Time, ClientIP, UserName, SiteName, ComputerName, ServerIP, Method, UriStem, UriQuery, HttpStatus, Win32Status, BytesSent, BytesRecv, TimeTaken, ServerPort, UserAgent, Cookie, Referer, ProtocolVersion, Host, HttpSubStatus" )
$ConfigFile.configuration.'system.applicationHost'.sites.siteDefaults.logfile.SetAttribute("period", "Hourly")
$ConfigFile.configuration.'system.applicationHost'.sites.siteDefaults.traceFailedRequestsLogging.SetAttribute("directory", [string]( Join-Path -Path $LogDrive -ChildPath "inetpub\logs\FailedReqLogFiles" ) )
$ConfigFile.configuration.'system.webServer'.httpCompression.SetAttribute("directory", [string]( Join-Path -Path $WebDrive -ChildPath "inetpub\temp\IIS Temporary Compressed Files" ) )
$ConfigFile.configuration.'system.webServer'.httpCompression.SetAttribute("maxDiskSpaceUsage", "2048" )
$ConfigFile.configuration.'system.webServer'.httpCompression.SetAttribute("minFileSizeForComp", "5120" )
# Save the file
$ConfigFile.Save($ConfigFilePath)
Remove-Variable -Name ConfigFile -ErrorAction SilentlyContinue

I do pretty much the same things for the file that controls the compilation of IIS files.

# XML Object that will be used for processing
$ConfigFile = New-Object -TypeName System.Xml.XmlDocument

# Change the Compilation settings in teh ASP.NET Web Config
$ConfigFilePath = "C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config\web.config"
Write-Host "Editing [$ConfigFilePath]" -ForegroundColor Yellow
# Load the Configuration File
$ConfigFile.Load($ConfigFilePath)
# Save a backup if one doesn't already exist
if ( -not ( Test-Path -Path "$ConfigFilePath.orig" -ErrorAction SilentlyContinue ) )
{
    Write-Host "Making Backup of $ConfigFilePath with '.orig' extension added" -ForegroundColor Yellow
    $ConfigFile.Save("$ConfigFilePath.orig")
}
# change the settings (create if missing, update if existing)
$ConfigFile.configuration.'system.web'.compilation.SetAttribute("tempDirectory", [string]( Join-Path -Path $WebDrive -ChildPath "inetpub\temp") )
$ConfigFile.configuration.'system.web'.compilation.SetAttribute("maxConcurrentCompilations", "16")
$ConfigFile.configuration.'system.web'.compilation.SetAttribute("optimizeCompilations", "true")
Write-Host "Saving [$ConfigFilePath]" -ForegroundColor Yellow
$ConfigFile.Save($ConfigFilePath)
# Save the file
$ConfigFile.Save($ConfigFilePath)
Remove-Variable -Name ConfigFile -ErrorAction SilentlyContinue

Redirecting More Folders

I’m going to leverage the New-Item -ItemType Junction command to create symbolic links like I did at the beginning for IIS specifically.

Here I’m applying it to a bunch of SolarWinds specific folders.

$Redirections = @()
$Redirections += New-Object -TypeName PSObject -Property ( [ordered]@{ Order = [int]1; SourcePath = "C:\ProgramData\SolarWinds"; TargetDrive = $ProgramsDrive } )
$Redirections += New-Object -TypeName PSObject -Property ( [ordered]@{ Order = [int]2; SourcePath = "C:\ProgramData\SolarWindsAgentInstall"; TargetDrive = $ProgramsDrive } )
$Redirections += New-Object -TypeName PSObject -Property ( [ordered]@{ Order = [int]3; SourcePath = "C:\Program Files (x86)\SolarWinds"; TargetDrive = $ProgramsDrive } )
$Redirections += New-Object -TypeName PSObject -Property ( [ordered]@{ Order = [int]4; SourcePath = "C:\Program Files (x86)\Common Files\SolarWinds"; TargetDrive = $ProgramsDrive } )
$Redirections += New-Object -TypeName PSObject -Property ( [ordered]@{ Order = [int]5; SourcePath = "C:\ProgramData\SolarWinds\Logs"; TargetDrive = $LogDrive } )
$Redirections | Add-Member -MemberType ScriptProperty -Name TargetPath -Value { $this.SourcePath.Replace("C:\", $this.TargetDrive ) } -Force
 
ForEach ( $Redirection in $Redirections | Sort-Object -Property Order )
{
    # Check to see if the target path exists - if not, create the target path
    if ( -not ( Test-Path -Path $Redirection.TargetPath -ErrorAction SilentlyContinue ) )
    {
        Write-Host "Creating Path for Redirection [$( $Redirection.TargetPath )]" -ForegroundColor Yellow
        New-Item -ItemType Directory -Path $Redirection.TargetPath | Out-Null
    }
    # Execute it
    Write-Host "Creating Junction point from $( $Redirection.SourcePath ) --> $( $Redirection.TargetPath ) " -NoNewline
    New-Item -ItemType Junction -Path $Redirection.SourcePath -Value $Redirection.TargetPath | Out-Null
    Write-Host "[COMPLETED]" -ForegroundColor Green
}

I also do a few more things in my personal environment like install Notepad++, some utilities, disabling the expiration of the local admin account, and importing a certificate so that I can run using secure web.  But if you don’t want to do any of that stuff, then you are done and ready to install any of the Orion products.

Importing my Wildcard SSL Certificate

#region Import Certificate
$CertName = "WildcardCert_demo.lab"
$CertPath = "\\Demo.Lab\Files\Data\Certificates\"
$PfxFile = Get-ChildItem -Path $CertPath -Filter "$CertName.pfx"
$PfxPass = ConvertTo-SecureString -String ( Get-ChildItem -Path $CertPath -Filter "$CertName.password.txt" | Get-Content -Raw ) -AsPlainText -Force
Import-PfxCertificate -FilePath $PfxFile.FullName -Password $PfxPass -CertStoreLocation "Cert:\LocalMachine\My"
#endregion