SharePoint Online – Performance – Basic Troubleshooting

Classic case: Customer reports in “SharePoint Online performance is slow. [period]”
Account Manager, Product Manager, Project Manager comes running 4 weeks after due date, customer wont accept the solution with the given performance. Ok…. Lets gather the tools for first steps:

Toolbox includes at this point
– PSping utility from PSTools(PSTools )
– Tracert utility from Microsoft (TraceRT )
– Browser Development Tools (F12 for Internet Explorer, Firefox or Chrome)

First step
Compare performance from your location with the client location using the Browser development tools. Are they the same?
If yes, problem usually lies in the configuration of SharePoint Online. Start testing from your location and don’t bother asking customer for client computer, remote login, network information.
If no, problem usually lies from customer client machine and SharePoint Online server. Start testing from customer client machine at their location. Ask them for information regarding their network configuration. Do they have proxy? Are they running old router/switch between?

When you have determined the location of your challenge, proceed to second step.

Second step
Second step of troubleshooting SharePoint Online performance is to measure ping time to the tenant as well as traceroute in order to locate route and geolocation of the tenant.
I use the following to complete that, either run it myself or ask the client to run it from their location.
psping.exe -n 20 tenant.sharepoint.com:443 > PsPingResult.txt
timeout 30 /nobreak
tracert tenant.sharepoint.com > TracertResult.txt
timeout 30 /nobreak

From that I derive the following:
1: For optimal performance, ping time is between 30-50ms stable.
2: Number of Route hops between 12-15.
3: Use https://www.iplocation.net/ to get most likely geolocation of tenant from returned public IP.

Psping.exe is part of SysInternals PSTools package.

Lastly again back to developer tools.
Collect performance report and look for errors in the console. Look for external http requests, certificate validation errors, script errors.

What the numbers show you
A: Ping times of more than 50MS or route hops more than 12-15 would lead me to look at the network with possible causes being:
Linespeed, old or outdated network equipment, tenant geolocation, proxy filtering, non-optimal routing.

B: Usually there are little or no problems in the direct network measurement and it will come down to the browser Development Tools. Note the difference between DOM loaded and Fully rendered. Client really dont care about DOM download, as they are only interested in the fully rendered page. So what can be causes of slow rendered pages:
Certificate errors, script errors, poor client hardware, proxy filtering, Antivirus, Poor coding.

Certificate Expiry – All Domain Servers – Powershell

This is a most lovely little piece of powershell.
“This script connects to the DC, gets a list of machines with the β€œOperatingSystem” value set to β€œ*Server*”, if it can succesfully ping the machine(s), it will return a list of certificates that expire after the date specified and compile a CSV file of the results.”

Do check it out and give probs to the man producing the most awesome powershell and now finally with his own blog πŸ™‚

Certificate Expiry

CRM Server – Scripted installation

Yet another nice script from Mads Hjort Larsen.
I simply hadn’t found any automated installation of CRM server, including prerequisites SQL Reporting Services native mode and Watson components, so we decided to work this out. The script requires both SQL installation media as well as CRM Server media.

Script runtime is long, approximately 1 hour. That is just how long it takes to install all components. For reference, here is a video of a script run.

NB: For some reason the parser is unable to parse to full script below.
Copy into Powershell ISE or similar will yield a fully color coded script.
NBB: This script has severe issues against named instances of SQL. We will issue an update once we have tested this.

Set-Location $PSScriptRoot

#region Variables
$CRMgroupOU = '<CRM OU>' # Specifies the Active Directory organizational unit (OU) where the security groups will be created, for example, <OU> OU=CRM, DC=MySubDomain, DC=MyDomain, DC=com</OU>.
$SQLserver = '<SQL SERVER>'

$SQLmedia = 'D:' # Location of SQL server install media
$CRMmedia = 'E:' # Location of CRM server install media

$CRMupdateDir = "$PSScriptRoot\updates" # Directory that SERVER, ROUTER, SRS and MUI update files will be downloaded to.

$CRMSetupUser = '<CRM Setup User>' # Local Admin on CRM Server, Local Admin on SQL Server, Delegate Full Control on CRM Group OU
$CRMSetupUserPassword = '<CRM Setup User Password>'

$svcCRMApp = '<CRM AppPool account>' # Specifies the service account used to run the Microsoft Dynamics CRM Unzip Service and ASP.NET AppPool application pool identity.
$svcCRMAppPassword = '<CRM AppPool account password>'

$svcCRMDeploy = '<CRM Deployment account>' # Specifies the service account to use for the Deployment Web service.
$svcCRMDeployPassword = '<CRM Deployment account password>'

$svcCRMSandbox = '<CRM Sandbox account>' # Specifies the service account to use for the Sandbox Processing service.
$svcCRMSandboxPassword = '<CRM Sandbox account password>'

$svcCRMVSS = '<CRM VSS account>' # Specifies the service account to use for the Microsoft Dynamics CRM VSS Writer service.
$svcCRMVSSPassword = '<CRM VSS account password>'

$svcCRMAsync = '<CRM aSynch account>' # Specifies the service account to use for the asynchronous processing services.
$svcCRMAsyncPassword = '<CRM aSynch account password>'

$svcCRMMonitor = '<CRM Monitor account>' # Specifies the service account to use for the Microsoft Dynamics CRM Monitoring service.
$svcCRMMonitorPassword = '<CRM Monitor account password>'

$svcCRMEmailRouter = '<CRM Email router account>' # Specifies the service account to use for the Microsoft Dynamics CRM Email Router service.
$svcCRMEmailRouterPassword = 'CRM Email router account password>'

$RSappPoolAccount = '<SQL Reporting Services account>'
$RSappPoolAccountPassword = '<SQL Reporting Services account password>'

$RSdbname = $env:COMPUTERNAME+'_SQL_Reporting' # SQL Reporting Services database name.
$ReportingServicesURL = $env:COMPUTERNAME+":80" # SQL Reporting services URL (name and port)

$RSemailServer = "smtp.mycompany.com"
$RSemailAddress = "reporting.services@mycompany.co.uk"

$DBinstance = '<SQL Client Alias>' # Should be a SQL Client connection alias (Not SQL server alias).
$RSinstance = '' # If this value is left blank "MSSQLSERVER" will be used

# https://technet.microsoft.com/en-us/library/hh699830.aspx
$patchUpdate = 'True' # Determines the behavior of the update Microsoft Dynamics CRM Server Setup technology. This feature lets Setup perform a one-time search for, and if applicable, download and apply the latest installation files for Microsoft Dynamics CRM.
$patchPath = '' # \\ServerName\ShareName\patchfile.msp

$licenseKey = '<License Key>' # Specifies the product key for this deployment. The configuration file can contain only one Microsoft Dynamics CRM product key.
$databaseCreate = 'true' # Values for this parameter are either true or false. True causes Setup to create a new Microsoft Dynamics CRM configuration database. False causes Setup to connect to an existing Microsoft Dynamics CRM configuration database.
$reportingUrl = "http://$env:COMPUTERNAME/ReportServer" # Specifies the URL of the Report Server.
$Collation = 'Latin1_General_CI_AI' # Specifies the SQL Server database collation name to use for the organization database. The default collation depends on the language of Microsoft Dynamics CRM Server that you’re installing, for example, Latin1_General_Cl_Al, which is the default collation for English (US) language deployments.

# After Setup is complete, you cannot change the base ISO currency code. However, you can change the base-currency name, base-currency symbol, and base-currency precision.
$currencycode="<ISO Currency code>" # Specifies the ISO three-letter currency-code, display name, and symbol to use for the base currency. For example, if you want to use U.S. dollars as the base currency, use isocurrencycode="USD". You must use a valid ISO currency description for the isocurrencycode attribute.
$currencyname="<Currency Name>" # You must also specify the currency-name and currency-symbol display names for the ISO base currency. For example, if the ISO currency code is USD, the currency name should be "US Dollar" and the currency symbol should be "$". However, you can use any string that you want for these attributes.
$currencysymbol="<Currency Symbol>" 
$currencyprecision="<Currency Precision>" # You must specify the precision for the base currency that you specified in the currencycode attribute. Valid values depend on the type of currency that you specify. For example, USD valid values are 1 – 9 and the default value is 2.

