This blog is actually derived from my earlier post about creating a Service Template for DotNetNuke, read about it here. I decided to separate this content from that post for 2 reasons, first is that it would have been too long, next is that this information can simply help any service templates using NLB and web.config connection strings for a SQL server deployed in the template. Note: these workarounds are not configured in the DotNetNuke template although the scripts are included in that download found here for your customization.

During this deployment we find 2 real world challenges arise which have multiple workarounds that depend on a number of things related to your use case and your environment. The first is changing the web.config files connection string to the database dynamically based on the SQL server’s name that is created as part of the deployment. The easiest workaround is to ask the user to enter a name for the SQL server during deployment and use that name in a script or as a parameter to modify the config file. This of course is not very dynamic and also not very smart as it doesn’t check DNS and confirm that the name is not already in use. Maybe if we use a Service request and/or Orchestrator to automate deployment we could manage the entries and test against DNS and other corporate standards. For my purposes I accepted the default name, which looks like ‘servicevm#####’. I could have even created a naming convention like SQLPROD####.

Challenge number 2 is that there is no entry in DNS at the end of the service deployment which points to the network load balancers IP address. The problem is that there is no command or PowerShell that can run as a pre or post install script to accomplish creating this entry.

These are essentially what can be called out of band template modifications as the main challenge is you cannot access some of this information via scripts until the template is completely deployed. So in my normal style of first over complicating things before I simply them I created this workaround. In my workaround I essentially created a PowerShell script that creates a Scheduled Task in the web tier VM. In this  CreateCustomScheduledTask.ps1   PowerShell script I create 4 Key Value Pair (KVP) entries that correspond to some required variables like VMM Server name and Service Template ID of the current service. I also create a KVP which tracks the result of the scheduled task, true or false. Using this I can control clean-up of the scheduled task or simply do nothing if successful in the past. To create the scheduled task I use:

$cmd = "Schtasks.exe /Create /TN ServiceTemplatedTask /XML " + $destDirectory + "\ST.xml /RU " + $ST_User + " /RP " + $ST_Password

In the above command the ST.xml file is the exported scheduled task I created during testing the task and simply exporting it from the Task Scheduler. I do have to modify the Actions node of the XML before importing with the correct command arguments to use for the scheduled task. I do this by running the following PowerShell in the CreateCustomScheduledTask.ps1   file:

$xml = New-Object XML
$xml.Load("$destDirectory\ST.xml")
$xml.Task.Actions.Exec.Arguments = "-command $destDirectory"+"\DynamicConnectStringConfig.ps1"
$xml.Save("$destDirectory\ST.xml")

In this case the arguments are the PowerShell script to run and the directory to find it. I have also broken every rule I can think of but I have included some hard coded variables in the script which include a user name and password in clear text which will be used as the run as account for the scheduled task. The scheduled task for dynamic connection string configuration requires the ability to remote into the VMM server so it will need to run under credentials that have access to do so. Sure there are other ways, I cheated so I could complete the project on time. Feel free to comment on better options or other choices you make. You can also see I hard coded the script name in the above snippet which should be a variable and/or argument as well.

Here is the full script I used:
$folder = $args[0] # Derived from the @NameOfIISSite@ parameter in the service template
$vmmServer = $args[1] # Virtual machine manager name
$serviceID = $args[2] # Derived from the known service ID parameter @serviceID@
$destDirectory = $env:ProgramData + "\CustomScripts"
$regPath = "HKLM:SOFTWARE\Microsoft\Virtual Machine\ServiceTemplate"
$ST_User = "contoso\!installer"
$ST_Password = Pass@word1

