Development environments with PowerShell DSC – Part 5

So, finally, we’ve got the prerequisites (Windows, Mongo, SQL) out of the way, we can get to installing Sitecore in this post. There are a load of ways of going about this, but my usual choice is automating the Sitecore .exe installer. Doing this via DSC gives you the basis of an installation which can be used across all your platforms. The process below is based on the approach I’ve used with ordinary PowerShell in the past, but adapted for DSC:

(Edited to add: Links to all posts in this series: 1:Introduction to DSC2:Windows Features3:Mongo DB4:SQL Server – 5:Sitecore – 6:Coveo CES7:Coveo REST API & Coveo for Sitecore)

[Before I get going, couple of notes about the examples I’m including in this series of posts: They all have a call to Start-DSCConfiguration using the “-Force” flag to make DSC run this configuration immediately in “push” mode. However, the Configuration blocks declared should all work in pull configurations. Writing the scripts this way just makes them easier for people to try out without any other setup effort. Also the examples are all pretty much self-contained, so the overall configuration of a server using them would rely on many separate scripts. You don’t necessarily need to have as many as I’m showing here. It’s more to keep the examples clear and usable than to represent the “right” architecture. You’re free to merge things together, or even break them apart further if that suits you.]

We’re going to need some more config…

The basic Sitecore install is going to do two things for us – getting Sitecore running, and enabling us to remotely add any packages we need to set up in future parts of our install. So we can add in the following to our basic configuration:

@{
    AllNodes = @(
        @{
            NodeName = "WIN-AQEKG7L9SE8"
            Role = "Setup, WindowsFeatures, IE, SqlServer, MongoDB, Sitecore"
            
            TempFolder = "c:\dsc"

            WWWRoot = "C:\inetpub\wwwroot"

            Sitecore = @{
                Installer = "Sitecore 8.0 rev. 150812.exe"
                License = "PartnerLicense-2015.xml"
                InstanceName = "eight"

                SQLServer = "localhost"
                SQLUser = "sa"
                SQLPassword = "p@55w0rd"

                PackageInstallFile = "PackageDeploy.aspx"
            }
         }
    );
}

This adds the disk path to the WWWRoot folder. This could be part of the Sitecore config block itself, but it struck me that this was something I’d end up using for things other than just Sitecore. Inside the Sitecore config block, the properties specify the name of the installer to use, the license file we’re using and an instance name to install. It also specifies the SQL server details to be used by Sitecore, and the ASPX file that will be used to enable us to install packages.

The core Sitecore install

As with our previous installs, the script starts from the same basic DSC pattern:

Configuration SitecoreInstall
{
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $PackagePath
    )
 
    Node $AllNodes.where{ $_.Role.Contains("Sitecore") }.NodeName
    {
         #
         # We'll put the resources here
         #
    }
}

The first thing we need to do is copy over the Sitecore installer and license files, which can be done with the File resource:

File SitecoreInstaller
{
    SourcePath = "$PackagePath\$($Node.Sitecore.Installer)"
    DestinationPath = "$($Node.TempFolder)\$($Node.Sitecore.Installer)"
    Type = "File"
    Ensure = "Present"
}
 
File SitecoreLicense
{
    SourcePath = "$PackagePath\$($Node.Sitecore.License)"
    DestinationPath = "$($Node.TempFolder)\$($Node.Sitecore.License)"
    Type = "File"
    Ensure = "Present"
}

As discussed in my previous posts about automating Sitecore installs, we need to get the .MSI package out of the Sitecore installer in order to automate it. We can do that with a script resource. I’ve not implemented the GetScript property for this resource, as it wasn’t immediately obvious what it should return. The TestScript just checks if our temp folder already includes the “SupportFiles” folder that this process should create:

Script ExtractSC8
{
    DependsOn = "[File]SitecoreInstaller"
    GetScript = {
    }
    TestScript = {
        $tmp = $using:Node.TempFolder
        Test-Path "$using:tmp\SupportFiles"
    }
    SetScript = {
        $tmp = $using:Node.TempFolder
        $installer = $using:Node.Sitecore.Installer

        Push-Location $tmp

        $path = "$tmp\$installer"

        &$path /q /ExtractCab | out-null

        Pop-Location
    }
}