$displayName = '<CRM Display Name>' # Specifies the long name of your organization. The name can be up to 250 characters long and extended characters are supported.
$uniqueName = '<Organizational Name>' # Specifies the name of your organization in the URL that users will use to access the deployment. There is a 30 character limit. Extended characters or spaces are not allowed. If you don’t specify this element or leave the value blank, Setup will generate a short name based on the <Organization> element value.

$createNewWebsite = 'True' # Specifies the website to be used for Microsoft Dynamics CRM Server. Use Create="true" to create a new Microsoft Dynamics CRM website and leave the value $webSitePath blank.
$websitePort = '<CRM Port>' # Use port="TCPportnumber", where TCPportnumber is a valid and available TCP port number, to specify the port for connecting to the Microsoft Dynamics CRM Server application. If left blank, the port number that will be used is 5555. If $createNewWebsite="false", the port attribute is ignored.
$webSitePath = '' # Only needed if $createNewWebsite = 'false'

$SQMoptin = 'False' # Specifies whether you will participate in the Customer Experience Improvement Program.
$MUoptin = 'False' # Specifies whether to use Microsoft Update to download and install updates to Microsoft Dynamics CRM Server and other installed applications. After the installation is completed, this feature helps keep your computer up-to-date on an ongoing basis.

$ifdsettings = 'False' # This option should only be used for Internet-facing deployment. Set enabled = "true" to notify Microsoft Dynamics CRM server Setup to configure the deployment for Internet access. If the <ifdsettings> element is not specified, the enabled attribute value is set to false.
$internalnetworkaddress = '' # IP address and subnet mask, such as This is the internal IP address and the associated subnet mask of the subnet where your internal users reside. The subnets you enter here will be for the computers that you want to be considered as internal and you do not want the users to login through the IFD environment when they are on these subnets. To enter multiple subnets use a comma to separate the values in the configuration file Note: If you leave this element blank, all communication to the Microsoft Dynamics CRM server will be considered as internal and users will default to windows authentication when hitting the Microsoft Dynamics CRM website.
$rootdomainscheme = 'http' # Must be https, which will use secure sockets layer (SSL), or http, which will use the nonsecure HTTP protocol. Note: Setup does not require SSL on the Web site where Microsoft Dynamics CRM is installed. We strongly recommend that you specify the https value in the rootdomainscheme element. In addition, after Setup is complete, to help protect information that is transmitted between users and Microsoft Dynamics CRM Server, we recommend that you configure the Web site to require SSL. 
$sdkrootdomain = 'api.contoso.com' # Specifies the domain name that will be used for applications that use the methods described in the Microsoft Dynamics CRM 4.0 Software Development Kit (SDK). The value that is set here will be prefixed by your unique organization name to form the URL so you only need to put in the domain.com
$webapplicationrootdomain = 'app.contoso.com' # Specifies the domain name that will be used for the Microsoft Dynamics CRM Web application and Microsoft Dynamics CRM for Outlook. The value that is set here will be prefixed by your unique organization name to form the URL so you only need to put in the domain.com 
$discoveryrootdomain = 'disc.contoso.com' # The root domain for the discovery Web service.

$ExchangeServerName = '' # Specifies the Microsoft Exchange Server computer or POP3 that will be used by the Email Router to route incoming email messages. If not specified and later the Email Router is used in the deployment, the computer must be added to the PrivUserGroup security group.
$nonHTTPS = 'True' # If you do not want to use HTTPS, set this to true - see https://msdn.microsoft.com/en-us/library/hh550122(v=crm.6).aspx for more info.

   $Server = 'http://download.microsoft.com/download/5/F/4/5F43956F-E1CF-4F15-96BE-967AF29E2005/CRM2015-Server-KB3010990-ENU-amd64.exe'
   $Router = 'http://download.microsoft.com/download/5/F/4/5F43956F-E1CF-4F15-96BE-967AF29E2005/CRM2015-Router-KB3010990-ENU-amd64.exe'
      $SRS = 'http://download.microsoft.com/download/5/F/4/5F43956F-E1CF-4F15-96BE-967AF29E2005/CRM2015-Srs-KB3010990-ENU-amd64.exe'
   $MUIENU = 'http://download.microsoft.com/download/5/F/4/5F43956F-E1CF-4F15-96BE-967AF29E2005/CRM2015-Mui-KB3010990-ENU-amd64.exe'
   $MUIDAN = 'http://download.microsoft.com/download/A/C/5/AC560071-42FF-44C0-AEE7-848FF99F74D9/CRM2015-Mui-DAN-amd64.exe'
$MUIDANUPD = 'http://download.microsoft.com/download/A/A/0/AA01E7CA-9120-48E4-8637-FFD8AD5E9C55/CRM2015-Mui-KB3056327-DAN-amd64.exe'

# It should not be neccessary to edit below this line ------------------- πŸ™‚ #

