Building my Orion Server [Scripting Edition] – Step 3

I’m keeping this original post active for historical reference, but you should not use the below, since I have an update: Step 3 is dead. Long live step 3.1

This is it.  The endgame.  Here I’ll give you the final steps in configuring my server.  We started with creating the virtual machine then moved to configuring the disks.  We’re at the end – where’s it’s time to do the final configurations.

In summary, we’re going to do several steps here.  They pretty much follows my other guide step-by-step, so I’ll be brief in covering them here.

  1. Variable Declaration
  2. Installing Windows Features
  3. Enabling Disk Performance Metrics
  4. Installing some Utilities
  5. Copying the IIS Folders to a new Location
  6. Enable Deduplication (optional)
  7. Removing unnecessary IIS Websites and Application Pools
  8. Tweaking the IIS Settings
  9. Tweaking the ASP.NET Settings
  10. Creating a location for the TFTP and SFTP Roots (for NCM)
  11. Configuring Folder Redirection
  12. Pre-installing ODBC Drivers (for SAM Templates)

It seems like a lot, but each script snippet is small.

Variable Declaration

This is just where we setup the drives where we’ll keep the various parts of the install.

#region Variable Declaration
$PageFileDrive = "D:\"
$ProgramsDrive = "E:\"
$WebDrive      = "F:\"
$LogDrive      = "G:\"
#endregion

Installing Windows Features

This is simple – we add the necessary Windows Features.  The SolarWinds Orion installer can do this, but I like doing it in advance so that I can tweak some settings.  Note – if you don’t want to use Data Deduplication for your log files, you can omit that feature and skip that script part where we enable the deduplication of the log drive.

#region Add Necessary Windows Features
# 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, 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-Ext, Web-Net-Ext45, Web-Asp-Net, Web-Asp-Net45, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Mgmt-Tools, Web-Mgmt-Console, Web-Mgmt-Compat, Web-Metabase, NET-Framework-Features, NET-Framework-Core, NET-Framework-45-Features, NET-Framework-45-Core, NET-Framework-45-ASPNET, NET-WCF-Services45, NET-WCF-HTTP-Activation45, NET-WCF-MSMQ-Activation45, NET-WCF-Pipe-Activation45, NET-WCF-TCP-Activation45, NET-WCF-TCP-PortSharing45, MSMQ, MSMQ-Services, MSMQ-Server, FS-SMB1, User-Interfaces-Infra, Server-Gui-Mgmt-Infra, Server-Gui-Shell, PowerShellRoot, PowerShell, PowerShell-V2, PowerShell-ISE, WAS, WAS-Process-Model, WAS-Config-APIs, WoW64-Support, FS-Data-Deduplication | Where-Object { -not $_.Installed }
$Features | Add-WindowsFeature
#endregion

Enabling Disk Performance Metrics

Like I said before, I got used to seeing these in my Task Manager from my Windows Client machines, so I wanted to put them back.

#region Enable Disk Performance Counters in Task Manager
Start-Process -FilePath "C:\Windows\System32\diskperf.exe" -ArgumentList "-Y" -Wait
#endregion

Installing some Utilities

Here I install NotePad++, 7-zip, and a few other tools that I like.

#region Install 7Zip
# This can now be skipped because I'm deploying this via Group Policy
# Start-Process -FilePath "C:\Windows\System32\msiexec.exe" -ArgumentList "/i", "\\Path\To\Installer\7z1604-x64.msi", "/passive" -Wait
#endregion

#region Install Notepad++
# Install NotePad++ (current version)
# Still need to install the Plugins manually at this point, but this is a start
Start-Process -FilePath "\\Path\To\Installer\npp.latest.Installer.exe" -ArgumentList "/S" -Wait
#endregion

#region Setup UTILS Folder
# This contains the SysInternals and Unix Utils that I love so much.
$RemotePath = "\\Path\To\UTILS\"
$LocalPath  = "C:\UTILS\"
Start-Process -FilePath "C:\Windows\System32\robocopy.exe" -ArgumentList $RemotePath, $LocalPath, "/E", "/R:3", "/W:5", "/MT:16" -Wait