The SetScript is pretty simple – it works out the location of our configured temp folder and the Sitecore installer. Then it changes directory to the temp folder and executes the installer passing in flags for “quiet” and “extract”. That results in a folder under temp containing the .MSI as well as a few other bits which we can ignore.

So the next step is to run the install. Again the GetScript is blank here. I think it should probably return something about any instance installed with our defined name – but that’s a task for another day. For the TestScript we’re checking if an instance is already installed with the specified name by checking if a folder exists for it under the website root folder we configured. That’s enough for us to decide whether to proceed with the install.

The SetScript for the install is a bit more complex here. This resource has been written to install v8 of Sitecore, but this could be easily adjusted to work with other versions.

Script SC8
{
    DependsOn = "[Script]ExtractSC8, [file]SitecoreLicense"
    GetScript = {
    }
    TestScript = {
        $wwwRoot = $using:Node.WWWRoot
        $instance = $using:Node.Sitecore.InstanceName
        $iisFolder = "$using:wwwRoot\$using:instance"

        Test-Path $iisFolder
    }
    SetScript = {
        $registryPath =  "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\"

        if(-not (Test-path $registryPath)){
            throw "The instance-testing path in the registry could not be found - not a 64bit machine?"
        }

        $scRegistryPath = "$registryPath\Sitecore CMS\"

        $instanceNumber = "1"

        if( Test-path $scRegistryPath ){
            Push-Location $scRegistryPath

            $lastID = dir | Get-ItemProperty -Name InstanceID | Sort-Object -property InstanceID -Descending | Select-Object -ExpandProperty InstanceID -First 1

            if( -not [string]::IsNullOrWhiteSpace($lastID)){
                Write-Verbose "Previous instance found: $lastID "

                $instanceString = $lastID.Remove(0,10)
                $instanceNumber = [int]::Parse($instanceString)
                $instanceNumber = $instanceNumber + 1
            } else {
                Write-Verbose "No previous instance found"
            }

            Pop-Location
        } else {
            Write-Verbose "No previous instance found"
        }

        Write-Verbose "Using instance number: $instanceNumber"

        $instanceID = "InstanceId$instanceNumber"

        $tmp = $using:Node.TempFolder
        $licenseFile = $using:Node.Sitecore.License
        $license = "$tmp\$licenseFile"
        $site = $using:Node.Sitecore.InstanceName
        $siteAppPool = "$($site)_AppPool"
        $sitePrefix = "$($site)_"
                
        $sqlServer = $using:Node.Sitecore.SQLServer
        $sqlUser = $using:Node.Sitecore.SQLUser
        $sqlPassword = $using:Node.Sitecore.SQLPassword

        $wwwRoot = $using:Node.WWWRoot

        msiexec.exe /qn /i "$tmp\SupportFiles\exe\Sitecore.msi" "TRANSFORMS=:$instanceID;:ComponentGUIDTransform5.mst" "MSINEWINSTANCE=1" "LOGVERBOSE=1" "SC_LANG=en-US" "SC_FULL=1" "SC_INSTANCENAME=$site" "SC_LICENSE_PATH=$license" "SC_SQL_SERVER_USER=$sqlUser" "SC_SQL_SERVER=$sqlServer" "SC_SQL_SERVER_PASSWORD=$sqlPassword" "SC_DBPREFIX=$sitePrefix" "SC_PREFIX_PHYSICAL_FILES=1" "SC_SQL_SERVER_CONFIG_USER=$sqlUser" "SC_SQL_SERVER_CONFIG_PASSWORD=$sqlPassword" "SC_DBTYPE=MSSQL" "INSTALLLOCATION=$wwwRoot\$site" "SC_DATA_FOLDER=$wwwRoot\$site\Data" "SC_DB_FOLDER=$wwwRoot\$site\Databases" "SC_MDF_FOLDER=$wwwRoot\$site\Databases\MDF" "SC_LDF_FOLDER=$wwwRoot\$site\Databases\LDF" "SC_NET_VERSION=4" "SITECORE_MVC=0" "SC_INTEGRATED_PIPELINE_MODE=1" "SC_IISSITE_NAME=$site" "SC_IISAPPPOOL_NAME=$siteAppPool" "SC_IISSITE_HEADER=$site" "SC_IISSITE_PORT=80" "SC_IISSITE_ID=" "/l*+v" "$tmp\Install.log" | out-null
    }
}