$SQLinstallFile = $SQLmedia+'\Setup.exe'
$CRMinstallFile = $CRMmedia+'\Server\amd64\SetupServer.exe' # Location of SetupServer.exe
$SRSInstallFile = $CRMmedia+'\Server\amd64\SrsDataConnector\SetupSrsDataConnector.exe'
$EmailRouterInstallFile = $CRMmedia+'\EmailRouter\amd64\SetupEmailRouter.exe' # Location of Email Router setup file.
$ADserver = ([ADSI]”LDAP://RootDSE”).dnsHostName

function Add-Clock {
    $code = { 
    $start = Get-Date
    do {
        $now = Get-Date
        $diff = $now-$start
        $title = ("$diff").Split('.')[0].ToString()
        [System.Console]::Title = "Approx. time elapsed: "+$title
        Start-Sleep -Seconds 1
        } while ($true)

$ps = [PowerShell]::Create()
$null = $ps.AddScript($code)

if (!(Test-Path placeholder.tmp)) {
Add-Clock | Out-Null
Write-Host ''
Write-Host ' β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—' -ForegroundColor Green
Write-Host ' β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•‘β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•' -ForegroundColor Green
Write-Host ' β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—' -ForegroundColor Green
Write-Host ' β–ˆβ–ˆβ•”β•β•β•β• β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•  β–ˆβ–ˆβ•‘β–„β–„ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ•β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β•  β•šβ•β•β•β•β–ˆβ–ˆβ•‘' -ForegroundColor Green
Write-Host ' β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘' -ForegroundColor Green
Write-Host ' β•šβ•β•     β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β• β•šβ•β•β–€β–€β•β•  β•šβ•β•β•β•β•β• β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β•   β•šβ•β•   β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•' -ForegroundColor Green
Write-Host ''

Write-Host 'Verifying SQL server setup...'
$Fulltext = Get-Service -ComputerName $sqlServer -DisplayName "SQL Full-Text*"
if ($Fulltext -eq $null){Write-Host "Full Text Search is not installed on $sqlServer" -ForegroundColor Red}
else{Write-Host $Fulltext.DisplayName "found on $SQLserver"}

$SQLAgent = Get-Service -ComputerName $sqlServer -DisplayName "SQL Server Agent*"
if ($SQLAgent -eq $null){Write-Host "SQL Agent is not installed on $sqlServer" -ForegroundColor Red}
else{Write-Host $SQLAgent.DisplayName "found on $SQLserver";Invoke-Command -ComputerName $sqlServer -ScriptBlock {Start-Service -Name SQLSERVERAGENT}}

if ($Fulltext -eq $null -OR $SQLAgent -eq $null){Exit}

New-Item placeholder.tmp -Type File -Force | Out-Null

#region HostsFileAndSQLAlias
Write-Host "`nAdding to Hosts file: " -NoNewline
$SQLServerIP = (Test-Connection -ComputerName $SQLserver -Count 1).IPV4Address.IPAddressToString

$hostsPath = "$env:windir\System32\drivers\etc\hosts"
$hosts = get-content $hostsPath
$hostLine = $SQLServerIP +' '+ $DBinstance
Write-Host $hostLine

if ($hosts -notcontains $hostLine){
    $newHosts = $hosts+$hostLine
    $newHosts | Out-File $hostsPath -Force

# Check SQL alias
Write-Host "`nCreating TCP/IP Aliases`n"
Start-Process $env:windir\System32\cliconfg.exe
Start-Process $env:windir\sysWOW64\cliconfg.exe
Sleep 1
Stop-Process -Name cliconfg

# These are the two Registry locations for the SQL Alias locations
$x86 = "HKLM:\Software\Microsoft\MSSQLServer\Client\ConnectTo"
$x64 = "HKLM:\Software\Wow6432Node\Microsoft\MSSQLServer\Client\ConnectTo"
# Test to see if the ConnectTo key already exists, and create it if it doesn't.
if ((test-path -path $x86) -ne $True){New-Item $x86 | Out-Null}
if ((test-path -path $x64) -ne $True){New-Item $x64 | Out-Null}
$TCPAlias = "DBMSSOCN," + $SQLserver
New-ItemProperty -Path $x86 -Name $DBinstance -PropertyType String -Value $TCPAlias -Force | Out-Null
New-ItemProperty -Path $x64 -Name $DBinstance -PropertyType String -Value $TCPAlias -Force | Out-Null

#region CreateUsers
[ScriptBlock]$ScriptBlock = { 

    function AddUser ($user,$pass){
        if(!(Get-ADUser -Filter {sAMAccountname -eq $user})) {
            "Creating $user"
            New-ADUser -Name $user -AccountPassword (ConvertTo-SecureString -AsPlainText $pass -Force) -Enabled $true -Path $CRMgroupOU
        else{"User $user already exists."}

    if(!(Get-ADOrganizationalUnit -Filter {DistinguishedName -like $CRMgroupOU})){
        "Creating OU ($CRMgroupOU)"
        New-ADOrganizationalUnit -Name $CRMgroupOU.Split(',')[0].Split('=')[1] -Path ($CRMgroupOU.Split(',')[1..($CRMgroupOU.SPlit(',').Count)] -join ',')
    else{"OU ($CRMgroupOU) already exists."}
    Write-Host "`nCreating accounts:"
    AddUser $CRMSetupUser $CRMSetupUserPassword
    AddUser $svcCRMApp $svcCRMAppPassword
    AddUser $svcCRMDeploy $svcCRMDeployPassword
    AddUser $svcCRMSandbox $svcCRMSandboxPassword
    AddUser $svcCRMVSS $svcCRMVSSPassword
    AddUser $svcCRMAsync $svcCRMAsyncPassword
    AddUser $svcCRMMonitor $svcCRMMonitorPassword
    AddUser $svcCRMEmailRouter $svcCRMEmailRouterPassword
    AddUser $RSappPoolAccount $RSappPoolAccountPassword

Invoke-Command -ComputerName $ADserver -ScriptBlock $ScriptBlock -ArgumentList $CRMgroupOU, $CRMSetupUser, $CRMSetupUserPassword, $svcCRMApp, $svcCRMAppPassword, $svcCRMDeploy, $svcCRMDeployPassword, $svcCRMSandbox, $svcCRMSandboxPassword, $svcCRMVSS, $svcCRMVSSPassword, $svcCRMAsync, $svcCRMAsyncPassword, $svcCRMMonitor, $svcCRMMonitorPassword, $svcCRMEmailRouter, $svcCRMEmailRouterPassword, $RSappPoolAccount, $RSappPoolAccountPassword

#region LogonAsService
# http://anexinetisg.blogspot.dk/2014/02/grant-users-log-on-as-service-right-via.html
function Grant-LogOnAsService{
    [string[]] $users
    #Get list of currently used SIDs 
    secedit /export /cfg tempexport.inf | Out-Null
    $curSIDs = Select-String .\tempexport.inf -Pattern "SeServiceLogonRight" 
    $Sids = $curSIDs.line 
    $sidstring = ""
    foreach($user in $users){
        $objUser = New-Object System.Security.Principal.NTAccount($user)
        $strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier])
        if(!$Sids.Contains($strSID) -and !$sids.Contains($user)){
            $sidstring += ",*$strSID"
        $newSids = $sids + $sidstring
        Write-Host "`nNew Sids: " -NoNewline
        $tempinf = Get-Content tempexport.inf
        $tempinf = $tempinf.Replace($Sids,$newSids)
        Add-Content -Path tempimport.inf -Value $tempinf
        secedit /import /db secedit.sdb /cfg ".\tempimport.inf" | Out-Null
        secedit /configure /db secedit.sdb | Out-Null
        gpupdate /force | Out-Null
        Write-Host "`nNo new sids"
    Remove-Item ".\tempimport.inf" -Force -ErrorAction SilentlyContinue
    Remove-Item ".\secedit.sdb" -Force -ErrorAction SilentlyContinue
    Remove-Item ".\tempexport.inf" -Force -ErrorAction SilentlyContinue

Grant-LogOnAsService -users $svcCRMApp, $svcCRMDeploy, $svcCRMSandbox, $svcCRMVSS, $svcCRMAsync, $svcCRMMonitor

#region Add-DomainUserToLocalGroup
# http://blogs.technet.com/b/heyscriptingguy/archive/2010/08/19/use-powershell-to-add-domain-users-to-a-local-group.aspx
function Add-DomainUserToLocalGroup { 
    $de = [ADSI]"WinNT://$computer/$Group,group" 

Add-DomainUserToLocalGroup -user $CRMSetupUser -group 'Administrators' -domain $env:USERDOMAIN -computer $env:COMPUTERNAME
Add-DomainUserToLocalGroup -user $CRMSetupUser -group 'Administrators' -domain $env:USERDOMAIN -computer $SQLserver

Add-DomainUserToLocalGroup -user $svcCRMApp -group 'Administrators' -domain $env:USERDOMAIN -computer $env:COMPUTERNAME
Add-DomainUserToLocalGroup -user $svcCRMApp -group 'Performance Log Users' -domain $env:USERDOMAIN -computer $env:COMPUTERNAME

Add-DomainUserToLocalGroup -user $svcCRMDeploy -group 'Administrators' -domain $env:USERDOMAIN -computer $env:COMPUTERNAME
Add-DomainUserToLocalGroup -user $svcCRMDeploy -group 'Administrators' -domain $env:USERDOMAIN -computer $SQLserver

Add-DomainUserToLocalGroup -user $svcCRMAsync -group 'Performance Log Users' -domain $env:USERDOMAIN -computer $env:COMPUTERNAME

# Delegate Full Control on CRM Group OU
$ScriptBlock = [Scriptblock]::Create('dsacls "'+$CRMgroupOU+'"'+" /G $env:USERDOMAIN\$CRMSetupUser"+':GA /I:T')
Invoke-Command -ComputerName $ADserver -ScriptBlock $ScriptBlock | Out-Null

# Grant $CRMsetupUser SysAdm rights on the SQL server
$ScriptBlock = {
    Import-Module -Name SQLPS -DisableNameChecking
    Add-Type -AssemblyName "Microsoft.SqlServer.Smo, Version=, Culture=neutral, PublicKeyToken=89845dcd8080cc91"
    $smo = New-Object Microsoft.SqlServer.Management.Smo.Server $env:COMPUTERNAME
    $sqlUser = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Login -ArgumentList $smo,"$env:USERDOMAIN\$CRMSetupUser"
    $sqlUser.LoginType = 'WindowsUser'
    $sqlUser.PasswordPolicyEnforced = $false
Invoke-Command -ComputerName $SQLserver -ScriptBlock $ScriptBlock -ArgumentList $CRMSetupUser

#region ReportingServices
if ($RSinstance -eq ''){$RSinstance = 'MSSQLSERVER'}

$temp = $WarningPreference
$WarningPreference = 'SilentlyContinue'
Install-WindowsFeature –Name NET-Framework-Core
$WarningPreference = $temp

$ArgumentList = '/ACTION="Install" /FEATURES=RS /RSINSTALLMODE="FilesOnlyMode" /INSTANCENAME="'+$RSinstance+'" /INSTANCEID="'+$RSinstance+'" /ENU="True" /QUIETSIMPLE="True" /IACCEPTSQLSERVERLICENSETERMS /UpdateEnabled="True" /UpdateSource="MU" /ERRORREPORTING="False" /SQMREPORTING="'+$SQMoptin+'" /RSSVCACCOUNT='+$env:USERDOMAIN+'\'+$RSappPoolAccount+' /RSSVCPASSWORD='+$RSappPoolAccountPassword+' /RSSVCSTARTUPTYPE="Automatic"'
Start-Process $SQLinstallFile -ArgumentList $ArgumentList -Wait

Write-Host "`nGenerating Reporting Services configuration databases..."

$s = New-Object Management.ManagementScope("\\localhost\root\Microsoft\SqlServer\ReportServer\RS_$RSinstance\v12\admin")

$sc = New-Object Management.ManagementClass("\\localhost\root\Microsoft\SqlServer\ReportServer\RS_$RSinstance\v12\admin:MSReportServer_ConfigurationSetting")

$insts = $sc.GetInstances()
foreach ($o in $insts) { $inst = $o; }

# Set Virtual Directories
$inst.RemoveURL("ReportServerWebService", "http://$ReportingServicesURL", 1033) | Out-Null
$inst.RemoveURL("ReportManager", "http://$ReportingServicesURL", 1033) | Out-Null

$inst.SetVirtualDirectory("ReportServerWebService", "ReportServer", 1033) | Out-Null
$inst.SetVirtualDirectory("ReportManager", "ReportManager", 1033) | Out-Null

$inst.ReserveURL("ReportServerWebService", "http://$ReportingServicesURL", 1033) | Out-Null
$inst.ReserveURL("ReportManager", "http://$ReportingServicesURL", 1033) | Out-Null

# Create Reporting Services Database
$script = $inst.GenerateDatabaseCreationScript($RSdbname, 1033, $false)

[scriptblock]$scriptBlock = {
    $script.Script | Out-File rs.sql
    sqlcmd -i rs.sql
    Remove-Item rs.sql
Invoke-Command -ComputerName $sqlServer -ScriptBlock $scriptBlock -ArgumentList $script,$DBinstance | Out-Null

$script = $inst.GenerateDatabaseRightsScript("$env:USERDOMAIN\$RSappPoolAccount", $RSdbname, $false, $true)

[scriptblock]$scriptBlock = {
    $script.Script | Out-File rs.sql
    sqlcmd -i rs.sql
    Remove-Item rs.sql
Invoke-Command -ComputerName $sqlServer -ScriptBlock $scriptBlock -ArgumentList $script,$DBinstance | Out-Null

$inst.SetDatabaseConnection($DBinstance, $RSdbname, 2, "", "") | Out-Null
$inst.SetWindowsServiceIdentity($false, "$env:USERDOMAIN\$RSappPoolAccount", $RSappPoolAccountPassword) | Out-Null
#$inst.SetUnattendedExecutionAccount("<domain\user>", "<password>") | Out-Null
$inst.SetServiceState($true, $true, $true) | Out-Null
$inst.SetEmailConfiguration($true, $RSemailServer, $RSemailAddress) | Out-Null

#region CRMserverPreReqs
Write-Host "`nInstalling CRM server prereqs:"
Write-Host "1of6 - Microsoft .NET Framework 4.5.2"
$file = gci ($CRMmedia+'\Redist\dotNETFX')
Start-Process $file.FullName -ArgumentList '/PASSIVE /NORESTART' -Wait

Write-Host "2of6 - Microsoft Application Error Reporting" 
$file = gci ($CRMmedia+'\Server\amd64\DW')
Start-Process $file.FullName -ArgumentList 'APPGUID=91710409-8000-11D3-8CFE-0150048383C9 /PASSIVE /NORESTART' -Wait

Write-Host "3of6 - SQL Native Client" 
$file = gci ($CRMmedia+'\Redist\SQLNativeClient')
Start-Process $file.FullName -ArgumentList '/PASSIVE /NORESTART' -Wait

Write-Host "4of6 - SQL Clr types" 
$file = gci ($CRMmedia+'\Redist\SQLSystemCLRTypes\*64*')
Start-Process $file.FullName -ArgumentList '/PASSIVE /NORESTART' -Wait

Write-Host "5of6 - SQL Server Management Objects" 
$file = gci ($CRMmedia+'\Redist\SQLSharedManagementObjects')
Start-Process $file.FullName -ArgumentList '/PASSIVE /NORESTART' -Wait

Write-Host "6of6 - Microsoft SQL Reporting Service Report Viewer Control" 
$file = gci ($CRMmedia+'\Redist\ReportViewer')
Start-Process $file.FullName -ArgumentList '/PASSIVE /NORESTART' -Wait

#region AutoRun&AutoLogon
New-ItemProperty HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce -Name "Test" -Value ("$pshome\powershell.exe -Command `"& `'$PSCommandPath`'`"") | Out-Null

Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultUserName -Value "$env:USERDOMAIN\$CRMSetupUser"
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultPassword -Value "$CRMSetupUserPassword"
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoAdminLogon -Value "1"
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name ForceAutoLogon -Value "1"

Write-Host "`nRebooting..."
Add-Clock | Out-Null
#region CRMinstall
Write-Host ''
Write-Host '  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ–ˆβ•—    β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•—     β–ˆβ–ˆβ•—     ' -ForegroundColor Green
Write-Host ' β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘    β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘     ' -ForegroundColor Green
Write-Host ' β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘    β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘     ' -ForegroundColor Green
Write-Host ' β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘    β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β•šβ•β•β•β•β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•‘     ' -ForegroundColor Green
Write-Host ' β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘    β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—' -ForegroundColor Green
Write-Host '  β•šβ•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•šβ•β•     β•šβ•β•    β•šβ•β•β•šβ•β•  β•šβ•β•β•β•β•šβ•β•β•β•β•β•β•   β•šβ•β•   β•šβ•β•  β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•' -ForegroundColor Green
Write-Host ''

$xml = $null
$CRMxmlfile = "$PSScriptRoot\CRMSetupconfig.xml"

Write-Host 'Creating CRM Server XML answer file...'

$xml += '<CRMSetup>';$xml += "`r`n"
$xml += '<Server>';$xml += "`r`n"
$xml += '<Patch update="'+$patchUpdate+'">'+$patchPath+'</Patch>';$xml += "`r`n"
$xml += "<LicenseKey>$licenseKey</LicenseKey>";$xml += "`r`n"
$xml += "<SqlServer>$DBinstance</SqlServer>";$xml += "`r`n"
$xml += '<Database create="'+$databaseCreate+'"/>';$xml += "`r`n"
$xml += '<Reporting URL="'+$reportingUrl+'" />';$xml += "`r`n"
$xml += "<OrganizationCollation>$collation</OrganizationCollation>";$xml += "`r`n"
$xml += '<basecurrency isocurrencycode="'+$currencycode+'" currencyname="'+$currencyname+'" currencysymbol="'+$currencysymbol+'" currencyprecision="'+$currencyprecision+'"/>';$xml += "`r`n"
$xml += "<Organization>$displayName</Organization>";$xml += "`r`n"
$xml += "<OrganizationUniqueName>$uniqueName</OrganizationUniqueName>";$xml += "`r`n"
$xml += "<OU>$CRMgroupOU</OU>";$xml += "`r`n"
$xml += '<WebsiteUrl create="'+$createNewWebsite+'" port="'+$websitePort+'">'+$webSitePath+'</WebsiteUrl>';$xml += "`r`n"
$xml += "<InstallDir>C:\Program Files\Microsoft Dynamics CRM</InstallDir>";$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<CrmServiceAccount type="DomainUser">';$xml += "`r`n"
$xml += "   <ServiceAccountLogin>$env:USERDOMAIN\$svcCRMApp</ServiceAccountLogin>" ;$xml += "`r`n"
$xml += "   <ServiceAccountPassword>$svcCRMAppPassword</ServiceAccountPassword>";$xml += "`r`n"
$xml += '</CrmServiceAccount>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<SandboxServiceAccount type="DomainUser">';$xml += "`r`n"
$xml += "  <ServiceAccountLogin>$env:USERDOMAIN\$svcCRMSandbox</ServiceAccountLogin>";$xml += "`r`n"
$xml += "  <ServiceAccountPassword>$svcCRMSandboxPassword</ServiceAccountPassword>";$xml += "`r`n"
$xml += '</SandboxServiceAccount>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<DeploymentServiceAccount type="DomainUser">';$xml += "`r`n"
$xml += "  <ServiceAccountLogin>$env:USERDOMAIN\$svcCRMDeploy</ServiceAccountLogin>";$xml += "`r`n"
$xml += "  <ServiceAccountPassword>$svcCRMDeployPassword</ServiceAccountPassword>";$xml += "`r`n"
$xml += '</DeploymentServiceAccount>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<AsyncServiceAccount type="DomainUser">';$xml += "`r`n"
$xml += "  <ServiceAccountLogin>$env:USERDOMAIN\$svcCRMAsync</ServiceAccountLogin>";$xml += "`r`n"
$xml += "  <ServiceAccountPassword>$svcCRMAsyncPassword</ServiceAccountPassword>";$xml += "`r`n"
$xml += '</AsyncServiceAccount>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<VSSWriterServiceAccount type="DomainUser">';$xml += "`r`n"
$xml += "  <ServiceAccountLogin>$env:USERDOMAIN\$svcCRMVSS</ServiceAccountLogin>";$xml += "`r`n"
$xml += "  <ServiceAccountPassword>$svcCRMVSSPassword</ServiceAccountPassword>";$xml += "`r`n"
$xml += '</VSSWriterServiceAccount>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<MonitoringServiceAccount type="DomainUser">';$xml += "`r`n"
$xml += "  <ServiceAccountLogin>$env:USERDOMAIN\$svcCRMMonitor</ServiceAccountLogin>";$xml += "`r`n"
$xml += "  <ServiceAccountPassword>$svcCRMMonitorPassword</ServiceAccountPassword>";$xml += "`r`n"
$xml += '</MonitoringServiceAccount>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<SQM optin="'+$SQMoptin+'"/>';$xml += "`r`n"
$xml += '<muoptin optin="'+$MUoptin+'"/>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += '<!-- Settings for IFD installation. May be skipped for intranet-only deployment or to configure IFD later. -->';$xml += "`r`n"
$xml += ' <ifdsettings enabled="'+$ifdsettings+'">';$xml += "`r`n"
$xml += '      <!-- Define what address considered internal, required only if enabled=true -->';$xml += "`r`n"
$xml += '      <internalnetworkaddress>'+$internalnetworkaddress+'</internalnetworkaddress>';$xml += "`r`n"
$xml += '      <!-- Define URLs with IFD authentication, required only if enabled=true -->';$xml += "`r`n"
$xml += '      <rootdomainscheme>'+$rootdomainscheme+'</rootdomainscheme>';$xml += "`r`n"
$xml += '      <sdkrootdomain>'+$sdkrootdomain+'</sdkrootdomain>';$xml += "`r`n"
$xml += '      <webapplicationrootdomain>'+$webapplicationrootdomain+'</webapplicationrootdomain>';$xml += "`r`n"
$xml += '      <discoveryrootdomain>'+$discoveryrootdomain+'</discoveryrootdomain>';$xml += "`r`n"
$xml += ' </ifdsettings>';$xml += "`r`n"
$xml += '';$xml += "`r`n"
$xml += ' <Email>';$xml += "`r`n"
$xml += '  <IncomingExchangeServer name="'+$ExchangeServerName+'"/>';$xml += "`r`n"
$xml += ' </Email>';$xml += "`r`n"
$xml += '<LaunchReportingExtensionsSetup>False</LaunchReportingExtensionsSetup>';$xml += "`r`n"
$xml += ' </Server>';$xml += "`r`n"
$xml += '</CRMSetup>';$xml += "`r`n"

$xml | Out-File $CRMxmlfile -Force

Write-Host "Installing CRM server... pull up a chair, this part could take a while, like 30-40 mins :)`n"
Start-Process $CRMinstallFile -ArgumentList "/Q /config $CRMxmlfile" -Wait
Remove-Item placeholder.tmp

#region SrsDataConnector
$xml = $null
$SRSxmlfile = "$PSScriptRoot\SrsDataConnectorSetupConfig.xml"

Write-Host 'Creating SrsDataConnector XML answer file...'

$xml += '<crmsetup>';$xml += "`r`n"
$xml += '<srsdataconnector>';$xml += "`r`n"
$xml += '<configdbserver>'+$SQLserver+'</configdbserver>';$xml += "`r`n"
$xml += '<autoupdateconfigdb>1</autoupdateconfigdb>';$xml += "`r`n"
$xml += '<reportserverurl>'+$reportingUrl+'</reportserverurl>';$xml += "`r`n"
$xml += '<autogroupmanagementoff>0</autogroupmanagementoff>';$xml += "`r`n"
$xml += '<instancename>'+$RSinstance+'</instancename>';$xml += "`r`n"
$xml += '<configsku>OnPremise</configsku>';$xml += "`r`n"
$xml += '<webstore enabled="false" configdb="false" />';$xml += "`r`n"
$xml += '<patch update="'+$patchUpdate+'" />';$xml += "`r`n"
$xml += '<muoptin optin="'+$MUoptin+'" />';$xml += "`r`n"
$xml += '<MonitoringServiceAccount type="DomainUser">';$xml += "`r`n"
$xml += '  <ServiceAccountLogin>'+$svcCRMMonitor+'</ServiceAccountLogin>';$xml += "`r`n"
$xml += '  <ServiceAccountPassword>'+$svcCRMMonitorPassword+'</ServiceAccountPassword>';$xml += "`r`n"
$xml += '</MonitoringServiceAccount>';$xml += "`r`n"
$xml += '</srsdataconnector>';$xml += "`r`n"
$xml += '</crmsetup>';$xml += "`r`n"
$xml | Out-File $SRSxmlfile

Write-Host " Installing SrsDataConnector...`n"
Start-Process $SRSInstallFile -ArgumentList "/Q /Config $SRSxmlfile" -Wait

#region EmailRouter
# https://support.microsoft.com/en-us/kb/951401
Write-Host "Downloading Microsoft Exchange Server MAPI Client and Collaboration Data Objects 1.2.1"
$source = 'http://bumboks.dk/ExchangeMapiCdo.MSI'
$destination = "$PSScriptRoot\ExchangeMapiCdo.msi"
Invoke-WebRequest $source -OutFile $destination
Write-Host " Installing ExchangeMapiCdo.msi`n"
Start-Process $destination -ArgumentList '/passive' -Wait

$xml = $null
$EmailRouterSetupXMLfile = "$PSScriptRoot\EmailRouterSetupConfig.xml"

Write-Host 'Creating Email Router XML answer file...'

$xml += '<CRMSetup>';$xml += "`r`n"
$xml += '<EmailRouter>';$xml += "`r`n"
$xml += '<Features>';$xml += "`r`n"
$xml += '  <SinkService />';$xml += "`r`n"
$xml += '  <RulesWizard />';$xml += "`r`n"
$xml += '</Features>';$xml += "`r`n"
$xml += '<Patch update="'+$patchUpdate+'"></Patch>';$xml += "`r`n"
$xml += '<muoptin optin="'+$MUoptin+'" />';$xml += "`r`n"
$xml += '<InstallDir>C:\Program Files\Microsoft Dynamics CRM Email Router</InstallDir>';$xml += "`r`n"
$xml += '</EmailRouter>';$xml += "`r`n"
$xml += '</CRMSetup>';$xml += "`r`n"
$xml | Out-File $EmailRouterSetupXMLfile

Write-Host " Installing Email Router...`n"
Start-Process $EmailRouterInstallFile -ArgumentList "/Q /CONFIG $EmailRouterSetupXMLfile" -Wait

if($nonHTTPS -eq $true) {New-Item -Path HKLM:\Software\Microsoft\MSCRM\DisableSecureDecryptionKey -Value "1" | Out-Null}
else {New-Item -Path HKLM:\Software\Microsoft\MSCRM\DisableSecureDecryptionKey -Value "0" | Out-Null}

# Make the Windows Service ”Microsoft CRM Email Router” run under the svcCRMEmailRouter account.
$service="displayname='Microsoft CRM Email Router'"

$svc=gwmi win32_service -filter $service
$svc.StopService() | Out-Null
$svc.change($null,$null,$null,$null,$null,$null,$account,$svcCRMEmailRouterpassword,$null,$null,$null) | Out-Null
$svc.StartService() | Out-Null

#region updates
function DownloadUpdate ($type,$url) {
    $file = $url.Split('/')[-1]
    $dest = "$CRMupdateDir\$file"
    Write-Host "Downloading $file"
    $uri = New-Object "System.Uri" "$url"
    $request = [System.Net.HttpWebRequest]::Create($uri)
    $response = $request.GetResponse()
    $totalLength = [System.Math]::Floor($response.get_ContentLength()/1024)
    $length = $response.get_ContentLength()
    $responseStream = $response.GetResponseStream()
    $destStream = New-Object -TypeName System.IO.FileStream -ArgumentList $dest, Create
    $buffer = New-Object byte[] 10KB
    $count = $responseStream.Read($buffer,0,$buffer.length)
    $downloadedBytes = $count
    while ($count -gt 0)
        [System.Console]::Write(" Downloaded {0}K of {1}K ({2}%)", [System.Math]::Floor($downloadedBytes/1024), $totalLength, [System.Math]::Round(($downloadedBytes / $length) * 100,0))
        $destStream.Write($buffer, 0, $count)
        $downloadedBytes += $count
    Write-Host ''
    New-Item "$CRMupdateDir\$type" -ItemType Directory | Out-Null
    Write-Host " Extracting... " -NoNewline
    Start-Process $CRMupdateDir\$file -ArgumentList "/extract:$CRMupdateDir\$type /passive" -Wait
    Write-Host "Installing... " -NoNewline
    if (Test-Path "$CRMupdateDir\$type\CrmUpdateWrapper.exe"){Start-Process "$CRMupdateDir\$type\CrmUpdateWrapper.exe" -ArgumentList "/quiet /norestart" -Verb RunAs -Wait}
    else{Start-Process msiexec.exe -ArgumentList "/i $CRMupdateDir\$type\MUISetup_1030_amd64.msi /quiet /norestart" -Verb RunAs -Wait}
    Write-Host "Done.`n"

if (!(Test-Path $CRMupdateDir)) {New-Item $CRMupdateDir -ItemType Directory | Out-Null}
DownloadUpdate Server $Server
DownloadUpdate Router $Router
DownloadUpdate SRS $SRS
DownloadUpdate MUI-ENU $MUIENU
DownloadUpdate MUI-DAN $MUIDAN

#region MaintenancePlan 
# https://habaneroconsulting.com/insights/create-developer-sql-maintenance-plans-using-sql-agent-jobs-and-powershell
Invoke-Command -ComputerName $SQLserver -ScriptBlock {
    $JobName = ("{0}_SQL_Maintenance" -f $env:COMPUTERNAME)
    $DropExisting = $false
    $InstanceName = "MSSQLSERVER"
    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null
    # Get the Default Instance
    $srv = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Server
    # Get the SQL Agent Service for the Instance
    $Service = Get-Service -DisplayName ("SQL Server Agent ({0})" -f $InstanceName)
    # Set the SQL Agent Service startup type to Automatic and attempt to start it
    if($Service -eq $null){
        Throw ("Sql Sserver Agent ({0}) Not Found" -f $InstanceName)
    } else {
    Write-Host "Setting SQL Agent Startup type to Automatic"
        $Service | Set-Service -StartupType Automatic -ErrorAction Stop
        if($Service.Status -ieq "Stopped"){
            Write-Host "Agent Not Running"
            Write-Host "Waiting for Service to Start." -NoNewline
            While($Service.Status -ieq "StartPending"){
                Write-Host "." -NoNewline
                Start-Sleep 2
            if($Service.Status -ine "Running"){
                Write-Host ""
                Throw "An error occurred while starting `"SQL SERVER AGENT ($InstanceName)`". Try starting the service Manually. Then re-run this command."
            Write-Host ".Started"
            Write-Host "Sleep for 10 Seconds While Agent wakes up..."
            Start-Sleep 10
            # Depending on the speed of your server, it may take longer than 10 seconds for the agent to wake up. You will know if the rest of the script fails.
    # Look for Existing Jobs with the same name.
        $Jobs = $srv.JobServer.Jobs
        foreach($Job in $Jobs){
            if($Job.Name -ieq $JobName){
                Write-Host ("This Server already has a Job Called `'{0}`'." -f $JobName)
                    Write-Host ("Dropping Job: `'{0}`'." -f $JobName)
                } else {
    } Catch {
        Throw $_
    # Define a Job object variable by supplying the Agent and the name arguments in the constructor and setting properties.
    $job = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Agent.Job -argumentlist $srv.JobServer, $JobName
    # Create the job on the instance of SQL Server Agent.
    Write-Host ("Creating job `'{0}`'." -f $JobName)
    $job.ApplyToTargetServer("(local)") #Job will run against the Agent's Local Server
    # Define a JobStep object variable by supplying the parent job and name arguments in the constructor.
    $jobstep = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Agent.JobStep -argumentlist $job, "Clean Up"

    $jobstep.Command = "SET QUOTED_IDENTIFIER ON 

    IF EXISTS (SELECT name from sys.indexes
                      WHERE name = N'CRM_AsyncOperation_CleanupCompleted')
          DROP Index AsyncOperationBase.CRM_AsyncOperation_CleanupCompleted
    CREATE NONCLUSTERED INDEX CRM_AsyncOperation_CleanupCompleted
    ON [dbo].[AsyncOperationBase] ([StatusCode],[StateCode],[OperationType])

    declare @DeleteRowCount int
    Select @DeleteRowCount = 2000
    declare @DeletedAsyncRowsTable table (AsyncOperationId uniqueidentifier not null primary key)
    declare @continue int, @rowCount int
    select @continue = 1
    while (@continue = 1)
    begin tran      
    insert into @DeletedAsyncRowsTable(AsyncOperationId)
          Select top (@DeleteRowCount) AsyncOperationId from AsyncOperationBase
          where OperationType in (1, 9, 12, 25, 27, 10) AND StateCode = 3 AND StatusCode in (30, 32)     
           Select @rowCount = 0
          Select @rowCount = count(*) from @DeletedAsyncRowsTable
          select @continue = case when @rowCount <= 0 then 0 else 1 end      
            if (@continue = 1)        begin
                delete WorkflowLogBase from WorkflowLogBase W, @DeletedAsyncRowsTable d
                where W.AsyncOperationId = d.AsyncOperationId             
     delete BulkDeleteFailureBase From BulkDeleteFailureBase B, @DeletedAsyncRowsTable d
                where B.AsyncOperationId = d.AsyncOperationId
     delete WorkflowWaitSubscriptionBase from WorkflowWaitSubscriptionBase WS, @DeletedAsyncRowsTable d
     where WS.AsyncOperationId = d.AsyncOperationID 
                delete AsyncOperationBase From AsyncOperationBase A, @DeletedAsyncRowsTable d
                where A.AsyncOperationId = d.AsyncOperationId             
                delete @DeletedAsyncRowsTable      
    --Drop the Index on AsyncOperationBase
    DROP INDEX AsyncOperationBase.CRM_AsyncOperation_CleanupCompleted"
    $jobstep.OnSuccessAction = [Microsoft.SqlServer.Management.SMO.Agent.StepCompletionAction]::GoToNextStep
    $jobstep.OnFailAction = [Microsoft.SqlServer.Management.SMO.Agent.StepCompletionAction]::QuitWithFailure
    # Create the job step on the instance of SQL Agent.

    $jobstep = New-Object -TypeName Microsoft.SqlServer.Management.SMO.Agent.JobStep -argumentlist $job, "Update Statistics"

    $jobstep.Command = "UPDATE STATISTICS [dbo].[AsyncOperationBase] WITH FULLSCAN
    UPDATE STATISTICS [dbo].[DuplicateRecordBase] WITH FULLSCAN
    UPDATE STATISTICS [dbo].[BulkDeleteOperationBase] WITH FULLSCAN
    UPDATE STATISTICS [dbo].[WorkflowWaitSubscriptionBase] WITH FULLSCAN"

    $jobstep.OnSuccessAction = [Microsoft.SqlServer.Management.SMO.Agent.StepCompletionAction]::QuitWithSuccess
    $jobstep.OnFailAction = [Microsoft.SqlServer.Management.SMO.Agent.StepCompletionAction]::QuitWithFailure
    # Create the job step on the instance of SQL Agent.
    # Define a JobSchedule object variable by supplying the parent job and name arguments in the constructor.
    $jobsch =  New-Object -TypeName Microsoft.SqlServer.Management.SMO.Agent.JobSchedule -argumentlist $job, "Monthly_Job_Schedule"
    # Set properties to define the schedule frequency, and duration.
    $jobsch.FrequencyTypes = [Microsoft.SqlServer.Management.SMO.Agent.FrequencyTypes]::MonthlyRelative
    $jobsch.FrequencyRelativeIntervals = [Microsoft.SqlServer.Management.SMO.Agent.FrequencyRelativeIntervals]::Last
    $jobsch.FrequencyInterval = [Microsoft.SqlServer.Management.SMO.Agent.MonthlyRelativeWeekDays]::EveryDay
    $jobsch.FrequencyRecurrenceFactor = 1
    $jobsch.ActiveStartDate = Get-Date
    $starttime = New-Object -TypeName TimeSpan -ArgumentList 23, 30, 0
    $jobsch.ActiveStartTimeOfDay = $starttime
    # Create the job schedule on the instance of SQL Agent.
    Write-Host ("Job `'{0}`' successfully created..." -f $JobName)


#region removeAutoLogon
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultUserName
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name DefaultPassword
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name AutoAdminLogon
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" -Name ForceAutoLogon

#region TheEnd
Write-Host ''
Write-Host '      .                             ..              .x+=:.         ..                                              ..  ' -ForegroundColor Green
Write-Host '  .x88888x.                   . uW8"       .8u     z`    ^%      dF                                               888B.' -ForegroundColor Green
Write-Host ' :8**888888X.  :>        u.   `t888       m888R-      .   <k    `88bu.             u.      u.    u.              48888E' -ForegroundColor Green
Write-Host ' f    `888888x./   ...ue888b   8888   .    98P      .@8Ned8"    `*88888bu    ...ue888b   x@88k u@88c.      .u    `8888Β΄' -ForegroundColor Green
Write-Host '`       `*88888~   888R Y888r  9888.z88N   ^8     .@^%8888"       ^"*8888N   888R Y888r ^"8888""8888"   ud8888.   Y88F ' -ForegroundColor Green
Write-Host ' \.    .  `?)X.    888R I888>  9888  888E  J"    x88:  `)8b.     beWE "888L  888R I888>   8888  888R  :888`8888.  `88  ' -ForegroundColor Green
Write-Host '  `~=-^   X88> ~   888R I888>  9888  888E +"     8888N=*8888     888E  888E  888R I888>   8888  888R  d888 `88%"   8F  ' -ForegroundColor Green
Write-Host '         X8888  ~  888R I888>  9888  888E         %8"    R88     888E  888E  888R I888>   8888  888R  8888.+"      4   ' -ForegroundColor Green
Write-Host '         488888   u8888cJ888   9888  888E          @8Wou 9%      888E  888F u8888cJ888    8888  888R  8888L        .   ' -ForegroundColor Green
Write-Host ' .xx.     88888X   "*888*P"   .8888  888"        .888888P`      .888N..888   "*888*P"    "*88*" 8888" `8888c. .+  u8N. ' -ForegroundColor Green
Write-Host '`*8888.   `88888>    `Y"       `%888*%"          `   ^"F         `"888*""      `Y"         ""   `Y"    "88888%   "*88% ' -ForegroundColor Green
Write-Host '  88888    `8888>                 "`                                ""                                   "YP`      ""  ' -ForegroundColor Green
Write-Host '  `8888>    `888                                                                                                       ' -ForegroundColor Green
Write-Host '   "8888     8%                                                                                                        ' -ForegroundColor Green
Write-Host '    `"888x:-"                                                                                                          ' -ForegroundColor Green
Write-Host '                                      Now go configure the CRM Email Router πŸ™‚                                         ' -ForegroundColor Cyan

Workflow Manager – Scripted Installation

Allrighty, I was sick and tired of NOT being able to install my Workflow Manager without the Web-platform Installer installation. Secondly I was tired of not being able to install Workflow Manager using my loved one, Powershell. So I calledΒ upon my good friendΒ Mads Hjort Larsen and we started to work on it.
The script below will make use of offline msi files for the installation. So make sure you grab those before you run the script. They should be located in the same folder as the script before you run it.

The script

Set-Location (Split-Path -Parent -Path $MyInvocation.MyCommand.Definition)

#region Variables
$SQLserver = '<SQL SERVER>' # Name of the database server.
$DBprefix = '<Database Pre-fix>' # All databases will be prefixed with this string.

$SBRunAsAccount = '<Service Bus service account>' # The account under which the service runs. This account must be a domain account.
$SBRunAsPasswordString = '<Service Bus service Account passwprd>' # Password for RunAs account (in cleartext).
$SBAdminGroup = '<Service Bus Administrator group>' # The admin group for Service Bus. (Default "BUILTIN\Administrators")
$SBCertificateAutoGenerationKey = '<Certificate passphrase>' # This passphrase is required for certificate auto generation. This parameter is mandatory if you want certificates to be auto generated.
$TcpPort = <Service Bus port> # The port that the Service Bus for Windows Server uses for TCP. (Default 9354) 
$MessageBrokerPort = <Message Broker port> 9356 # The port that the Service Bus for Windows Server uses for MessageBroker communication. (Default 9356)
$InternalPortRangeStart = <Port Range> # The start of the port range that Service Bus for Windows Server uses for internal communication purposes. (Default 9000) 

$WFRunAsAccount = '<Service Bus service account>' # The account under which the service will be running. This account must be a domain account.
$WFRunAsPasswordString = '<Service Bus service account password>' # Password for RunAs account (in cleartext).
$WFAdminGroup = '<Service Bus Administrator group>' # The set of users who are considered workflow administrators. (Default BUILTIN\Administrators)
$WFCertificateAutoGenerationKey = '<Certificate Passphrase>' # This passphrase is required for certificate auto generation. This is a mandatory parameter if you want certificates to be auto generated.
$HttpsPort = <Workflow HTTPS Port> # The port that will be used by the workflow for HTTPS communication. (Default 12290)
$HttpPort = <Workflow HTTP Port> # The port that will be used by the workflow for HTTP communication. (Default 12291)

$SBNamespace = '<Service Bus Namespace>' # Specifies the name for the new Service Bus for Windows Server service namespace.
$AddressingScheme = 'Path' # Specifies the addressing scheme used in the service namespace. The possible values for this parameter are Path (default value) and DNSRegistered. If the value DNSRegistered is specified, the -DnsEntry parameter is required.
$ManageUsers = '<Workflow Namespace Administrator01>','<Workflow Namespace Administrator02>' # Specifies user or group names that will be managers of the service namespace.

$WF  = "WindowsFabric.msi"
$SB  = "Service_Bus.msi"
$WMC = "WorkflowManagerClient_x64.msi"
$WM  = "Workflow_Manager.msi"

## Can't touch this... or anything below this line ;P

$Sleep = 90 # Sleep duration is set to 90 seconds because that what all the cool kids online are doing.

    "IACCEPTEULA=yes" is required for quiet install of Windows Fabric, 
    "WEBPI=1" is required for installation of Service Bus and Workflow Manager outside of the Web Platform Installer, 
    "/QUIET" = Quiet mode, no user interaction, 
    "/NORESTART" = Do not restart after the installation is complete.

$fileNotFound = $null

$files = $WF,$SB,$WMC,$WM
foreach ($file in $files) {
    if (Test-Path -Path $file){
        Write-Host "βœ“ $file" -ForegroundColor Green
    }else{Write-Host "X $file" -ForegroundColor Red;$fileNotFound = $true}
}if ($fileNotFound -eq $true){Exit}
foreach ($file in $files) {
    Write-Host "Installing $file... " -NoNewline
    Start-Process $file -ArgumentList $ArgumentList -Wait
    Write-Host "Done"

[Environment]::SetEnvironmentVariable("PSModulePath", [Environment]::GetEnvironmentVariable("PSModulePath","Machine"))
Import-Module WorkflowManager

#region StringBuilding
# The strings are created like this to make it easier to put them on different SQL servers
function BuildString ($type, $dbName, $SQLserver) {
    New-Variable -Name $type'DBConnectionStringDataSource' -Value $SQLserver
    New-Variable -Name $type'DBConnectionStringInitialCatalog' -Value $DBprefix$dbName
    New-Variable -Name $type'DBConnectionString' -Scope Script -Value ("Data Source="+(Get-Variable -Name $type'DBConnectionStringDataSource' -ValueOnly)+";Initial Catalog="+(Get-Variable -Name $type'DBConnectionStringInitialCatalog' -ValueOnly)+";Integrated Security=True;Encrypt=False") -Force

# Example ConnectionString: Data Source=SQL01;Initial Catalog=Udv_service_SBManagement;Integrated Security=True;Encrypt=False
BuildString SBFarm SBManagement $SQLserver
BuildString GateWay SBGateway $SQLserver
BuildString MessageContainer SBMessageContainer $SQLserver
BuildString WFFarm WFManagement $SQLserver
BuildString Instance WFInstanceManagement $SQLserver
BuildString Resource WFResourceManagement $SQLserver

$SBCertificateAutoGenerationKey = ConvertTo-SecureString -AsPlainText -Force -String $SBCertificateAutoGenerationKey -Verbose
New-SBFarm -SBFarmDBConnectionString $SBFarmDBConnectionString -InternalPortRangeStart $InternalPortRangeStart -TcpPort $TcpPort -MessageBrokerPort $MessageBrokerPort -RunAsAccount $SBRunAsAccount -AdminGroup $SBAdminGroup -GatewayDBConnectionString $GatewayDBConnectionString -CertificateAutoGenerationKey $SBCertificateAutoGenerationKey -MessageContainerDBConnectionString $MessageContainerDBConnectionString -Verbose
New-WFFarm -WFFarmDBConnectionString $WFFarmDBConnectionString -RunAsAccount $WFRunAsAccount -AdminGroup $WFAdminGroup -HttpsPort $HttpsPort -HttpPort $HttpPort -InstanceDBConnectionString $InstanceDBConnectionString -ResourceDBConnectionString $ResourceDBConnectionString -CertificateAutoGenerationKey $SBCertificateAutoGenerationKey -Verbose

$SBRunAsPassword = ConvertTo-SecureString -AsPlainText -Force -String $SBRunAsPasswordString -Verbose
Add-SBHost -SBFarmDBConnectionString $SBFarmDBConnectionString -RunAsPassword $SBRunAsPassword -EnableFirewallRules $true -CertificateAutoGenerationKey $SBCertificateAutoGenerationKey -Verbose

Try {
    New-SBNamespace -Name $SBNamespace -AddressingScheme $AddressingScheme -ManageUsers $ManageUsers -Verbose
    for ($i=$Sleep; $i -gt 1; $i--) {
        Write-Progress -Activity "Creating new SB Namespace" -Status "Sleeping" -SecondsRemaining $i
        Start-Sleep 1
Catch [system.InvalidOperationException]{}

$SBClientConfiguration = Get-SBClientConfiguration -Namespaces $SBNamespace -Verbose
$WFRunAsPassword = ConvertTo-SecureString -AsPlainText -Force -String $WFRunAsPasswordString -Verbose
Add-WFHost -WFFarmDBConnectionString $WFFarmDBConnectionString -RunAsPassword $WFRunAsPassword -EnableFirewallRules $true -SBClientConfiguration $SBClientConfiguration -CertificateAutoGenerationKey (ConvertTo-SecureString $WFCertificateAutoGenerationKey -AsPlainText -Force) -Verbose

(The script is currently using a self-signed certificate.)

How to get MSI files
WebPICMD.exe /offline /products:”WindowsFabric,ServiceBus_1_1,WorkflowManager” /Path:c:\temp\

WebPICMD.exe /offline /products:WindowsFabric /Path:c:\temp\

WebPICMD.exe /offline /Products:ServiceBus_1_1 /Path:c:\temp\

WebPICMD.exe /offline /Products:WorkflowManager /Path:c:\temp\

WebPICMD.exe /offline /Products:WorkflowManager /Path:c:\temp\

Updated 17-June-2015: Incorporated Martin Sandersens comment into the script, removing the need for customized msi files.
Updated 07-August-2015: Updated script for fewer lines of code.

SharePoint – ConfigDB – Growing

Alright, so one of my developers was complaining that his config database on his development environment was more than 40GB(If your config database is larger than 10GB, you should continue reading), or actually it was the sysadmin that complained, but my developer was targeted. He asked if I could have a look at it. Sure of course, properly transaction logs gone wild…. So logging on the server, finding the trans logs were all good. All right further digging came up with the TimerJobHistory table being filled with around 100 million records. Alright, now what?

Some posts have some good Powershell scripts, that will incrementally delete your timerjob history, maybe also change your retention time. Sure I started to run those, but that didn’t quite do the trick. So what was wrong then?

Alright here is the deal. The “Delete Job History” job, has a timeout of 5 minutes. That means it will delete records, but if the delete is not completed within 5 minutes, it throws a timeout. It actually fails the timer job. (Timeout) This timer job is set to run only once a week. So guess what happens when you create more timer jobs per week than the Delete Timer job can remove in 5 minutes? – Yup, the TimerJobHistory table will grow…

I found the easiest fix, just to change the schedule for the “Delete Job History”. For the 100 million records, I changed it to every 7’th minute. I know that it will not run longer than 5 minutes, so a 2 minutes slack should be sufficient. So what should the setting be? Adjust according to your environment.

SharePoint ConfifDB growing beyond the normal 4-8GB. The data file, not the log file.

Root cause
The Delete Job History cannot delete more rows than created timer jobs per week within the 5 minute timeout. This can be related to heavy deployment or insufficient resources.

Change the schedule of the Delete Job History to run more frequently.

Get-SPTimerJob job-Delete-Job-History | Set-SPTimerJob -Schedule “daily at 05:00:00”

Which interval is required depends on your environment or the amount of rows in backlog. So set schedule accordingly and following the types for Set-SPTimerJob.
The type must be a valid SharePoint Timer service (SPTimer) schedule in the form of any one of the following schedules:
– Every 5 minutes between 0 and 59
– Hourly between 0 and 59
– Daily at 15:00:00
– Weekly between Fri 22:00:00 and Sun 06:00:00
– Monthly at 15 15:00:00
– Yearly at Jan 1 15:00:00

IIS – Authentication Settings – Powershell

Doing my work I had to setup some Kerberos authentication for an IIS site. The customer and the developer wanted it done using Powershell. I’m not all too impressed by the IIS powershell, it reminds me of WMI and I did not like that either.
Set that aside, I actually had a hard time finding something simple to get it to work.
All the below lines needs is the sitename, found from the IIS manager.

$SiteName = β€˜β€™ #Example: 'Default Web Site'
$PSPath = β€œIIS:\Sites\$SiteName”

#-- ASP.NET Impersontation enable
Set-WebConfigurationProperty –filter system.web/identity –Name impersonate –Value True –PSPath $PSPath

#-- Windows Authentication enable
Set-WebConfigurationProperty –filter /system.webServer/security/authentication/WindowsAuthentication –Name enabled –value True –PSPath $PSPath