Tuesday, August 16, 2011

A Domain Specific Language For SharePoint 2010 Deployment

Visual Studio 2010 and SharePoint 2010 has made great improvements to deployments, but still lacks important features. This method is designed to allow for:
  • Easily moving to a new environment
  • Ease of deployment script enhancements without having to change all files
  • XML driven deployment scripts
  • Reduce need for having to know the SharePoint API
To achive this, we will use a Domain Specific Language (DSL) using XML and powershell as the processor.

We'll start out creating the shared environment variables. These are the only part of the method that is environment specific. I generally have one file that I copy to all environments and only uncomment the environment section.
Environmental Variables: SharedEnvironment.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#
# Environment specific variables for use in powershell scripts
#   Usage:
#       . (Join-Path $currentDirectory SharedEnvironment.ps1);
#
#   Maintenance:
#       Initial development
#
 
## VM
$SPRootURL = "http://localhost";
$SPEnvironment = "VM";
$SharePointDeploymentFolder = "C:\SharePointDeploy";
$SharePointSolutionCache = "C:\SharePointDeploy\wsp";
## DEV
#$SPRootURL = "http://SPUrlRoot"; # no ending slash
#$SPEnvironment = "DEV";
#$SharePointDeploymentFolder = "C:\SharePointDeploy";
#$SharePointSolutionCache = "C:\SharePointDeploy\wsp";
The processor is the heart of the method. It takes in the XML document and performs tasks based on the documents contents and what actions are implemented.
SharePoint DSL Parser: SPDeploy.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
param (
    [string] $xmlPath = $(Throw 'Missing: xmlPath'),
    [Switch] $remove
)
 
[void][System.Reflection.Assembly]::LoadWithPartialName( "Microsoft.SharePoint" );
if((Get-PSSnapin | Where-Object {$_.Name -eq "Microsoft.SharePoint.PowerShell"}) -eq $null) { Add-PSSnapIn "Microsoft.SharePoint.Powershell"; }
$currentDirectory = Split-Path $myInvocation.MyCommand.Path; # $myInvocation.MyCommand.Path;
$WSPCache = ".";
. (Join-Path $currentDirectory MemberCatalogEnvironment.ps1);
 
# Solution Handling
function ProcessSolution( [System.Xml.XmlNode] $solutionNode )
{
    if ($remove) {
        Retract-Solution $solutionNode;
    } else {
        Install-Solution $solutionNode;
    }
}
 
function Install-Solution( [System.Xml.XmlNode] $solutionNode )
{
    $curDir = Split-Path -Parent $MyInvocation.ScriptName;
    $fileName = $curDir+"\"+$WSPCache+$solutionNode.File;
    $name = $solutionNode.File;
     
    try
    {
        if (!(test-path $fileName)) {
            $(throw "The file $path does not exist.");
        }
     
        $solution = Get-SPSolution $name -ErrorAction SilentlyContinue
        if ($solution -eq $null) {
            Write-Host "Install Solution: $name";
            #Add solution to SharePoint
             
            Write-Host "Adding solution $name..."
            $solution = Add-SPSolution (get-item $fileName).FullName
             
            #Deploy the solution
            if ($solution.ContainsWebApplicationResource -and $solutionNode.AllWebApplications) {
                Write-Host "Deploying solution $name to $webApplication..."
                $solution | Install-SPSolution -GACDeployment -CASPolicies:$false -AllWebApplications -Confirm:$false
            }
            elseif ($solution.ContainsWebApplicationResource) {
                Write-Host "Deploying solution $name to $webApplication..."
                $solution | Install-SPSolution -GACDeployment -CASPolicies:$false -WebApplication $webApplication -Confirm:$false
            } else {
                Write-Host "Deploying solution $name to the Farm..."
                $solution | Install-SPSolution -GACDeployment -CASPolicies:$false -Confirm:$false
            }
        } else {
            Write-Host "Update Solution: $name";
            try {
                Update-SPSolution –Identity $name –LiteralPath $fileName –GACDeployment -CASPolicies:$false -Confirm:$false
            } catch [Exception] {
                $_ | gm;
                if ($_ -contains "Cannot uninstall the LanguagePack 0 because it is not deployed") {
                    Retract-Solution $solutionNode;
                    Install-Solution $solutionNode;
                }
                Else {
                    throw $_
                }
            }
        }
 
        WaitForJobToFinish $solutionNode.File
    } catch [Exception] {
        Write-Error $_;
        log -message $_ -type "Error";
 
    }
}
 
