torsdag 15 november 2012

EPiServer Powershell deploy script

Tjipp! som Bengt Alsterlind säger här i Värmland (iaf inte hört någon annan använda ordet).

Blivit mycket powershell för min del på sistone. Men måste säga att jag gillar språket mer och mer, ett klart fall framåt från gamla dos-batch. Här kommer ytterligare lite powershell:

Tänkte presentera ett litet förslag på hur ett EPiServer powershell deploy script kan se ut. Alltså ett script som kopierar visst innehåll från en Visual Studio projekt-katalog till en annan katalog i syfte att kunna zippa ihop innehållet och flänga upp på en testserver/prod.server. Har läst att det ska finnas färdiga såna här templates i EPiServer 7 (vet inte om det är MSBuild eller PS), men vet inte mer om detta just nu.

<Dicsclaimer>Observera att alla webbplatser har sina egna krav och behov och att detta script antagligen inte överensstämmer med just dina krav.</Disclaimer> :P


# DeployPackage.ps1
# 2012-11-14 Määts

# Example call:
# ./DeployPackage.ps1 -verbose -c "Release" -s "C:\Path\To\My\EPiServer\Project\" -d "C:\Path\To\My\Deploy\Folder\"

# Script params
param([switch]$verbose, [string]$configurationMode, [string]$sourceDir, [string]$destDir)

$verboseoutput = @"
Executing script with params:
configurationMode: $configurationMode
sourceDir:         $sourceDir
destDir:           $destDir
"@

# Handle switches
if ($verbose.IsPresent) {
    Write-Host $verboseoutput
    $VerbosePreference = "Continue"
} else {
    $VerbosePreference = "SilentlyContinue"
}
# Validate params
if (-NOT($sourceDir -eq $NULL) -and (Test-Path $sourceDir) -eq $FALSE) {
    Write-Host "ERROR: Parameter ""sourceDir"" does not appear to be a valid path... aborting."
    Write-Host $sourceDir
    break
}

if (-NOT($destDir -eq $NULL) -and (Test-Path $destDir) -eq $FALSE) {
    Write-Host "ERROR: Parameter ""destDir"" does not appear to be a valid path... aborting."
    Write-Host $destDir
    break
}
if ($configurationMode -eq $NULL)
{
    Write-Host "ERROR: Parameter ""configurationMode"" not set... aborting."
    break
}
if (([string]$configurationMode).ToLower() -ne "release") {
    Write-Host "INFO: Parameter ""configurationMode"" not set to release mode... aborting."
    break
}

# Declare function
function DeployPackage() {
<#
    .SYNOPSIS
    Creates an EPiServer site deployment package
   
    .DESCRIPTION
   
    .PARAMETER
   
    .EXAMPLE
   
    .INPUTS
   
    .OUTPUTS
#>

    # Function params
    param([string]$configurationMode, [string]$sourceDir, [string]$destDir)
       
    # Variables
    $dateString = (Get-Date -format "yyyy-MM-dd_HH.mm.ss") # String representation of current date that will be used when naming the deploy package
    $fileTypes = @("*.aspx","*.ascx","*.jpg","*.gif","*.png","*.asmx","*.resx","*.xml","*.jpg","*.js","*.css","*.dll",
                   "*.pdb","*.swf","*.txt","*.html","*.htm","*README*","*.php","*.pdn","*.master","*.xsd","*.fla",
                   "*.as","*.asax") # This is the filetypes that the script will copy into the deploy
    $excludeFolders = @("obj")
    $excludeFiles = @("EPiServerErrorLog.txt")
                 
    $newDestDir = "$destDir\DeployPackage_$dateString\"
   
    # Create new folder
    New-Item $newDestDir -type directory
 
    #Get all files to copy
    $filesToCopy = Get-ChildItem $sourceDir -recurse -include $fileTypes
   
    # Copy items
    foreach ($file in $filesToCopy)
    {
        $newFilePath = $file.Fullname.Replace($sourceDir,$newDestDir)
       
        # Create new file (unix touch) in order to maintain folder structure
        New-Item -ItemType File -Path $newFilePath -Force
       
        # Copy file (overwrite)
        Copy-Item $file.Fullname $newFilePath -Force
    }
   
    # Remove unwanted folders
    Get-ChildItem $newDestDir -include $excludeFolders -recurse | Where-Object { $_.PSIsContainer } | Foreach-Object { Remove-Item $_.Fullname -recurse -Force }
   
    # Remove unwanted files
    Get-ChildItem $newDestDir -recurse -include $excludeFiles -Force | Foreach-Object { Remove-Item $_.Fullname }
}

# Call function
DeployPackage -c $configurationMode -s $sourceDir -d $destDir


Kommentar:
För att köra scriptet slänger man in följande i post-build events i Visual Studio för EPiServer webb-projektet.

 Powershell.exe -file "$(SolutionDir)DeployPackage.ps1" -ExecutionPolicy Unrestricted -verbose -c "$(ConfigurationName)" -d "C:\Deploy" -s "$(ProjectDir)\"

 Mitt script ligger alltså i "Solutiondir"-roten. Jag skickar in $(ConfigurationName) eftersom jag enbart vill att en deploy mapp ska skapas när projektet byggs i release-mode. Slutligen skickar jag med aktuell projektkatalog och vart jag vill att deploy-mappen ska skapas.