The first job it needs to do is work out the “instance number” property that needs to be passed to the installer command. The process for working this out here is just a DSC translation of the process discussed in my previous post on this parameter. (This post also explains why we need it)

The code checks for the existence of the registry keys that the Sitecore installer creates when it runs. If the registry path exists, then the largest instance ID can be extracted and incremented to get the one for us to use. If the registry path doesn’t exist, then we can assume no previous installs have happened and we can default to instance one.

Once the instance number has been found, the code extracts all the config variables that the install requires.

Then finally, it runs msiexec.exe, passing in the .MSI file we generated and all the relevant properties to let Windows run the installation. Once this completes, Sitecore is ready to run.

Now for packages…

Next, we can add the remote deployment ability, in order to add any packages we want to extend Sitecore with. As discussed in another of my previous posts, there are various ways of remotely deploying packages, but the approach I’m re-using is a simple simple .ASPX file to act as an endpoint where package installs can be requested. (The ASPX file I borrowed from another developer is here) You can use this to install whatever packages you need, or you can use it to install something like PowerShell Extensions which provide a better installation endpoint. But to get going, we just need a file resource to put the file in place:

File AddRemoteDeploy
{
    DependsOn = "[script]SC8"
    SourcePath = "$PackagePath\$($Node.Sitecore.PackageInstallFile)"
    DestinationPath = "$($Node.WWWRoot)\$($Node.Sitecore.InstanceName)\website\$($Node.Sitecore.PackageInstallFile)"
    Type = "File"
    Ensure = "Present"
}

Now, one key aspect to getting packages installed correctly via this route is the response time of Sitecore when you make one of these requests. Especially with large packages and Sitecore 8, it’s quite possible to end up with your request for the package to be installed timing out. Hence it’s sensible to make sure Sitecore is started up before you try to install anything. And this can be done with another script resource:

Script WarmSitecore
{
    DependsOn = "[script]SC8"
    GetScript = {
    }
    TestScript = {
        $false
    }
    SetScript = {
        $url = "http://$($using:Node.Sitecore.InstanceName)/"

        Write-Verbose "Starting Sitecore..."

        $result = $null

        do
        {
            Write-Verbose " -- trying to start Sitecore -- "
            try
            {
                $result = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 0
            }
            catch
            {
                Write-Verbose " -- start failed - retying [$($result.StatusCode)] -- " 
            }
        }
        while($result.StatusCode -ne 200)
        Write-Verbose "Public site started..."

        $url = $url + "sitecore/"

        do
        {
            Write-Verbose " -- Retry starting Admin -- "
            try
            {
                $result = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 0
            }
            catch
            {
                Write-Verbose " -- start failed - retying [$($result.StatusCode)] -- " 
            }
        }
        while($result.StatusCode -ne 200)
        Write-Verbose "Admin site started..."
    }
}

The GetScript and TestScript properties aren’t filled in here, as they don’t really make sense to me in this context. The SetScript makes HTTP requests to the sitecore public site until we get back a “success” response. (Whilst we’re telling Invoke-WebRequest to apply no timeout, you do still get timeout exceptions from this code – hence the try/catch block) We then repeat the same process to load the Sitecore admin site as well.

And that’s Sitecore ready to go for your development.

Advertisements

2 thoughts on “Development environments with PowerShell DSC – Part 5

  1. Pingback: Development environments with PowerShell DSC – Part 6 | Jeremy Davis
  2. Pingback: Development environments with PowerShell DSC – Part 7 | Jeremy Davis

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