$MachinePathVariable = [Environment]::GetEnvironmentVariable("Path", "Machine")
if ( -not ( $MachinePathVariable -like '*$( $LocalPath )*' ) )
{
    $MachinePathVariable += ";$LocalPath;"
    $MachinePathVariable = $MachinePathVariable.Replace(";;", ";")
    Write-Host "Adding C:\UTILS to the Machine Path Variable" -ForegroundColor Yellow
    Write-Host "You must close and reopen any command prompt windows to have access to the new path"
    [Environment]::SetEnvironmentVariable("Path", $MachinePathVariable, "Machine")
}
else
{
    Write-Host "[$( $LocalPath )] already contained in machine environment variable 'Path'"
}
#endregion

Copying the IIS Folders to a new Location

I don’t like running my web-stuff on the operating system disk.  So, I’m moving it elsewhere.  Actually, I’m copying it because I want to use the original location as a source for the permissions that I’ll need

#region Copy the IIS Root to the Web Drive
# I can do this with Copy-Item, but I find that robocopy works better at keeping permissions
Start-Process -FilePath "robocopy.exe" -ArgumentList "C:\inetpub", ( Join-Path -Path $WebDrive -ChildPath "inetpub" ), "/E", "/R:3", "/W:5" -Wait
#endregion

#region Fix IIS temp permissions
$FolderPath = Join-Path -Path $WebDrive -ChildPath "inetpub\temp"
$CurrentACL = Get-Acl -Path $FolderPath
$AccessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList "NT AUTHORITY\NETWORK SERVICE", "FullControl", ( "ContainerInherit", "ObjectInherit" ), "None", "Allow"
$CurrentACL.SetAccessRule($AccessRule)
$CurrentACL | Set-Acl -Path $FolderPath
#endregion

Enable Deduplication

Super-simple here – just turn on Deduplication for the log drive.

