Low-effort Solr installs

I’m sure I’ve said before that any task you have to do more than once is worth automating. Well recently I’ve found myself needing to install Solr in a variety of places – so obviously my mind turned to automation. There are lots of ways this can be approached, and some people have already had a go at it for their own needs, but here’s my take.

Edited to add: Kam Figy makes the valid points that the downloads should really use HTTPS, and it would be even easier if the script added a host entry if necessary. I’ve updated this based on those suggestions…

The problem I wanted to solve

I needed to be able to easily set up development instances of different versions of Solr. My company has tended to use NSSM as it’s approach to running Solr as a service, so I wanted to stick with that. Some of these Solr instances need to work over HTTPS, so I also wanted to be able to set up certificates for them. And I needed it to work in a scenario where I had to run multiple versions of Solr at once.

The instances of Solr were required for v8 Sitecore projects, so the script needed to work without SIF. That meant plain PowerShell script seemed the best way to go. So, firing up the PowerShell ISE…

Getting started…

The first thing to note is that installing services requires admin privileges. That means the script should probably report an error if it’s run from a session that’s not elevated. One approach to testing that state looks like:

$elevated = [bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544")
if($elevated -eq $false)
{
    throw "In order to install services, please run this script elevated."
}

That checks if the current user has the Administrator security principal, and if not, throws an error to stop the script.

Now the script is going to need some input from the user to decide what it needs to do:

Param(
    $installFolder = "c:\solr",
    $nssmVersion = "2.24",
    $solrVersion = "6.6.2",
    $solrPort = "8983",
    $solrHost = "localhost",
    $solrSSL = $true,
    $JREVersion = "1.8.0_151"
)

It needs to know where to put the stuff being installed via $installFolder, and it needs to know versions of the software we want to install via $nssmVersion and $solrVersion. Solr needs a port number, a host name and whether it should used HTTPS or not, via $solrVersion, $solrHost and $solrSSL. And finally we need to know what version of the Java runtime Solr should use when it runs if there’s not global setting for this, via $JREVersion.

Based on those settings, the script can compute a few other values that will be used later:

$JREPath = "C:\Program Files (x86)\Java\jre$JREVersion"
$solrName = "solr-$solrVersion"
$solrRoot = "$installFolder\$solrName"
$nssmRoot = "$installFolder\nssm-$nssmVersion"
$solrPackage = "https://archive.apache.org/dist/lucene/solr/$solrVersion/$solrName.zip"
$nssmPackage = "https://nssm.cc/release/nssm-$nssmVersion.zip"
$downloadFolder = "~\Downloads"

Next we need to ensure that Solr and NSSM are downloaded and unzipped to the right place. Since the same task has to happen for two things, that suggests a function… To avoid downloading and unzipping when it’s not necessary this should check if the destination folder we’re using exists, and do nothing if it does. If it doesn’t exist then it should check if the zip file exists before downloading it:

function downloadAndUnzipIfRequired
{
    Param(
        [string]$toolName,
        [string]$toolFolder,
        [string]$toolZip,
        [string]$toolSourceFile,
        [string]$installRoot
    )

    if(!(Test-Path -Path $toolFolder))
    {
        if(!(Test-Path -Path $toolZip))
        {
            Write-Host "Downloading $toolName..."
            Start-BitsTransfer -Source $toolSourceFile -Destination $toolZip
        }

        Write-Host "Extracting $toolName to $toolFolder..."
        Expand-Archive $toolZip -DestinationPath $installRoot
    }
}

That can be called once for each of the zip files we want downloaded and extracted:

$solrZip = "$downloadFolder\$solrName.zip"
downloadAndUnzipIfRequired "Solr" $solrRoot $solrZip $solrPackage $installFolder

$nssmZip = "$downloadFolder\nssm-$nssmVersion.zip"
downloadAndUnzipIfRequired "NSSM" $nssmRoot $nssmZip $nssmPackage $installFolder

Solr is a Java application, so it needs to know which Java runtime to make use of. You can set this in the Solr config files, but my preference is to ensure that there’s a global environment variable to specify this, as that makes it easier to change it for everything when the runtime gets updated. So if the system doesn’t have the right value for the JAVA_HOME environment variable, the script needs to set that to match the version of Java the parameter specifies:

$jreVal = [Environment]::GetEnvironmentVariable("JAVA_HOME", [EnvironmentVariableTarget]::Machine)
if($jreVal -ne $JREPath)
{
    Write-Host "Setting JAVA_HOME environment variable"
    [Environment]::SetEnvironmentVariable("JAVA_HOME", $JREPath, [EnvironmentVariableTarget]::Machine)
}

The next thing we need, is to deal with the configuration for Solr. If the parameters for the script say it’s using HTTP, that’s pretty easy:

if($solrSSL -eq $false)
{
    if(!(Test-Path -Path "$solrRoot\bin\solr.in.cmd.old"))
    {
        Write-Host "Rewriting solr config"

        $cfg = Get-Content "$solrRoot\bin\solr.in.cmd"
        Rename-Item "$solrRoot\bin\solr.in.cmd" "$solrRoot\bin\solr.in.cmd.old"
        $newCfg = $newCfg | % { $_ -replace "REM set SOLR_HOST=192.168.1.1", "set SOLR_HOST=$solrHost" }
        $newCfg | Set-Content "$solrRoot\bin\solr.in.cmd"
    }
}

Most of the config parameters for Solr can be set by amending the defaults in the solr.in.cmd file. So the script can read that, find the line that needs updating, and replace it with the right value. Before changing anything it backs up the original file – which is also an easy way to tell if the changes have already been made.

If the host name we’re using for Solr here isn’t “localhost” then it’s proably also useful to add that to the machine’s hosts file:

if($solrHost -ne "localhost")
{
    $hostFileName = "c:\\windows\system32\drivers\etc\hosts"
    $hostFile = [System.Io.File]::ReadAllText($hostFileName)
    if(!($hostFile -like "*$solrHost*"))
    {
        Write-Host "Updating host file"
        "`r`n127.0.0.1`t$solrHost" | Add-Content $hostFileName
    }
}

Life is a bit more complex if HTTPS is enabled, as the certificates need setting up as well. That involves four things: Generating a certificate, exporting it so Solr can make use of it for its web server, trusting the certificate and finally more config changes.

Being a Java application, Solr’s default is to use the not-very-Windows-friendly “JKS” export format for the certificates. If you prefer that approach, the code is out there. But newer versions of Solr can use “PFX” files, which is easier to deal with in a PowerShell-native way.

if($solrSSL -eq $true)
{
    $existingCert = Get-ChildItem Cert:\LocalMachine\Root | where FriendlyName -eq "$solrName"
    if(!($existingCert))
    {
        Write-Host "Creating & trusting an new SSL Cert for $solrHost"

        $cert = New-SelfSignedCertificate -FriendlyName "$solrName" -DnsName "$solrHost" -CertStoreLocation "cert:\LocalMachine" -NotAfter (Get-Date).AddYears(10)

        $store = New-Object System.Security.Cryptography.X509Certificates.X509Store "Root","LocalMachine"
        $store.Open("ReadWrite")
        $store.Add($cert)
        $store.Close()

        # remove the untrusted copy of the cert
        $cert | Remove-Item
    }

    if(!(Test-Path -Path "$solrRoot\server\etc\solr-ssl.keystore.pfx"))
    {
        Write-Host "Exporting cert for Solr to use"

        $cert = Get-ChildItem Cert:\LocalMachine\Root | where FriendlyName -eq "$solrName"
    
        $certStore = "$solrRoot\server\etc\solr-ssl.keystore.pfx"
        $certPwd = ConvertTo-SecureString -String "secret" -Force -AsPlainText
        $cert | Export-PfxCertificate -FilePath $certStore -Password $certpwd | Out-Null
    }

    if(!(Test-Path -Path "$solrRoot\bin\solr.in.cmd.old"))
    {
        Write-Host "Rewriting solr config"

        $cfg = Get-Content "$solrRoot\bin\solr.in.cmd"
        Rename-Item "$solrRoot\bin\solr.in.cmd" "$solrRoot\bin\solr.in.cmd.old"
        $newCfg = $cfg | % { $_ -replace "REM set SOLR_SSL_KEY_STORE=etc/solr-ssl.keystore.jks", "set SOLR_SSL_KEY_STORE=$certStore" }
        $newCfg = $newCfg | % { $_ -replace "REM set SOLR_SSL_KEY_STORE_PASSWORD=secret", "set SOLR_SSL_KEY_STORE_PASSWORD=secret" }
        $newCfg = $newCfg | % { $_ -replace "REM set SOLR_SSL_TRUST_STORE=etc/solr-ssl.keystore.jks", "set SOLR_SSL_TRUST_STORE=$certStore" }
        $newCfg = $newCfg | % { $_ -replace "REM set SOLR_SSL_TRUST_STORE_PASSWORD=secret", "set SOLR_SSL_TRUST_STORE_PASSWORD=secret" }
        $newCfg = $newCfg | % { $_ -replace "REM set SOLR_HOST=192.168.1.1", "set SOLR_HOST=$solrHost" }
        $newCfg | Set-Content "$solrRoot\bin\solr.in.cmd"
    }
}

If a certificate for the host name doesn’t already exist in the trusted store, then the code can use New-SelfSignedCertificate to make one. It can then use the certificate store API to copy the certificate to the root certificate bit of the store, before deleting the untrusted copy.

If no certificate exists in the Solr folders then it can be exported to there in PFX format. This script is only aimed at development environments, so I’ve not bothered with a strong password for this file – but that would be easy to add if you needed it.

Finally, the same tactic for updating the config file can set the host, and the relevant settings for SSL.

With all that in place, Solr is ready to be started, so the service is the next thing to sort out:

$svc = Get-Service "$solrName" -ErrorAction SilentlyContinue
if(!($svc))
{
    Write-Host "Installing Solr service"
    &"$installFolder\nssm-$nssmVersion\win64\nssm.exe" install "$solrName" "$solrRoot\bin\solr.cmd" "-f" "-p $solrPort"
    $svc = Get-Service "$solrName" -ErrorAction SilentlyContinue
}
if($svc.Status -ne "Running")
{
    Write-Host "Starting Solr service"
    Start-Service "$solrName"
}

It checks if a correctly named service exists, and if not gets NSSM to set one up. If the service isn’t running it gets started.

Finally, to prove it worked, the script opens the Solr UI in your default browser:

$protocol = "http"
if($solrSSL -eq $true)
{
    $protocol = "https"
}
Invoke-Expression "start $($protocol)://$($solrHost):$solrPort/solr/#/"

And there’s Solr ready to configure for use with Sitecore, or whatever other uses you have for it…

The complete source is available as a Gist if you want to make use of this in your work.

Having put that together, I’ve started thinking about how these techniques could be adapted to working with Sitecore’s SIF framework. But that’s a blog post for another day, as there’s a pile of refactoring to do, in order to make it fit in with the style of SIF’s approach. [Edited to add: That other day came – the post is avalable here]

Advertisements

11 thoughts on “Low-effort Solr installs

  1. Hey Jeremy,
    nice post! The one thing I have been solving recently is the inability of the local XP0 install script from Sitecore to restart a 6.6.2 Solr instance running as a service using NSSM. Did you also face something similar? I am now exploring ways how to containerize Solr just to get rid of installing it altogether…

    • Thanks! I’ve not seen that issue – but I’ve not tried this as part of a scripted install of V9 yet, so maybe that fun is yet to come… I’m hoping to get time to do a follow-up post about integrating this type of install (but rewritten somewhat to fit with SIF’s style) in the future, so if I do see that issue, I’ll look into sorting it out…

    • Well – I have figured out where the problem was. Geoff – I was using the prerelease in 64bits mode. The problem was, that I was starting Solr without the -f (foreground) parameter. That meant, that when the script tried to restart the service, it essentially tried to spin up another instance of Solr that conflicted on the port number with the one already running in the background. Duh… So the trick is to start always with the -f parameter.

  2. Pingback: How to install Solr as a Windows Service | Sitecore Blog
  3. Pingback: Sitecore 9 SIF installation error : ERROR_SCRIPTDOM_NEEDED_FOR_SQL_PROVIDER | Naveed's Blog
  4. Pingback: Jak nainstalovat Solr jako Windows Službu | Sitecore Blog
    • My pleasure. I was working on Windows 10 for this, so I’m not surprised it needed tweaking for Windows 8. If you want to share what you had to do to make it work I’ll add a link to that, as I suspect others would find it useful.

  5. Pingback: Solr installs with SIF | Jeremy Davis
  6. Pingback: Installing Sitecore 9 – How I did it | Visions In Code

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s