try{
    # Creates registry key if it doesn;t already exist. This is where we track if this script completes.

    If (!(Test-Path $regPath)) 
    {
        New-Item $regPath | Out-Null
    }
        Set-ItemProperty -Path $regPath -Name 'NameOfIISSite' -Value $folder
        Set-ItemProperty -Path $regPath -Name 'vmmServer' -Value $vmmServer
        Set-ItemProperty -Path $regPath -Name 'serviceID' -Value $serviceID
        Set-ItemProperty -Path $regPath -Name 'DynamicConnectResult' -Value 'False'

if ((Test-Path -path $destDirectory) -ne $True){ New-Item $destDirectory -type directory}

$sourceDirecotry = Split-Path -Path $MyInvocation.MyCommand.Definition
$files = Get-ChildItem -Path:$sourceDirecotry

foreach($file in $files)
{
    $path = $file.FullName
    Copy-Item -Path:$path -Destination:$destDirectory -Force -Confirm:$false
}

$xml = New-Object XML
$xml.Load("$destDirectory\ST.xml")
$xml.Task.Actions.Exec.Arguments = "-command $destDirectory"+"\DynamicConnectStringConfig.ps1"
$xml.Save("$destDirectory\ST.xml")

$cmd = "Schtasks.exe /Create /TN ServiceTemplatedTask /XML " + $destDirectory + "\ST.xml /RU " + $ST_User + " /RP " + $ST_Password
$result = invoke-expression "cmd.exe /c `"`"$cmd 2>&1`"`""

        # Writing an event
        $EventLog = New-Object System.Diagnostics.EventLog('Application')
        $EventLog.MachineName = "."
        $EventLog.Source = "Create Scheduled Task"
        $EventLog.WriteEntry("$result","Information", "1000")

 

} Catch [Exception]{
        $EventLog = New-Object System.Diagnostics.EventLog('Application')
        $EventLog.MachineName = "."
        $EventLog.Source = "Create Scheduled Task"
        $EventLog.WriteEntry("Failed to create scheduled task for service template deployment. The error message: $_.Exception.Message POSH Error: $error","Error", "1000")
        }

Dynamic Web.Config connectionstring

For dynamically modifying web.config connection strings the script needs to talk to Virtual Machine Manager so in my lab that means I need to add a service account I’ll use later to the WinRMRemotingWMIUsers_ local group on VMM. I did that by running the following in PowerShell as Administrator:

Enter-PSSession -ComputerName vmm01
net localgroup WinRMRemoteWMIUsers__ /add contoso\!installer

I then added that service account to VVM Administrators Role by running the following commands:


Import-Module-Namevirtualmachinemanager
$userRole = Get-SCUserRole -Name "Administrator"
Set-SCUserRole -UserRole $userRole -Description "Administrator User Role" -JobGroup "d65c8a6e-fe0f-460c-a789-95a4bc9712ec" -AddMember @("CONTOSO\!installer")

I have already created a run as account in VMM for my CONTOSO\!installer service account, if not do that now. We also have 2 hard coded variables here that can be parameters submitted at run time as arguments are stored elsewhere. The first is what will the machine tire name be for the SQL server we want to get it name for. The second is the location we use for Key Value Pairs (KVP).

# Parameters to be modified to reflect variables from service template
$tierName = "SQL2012-SP1 - Machine Tier 1"
$regPath = "HKLM:SOFTWARE\Microsoft\Virtual Machine\ServiceTemplate"

Then get the registry entries from earlier and using the KVP that determines if the task completed or not we will test and if false, which it is by default, we run the logic that gets SQL’s name and modifies the config file as needed. If it works we change the KVP to false and for testing purposes instead of deleting the scheduled task we disable it.

  if($result -eq 'False')
    {
        $webConfig = "C:\inetpub\wwwroot\$folder\web.config"
        $currentDate = (get-date).tostring("mm_dd_yyyy-hh_mm_s") # month_day_year - hours_mins_seconds

         $session = New-PSSession -ComputerName $vmmServer
        Invoke-Command –Session $Session {Import-Module –Name VirtualMachineManager}
        Import-PSSession -Session $session -Module VirtualMachineManager

        Get-SCVMMServer $vmmServer
        $ComputerTier = Get-SCComputerTier | where { $_.Name -eq $tierName -and $_.ServiceId -eq $serviceID}

        $ServerName = $ComputerTier.VMs
        $application = Get-SCApplication | where { $_.Parent -eq $ServerName}
        Exit-PSSession

        $SQLServerInstance = $application.SQLInstanceName
        $SQLServerDACName = $application.DACInstanceName
        $SQLServerName = $ServerName.split(".")

        if (($SQLServerName[0] -ne $null) -and ($SQLServerName[0] -ne ""))
        {
            if (($SQLServerInstance -ne $null) -and ($SQLServerInstance -ne ""))
            {
                 $SQLServerName[0] = $SQLServerName[0] + "\" + $SQLServerInstance
            }
            $dataSource = $SQLServerName[0].ToLower()
            $doc = new-object System.Xml.XmlDocument
            $doc.Load($webConfig)
            $backup = $webConfig + "_$currentDate"
            $doc.Save($backup)
            $root = $doc.get_DocumentElement()

            $connectionString = "Data Source=$dataSource;Initial Catalog=$SQLServerDACName;Integrated Security=True"

            $root.connectionStrings.add.connectionString = $connectionString
            $doc.Save($webConfig)

        }

        $result = 'True'
        # Set registry key to True = complete
        Set-ItemProperty -Path $regPath -Name 'DynamicConnectResult' -Value $result
   
    if($result -eq 'True')
    {
        & SCHTASKS /Change /DISABLE /TN "\ServiceTemplatedTask"
        # Writing an event
        $EventLog = New-Object System.Diagnostics.EventLog('Application')
        $EventLog.MachineName = "."
        $EventLog.Source = "Scheduled Task - ServiceTemplatedTask"
        $EventLog.WriteEntry("Scheduled task completed, the web.config has been modified with the service templates SQL name. New connectionstring = $connectionString","Information", "1000")

    }
}