#region Enable Deduplication on the Log Drive
Enable-DedupVolume -Volume ( $LogDrive.Replace("\", "") )
Set-DedupVolume -Volume ( $LogDrive.Replace("\", "") ) -MinimumFileAgeDays 0 -OptimizeInUseFiles -OptimizePartialFiles
#endregion

Removing unnecessary IIS Websites and Application Pools

Since Orion will install it’s own website and application pools, I can remove the existing ones to save on resources.

#region Delete Unnecessary Web Stuff
Get-WebSite -Name "Default Web Site" | Remove-WebSite -Confirm:$false
Remove-WebAppPool -Name ".NET v2.0" -Confirm:$false
Remove-WebAppPool -Name ".NET v2.0 Classic" -Confirm:$false
Remove-WebAppPool -Name ".NET v4.5" -Confirm:$false
Remove-WebAppPool -Name ".NET v4.5 Classic" -Confirm:$false
Remove-WebAppPool -Name "Classic .NET AppPool" -Confirm:$false
Remove-WebAppPool -Name "DefaultAppPool" -Confirm:$false
#endregion

Tweaking the IIS Settings

This is the most complex portion.  The summary is that I’ll be opening up the IIS and ASP.NET configuration files and changing a bunch of settings.  I do this using the System.Xml.XmlDocument type because it’s much easier to navigate.  A summary of the changes made can be found on my original post.

#region Change IIS Application Host Settings
# 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 File" ) )
$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
#endregion

Tweaking the ASP.NET Configuration Settings

Also complex, but handled the same way.

#region Change the ASP.NET Compilation Settings
# 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
#endregion

Creating a location for the TFTP and SFTP Roots

These two roots are going to be used after the installation, but I hate that they normally get put on the C:\ Drive.  So I create the folders here and then I’ll have to change them in the application after we install it.

#region Create SFTP and TFTP Roots on the Web Drive
# Check for & Configure SFTP and TFTP Roots
$Roots = "SFTP_Root", "TFTP_Root"
ForEach ( $Root in $Roots )
{
    if ( -not ( Test-Path -Path ( Join-Path -Path $WebDrive -ChildPath $Root ) ) )
    {
        New-Item -Path ( Join-Path -Path $WebDrive -ChildPath $Root ) -ItemType Directory
    }
}
#endregion

Configuring Folder Redirection

This was (by far) the hardest part to script out – only because I wanted to add a few more changes from the original post.  It turns out that “mklink” isn’t an executable.  It’s a function (like “dir”) that it executed in the command shell.  So figuring out the best way to handle this was a little tricky.  I finally found the answer by simply executing a command shell and passing it the entire command.

What did I add?  I moved the SolarWinds Log folder to the Log Drive and I moved the default program installation location to the Programs Drive.  Now I don’t need to remember to change the target for the first installer!

You’ll notice the “Order” Property that I added.  I did this because these need to be run in a specific order as to not cause linking failures (a link is being created to a subfolder, even through the parent folder doesn’t exist).

#region Folder Redirection
$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
    }
    # Build the string to send to the command prompt
    $CommandString = "mklink /D /J `"$( $Redirection.SourcePath )`" `"$( $Redirection.TargetPath )`""
    Write-Host "Executing [$CommandString]... " -ForegroundColor Yellow -NoNewline
    # Execute it
    Start-Process -FilePath "cmd.exe" -ArgumentList "/C", $CommandString -Wait
    Write-Host "[COMPLETED]" -ForegroundColor Green
}
#endregion

Pre-installing ODBC Drivers

Because I use a bunch of different database engines in my environment that I want SAM to monitor, I have to install the ODBC drivers for each engine.  The problem is that I always forget to install them until I’m actually trying to configure the Application Template.  This annoys me, so I figured it might be better to install them in advance.

#region Pre-Orion Install ODBC Drivers
#
# This is for any ODBC Drivers that I want to install to use with SAM
# You don't need to include any driver for Microsoft SQL Server - it will be done by the installer
# I have the drivers for MySQL and PostgreSQL in this share
#
# There is also a Post- share which includes the files that I want to install AFTER I install Orion.
$Drivers = Get-ChildItem -Path "\\Path\To\ODBC\Drivers\Pre\" -File
ForEach ( $Driver in $Drivers )
{
    if ( $Driver.Extension -eq ".exe" )
    {
        Write-Host "Executing $( $Driver.FullName )... " -ForegroundColor Yellow -NoNewline
        Start-Process -FilePath $Driver.FullName -Wait
        Write-Host "[COMPLETED]" -ForegroundColor Green
    }
    elseif ( $Driver.Extension -eq ".msi" )
    {
        # Install it using msiexec.exe
        Write-Host "Installing $( $Driver.FullName )... " -ForegroundColor Yellow -NoNewline
        Start-Process -FilePath "C:\Windows\System32\msiexec.exe" -ArgumentList "/i", "`"$( $Driver.FullName )`"", "/passive" -Wait
        Write-Host "[COMPLETED]" -ForegroundColor Green
    }
    else
    {
        Write-Host "Bork-Bork-Bork on $( $Driver.FullName )"
    }
}
#endregion

Results

So, if I put this all together and run it as an Administrator I get this:

The entire process took only 2:13!  And of that the majority (1:44) was waiting for the Windows Features to install.  This process (just step 3) used to take me at least 30 minutes with a checklist.

Now, I was able to go from no VM at all to Ready for SolarWinds Orion installation bits in under 20 minutes (10 of which are installing the O/S).

Last thing:  Apply Windows updates and reboot.  Then do it again.  And again.

So that’s it.  That’s my 3 step process to building better Orion Servers.  Please feel free to provide feedback on anything you see here.  I’m always happy to get it!

2 thoughts on “Building my Orion Server [Scripting Edition] – Step 3”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.