Som synes har jag slarvat lite i scriptet och inte orkat skriva klart kommentarer. Erkänner också villigt att #Remove unwanted files/folders inte är speciellt snyggt utan hade kunnat filtrerats redan i kopieringsstadiet. Det hade också varit snyggt om jag kunde avslutat med att zippa ihop mappen men detta är inte sååå jobbigt att göra själv jämfört med att sitta och hantera alla filtyper osv. 

Om nån läsare har kritik/bättre idéer så är jag idel öra, är som sagt fortfarande grön i powershell träsket :)

fredag 9 november 2012

SharePoint 2010 - Uppdatera Shared DataSource på alla rapporter i ett documentbibliotek mha Powershell

Aye Caramba!

Idag när jag köttade lite Powershell bestämde jag mig för att lösa ett jäkligt drygt problem jag haft i SharePoint när jag exporterat/importerat (backup/restore) en SiteCollection innehållandes massa rapporter och alla rapporter tappat sin DataSource-link. Vilket innebar massa manuellt klickande och klipp & klistrande.

Med mina nya powershell kunskaper kom jag fram till följande script:

1. Uppdaterar connectionString i min shared datasource och sätter lite andra variabler
2. Anropar SharePoints ReportServer-webbservice och ändrar datasource till ovan nämnda shared datasource på samtliga rapporter.

Nåväl, here goes (tänk på att du får ändra variablerna så de funkar i din miljö):

#Powershell
Clear-Host

# Add SharePoint snapin if needed
if ((Get-PSSnapin -Name Microsoft.SharePoint.Powershell -ErrorAction SilentlyContinue)     -eq $null)
{
    Add-PSSnapin Microsoft.SharePoint.Powershell
}

# Variables
$webApplicationUrl = "http://my.hostheader.com/"
$hostHeader = $webApplicationUrl -replace "http://", ""
$connectionString = "Data Source=MyDbServer;Initial Catalog=MyDataBase;Integrated    Security=SSPI;" 
$webApplicationUNCPath = "\\$hostHeader\DavWWWRoot\"
$dataSourcesLibraryName = "Data Sources"
$dataSourceDefinitionName = "MySharedDataSource.rsds"
$dataSourceLink =    "$webApplicationUrl/$dataSourcesLibraryName/$dataSourceDefinitionName"
$dataSourceUNCFilePath = $webApplicationUNCPath + $dataSourcesLibraryName + "\" +     $dataSourceDefinitionName
$reportsLibraryName = "MyReports"
$reportServerUri = "$webApplicationUrl/_vti_bin/ReportServer/ReportService2010.asmx?WSDL"

# Script
$site = Get-SPSite($webApplicationUrl)
$web = $site.OpenWeb()

$dataSources = $web.Lists | Where-Object { $_.Title -eq $dataSourcesLibraryName }
$dataSource = $dataSources.Items | Where-Object { $_.Name -eq   $dataSourceDefinitionName }
$reports = $web.Lists | Where-Object { $_.Title -eq $reportsLibraryName }

## Change properties on the shared data source object (through xml manipulation)
$xml = [xml](Get-Content $dataSourceUNCFilePath)
$xml.DataSourceDefinition.ConnectString = $connectionString
$xml.DataSourceDefinition.CredentialRetrieval = "Integrated"
$xml.DataSourceDefinition.Enabled = "True"
$windowsCredentialsNode = $xml.DataSourceDefinition.ChildNodes | Where-Object { $_.Name -eq "WindowsCredentials" }
if ($windowsCredentialsNode -ne $NULL)
{
    $xml.DataSourceDefinition.RemoveChild($windowsCredentialsNode)
}
$ImpersonateUserNode = $xml.DataSourceDefinition.ChildNodes | Where-Object { $_.Name -eq "ImpersonateUser" }
if ($ImpersonateUserNode -ne $NULL)
{
    $xml.DataSourceDefinition.RemoveChild($ImpersonateUserNode)}
$xml.Save($dataSourceUNCFilePath)

# Iterate through reports and set correct shared datasource
$Proxy = New-WebServiceProxy -Uri $reportServerUri -UseDefaultCredential ;

# Gather the webservice types for later use 
$WebServiceTypes = @{}
foreach ($Type in $Proxy.GetType().Assembly.GetExportedTypes())
{
    $WebServiceTypes.Add($Type.Name, $Type.FullName);
}

# Get list of all reports
$ReportItems = $Proxy.ListChildren("/", $true) | Where-Object { $_.Name -Like "*.rdl" } | Where-Object { $_.Path -Like "$webApplicationUrl/$reportsLibraryName" }
$dataSources = $web.Lists | Where-Object { $_.Title -eq $dataSourcesLibraryName }
$dataSource = $dataSources.Items | Where-Object { $_.Name -eq     $dataSourceDefinitionName }