function Retract-Solution( [System.Xml.XmlNode] $solutionNode )
{
    Write-Host "Retracting solution $solutionNode.Name...";
     
    # Solution must be uninstalled and removed.
    $curDir = Split-Path -Parent $MyInvocation.ScriptName
    $fileName = $curDir+"\"+$WSPCache+$solutionNode.File
     
    try
    {
        [Microsoft.SharePoint.Administration.SPSolution] $solution = (Get-SPSolution $name -ErrorAction SilentlyContinue)[0];
        if (($solution -ne $null) -and ($solution.Deployed)) {
            Write-Host "Retracting solution."
 
            if ($solution.ContainsWebApplicationResource -and $solutionNode.AllWebApplications) {
                Write-Host "Retracting solution $name to $webApplication..."
                $solution | Uninstall-SPSolution -AllWebApplications -Confirm:$false
            }
            elseif ($solution.ContainsWebApplicationResource) {
                Write-Host "Retracting solution $name to $webApplication..."
                $solution | Uninstall-SPSolution -WebApplication $webApplication -Confirm:$false
            } else {
                Write-Host "Retracting solution $name to the Farm..."
                $solution | Uninstall-SPSolution -Confirm:$false
            }
            #Uninstall-SPSolution -Identity $solutionNode.File -Confirm:$false
            WaitForJobToFinish $solutionNode.File
 
            Write-Host "Deleting solution."
            Remove-SPSolution -Identity $solutionNode.File -Confirm:$false
        }elseif (($solution -ne $null) -and ($solution.Deployed))
        {
            Write-Host "Deleting solution."
            Remove-SPSolution -Identity $solutionNode.File -Confirm:$false
        }
    } catch [Exception] {
        Write-Error $_;
        log -message $_ -type "Error";
    }
}
 
# Feature Handling
function ProcessFeatureActivation( [System.Xml.XmlNode] $featureNode, $retry = 2 )
{
    try
    {
        if (-not $remove) {
            [Microsoft.SharePoint.Administration.SPFeatureDefinition] $feature = Get-SPFeature | ? {$_.DisplayName -eq $featureNode.Name};
 
            if ($feature -eq $null) {
                Install-SPFeature -path $featureNode.Name;
            }
 
            if( ($featureNode.Url -ne $null) -and ($featureNode.Url -ne "") )
            {
                $url = $SPRootURL + $featureNode.Url;
                Write-Host 'Enable feature:' $featureNode.Name;
                Enable-SPFeature -identity $featureNode.Name -URL $url;
            }
            else
            {
                Write-Host 'Enable feature:' $featureNode.Name
                Enable-SPFeature -identity $featureNode.Name
            }
        }
    } catch [Exception] {
        Write-Error $_;
        log -message $_ -type "Error";
    }
}
 
function ProcessFeatureDeactivation( [System.Xml.XmlNode] $featureNode )
{
    try
    {
        if( ($featureNode.Url -ne $null) -and ($featureNode.Url -ne "") )
        {
            $url = $SPRootURL + $featureNode.Url;
            #stsadm -o deactivatefeature -id $featureNode.Id -url $url
            Write-Host 'Disable feature:' $featureNode.Name;
            Disable-SPFeature -identity $featureNode.Name -confirm:$false -url $url;
        }
        else
        {
            #stsadm -o deactivatefeature -id $featureNode.Id
            Write-Host 'Disable feature:' $featureNode.Name;
            Disable-SPFeature -identity $featureNode.Name -confirm:$false;
        }
    } catch [Exception] {
        Write-Error $_;
        log -message $_ -type "Error";
    }
}
 