We also have the catch block which will capture the exception and log into the event log as a failure, providing there is one. In the catch block we should add some more logic that either counts number of failures and stores in KVP so after X number of tries it disables or simply disable on first failure, I’ll let you decide and add as needed.

catch
[Exception] {
        $EventLog = New-Object System.Diagnostics.EventLog('Application')
        $EventLog.MachineName = "."
        $EventLog.Source = "Scheduled Task - ServiceTemplatedTask"
        $EventLog.WriteEntry("The scheduled task failed to complete. The error message: $_.Exception.Message. Error from POSH: $Error","Error", "9000")
        }

Add NLB to DNS

A slightly less tested but simpler script is the one I use to add a DNS entry for the NLB IP address. I simply use the create Scehduled Task script as another Post_Install task of the service template but change the line that modifies the importable XML to which includes the DNS server FQDN and the Alias to add for the A pointer( these can be additional @parameters@ as needed, I was just lazy):

xml.Task.Actions.Exec.Arguments = "-command $destDirectory"+"\AddNLBEntryToDns.ps1 DNSServer.comtoso.com DotNetNuke"

This script was original designed to run stand alone after the deployment so it has some legacy code I just left in, like report back if arguments are missed. The rest is easy. We get and set some variables that we will use, again some are hard coded but I don’t have to explain why anymore J If an IP address is not submitted as an argument we will use the Get-NlbClusterVIP command to get it for us. For that we had to add the RSAT-NLB Windows Feature, if not already added, using the following:

If (!(Get-WindowsFeature RSAT-NLB)){Add-WindowsFeature RSAT-NLB}

The command to actually add an a pointer via WMI is fairly simple and basically 2 lines:

$Arec
= [WmiClass]\\$server\root\MicrosoftDNS:MicrosoftDNS_AType
$Arec
.CreateInstanceFromPropertydata($server, $zone, $name, $class, $ttl, $address.IPAddress)

The entire PowerShell file looks like this, minus the arguments check section:

try{
# This section adds Features for NLB management.
If (!(Get-WindowsFeature RSAT-NLB)){Add-WindowsFeature RSAT-NLB}

$server
= $args[0]
$serverSplit = $server.split(".")
$zone = $serverSplit[1] + "." + $serverSplit[2]
$name = $args[1]  + "." + $zone
$address = $args[2]
$class = 1 
$ttl = 3600

# Test if an value has been submitted for IP address and if not get local host name and then IP address in IPv4 format
if(!$address)
{
    $address = Get-NlbClusterVip
}

 # TODO: It would be nice to first test the name being added and if it exist to add a number and restest, like name01, name02
$Arec = [WmiClass]"\\$server\root\MicrosoftDNS:MicrosoftDNS_AType" 
$Arec.CreateInstanceFromPropertydata($server, $zone, $name, $class, $ttl, $address.IPAddress)

        # Writing an event
        $EventLog = New-Object System.Diagnostics.EventLog('Application')
        $EventLog.MachineName = "."
        $EventLog.Source = "Add NLB to DNS Script"
        $EventLog.WriteEntry("$result","Information", "1000")

}catch [Exception]{
    $EventLog = New-Object System.Diagnostics.EventLog('Application')
    $EventLog.MachineName = "."
    $EventLog.Source = "$ScriptName"
    $EventLog.WriteEntry("Script failed. The error message: $_.Exception.Message","Error", "1000")
    }

Please feel free suggest your changes and ways to accomplish as I pointed out in the beginning there is always more ways to write that code.