# Declare DataSourceReference object
$ref = New-Object $WebServiceTypes.DataSourceReference
$ref.Reference = $dataSourceLink;

# Declare DataSourceReference object
$ds = New-Object $WebServiceTypes.DataSource
$ds.Name = $dataSource.DisplayName
$ds.Item = $ref

# Apply DataSource to all Reports
foreach ($ReportItem in $ReportItems)
{
    $dss = $Proxy.GetItemDataSources($ReportItem.Path)      
    $dss[0] = $ds
    Try 
    {
        $Proxy.SetItemDataSources($ReportItem.Path, $dss)
    }
    Catch [System.Exception]
    {
        "Error: Could not fix datasource for report: {0}" -f $ReportItem.Name
    }
}
$Proxy.Dispose()
$site.Dispose()

Edit #1: Kan vara intressant att veta att min sitecollection ligger på webbapplikationens rot-nivå också.
Edit #2: Bytte implementationen med att hämta objekt ur xml eftersom den gamla inte returnerade System.Xml.XmlElement objekt som fungerar ihop med RemoveChild-metoden. Rättade även så att enbart rapporter i ett utvalt dokumentbibliotek ($reportsLibraryName) "fixas" utav scriptet ifall man kör på en farm innehållandes massa olika rapporter.

Sådärja, jämt dretgött detta! :)

tisdag 6 november 2012

Fel vid installation av EPiServer Mail (Invalid Application Pool Name)

Ett annat problem som dök upp nyligen:

Skulle installera EPiServer Mail på en utvecklingsmiljö med EPiServer CMS 6 när följande fel inträffade (i EPiServer Deployment Center):




















Felet orsakas av att egenskapen "AppPoolName" aldrig vidarebefordras från EPiServer Mail-installationsscriptet. Att ändra i detta script så att det skickar med rätt variabel kräver dock manipulering utav objekt-medlemmar i powershell, något som jag inte bemästrar i skrivande stund.

Så ett enklare sätt att lösa felet är följande:

1. Ändra Powershell ExecutionPolicy

Eftersom alla EPiServer-ps-script är signade med nån hashhistoria behöver vi ändra executionpolicy på webbservern så att scriptet tillåts att köras även om vi modifierar det:

Börja med att köra:




Notera vad du får för svar (i detta fall "AllSigned").

Starta en PowerShell prompt och kör:





Svara ja på varningsmeddelandet som dyker upp.

2. Modifiera script "Install Site (SqlServer).ps1

Öppna upp IIS:en och kolla vad namnet på applikationspoolen är för den site där du ska installera EPiServer Mail.

Öppna filen "Install Site (SqlSErver).ps1" (samma fil som refereras till i felmeddelandet ovan).
Ändra rad 157 enligt (jag behöll den gamla implementationen för att kunna ändra tillbaks senare):




Vi ersätter alltså variabel-värdet med ett hårdkodat strängvärde som alltså ska motsvara namnet på applikationspoolen som vi nyss tog reda på.

3. Kör EPiServer Mail installation på nytt

Kör nu EPiServer Mail installationen på nytt via EPiServer Deployment Center. Denna gång bör det fungera!


4. Återställ

Ändra först tillbaks Install Site (SqlSErver).ps1 (ta bort den modifierade raden och återställ den utkommenterade raden).

Ändra sedan tillbaks ExecutionPolicy:n genom att köra Set-ExecutionPolicy till det ursprungliga värdet.


5. Fira att det fungerar

När man löser något drygt problem bör man alltid ta sig tid att fira detta meddelst kaffe & bulle eller annat gôtt.








Search Server Application - Access Denied

Hade ett problem nyligen där jag inte fick åtkomst till Search Server Application-gränssnittet (via SharePoint Central Admin) på en Search Server 2010 Express.













Det visar sig att det är en liten trilskande webpart "Shortcuts" på startsidan för sökadministrationen som medför detta. Den visar tydligen länkar som jag inte får se. Så att stänga denna gör alltså susen. Dock hade jag inte tillgång till något konto på servern som faktiskt kunde komma åt sidan, så jag började kika på att göra det programmatiskt. Jag valde en powershell implementation:


$site = new-Object Microsoft.SharePoint.SPSite("http://path-till-central-admin:1234/")
$web = $site.OpenWeb()
$page = $web.Url + "/searchadministration.aspx"
$webpartmanager = $web.GetLimitedWebPartManager($page, [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared)
for($i=0;$i -lt $webpartmanager.WebParts.Count;$i++)
{
    if ($webpartmanager.WebParts[$i].title -eq "Shortcuts")
    {
       $webpartmanager.DeleteWebPart($webpartmanager.Webparts[$webpartmanager.WebParts[$i].ID])
    }
}
$web.Update();
$site.Dispose();

Spara detta som en .ps1-fil och kör scriptet via "SharePoint 2010 Management Shell" och åtkomst fungerar!

Gött!