function ProcessScript( [System.Xml.XmlNode] $scriptNode )
{
    "Executing $($scriptNode.Name)...";
    Invoke-Expression $scriptNode."#text";
}
 
function ProcessCopyFile( [System.Xml.XmlNode] $copyfileNode )
{
    "Copying $copyfileNode.file to $copyfileNode.destination...";
    $curDir = Split-Path -Parent $MyInvocation.ScriptName
    $fileName = $curDir+"\"+$WSPCache+$copyfileNode.file
    xcopy /Y /E /R $filename $copyfileNode.destination
    if ($LASTEXITCODE -ne 0) {
        Write-Error("Error Copying: " + $filename);
    }
}
 
function Main
{
    [string]$xmlName = Split-Path -Path $xmlPath -Leaf
    Start-Transcript "$currentDirectory\$xmlName-log.txt";
    Write-Host "Current Directory: $currentDirectory";
    Write-Host "Config: $xmlPath";
     
    if (test-path $xmlPath) {
        # Found the file in the pwd or via the absolution path
        #"Path Found.";
        $configFileItem = Get-Item $xmlPath;
    } else {
        # Attempt to find the Config XML in the script's directory
        #"Path Not Found.";
        $idx = $inputConfigFile.LastIndexOf("\");
        $configFileItem = Get-Item $(Join-Path $currentDirectory $xmlPath.substring($idx+1,$xmlPath.Length-1-$idx));
    }
    #$configFileItem;
    $configXml = New-Object System.Xml.XmlDocument;
    $configXml.Load( $configFileItem.FullName );
    if ($configXml.SharePointDeploymentConfig.WSPCache) {
        $WSPCache = $configXml.SharePointDeploymentConfig.WSPCache;
    }
    Write-Host "WSP cache location: $WSPCache"; Write-Host("");
     
    #foreach ($taskNode in $configXml.SharePointDeploymentConfig.get_ChildNodes()|?{$_ -ne $null}) { ProcessTask $taskNode;  }
    $configXml.SharePointDeploymentConfig.get_ChildNodes()|?{$_ -ne $null} | % { ProcessTask $_;  }
     
    Write-Host("");
     
    #[Microsoft.SharePoint.Administration.SPFarm]::Local.solutions | format-table -property name, Deployed, DeployedWebApplications, DeploymentState;
    #"---";[Microsoft.SharePoint.Administration.SPFarm]::Local.solutions | ? { $_.LastOperationResult -ne "DeploymentSucceeded" } | % { $_.Name; $_.LastOperationDetails; $_; };"Done.";
    Stop-Transcript;
}
 
function ProcessTask( [System.Xml.XmlNode] $taskNode )
{
    #Write-Host("");Write-Host("---")
    # For multiple web.config modifications, sleeping between activations reduces errors.
    if ($taskNode.Sleep -ne $null) {
        Sleep-For $taskNode.Sleep;
    }
 
    if( $taskNode.get_Name() -eq "Solution" )
    {
        #"Solution"
        #$taskNode;
        ProcessSolution $taskNode
    }
    elseif( $taskNode.get_Name() -eq "SiteCollection" )
    {
        #"SiteCollection"
        ProcessSiteCollection $taskNode
    }
    elseif( $taskNode.get_Name() -eq "FeatureActivate" )
    {
        #"FeatureActive"
        ProcessFeatureActivation $taskNode
    }
    elseif( $taskNode.get_Name() -eq "FeatureDeactivate" )
    {
        #"FeatureDeactive"
        ProcessFeatureDeactivation $taskNode
    }
    elseif( $taskNode.get_Name() -eq "BdcAppDef" )
    {
        "BdcAppDef"
        "Not Implemented"
        ProcessBusinessDataCatalogAppDef $taskNode
    }
    elseif( $taskNode.get_Name() -eq "CopyFile" )
    {
        #"CopyFile"
        ProcessCopyFile $taskNode
    }
    elseif( $taskNode.get_Name() -eq "Script" )
    {
        #"Script"
        ProcessScript $taskNode
    }
}
 
function WaitForJobToFinish([string]$SolutionFileName)
{
    $JobName = "*solution-deployment*$SolutionFileName*"
    $job = Get-SPTimerJob | ?{ $_.Name -like $JobName }
    if ($job -eq $null)
    {
        Write-Host 'Timer job not found'
    }
    else
    {
        $JobFullName = $job.Name
        Write-Host -NoNewLine "Awaiting job $JobFullName"
         
        while ((Get-SPTimerJob $JobFullName) -ne $null)
        {
            Write-Host -NoNewLine .
            Start-Sleep -Seconds 2
        }
        Write-Host  "Finished."
    }
}
 
function Pause ($Message="Press any key to continue...")
{
    Write-Host -NoNewLine $Message
    $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
    Write-Host ""
}
 
function log {
    param (
        [string] $message = $(throw "Please specify a message."),
        [string] $type = "Information",
        [string] $logName = "Application",
        [string] $sourceName = "Logos Inc Deployment"
    )
     
    $EventLog = Get-EventLog -list | Where-Object {$_.Log -eq $logName}
    $EventLog.MachineName = "."
    $EventLog.Source = $sourceName;
    $EventLog.WriteEntry($message, $type, 1103);
}
 
. main
After all of that, we can create our simple XML files. The number of XML files depends on the number of deployment options required. There could be 1 XML which deploys the whole environment (see example below) and a couple other XML files to deploy a small subset of functionality.
Some options not used in this example and I will leave it to the reader as an exercise to implement:
  • BCS
  • Site collection restore
  • Copy files elsewhere on the servers - this may be needed if placing files in places where the WSP won't deploy to
Note: Due to an apparent limitation in the syntax highlighter functionality, the XML brush does not handle self closing nodes correctly. For presentation purposes, I have changed the self-closing nodes to have end tags.
Example DSL: Deploy-Logos-EVERYTHING.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<sharepointdeploymentconfig name="Logos SP Deploy EVERYTHING" wspcache="wsp\">
    <featuredeactivate name="LogosIncBranding" url="/"></featuredeactivate>
    <featuredeactivate name="SearchEnhancement" url="/"></featuredeactivate>
    <featuredeactivate name="LogosWebConfigMods" url="/"></featuredeactivate>  
    <featuredeactivate name="CustomXMLFeatureProcessor" url="/"></featuredeactivate>
     
    <Script Name="Remove Session">
        Disable-SPSessionStateService;
    </Script>
     
    <Script Name="Remove Blocked Extension">
        & .\RemoveBlockedExtension -webApplication "http://localhost";
    </Script>
     
    <solution file="CustomXMLFeatureProcessor.wsp" url="/" allwebapplications="false"></solution>
    <solution file="SearchEnhancement.wsp" url="/" allwebapplications="false"></solution>
    <solution file="LogosIncBranding.wsp" url="/" allwebapplications="false"></solution>
    <solution file="LogosManager.wsp" url="/" allwebapplications="false"></solution>
 
    <Script Name="Enable Session">
        Enable-SPSessionStateService -DatabaseName "LogosIncSessionState" -SessionTimeout 120
    </Script>
     
    <featureactivate name="CustomXMLFeatureProcessor" url="/"></featureactivate>
    <featureactivate name="LogosWebConfigMods" url="/"></featureactivate>
    <featureactivate name="SearchEnhancement" url="/"></featureactivate>
    <featureactivate name="LogosIncBranding" url="/"></featureactivate>
</sharepointdeploymentconfig>


Now that we have the files needed, we can put them into the directory structure:
1
2
C:\SharePointDeploy - Powershell Scripts and XML files
C:\SharePointDeploy\wsp - Cache of WSP files and other deployment files
Usage from the C:\SharePointDeploy directory:
Deploying
1
.\SPDeploy.ps1 .\Deploy-Logos-EVERYTHING.xml
Removing
1
.\SPDeploy.ps1 .\Deploy-Logos-EVERYTHING.xml -remove

No comments: