Showing posts with label SharePoint. Show all posts
Showing posts with label SharePoint. Show all posts

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.
#
# 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.
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.

	
	
		
	
	
	<Script Name="Remove Session">
		Disable-SPSessionStateService;
	</Script>
	
	<Script Name="Remove Blocked Extension">
		& .\RemoveBlockedExtension -webApplication "http://localhost";
	</Script>
	
	
	
	
	

	<Script Name="Enable Session">
		Enable-SPSessionStateService -DatabaseName "LogosIncSessionState" -SessionTimeout 120
	</Script>
	
	
	
	
	



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

Saturday, August 6, 2011

Beginning Scriptable Remote SharePoint 2010 Deployment

This post is going to cover how I setup my environment to remotely deploy to SharePoint 2010. It assumes that you have a Powershell script that will perform all of your deployment needs as if you were executing it locally on the server. I will cover my deployment script in a different post.

The following is an example of a non-scripted/able remote deployment. It will attach to the server using CredSSP and prompt for credentials, then it will setup the SharePoint Management Console. Each line must be run separately.

Enter-PSSession -ComputerName devel39.portaldev.doitbestcorp.com -Authentication CredSSP -Credential $([Security.Principal.WindowsIdentity]::GetCurrent().Name);
& ' C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\CONFIG\POWERSHELL\Registration\sharepoint.ps1 '; 
Set-Location C:\Deploy; 
.\Deploy.ps1;
Exit-PSSession;

There are a couple things of note. Enter-SPSession is not useful for scripting as it doesn't send the subsequent commands to the remote session. Start-Transaction, to record the script output, throws an error and continues because it is not supported through PSSession.
This is all well and good if you want to put a lot of effort into deployment.

Powershell provides New-PSSession to open a remote connection. Use the Invoke-Command to send commands to that session.

$remote = New-PSSession -ComputerName name.domain.com;
Invoke-Command -session $remote -scriptblock {
    sl "C:\SharePointDeploy";
    & .\Deploy.ps1 
};
Remove-PSSession $remote;
We can build on this a bit by prompting for credentials and invoking the command as a job. "AsJob" can be loosely equivocated to threads in powershell. There may be many times where the order in which commands end does not matter or you can benefit from an asynchronous programming model which this argument provides.
$remote = New-PSSession -ComputerName name.domain.com -Authentication CredSSP -Credential $([Security.Principal.WindowsIdentity]::GetCurrent().Name); Invoke-Command -session $remote -scriptblock { 
    sl "C:\SharePointDeploy"; 
    & .\Deploy.ps1 
} -AsJob;
Remove-PSSession $remote;

Friday, November 13, 2009

SharePoint 2007 Forms Authentication Error "Value cannot be null."

Every so often, users logging into a SharePoint 2007 portal via forms authentication with a custom membership provider get the following error message. Once the message starts to be received, it will continue to function in that manner.

Value cannot be null.
Parameter name: value at System.String.EndsWith(String value, StringComparison comparisonType)
at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.PostAuthenticateRequestHandler(Object oSender, EventArgs ea)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

The error was not resulting from any of my code which made this incredibly hard to figure out. If you restart the IIS and the OWSTimer, the issue goes away (for a while), but doesn't fix this issue.

This morning I was searching around with a very odd set keywords and ran across this article, Fixing the Elusive “Value Cannot Be Null” FBA Authentication Error. The page doesn't contain the error text or a specific solution, but it does contain a screen shot of exactly what I was experiencing.

I have included the text and solution in this blog to make it crawl-able.

The solution:
The issue appears to be the fact that the web.config, C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\web.config, in the layouts directory of the hive has batch compilation set to false.
<compilation batch="false" batchTimeout="600" maxBatchSize="10000" maxBatchGeneratedFileSize="10000" />

The line should read:

<compilation batch="true" batchTimeout="600" maxBatchSize="10000" maxBatchGeneratedFileSize="10000" />

After making this change, the performance of the site in general improved significantly on the Windows and Forms authentication sides. It looks like an all-around win.


Batch Compilation Information:
From Microsoft's KB article, How to use the "batch" attribute of the Web.config file compilation element in SharePoint Server 2007 and in Windows SharePoint Services 3.0:
The batch attribute is used by the ASP.NET compilation element. This attribute controls all compilation for applications to which the Web.config file applies.

When the attribute is set to "true," the delay that you experience when you access files for the first time can be decreased. This is because, when the batch attribute is set to "true," all uncompiled files will be compiled in batch mode by ASP.NET.

However, for larger applications, there may be a significant delay when files are compiled for the first time because there are more file batches to compile. After the initial compilation, delays are decreased when you access the compiled files.

Microsoft has an excellent web performance best practices site, Developing High-Performance ASP.NET Applications. The pertinent part for our issue:
Consider precompiling
A Web application is batch-compiled on the first request for a resource such as an ASP.NET Web page. If no page in the application has been compiled, batch compilation compiles all pages in a directory in chunks to improve disk and memory usage. You can use the ASP.NET Compilation Tool (Aspnet_compiler.exe) to precompile a Web application. For in-place compilation, the compilation tool calls the ASP.NET runtime to compile the site in the same manner as when a user requests a page from the Web site. You can precompile a Web application so that the UI markup is preserved, or precompile the pages so that source code cannot be changed. For more information, see How to: Precompile ASP.NET Web Sites.

This article explains in depth how debug and batch compilation works. It is worth the read. ASP.NET Resources - Beware Of Deploying Debug Code In Production