DOrcDeployModule.psm1

function Invoke-VsDbCmd {
    Param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $ManifestFile,
        
        [Parameter(Mandatory = $true, Position = 1)]
        [string] $ConnectionString,
        
        [Parameter(Mandatory = $false)]
        [string] $TargetDatabase,
        
        [Parameter(Mandatory = $false)]
        [string] $OutputFile,
        
        [Parameter(Mandatory = $false)]
        [switch] $RecreateDatabase,
        
        [Parameter(Mandatory = $false)]
        [switch] $Deploy,
        
        [Parameter()]
        [hashtable] $Properties
    )
    Process {
        $builder = New-Object -TypeName System.Text.StringBuilder
        $builder.AppendFormat("/a:Deploy /Manifest:`"{0}`" /cs:`"{1}`" /dsp:SQL", $ManifestFile, $ConnectionString) | Out-Null
        
        if ($OutputFile -ne $null -and $OutputFile.Length -gt 0) { $builder.AppendFormat(" /script:`"{0}`"", $OutputFile) | Out-Null }
        
        if ($TargetDatabase -ne $null) { $builder.AppendFormat(" /p:TargetDatabase=`"{0}`"", $TargetDatabase) | Out-Null }
        
        if ($RecreateDatabase) { $builder.Append(" /p:AlwaysCreateNewDatabase=True") | Out-Null }
                
        if ($Deploy) { $builder.Append(" /dd:+") | Out-Null }
        
        if ($Properties -ne $null) {
            foreach ($property in $Properties.GetEnumerator()) {
                $builder.AppendFormat(" /p:{0}", $property.Key) | Out-Null
                if ($property.Value -ne $null) { $builder.AppendFormat("={0}", $property.Value) | Out-Null }
            }
        }
        
        $builder.ToString()
        
        Start-Process vsdbcmd.exe $builder.ToString() -Wait
    }
}

function SendEmailToDOrcSupport([string] $StrSubject) {
    $Msg = New-Object Net.Mail.MailMessage
    $Smtp = New-Object Net.Mail.SmtpClient($DOrcSupportEmailSMTPServer)
    $Msg.From = $DOrcSupportEmailFrom
    $Msg.To.Add($DOrcSupportEmailTo)
    $Msg.Subject = $StrSubject
    $Smtp.Send($Msg)
    $Smtp = $null
    $Msg = $null
}

function GetDateReverse() {
    $dtNow = Get-Date
    return $dtNow.Year.ToString() + "-" + $dtNow.Month.ToString().PadLeft(2, '0') + "-" + $dtNow.Day.ToString().PadLeft(2, '0') + "_" + $dtNow.TimeOfDay.Hours.ToString().PadLeft(2, '0') + "-" + $dtNow.TimeOfDay.Minutes.ToString().PadLeft(2, '0')
}

function CheckDiskSpace([string[]] $servers, [int] $minMB = 100) {
    $bolSpaceCheckOK = $true
    Write-Host " Checking disk space..."
    foreach ($server in $servers) {
        $serv = "[" + $server.Trim() + "]"
        $ntfsVolumes = Get-WmiObject -Class win32_volume -cn $server | Where-Object {($_.FileSystem -eq "NTFS") -and ($_.driveletter)}
        foreach ($ntfsVolume in $ntfsVolumes) {
            #Considered checking for the existance of the pagefile file but it could be on C: which we care about (either orphaned or current)
            if ($ntfsVolume.DriveLetter -eq "P:") { write-host " " $ntfsVolume.DriveLetter "Skipped..."}
            else {
                $freeSpace = [math]::Round($ntfsVolume.freespace / 1000000)
                if ($freeSpace -gt $minMB) {
                    Write-Host " " $serv $ntfsVolume.DriveLetter $freeSpace"MB free - OK"
                }
                else {
                    $strMsgSubject = " " + $serv + " " + $ntfsVolume.DriveLetter + " " + $freeSpace + "MB free - TOO LOW!"
                    Write-Host $strMsgSubject
                    SendEmailToDOrcSupport $strMsgSubject
                    $strMsgSubject = $null
                    $bolSpaceCheckOK = $false
                }
            }
        }
    }
    return $bolSpaceCheckOK
} 

function UnInstallProducts([string] $strComputerName, $ProductsToRemove) {
    $bolReturn = $true
    if (CheckDiskSpace $strComputerName) {
        $Products = Get-WmiObject Win32_Product -ComputerName $strComputerName
        foreach ($Product in $Products) {
            foreach ($strProductName in $ProductsToRemove) {
                if ($Product.Name -ne $null) {
                    if ($strProductName.Contains("*") -and $Product.Name.Contains($strProductName.Replace("*", ""))) {
                        Write-Host "Removing:" $Product.Name " Version:" $Product.Version " GUID:" $Product.IdentifyingNumber " from" $strComputerName
                        if ($Product.Uninstall().ReturnValue -eq 0) {
                            Write-Host " Success..."
                        }
                        else {
                            $bolReturn = $false
                            Write-Host "FAILED to remove" $Product.Name
                        }
                    }
                    if ($strProductName -eq $Product.Name) {
                        Write-Host "Removing:" $Product.Name " Version:" $Product.Version " GUID:" $Product.IdentifyingNumber " from" $strComputerName
                        if ($Product.Uninstall().ReturnValue -eq 0) {
                            Write-Host " Success..."
                        }
                        else {
                            $bolReturn = $false
                            Write-Host "FAILED to remove" $Product.Name
                        }
                    }
                }
            }
        }
    }
    else {
        Write-Host "Disk space check failed..."
        $bolReturn = $false
    }
    $Products = $null
    return $bolReturn
}

function RemoveMSI([string] $strComputerName, [string] $strMSIFullName, $ProductsToRemove) {
    $bolReturn = $true
    if (CheckDiskSpace $strComputerName) {
        Write-Host "Attempting to remove using msi ProductCode"
        $bolReturn = UninstallProduct -strComputerName $strComputerName -strMSIFullName $strMSIFullName

        $Products = Get-WmiObject Win32_Product -ComputerName $strComputerName
        foreach ($Product in $Products) {
            foreach ($strProductName in $ProductsToRemove) {
                if ($Product.Name -ne $null) {
                    if ($strProductName.Contains("*") -and $Product.Name.Contains($strProductName.Replace("*", ""))) {
                        Write-Host "Removing:" $Product.Name " Version:" $Product.Version " GUID:" $Product.IdentifyingNumber " from" $strComputerName
                        Write-Host "Unable to uninstall based on Product Code, reverting to WMI"
                        if ($Product.Uninstall().ReturnValue -eq 0) {
                            Write-Host " Success..."
                        }
                        else {
                            $bolReturn = $false
                            Write-Host "FAILED to remove" $Product.Name
                        }
                    }
                    if ($strProductName -eq $Product.Name) {
                        Write-Host "Removing:" $Product.Name " Version:" $Product.Version " GUID:" $Product.IdentifyingNumber " from" $strComputerName
                        Write-Host "Unable to uninstall based on Product Code, reverting to WMI"
                        if ($Product.Uninstall().ReturnValue -eq 0) {
                            Write-Host " Success..."
                        }
                        else {
                            $bolReturn = $false
                            Write-Host "FAILED to remove" $Product.Name
                        }

                    }
                }
            }
        }
    }
    else {
        Write-Host "Disk space check failed..."
        $bolReturn = $false
    }
    $Products = $null
    return $bolReturn
}

function UninstallProduct([string] $strComputerName, [string] $strMSIFullName) {
    # Read property from MSI database
    $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
    $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($strMSIFullName, 0))
    $Query = "SELECT Value FROM Property WHERE Property = 'ProductCode'"
    $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))
    $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)
    $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)
    $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)
 
    # Commit database and close view
    $MSIDatabase.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDatabase, $null)
    $View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null)           
    $MSIDatabase = $null
    $View = $null
 
    $bolReturn = $false
    $strMSIName = $strMSIFullName.SubString(($strMSIFullName.LastIndexOf("\") + 1), ($strMSIFullName.Length - ($strMSIFullName.LastIndexOf("\") + 1)))
    $DestFolderCollection = Invoke-Command -ComputerName $strComputerName { Get-Item env:TEMP }
    $LocalMSI = Join-Path $DestFolderCollection.Value $strMSIName
    $DestFolder = Join-Path \\$strComputerName $DestFolderCollection.Value
    $DestFolder = $DestFolder -replace ":", "$"
    $strLogFile = $DestFolderCollection.Value + "\" + $strMSIName.Replace(".msi", ".uninstall.log")
    $strUNCInstallScript    = $DestFolder + "\" + $strMSIName.Replace(".msi", ".cmd")
    $strLocalInstallScript    = $DestFolderCollection.Value + "\" + $strMSIName.Replace(".msi", ".cmd")
    $strAllParameters = "/qn /lvx " + [char]34 + $strLogFile + [char]34
    $strUNCLogFileName = $DestFolder + "\" + $strMSIName.Replace(".msi", ".uninstall.log")
    $strUNCMSIName = Join-Path $DestFolder $strMSIName

    if (Test-Path $strUNCInstallScript) {
        Write-Host "Removing: " $strUNCInstallScript
        Remove-Item $strUNCInstallScript -force
    }
    if (Test-Path $strUNCLogFileName) {
        Write-Host "Removing: " $strUNCLogFileName
        Remove-Item $strUNCLogFileName -force
    }
    if (Test-Path $strUNCMSIName) {
        Write-Host "Removing: " $strUNCMSIName
        Remove-Item $strUNCMSIName -force
    }
    if ((Test-Path $strUNCInstallScript) -or (Test-Path $strUNCLogFileName) -or (Test-Path $strUNCMSIName)) {
        Write-Host "ERROR: Temporary installation script exists..."
    }
    else {
        Copy-Item "$strMSIFullName" "$DestFolder" -Verbose | Out-Host
        Write-Host "Uninstalling:" $strMSIFullName
        Write-Host "msiexec /x " $Value " " $strAllParameters
        $Stream = [System.IO.StreamWriter] $strUNCInstallScript
        $Stream.WriteLine("msiexec /x " + $Value + " " + $strAllParameters)
        $Stream.Close()
        $Script = "&$strLocalInstallScript"
        $ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($Script)
        $Result = Invoke-Command -Computer $strComputerName -ScriptBlock $ScriptBlock
        Start-Sleep -Seconds 10
        if (Test-Path $strUNCLogFileName) {
            $bolExtraAnalysis = $false
            $WiLogUtl = $WiLogUtlPath
            if (Test-Path $WiLogUtl) {
                $bolExtraAnalysis = $true
                Write-Host "[WiLogUtl] WiLogUtl has been detected, additional analysis of the MSI log will be performed..."
                $dateRev = GetDateReverse
                $outputFolder = $MSILogsRoot + "\" + $dateRev + "_" + $EnvironmentName + "_" + $strMSIName.Replace(".msi", "") + "_Remove"
                write-host "[WiLogUtl] Output folder:" $outputFolder
                $capture = New-Item -ItemType directory -Path $outputFolder | Out-Null
                . $WiLogUtl /q /l $strUNCLogFileName /o $outputFolder
                Start-Sleep -Seconds 10
            }
            Write-Host "Checking: " $strUNCLogFileName
            $LogContent = Get-Content $strUNCLogFileName
            $bolLogCheck01 = $false
            $bolLogCheck02 = $false
            foreach ($strLine in $LogContent) {
                if ($strLine.Contains("-- Removal completed successfully.") -or $strLine.Contains("-- Removal operation completed successfully.")) {
                    Write-Host "Detected: " $strLine
                    $bolLogCheck01 = $true
                }
                if ($strLine.Contains("Windows Installer removed the product.") -and $strLine.Contains("success or error status: 0.")) {
                    Write-Host "Detected: " $strLine
                    $bolLogCheck02 = $true
                }
            }
            if ($bolLogCheck01 -and $bolLogCheck02) {
                Write-Host "Apparently the MSI has been removed, cleaning up temp files..."
                $bolReturn = $true
                Remove-Item $strUNCInstallScript -force
                Remove-Item $strUNCMSIName -force
                if ((Test-Path $strUNCInstallScript) -or (Test-Path $strUNCMSIName)) {
                    Write-Host "ERROR: Error during the tidy up stage..."
                }
            }
            else {
                Write-Host "Susccess criteria couldn't be found in the MSI log file..."
                if ($bolExtraAnalysis) {
                    $oErrorsFile = Get-ChildItem -Path $outputFolder -Filter "*_Errors.txt"
                    write-host "[WiLogUtl] Importing" $oErrorsFile.FullName
                    $errors = Get-Content $oErrorsFile.FullName
                    foreach ($strLine in $errors) {
                        if ($strLine.Trim().Length -gt 0) {
                            Write-Host " " $strLine
                        }
                    }
                    $errors = $null
                }
            }
        }
        else {
            Write-Host "ERROR: Can't find log file to check..."
        }
    }

    return $bolReturn
}

    
function InstallMSI([string] $strComputerName, [string] $strMSIFullName, $arrParameters) {
    $bolReturn = $false
    $strMSIName = $strMSIFullName.SubString(($strMSIFullName.LastIndexOf("\") + 1), ($strMSIFullName.Length - ($strMSIFullName.LastIndexOf("\") + 1)))
    $DestFolderCollection = Invoke-Command -ComputerName $strComputerName { Get-Item env:TEMP }
    $LocalMSI = Join-Path $DestFolderCollection.Value $strMSIName
    $DestFolder = Join-Path \\$strComputerName $DestFolderCollection.Value
    $DestFolder = $DestFolder -replace ":", "$"
    $strLogFile = $DestFolderCollection.Value + "\" + $strMSIName.Replace(".msi", ".log")
    $strUNCInstallScript    = $DestFolder + "\" + $strMSIName.Replace(".msi", ".cmd")
    $strLocalInstallScript    = $DestFolderCollection.Value + "\" + $strMSIName.Replace(".msi", ".cmd")
    $strAllParameters = "/qn /lvx " + [char]34 + $strLogFile + [char]34
    $strUNCLogFileName = $DestFolder + "\" + $strMSIName.Replace(".msi", ".log")
    $strUNCMSIName = Join-Path $DestFolder $strMSIName
    if ($arrParameters.Count -gt 0) {
        Write-Host "[InstallMSI] Attempting to install:" $strMSIFullName "on:" $strComputerName "with the following parameters:"
        foreach ($strParameter in $arrParameters) {
            if (($strParameter.ToLower().Contains("password")) -or ($strParameter.ToLower().Contains("pswd")) -or ($strParameter.ToLower().Contains("pass"))) {
                Write-Host " "$strParameter.Split("=")[0]
            }
            else {
                Write-Host " " $strParameter
            }
            $strAllParameters = $strAllParameters + " " + $strParameter.replace('%','%%')
        }
    }
    else {
        Write-Host "[InstallMSI] Attempting to install:" $strMSIFullName "on:" $strComputerName
    }
    $cmdLine = "msiexec /i " + $LocalMSI + " " + $strAllParameters
    if (Test-Path $strUNCInstallScript) {
        Write-Host "[InstallMSI] Removing: " $strUNCInstallScript
        Remove-Item $strUNCInstallScript -force
    }
    if (Test-Path $strUNCLogFileName) {
        Write-Host "[InstallMSI] Removing: " $strUNCLogFileName
        Remove-Item $strUNCLogFileName -force
    }
    if (Test-Path $strUNCMSIName) {
        Write-Host "[InstallMSI] Removing: " $strUNCMSIName
        Remove-Item $strUNCMSIName -force
    }
    if ((Test-Path $strUNCInstallScript) -or (Test-Path $strUNCLogFileName) -or (Test-Path $strUNCMSIName)) {
        Write-Host "[InstallMSI] ERROR: Temporary installation script exists..."
    }
    elseif ($cmdLine.Length -gt 8191) { 
        write-host "[InstallMSI] ERROR: Command line is too long:" $cmdLine.Length
    }
    else {
        Write-Host "[InstallMSI] command line length:" $cmdLine.Length
        Write-Host "[InstallMSI] Copying installer..."
        Copy-Item "$strMSIFullName" "$DestFolder" | Out-Host
        Write-Host "[InstallMSI] Installing:" $strMSIFullName
        $isCitrix = (Check-IsCitrixServer -compName $strComputerName)
        if ($isCitrix) { Write-Host "[InstallMSI] Adding Citrix parameters..." }
        $Stream = [System.IO.StreamWriter] $strUNCInstallScript
        if ($isCitrix) { $Stream.WriteLine("change user /install") }
        $Stream.WriteLine($cmdLine)
        if ($isCitrix) { $Stream.WriteLine("change user /execute") }
        $Stream.Close()
        
        $Result = [System.Object]
        
        Write-Host "[InstallMSI] Checking CredSSP Status..."
        $Password = $DeploymentServiceAccountPassword | ConvertTo-SecureString -asPlainText -Force 
        $Credential = New-Object System.Management.Automation.PSCredential($DeploymentServiceAccount,$Password)

        try {
            $CredSSPEnabled = (Get-DorcCredSSPStatus -ComputerName $strComputerName -Credential $Credential -Test -ErrorAction Stop).CredSSPWorks
        }
        catch {
            Write-Warning -Message "Failed to determine CredSSP status of [$strComputerName] - this is expected on hosts with PowerShell 2.0; full exception:`n$_"
            $CredSSPEnabled = $false
        }
        
        if ($CredSSPEnabled -eq $true) {
            Write-Host "[InstallMSI] Performing CredSSP authentication installation..."
            try {
                 $Result = Invoke-Command -Computer $strComputerName -ScriptBlock { Start-Process -FilePath $using:strLocalInstallScript -Verb runAs -Wait } -Authentication CredSSP -Credential $Credential -ErrorAction Stop
            }
            catch {
                Throw "[InstallMSI] Failed to establish remote PS session to $strComputerName using CredSSP authentication."
            }
        }
        else {
            Write-Host "[InstallMSI] Performing double hop authentication installation..."
            $Script = "&$strLocalInstallScript"
            $ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($Script)

            $Result = Invoke-Command -Computer $strComputerName -ScriptBlock $ScriptBlock
        }

        Start-Sleep -Seconds 10
        if (Test-Path $strUNCLogFileName) {
            $bolExtraAnalysis = $false
            $WiLogUtl = $WiLogUtlPath
            if (Test-Path $WiLogUtl) {
                $bolExtraAnalysis = $true
                Write-Host "[WiLogUtl] WiLogUtl has been detected, additional analysis of the MSI log will be performed..."
                $dateRev = GetDateReverse
                $outputFolder = $MSILogsRoot + "\" + $(get-date -Format "yyyy-MM-dd_HH-mm-ss-ms") + "_" + $EnvironmentName + "_" + $strMSIName.Replace(".msi", "")
                write-host "[WiLogUtl] Output folder:" $outputFolder
                $capture = New-Item -ItemType directory -Path $outputFolder | Out-Null
                . $WiLogUtl /q /l $strUNCLogFileName /o $outputFolder
                Start-Sleep -Seconds 10
            }
            Write-Host "[InstallMSI] Checking: " $strUNCLogFileName
            $LogContent = Get-Content $strUNCLogFileName
            $bolLogCheck01 = $false
            $bolLogCheck02 = $false
            foreach ($strLine in $LogContent) {
                if ($strLine.Contains("-- Installation completed successfully.") -or $strLine.Contains("-- Installation operation completed successfully.") -or $strLine.Contains("-- Configuration completed successfully") ) {
                    Write-Host "[InstallMSI] Detected: " $strLine
                    $bolLogCheck01 = $true
                }
                if (($strLine.Contains("Windows Installer installed the product.") -and $strLine.Contains("Installation success or error status: 0."))`
                -or ($strLine.Contains("Windows Installer reconfigured the product.") -and $strLine.Contains("Reconfiguration success or error status: 0."))) {
                    Write-Host "[InstallMSI] Detected: " $strLine
                    $bolLogCheck02 = $true
                }
            }
            if ($bolLogCheck01 -and $bolLogCheck02) {
                Write-Host "[InstallMSI] Apparently the MSI has installed, cleaning up temp files..."
                $bolReturn = $true
                Remove-Item $strUNCInstallScript -force
                Remove-Item $strUNCMSIName -force
                if ((Test-Path $strUNCInstallScript) -or (Test-Path $strUNCMSIName)) {
                    Write-Host "[InstallMSI] ERROR: Error during the tidy up stage..."
                }
            }
            else {
                Write-Host "[InstallMSI] Susccess criteria couldn't be found in the MSI log file..."
                if ($bolExtraAnalysis) {
                    $oErrorsFile = Get-ChildItem -Path $outputFolder -Filter "*_Errors.txt"
                    write-host "[WiLogUtl] Importing" $oErrorsFile.FullName
                    $errors = Get-Content $oErrorsFile.FullName
                    foreach ($strLine in $errors) {
                        if ($strLine.Trim().Length -gt 0) {
                            Write-Host " " $strLine
                        }
                    }
                    $errors = $null
                }
            }
        }
        else {
            Write-Host "[InstallMSI] ERROR: Can't find log file to check..."
        }
    }
    return $bolReturn
}

function GACAdder([string] $strAction, [string] $strServer, [string] $strLibrary) {
    $strGACFunction = $null
    switch ($strAction) {
        "Install" { $strGACFunction = "GacInstall" }
        "Remove" { $strGACFunction = "GacRemove" }
    }
    $Script = @"
    Add-Type -AssemblyName "System.EnterpriseServices";
    [System.EnterpriseServices.Internal.Publish];
    `$publish = New-Object System.EnterpriseServices.Internal.Publish;
    `$publish.$strGACFunction('$strLibrary')
"@

    $ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($Script)
    $Result = Invoke-Command -Computer $strServer -ScriptBlock $ScriptBlock | Out-Host
    Write-Host $Result
}

function GetDbInfoByTypeForEnv([string] $strEnvironment, [string] $strType) {
    $securePassword = ConvertTo-SecureString $DorcApiAccessPassword -AsPlainText -Force
    $credentials = New-Object System.Management.Automation.PSCredential ($DorcApiAccessAccount, $securePassword)
    $uri=$RefDataApiUrl + 'RefDataEnvironments?env=' + $strEnvironment
    $EnvId=(Invoke-RestMethod -Uri $uri -method Get -Credential $credentials -ContentType 'application/json').EnvironmentId
    $uri=$RefDataApiUrl + 'RefDataEnvironmentsDetails/' + $EnvId
    $table=Invoke-RestMethod -Uri $uri -method Get -Credential $credentials -ContentType 'application/json'
    $table=$table.DbServers | where {$_.Type -eq $strType} | Select Name, ServerName
    $strResult="Invalid"
    if ($table.Name.count -eq 0) {
        Write-Host "No entries returned for $strEnvironment $strType"
    }
    elseif ($table.Name.Count -eq 1) {
            $strResult = $table.ServerName + ":" + $table.Name
    }
    else {
        throw "Too many entries for $strEnvironment $strType"
    }
    return $strResult
}

function GetLatestBackupForDb([string] $strInstance, [string] $strDatabase) {
    $strResult = "Invalid"
    $connection = New-Object System.Data.SqlClient.SqlConnection
    $connection.ConnectionString = "Server=$strInstance;Database=master;Integrated Security=true"
    $connection.Open()
    $command = $connection.CreateCommand()
    
    $command.CommandText = @"
    USE master
    SELECT TOP 1
        msdb.dbo.backupset.database_name,
        msdb.dbo.backupset.backup_start_date,
        msdb.dbo.backupset.backup_finish_date,
        msdb.dbo.backupset.expiration_date,
        CASE msdb.dbo.backupset.type
            WHEN 'D' THEN 'Database'
            WHEN 'I' THEN 'Differential'
            WHEN 'L' THEN 'Log'
        END AS backup_type,
        msdb.dbo.backupset.backup_size,
        msdb.dbo.backupmediafamily.logical_device_name,
        msdb.dbo.backupmediafamily.physical_device_name,
        msdb.dbo.backupset.name AS backupset_name,
        msdb.dbo.backupset.description,
        msdb.dbo.backupset.differential_base_guid
    FROM
        msdb.dbo.backupmediafamily
    INNER JOIN
        msdb.dbo.backupset
    ON
        msdb.dbo.backupmediafamily.media_set_id = msdb.dbo.backupset.media_set_id
    WHERE
        msdb.dbo.backupset.database_name = @DatabaseName
    AND
        (CONVERT(datetime, msdb.dbo.backupset.backup_start_date, 102) >= GETDATE() - 21)
    AND
        (msdb.dbo.backupset.type = 'D' OR msdb.dbo.backupset.type = 'I')
    ORDER BY
        msdb.dbo.backupset.database_name,
        msdb.dbo.backupset.backup_finish_date DESC
"@

    $command.Parameters.AddWithValue("@DatabaseName", $strDatabase)

    $table = new-object "System.Data.DataTable"
    $table.Load($command.ExecuteReader())
    $connection.Close()
    if ($table.Rows.Count -eq 0) {
        Write-Host "No entries returned for $strInstance $strDatabase"
    }
    else {
        foreach ($Row in $table.Rows) {
            $strResult = $Row.Item("physical_device_name")
        }
    }
    return $strResult
}

function SsasRemoveDatabase([string] $strInstance, [string] $strDatabase, [bool] $bolPartialNameMatch) {
    [Void][System.reflection.Assembly]::LoadWithPartialName("Microsoft.AnalysisServices")
    $svr = new-Object Microsoft.AnalysisServices.Server
    $svr.Connect($strInstance)
    $arrDbsToDrop = New-Object System.Collections.ArrayList($null) 
    foreach ($db in $svr.Databases) {    
        if ($bolPartialNameMatch -eq $false -and $db.Name -eq $strDatabase) {
            [Void]$arrDbsToDrop.add($db.Name)
        }
        if ($bolPartialNameMatch -eq $true -and $db.Name.Contains($strDatabase)) {
            [Void]$arrDbsToDrop.add($db.Name)
        }
    }
    $svr.Disconnect()
    foreach ($strDb in $arrDbsToDrop) {
        $svr.Connect($strInstance)
        $db = $svr.Databases[$strDb]
        Write-Host "Dropping:" $db.Name
        $db.drop()
        $svr.Disconnect()
    }
}

function SsasPermissionDatabase([string] $strInstance, [string] $strDatabase, [bool] $bolPartialNameMatch, [string] $strRole, [string] $strAccount) {
    [Void][System.reflection.Assembly]::LoadWithPartialName("Microsoft.AnalysisServices")
    $svr = new-Object Microsoft.AnalysisServices.Server
    [Microsoft.AnalysisServices.Role] $SSASRole = new-Object([Microsoft.AnalysisServices.Role])($strRole)
    $svr.Connect($strInstance)
    $arrDbsToPermission = New-Object System.Collections.ArrayList($null) 
    foreach ($db in $svr.Databases) {    
        if ($bolPartialNameMatch -eq $false -and $db.Name -eq $strDatabase) {
            [Void]$arrDbsToPermission.add($db.Name)
        }
        if ($bolPartialNameMatch -eq $true -and $db.Name.Contains($strDatabase)) {
            [Void]$arrDbsToPermission.add($db.Name)
        }
    }
    $svr.Disconnect()
    foreach ($strDb in $arrDbsToPermission) {
        $svr.Connect($strInstance)
        $db = $svr.Databases[$strDb]
        Write-Host "Setting roles on:" $strInstance":"$db.Name
        foreach ($DatabaseRole in $db.Roles) {
            if ($DatabaseRole.Name -eq $strRole) {
                Write-Host "Setting up role:" $DatabaseRole.Name
                Write-Host " Adding:" $strAccount
                $newRole = New-Object Microsoft.AnalysisServices.RoleMember($strAccount)
                if ($DatabaseRole.Members.Name) {
                    if ($DatabaseRole.Members.Name.Contains($strAccount.ToUpper())) {
                        Write-Host "Account" $strAccount "already exists in role Process"
                    }                
                    else {
                        $DatabaseRole.Members.Add($newRole)
                        $DatabaseRole.Update()
                        $dbperm = $db.DatabasePermissions.FindByRole($DatabaseRole.ID)
                        $dbperm.ReadDefinition = [Microsoft.AnalysisServices.ReadDefinitionAccess]::Allowed
                        $dbperm.Update()
                        $db.Update()
                    }
                }
                else {
                    $DatabaseRole.Members.Add($newRole)
                    $DatabaseRole.Update()
                    $dbperm = $db.DatabasePermissions.FindByRole($DatabaseRole.ID)
                    $dbperm.ReadDefinition = [Microsoft.AnalysisServices.ReadDefinitionAccess]::Allowed
                    $dbperm.Update()
                    $db.Update()
                }
            }
        }
        $svr.Disconnect()
    }
}

function GetDBStatus([string] $strInstance, [string] $strDatabase) {    
    [Void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO')
    $serverInstance = New-Object ('Microsoft.SqlServer.Management.Smo.Server') $strInstance
    try {
        $strStatus = $serverInstance.Databases[$strDatabase].Status
    }
    Catch { [system.exception] }
    return $strStatus
}

function Get-TSSessions([string] $strComputerName) {
    qwinsta /server:$strComputerName |
        #Parse output
    ForEach-Object {
        $_.Trim() -replace "\s+", ","
    } |
        #Convert to objects
    ConvertFrom-Csv
}

function LogOffUsers([string] $strComputerName) {
    $Sessions = Get-TSSessions $strComputerName
    foreach ($Session in $Sessions|where {$_.SESSIONNAME.ToString().StartsWith("rdp-tcp") -and ($_.STATE -eq "Active")}) {
        Write-Host "Logging off user:" $Session.username
        rwinsta $Session.ID /server:$strComputerName
    }
}

function DeleteRabbit([string]$mode, [string[]]$deleteStrings, [string]$RabbitUserName, [string]$RabbitPassword, [string]$RabbitAPIPort, [string] $Server, [bool]$discardMessages = $false) {
    $exitCode = 0

    trap {
        $e = $error[0].Exception
        $e.Message
        $e.StackTrace
    }
    $protocol = "http"
    $UnEscapeDotsAndSlashes = 0x2000000
     
    # GetSyntax method, which is static internal, gets registered parsers for given protocol
    $getSyntax = [System.UriParser].GetMethod("GetSyntax", 40)
    # field m_Flags contains information about Uri parsing behaviour
    $flags = [System.UriParser].GetField("m_Flags", 36)
     
    $parser = $getSyntax.Invoke($null, $protocol)
    $currentValue = $flags.GetValue($parser)
    # check if un-escaping enabled
    if (($currentValue -band $UnEscapeDotsAndSlashes) -eq $UnEscapeDotsAndSlashes) {
        $newValue = $currentValue -bxor $UnEscapeDotsAndSlashes
        # disable unescaping by removing UnEscapeDotsAndSlashes flag
        $flags.SetValue($parser, $newValue)
    }

    $secpasswd = ConvertTo-SecureString $RabbitPassword -AsPlainText -Force
    $Credentials = New-Object System.Management.Automation.PSCredential ($RabbitUserName, $secpasswd)

    Add-Type -AssemblyName System.Web   

    foreach ($deleteString in $deleteStrings) {
        if ($mode -eq 'exchange') {
            $url = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/exchanges/%2f/"

            $exchanges = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Get
            $deleteExchanges = @()

            foreach ($exchange in $exchanges | where {-not ($_.name.Contains('amq.'))}) {    
                if ($deleteString.Length -gt 0) {
                    if ($exchange.name.Contains($deleteString)) {
                        $deleteExchanges += $exchange
                    }
                }
                else {
                    if ($exchange.name.Length -gt 0) {
                        $deleteExchanges += $exchange
                    }
                }
            }

            foreach ($deleteExchange in $deleteExchanges) {
                Write-Host "Deleting exchange: " $deleteExchange.name
                $url = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/exchanges/%2f/$([System.Web.HttpUtility]::UrlEncode($deleteExchange.name))"
                Write-Host "Invoking Rest api URL: " $url
                $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Delete -ContentType "application/json" -Body $bodyJson
                Write-Host "Deleted exchange: " $deleteExchange.name
            }
        }
        elseif ($mode = 'queue') {
            $url = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/queues/%2f/"

            $queues = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Get
            $deleteQueues = @()
            
            foreach ($queue in $queues) {
                if ($deleteString.Length -gt 0) {
                    if ($queue.name.Contains($deleteString)) {
                        $deleteQueues += $queue
                    }
                }
                else {
                    $deleteQueues += $queue
                }
            }
            
            foreach ($deleteQueue in $deleteQueues) {
                Write-Host "Deleting queue: " $deleteQueue.name
                
                $url = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/queues/%2f/$([System.Web.HttpUtility]::UrlEncode($deleteQueue.name))"
                Write-Host "Invoking Rest api URL: " $url
                
                $queueDetails = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Get
                
                if (($queueDetails.messages -gt 0) -and $discardMessages) {
                    Write-Host "Warning: Messages found on " $deleteQueue.name " but discard messages set, messages will be deleted"
                    $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Delete -ContentType "application/json" -Body $bodyJson
                    Write-Host "Deleted queue: " $deleteQueue.name        
                }
                elseif (($queueDetails.messages -gt 0) -and -not($discardMessages)) {
                    Write-Host "Warning: cannot delete " $deleteQueue.name " as it has messages on it"
                }
                else {
                    $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Delete -ContentType "application/json" -Body $bodyJson    
                    Write-Host "Deleted queue: " $deleteQueue.name        
                }
            }
        }
    }
}

function ConfigureRabbit([string] $XMLConfigFile, [string] $RabbitUserName, [string] $RabbitPassword, [string] $RabbitAPIPort, [string] $Server, [bool] $discardMessages) {
    $exitCode = 0
    trap {
        $e = $error[0].Exception
        $e.Message
        $e.StackTrace
    }
    $protocol = "http"
    $UnEscapeDotsAndSlashes = 0x2000000

    # GetSyntax method, which is static internal, gets registered parsers for given protocol
    $getSyntax = [System.UriParser].GetMethod("GetSyntax", 40)
    # field m_Flags contains information about Uri parsing behaviour
    $flags = [System.UriParser].GetField("m_Flags", 36)
  
    $parser = $getSyntax.Invoke($null, $protocol)
    $currentValue = $flags.GetValue($parser)
    
    # check if un-escaping enabled
    if (($currentValue -band $UnEscapeDotsAndSlashes) -eq $UnEscapeDotsAndSlashes) {
        $newValue = $currentValue -bxor $UnEscapeDotsAndSlashes
        # disable unescaping by removing UnEscapeDotsAndSlashes flag
        $flags.SetValue($parser, $newValue)
    }

    $secpasswd = ConvertTo-SecureString $RabbitPassword -AsPlainText -Force
    $Credentials = New-Object System.Management.Automation.PSCredential ($RabbitUserName, $secpasswd)
    
    [xml]$config = Get-Content $XMLConfigFile
    $excahnges = $config.FirstChild.Exchanges
    $queues = $config.FirstChild.Queues
    $bindings = $config.FirstChild.Bindings

    Add-Type -AssemblyName System.Web   

    # Exchange section
    foreach ($exchange in $excahnges.ChildNodes) {
        $name = $exchange.name
        
        $body = @{
            type = $exchange.type
        }

        if ($exchange.durable -eq "true") { $body.Add("durable", $true) }      
        if ($exchange.auto_delete -eq "true") { $body.Add("auto_delete", $true) }
        if ($exchange.internal -eq "true") { $body.Add("internal", $true) }    
        
        $bodyJson = $body | ConvertTo-Json
        
        # Check for existing exchanges
        $exchangeCheckUrl = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/exchanges/%2f/"
        $existingExchanges = Invoke-RestMethod $exchangeCheckUrl -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Get
        
        $url = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/exchanges/%2f/$([System.Web.HttpUtility]::UrlEncode($name))"
        
        Write-Host "Invoking REST API: $url"
        
        # Delete existing exchange if exists
        foreach ($existingExchange in $existingExchanges | where {$_.name -eq $exchange.name}) {
            Write-Host "Warning: Exchange " $exchange.name " already exists. Deleting"
            $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Delete -ContentType "application/json" -Body $bodyJson
        }

        # Create or update exchange
        $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Put -ContentType "application/json" -Body $bodyJson

        Write-Host 'Created exchange: ' $exchange.name
    }

    # Queues section
    foreach ($queue in $queues.ChildNodes) {
        $name = $queue.name
        
        $body = @{}
        $arguments = @{}
        
        [Long]$ttl = 0
        $ttl = $queue.message_ttl

        if ($queue.durable -eq "true") { $body.Add("durable", $true) }      
        if ($queue.auto_delete -eq "true") { $body.Add("auto_delete", $true) }
        if ($ttl -gt 0) { 
            $arguments.Add("x-message-ttl", $ttl)
            $body.Add("arguments", $arguments)
        }
        
        $bodyJson = $body | ConvertTo-Json -Compress
        
        # URL for queue
        $url = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/queues/%2f/$([System.Web.HttpUtility]::UrlEncode($name))"
        
        Write-Host "Invoking REST API: $url"
        
        # Check for existing queues
        $queueCheckUrl = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/queues/%2f/"
        $existingQueues = Invoke-RestMethod $queueCheckUrl -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Get

        $queueExists = $existingQueues | Where-Object { $_.name -eq $queue.name }
        
        if ($queueExists) {
            $queueDetails = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Get
            
            # Check if queue has messages
            if (($queueDetails.messages -gt 0) -and $discardMessages) {
                Write-Host "Warning: Messages found on $name, but discard messages set. Messages will be deleted."
                $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Delete -ContentType "application/json" -Body $bodyJson
                Write-Host "Deleted queue: $name"
            }
            elseif (($queueDetails.messages -gt 0) -and -not($discardMessages)) {
                Write-Host "Warning: cannot delete $name as it has messages."
                Write-Host "Warning: Will not attempt to create queue $name"
                continue
            }
            else {
                $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Delete -ContentType "application/json" -Body $bodyJson    
                Write-Host "Deleted queue: $name"
            }
        }

        # Create or update queue
        $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Put -ContentType "application/json" -Body $bodyJson
        
        Write-Host 'Created queue:' $name
    }

    # Bindings section
    foreach ($binding in $bindings.ChildNodes) {
        $name = $binding.destination
        $exchangeName = $binding.source
        $url = "http://$([System.Web.HttpUtility]::UrlEncode($Server)):$([System.Web.HttpUtility]::UrlEncode($RabbitAPIPort))/api/bindings/%2f/e/$([System.Web.HttpUtility]::UrlEncode($exchangeName))/q/$([System.Web.HttpUtility]::UrlEncode($name))"
        
        $body = @{
            "routing_key" = $binding.routingKey
        }

        $bodyJson = $body | ConvertTo-Json -Compress

        Write-Host "Invoking REST API: $url"
        
        $result = Invoke-RestMethod $url -Credential $Credentials -DisableKeepAlive -ErrorAction Continue -Method Post -ContentType "application/json" -Body $bodyJson
        
        Write-Host 'Created binding: ' $binding.source --- $binding.destination
    }
}

function Stop-Services {
    [CmdLetBinding()]
    param (
        $arrServiceList, 
        [string] $strComputer, 
        [int] $retryCount = 10,
        [int] $retryTime = 10
    )
    foreach ($strService in $arrServiceList) { 
        $strRemServiceName = $null
        $strRemServiceStatus = $null
        $strServiceNameGen = $strService + "*"
        $oService = Get-Service $strServiceNameGen -computer $strComputer | select -First 1 #ensure only 1 service processed at a time
        $strRemServiceName = $oService.Name
        $strRemServiceStatus = $oService.Status
        $oService = $null
        
        if ($strRemServiceName -eq $strService) {
            if ($strRemServiceStatus -eq "Running") {
                Write-Host " Stopping" $strService "on" $strComputer
                for ($i = 0; $i -le $retryCount; $i++) {
                    write-host " Attempt to stop number:" ($i + 1)
                    Invoke-Command -ComputerName $strComputer { param($strService) Stop-Service $strService -Force} -Args $strService
                    Start-Sleep $retryTime
                    $oService = Get-Service $strServiceNameGen -computer $strComputer
                    $strRemServiceStatus = $oService.Status
                    if ($strRemServiceStatus -eq "Stopped") {break}
                    if ($i -eq $retryCount) {
                        $ServicePID = $null
                        $ServicePID = (get-wmiobject win32_service -computername $strComputer | where { $_.name -eq $strService}).processID
                        write-host " Killing PID:" $ServicePID
                        Invoke-Command -ComputerName $strComputer {param($ServicePID) Stop-Process $ServicePID -Force} -Args $ServicePID -ErrorAction SilentlyContinue
                    }
                }
                $oService = Get-Service $strServiceNameGen -computer $strComputer
                $strRemServiceStatus = $oService.Status
                if ($strRemServiceStatus -ne "Stopped") {throw " " + $strRemServiceName + "is still" + $oService.Status}
                else {write-host " "$strRemServiceName "has been stopped"}
            }
            else {
                Write-Host " "$strService "on" $strComputer "is" $strRemServiceStatus", nothing to do..."
            }
        }
        else {
            Write-Host " WARNING:" $strService "is not installed on" $strComputer
        }
    }
}

function StartServices($arrServiceList, [string] $strComputer) {
    foreach ($strService in $arrServiceList) {
        $strRemServiceName = $null
        $strRemServiceStatus = $null
        $strServiceNameGen = $strService + "*"
        $oService = Get-Service $strServiceNameGen -computer $strComputer
        $strRemServiceName = $oService.Name
        $strRemServiceStatus = $oService.Status
        $oService = $null
        if ($strRemServiceName -eq $strService) {
            if ($strRemServiceStatus -eq "Stopped") {
                Write-Host " Starting" $strService "on" $strComputer
                Get-WmiObject -Class win32_service -Filter "Name = '$($strService)'"-ComputerName $strComputer -EnableAllPrivileges | Invoke-WmiMethod -Name StartService -ErrorAction Stop
                start-sleep -Seconds 5
                For ($i=0; $i -le 10; $i++) {
                    $oService = Get-Service $strServiceNameGen -computer $strComputer
                    $strRemServiceStatus = $oService.Status
                    If ($strRemServiceStatus -eq "Running") {write-host "Running";break}
                    Write-Host " wait 10 seconds"
                    start-sleep -Seconds 10
                    if ($i -eq 9) {throw " $strService can't be Started"}
                }
                write-host " $strService is Started"
            }
            else {
                Write-Host " "$strService "on" $strComputer "is" $strRemServiceStatus", won't attempt to start..."
            }
        }
        else {
            throw " ERROR: $strService is not installed on $strComputer"
        }
    }
}

function RunMTMTests($arrParameters) {
    $bolReturn = $false
    if ($arrParameters.Count -gt 0) {
        foreach ($strParameter in $arrParameters) {
            Write-Host "Setting" $strParameter.Split("=")[0] "to:" $strParameter.Split("=")[1]
            Set-Variable -Name  $strParameter.Split("=")[0] -Value $strParameter.Split("=")[1]
        }
        ## Sync
        $UpdateParams = " testcase /import /collection:" + $TFSInstance + " /teamproject:" + $TFSTeamProject + " /storage:" + $TestDll + " /syncsuite:" + $TestSuite
        $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
        $ProcessInfo.FileName = $TCM2013ToolPath
        $ProcessInfo.RedirectStandardError = $true
        $ProcessInfo.RedirectStandardOutput = $true
        $ProcessInfo.UseShellExecute = $false
        $ProcessInfo.Arguments = $UpdateParams
        $Process = New-Object System.Diagnostics.Process
        $Process.StartInfo = $ProcessInfo
        $Process.Start()
        $stdout = $Process.StandardOutput.ReadToEnd()
        $stderr = $Process.StandardError.ReadToEnd()
        Write-Host "stdout: $stdout"
        Write-Host "stderr: $stderr"
        Write-Host "exit code: " $Process.ExitCode
        $syncErrorCode = $Process.ExitCode
        $ProcessInfo = $null
        $Process = $null
        $stdout = $null
        $stderr = $null
        
        if ($syncErrorCode -eq 0) {
            ## Invoke
            $RunParams = " run /create /title:" + $TestEnvironment + ":" + $BuildFolder + " /planid:" + $AcceptanceTestPlanID + " /suiteid:" + $TestSuite + " /configid:" + $TestConfigId + " /settingsname:" + $TestSettingsName + " /testenvironment:" + $TestEnvironment + " /collection:" + $TFSInstance + " /teamproject:" + $TFSTeamProject + " /builddir:" + $TestTempFolder + " /include"
            $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
            $ProcessInfo.FileName = $TCM2013ToolPath
            $ProcessInfo.RedirectStandardError = $true
            $ProcessInfo.RedirectStandardOutput = $true
            $ProcessInfo.UseShellExecute = $false
            $ProcessInfo.Arguments = $RunParams
            $Process = New-Object System.Diagnostics.Process
            $Process.StartInfo = $ProcessInfo
            $Process.Start()
            $stdout = $Process.StandardOutput.ReadToEnd()
            $stderr = $Process.StandardError.ReadToEnd()
            Write-Host "stdout: $stdout"
            Write-Host "stderr: $stderr"
            Write-Host "exit code: " $Process.ExitCode
            $ProcessInfo = $null
            $Process = $null
            $stdout = $null
            $stderr = $null    

            ## Check
            ## TODO
        }
        else {
            write-host "Error during the sync stage..."
        }
    }
    else {
        throw "Cannot continue, parameters zero parameters passed to RunMTMTests..."
    }
    return $bolReturn
}

function Start-Services {
    [CmdLetBinding()]
    param (
        $arrServiceList, 
        [string] $strComputer, 
        [int] $retryCount = 10,
        [int] $retryTime = 10
    )
    foreach ($strService in $arrServiceList) { 
        $oService = $null
        $oService = Get-Service $strService -computer $strComputer -ErrorAction SilentlyContinue
        if ($oService) {
            if ($oService.Status -eq "Running") {Write-Host " "$strService "on" $strComputer "is" $oService.Status", nothing to do..."}
            else {
                Write-Host " Starting" $strService "on" $strComputer
                for ($i = 0; $i -le $retryCount; $i++) {
                     Write-host " Attempt to start number:" ($i + 1)
                     Invoke-Command -ComputerName $strComputer { param($strService) Start-Service $strService -ErrorAction SilentlyContinue} -Args $strService          
                     Start-Sleep $retryTime
                     $oService = Get-Service $strService -computer $strComputer
                     if ($oService.Status -eq "Running") {break}            
                }
                $oService = Get-Service $strService -computer $strComputer
                if ($oService.Status -ne "Running") {throw " " + $strService + "is still " + $oService.Status}
                else {Write-host " "$strService "has been started"}
            }            
         }
         else {Write-Host " WARNING:" $strService "is not installed on" $strComputer}
    }
}

function IsMSIx64([string] $strWiXToolsDir, [string] $strMSIFullName) {
    $bolIsMSIx64 = $false
    $strDarkEXE = $strWiXToolsDir + "\dark.exe"
    if (Test-Path $strDarkEXE) {
        Write-Host " Using: " $strDarkEXE
        if (Test-Path $strMSIFullName) {
            $strQuotedMSIFullName = [char]34 + $strMSIFullName + [char]34
            Write-Host " Checking:" $strQuotedMSIFullName
            $oTEMP = Get-Item env:TEMP
            $strTempFile = $oTEMP.Value + "\" + [System.IO.Path]::GetRandomFileName().Replace(".", "") + ".xml"
            #Write-Host "Tempfile:" $strTempFile
            . $strDarkEXE -nologo $strQuotedMSIFullName $strTempFile
            [xml]$xTemp = Get-Content $strTempFile
            if ($xTemp.Wix.Product.Package.Platform -contains "x64") {
                $bolIsMSIx64 = $true
            }
            $xTemp = $null
            Remove-Item $strTempFile -force
            $strTempFile = $null
            $strQuotedMSIFullName = $null
        }
        else {
            Write-Host $strMSIFullName "not found..."
            throw " "
        }
    }
    else {
        Write-Host $strDarkEXE "not found..."
        throw " "
    }
    return $bolIsMSIx64
}

function GetRemPSCompName([string] $strServerName) {
    $Result = ""
    $Result = icm -ComputerName $strServerName {Get-Content env:computername} -erroraction SilentlyContinue
    return $Result
}

function Restart-Servers {
    [CmdLetBinding()]
    param (
        $TargetServers
    )
    foreach    ($TargetServer in $TargetServers) {
        $LastBootUpTime = $null
        try {
            $wmi = Get-WmiObject -Class Win32_OperatingSystem -Computer $TargetServer -ErrorAction Stop
            $LastBootUpTime = $wmi.ConvertToDateTime($wmi.LastBootUpTime)
        }
        catch {
            Write-Error $_ | out-string
        }
        if ([String]::IsNullOrEmpty($LastBootUpTime)){
            Write-Host "Unable to retrieve LastBootUpTime for:" $TargetServer "won't reboot this server..."
            continue
        }
        
        Write-Host "Restarting: $TargetServer"
        try {
            #reboot computer using WMI reboot method is more reliable than Restart-Computer
            Write-host "Rebooting $TargetServer using WMI"
            Get-WmiObject Win32_OperatingSystem -ComputerName $TargetServer -EnableAllPrivileges -ErrorAction Stop | Invoke-WmiMethod -Name reboot -ErrorAction Stop | Out-Null
        }
        catch {
            #last resort - use shutdown.exe
            Write-Host "Reboot via WMI reboot method failed. Tying Shutdown.exe..."
            Invoke-Command -ScriptBlock {
                shutdown.exe /r /f /m \\$TargetServer /d p:4:2 /t 0 /c "Reboot initiated by DOrc" 2>$null
                if ($LastExitCode -ne 0) {
                    Write-Error "Shutdown.exe failed: ExitCode [$LastExitCode]"
                }
            }
        }
    }
    
    #Verify PS Remoting works - separate foreach loop to save time
    foreach    ($TargetServer in $TargetServers) {
        $attempt = 1
        do {
            $WSMan = $null
            #sleep for 30 secs to allow some time to shutdown
            Start-Sleep 30
            
            #Test WSMan connection
            $WSMan = Test-WSMan -ComputerName $TargetServer -Authentication Kerberos -ErrorAction SilentlyContinue
            
            #Check uptime
            $10min = New-TimeSpan -Minutes 10
            try {
                $wmi = Get-WmiObject -Class Win32_OperatingSystem -Computer $TargetServer -ErrorAction Stop
                $LastBootUpTime = $wmi.ConvertToDateTime($wmi.LastBootUpTime)
            }
            catch {
                Write-verbose "WMI not yet available on server [$TargetServer]"
                $LastBootUpTime = Get-Date 01/01/1900 #set way in past to fail the condition inside "until"
            }
                
            #increment attempt up to 50
            $attempt ++
        }
        #Fail the loop if either timeout is reached or both WSMAN listener is up and Uptime is within the last 10 minutes
        until (($attempt -eq 50) -or (($WSMan.wsmid -match "http://") -and (($(get-date) - $LastBootUpTime) -lt $10min)))
        #fail after 50 attempts (50*30sec = 25min)
        if ($attempt -eq 50) {    
            Throw "Server $TargetServer failed to come up in a timely manner."
        }
        else {
            Write-Host "$TargetServer successfully rebooted at $LastBootUpTime"
        }
    }
}

function GetServersOfType([string] $strEnvironment, [string] $strType = "") {    
    $securePassword = ConvertTo-SecureString $DorcApiAccessPassword -AsPlainText -Force
    $credentials = New-Object System.Management.Automation.PSCredential ($DorcApiAccessAccount, $securePassword)
    $uri=$RefDataApiUrl + 'RefDataEnvironments?env=' + $strEnvironment    
    $EnvInfo = Invoke-RestMethod -Uri $uri -method Get -Credential $credentials -ContentType 'application/json'
    $EnvId = $EnvInfo.EnvironmentId
    $uri = $RefDataApiUrl + 'RefDataEnvironmentsDetails/' + $EnvId    
    $EnvDetailsInfo = Invoke-RestMethod -Uri $uri -method Get -Credential $credentials -ContentType 'application/json'
    $Servers=$EnvDetailsInfo.AppServers.Name
    $table=new-object "System.Data.DataTable"
    $ColumnNames='Env_ID','Env_Name','Owner','Thin_Client_Server','Restored_From_Backup','Last_Update','File_Share','Env_Note','Description','Build_ID','Locked','Env_ID1','Server_ID','Server_ID1','Server_Name','OS_Version','Application_Server_Name'
    foreach ($ColumnName in $ColumnNames) {
        $Col = New-Object system.Data.DataColumn $ColumnName, ([string])
        $table.columns.add($col)
        }
    Foreach ($server in $Servers) {
        $Row = $table.NewRow()
        $Row.Env_ID = $EnvInfo.EnvironmentId
        $Row.Env_Name = $EnvInfo.EnvironmentName
        $Row.Owner = $EnvInfo.Details.EnvironmentOwner
        $Row.Thin_Client_Server = $EnvInfo.Details.ThinClient
        $Row.Restored_From_Backup = $EnvInfo.Details.RestoredFromSourceDb
        $Row.Last_Update = $EnvInfo.Details.LastUpdated
        $Row.File_Share = $EnvInfo.Details.FileShare
        $Row.Env_Note = $EnvInfo.Details.Notes
        $Row.Description = $EnvInfo.Details.Description
        $Row.Build_ID = $NULL
        $Row.Locked = $NULL
        $Row.Env_ID1 = $EnvId
        $Row.Server_ID = ($EnvDetailsInfo.AppServers | Where {$_.Name -eq $Server}).ServerId
        $Row.Server_ID1 = ($EnvDetailsInfo.AppServers | Where {$_.Name -eq $Server}).ServerId
        $Row.Server_Name = $server
        $Row.OS_Version = ($EnvDetailsInfo.AppServers | Where {$_.Name -eq $Server}).OsName
        $Row.Application_Server_Name = ($EnvDetailsInfo.AppServers | Where {$_.Name -eq $Server}).ApplicationTags
        If ($strType -and ($Row.Application_Server_Name.Split(";") -notcontains $strType) -and ($strType -ne ';') -and ($strType -ne '%')) {}
        Else {
            $table.Rows.Add($Row) 
             } 
        }
    return @(, $table)
}

function GetServersOfType_V2 ([string] $strType = "") {
    $Servers=$AllServers    
    $AllServerVariables= Get-Variable -Name ServerNames_*
    $table=new-object "System.Data.DataTable"
    $ColumnNames='Server_Name','Application_Server_Name'
    foreach ($ColumnName in $ColumnNames) {
    $Col = New-Object system.Data.DataColumn $ColumnName, ([string])
    $table.columns.add($col)
    }
    foreach ($Server in $Servers) {
        $ServerTags = $NULL
        Foreach ($ServerVariable in $AllServerVariables) {                        
            If ($ServerVariable.Value -like $Server) {                
                $ServerTags+=$ServerVariable.name.Replace('ServerNames_','')+';'
            }
        }
        If ($ServerTags) {$ServerTags=$ServerTags.Substring(0, $ServerTags.Length - 1)}
        $Row = $table.NewRow()
        $Row.Server_Name = $server
        $Row.Application_Server_Name = $ServerTags
        If ($strType) {$strTypeNoSpaces=$strType.Replace(' ','_')}
        If ($ServerTags) {} Else {$ServerTags=';'}
        If ($strType -and ($ServerTags.Split(";") -notcontains $strTypeNoSpaces) -and ($strType -ne ';') -and ($strType -ne '%')) {}
        Else {
            $table.Rows.Add($Row) 
        }       
    }
    return @(, $table)
}

function BuildServersOfType([string] $strEnvironment, [string] $strServers, [string] $strType) {
    $ServerInfoCheck = new-object "System.Data.DataTable"
    $ServerInfoCheck = GetServersOfType $EnvironmentName ""
    $ServerInfoResult = new-object "System.Data.DataTable"
    $Col1 = New-Object system.Data.DataColumn Server_Name, ([string])
    $Col2 = New-Object system.Data.DataColumn Application_Server_Name, ([string])
    $ServerInfoResult.columns.add($col1)
    $ServerInfoResult.columns.add($col2)
    foreach ($server in $strServers.Split("`;")) {
        $bolServerBelongsToEnv = $false        
        foreach ($Row in $ServerInfoCheck) {
            if ($server.ToLower() -eq $Row.Server_Name.Trim().ToLower()) {
                $bolServerBelongsToEnv = $true
            }
        }
        if ($bolServerBelongsToEnv) {
            $Row = $ServerInfoResult.NewRow()
            $Row.Server_Name = $server
            $Row.Application_Server_Name = $strType
            $ServerInfoResult.Rows.Add($Row)
        }
        else {
            Write-Host $Server "does not exist in" $strEnvironment
            throw ""
        }
    }
    return @(, $ServerInfoResult)
}

function DeployDACPAC([string] $sqlPackagePath, [string] $strInstance, [string] $strDatabase, [string] $strDACPAC, [string] $strPublishProfile, [array] $arrVariables, [string] $strBlackList, [string] $strRollbackMode = "None",[string] $EnvironmentPostScript="") {
    Import-Module SqlServer -DisableNameChecking
    #TODO If database doesn't exist skip check for alter
    #TODO Database.WhiteList.xml in DevTools
    
    Write-Host "Variables: " $arrVariables.Count
    $strVariables = ""
    if ($arrVariables.Count -gt 0) {
        foreach ($strVariable in $arrVariables) {
            Write-Host " " $strVariable
            $strVariables = $strVariables + " /v:" + $strVariable
        }
    }
    Write-Host ""

    if (Test-Path $strDACPAC) {
        $strEnvironmentPostSQLFile = $strDACPAC.Replace(".dacpac", ".$EnvironmentPostScript.sql")
        $strApplyDataSQLFile = $strDACPAC.Replace(".dacpac", ".ApplyDataChanges.sql")
        $strRollbackDataSQLFile = $strDACPAC.Replace(".dacpac", ".RollbackDataChanges.sql")
        $strPreSQLFile = $strDACPAC.Replace(".dacpac", ".PreSQL.sql")
        $strPostSQLFile = $strDACPAC.Replace(".dacpac", ".PostSQL.sql")
        $bolBlackStatements = $false
        
        #If Running as Data Rollback then apply script then return
        if ($strRollbackMode -eq "Data") {
            # If Present check and process PreSQL
            if (Test-Path $strRollbackDataSQLFile) {
                Write-Host "Checking: " $strRollbackDataSQLFile
                $preResult = CheckAndApplySQL $strInstance $strDatabase $strRollbackDataSQLFile $strBlackList
                if (!$preResult) {
                    throw "Error applying Rollback sql"
                }
            } 
            else {
                Write-Host "No Rollback SQL: " $strRollbackDataSQLFile
            }
            Write-Host ""
            return;
        }

        if ($strRollbackMode -eq "None" -Or $strRollbackMode -eq "Schema") {
            # If Present check and process PreSQL
            if (Test-Path $strPreSQLFile) {
                Write-Host "Checking: " $strPreSQLFile
                $preResult = CheckAndApplySQL $strInstance $strDatabase $strPreSQLFile $strBlackList
                if (!$preResult) {
                    throw "Error applying pre-sql"
                }
            } 
            else {
                Write-Host "No PreSQL: " $strPreSQLFile
            }
        
            # Script & Apply DACPAC
            $resScript = ActionDACPAC "Script" $sqlPackagePath $strInstance $strDatabase $strDACPAC $strPublishProfile $strVariables $strBlackList
            if ($resScript) {
                $resDeployReport = ActionDACPAC "DeployReport" $sqlPackagePath $strInstance $strDatabase $strDACPAC $strPublishProfile $strVariables $strBlackList
                if ($resDeployReport) {
                    $resPublish = ActionDACPAC "Publish" $sqlPackagePath $strInstance $strDatabase $strDACPAC $strPublishProfile $strVariables $strBlackList
                    if ($resPublish) {
                        Write-Host ""
                        Write-Host " *** Sucessfully Deployed the DACPAC ***"
                        Write-Host ""
                    }
                    else {
                        throw "Aborting..."
                    }
                }
                else {
                    throw "Aborting..."
                }
            }
            else {
                throw "Aborting..."
            }
         
            # If Present check and process PostSQL
            Write-Host ""
            if (Test-Path $strPostSQLFile) {
                Write-Host "Checking: " $strPostSQLFile
                $preResult = CheckAndApplySQL $strInstance $strDatabase $strPostSQLFile $strBlackList
                if (!$preResult) {
                    throw "Error applying post-sql"
                }
            }
            else {
                Write-Host "No PostSQL: " $strPostSQLFile
            }

            # If Running as Rollback Mode None then apply Data script
            if ($strRollbackMode -eq "None") {
                # If Present check and process ApplyDataChangeSql
                if (Test-Path $strApplyDataSQLFile) {
                    Write-Host "Checking: " $strApplyDataSQLFile
                    $preResult = CheckAndApplySQL $strInstance $strDatabase $strApplyDataSQLFile $strBlackList
                    if (!$preResult) {
                        throw "Error applying Data Changes-sql"
                    }
                } 
                else {
                    Write-Host "No Apply Data Changes: " $strApplyDataSQLFile
                }
                Write-Host ""

              # If Present check and process ApplyDataChangeSql
                     if (Test-Path $strEnvironmentPostSQLFile)
                     {
                         Write-Host "Checking: " $strEnvironmentPostSQLFile
                         $preResult = CheckAndApplySQL $strInstance $strDatabase $strEnvironmentPostSQLFile $strBlackList
                         if (!$preResult)
                         {
                             throw "Error applying $strEnvironmentPostSQLFile-sql"
                         }
                     } 
                     else
                     {
                         Write-Host "No Environment Specific SQL Changes: " $strEnvironmentPostSQLFile
                     }
                     Write-Host ""
            }
        }
    }
    else {
        Write-Host $strDACPAC "not found!"
        throw ""
    }
}

function ActionDACPAC([string] $strAction, [string] $sqlPackagePath, [string] $strInstance, [string] $strDatabase, [string] $strDACPAC, [string] $strPublishProfile, [string] $strVariables, [string] $strBlackList) {
    $ErrorActionPreference = 'stop'
    $bolReturn = $false
    $intProcExit = 0
    $dtNow = Get-Date
    $dateReverse = $dtNow.Year.ToString() + "-" + $dtNow.Month.ToString().PadLeft(2, '0') + "-" + $dtNow.Day.ToString().PadLeft(2, '0') + "_" + $dtNow.TimeOfDay.Hours.ToString().PadLeft(2, '0') + "-" + $dtNow.TimeOfDay.Minutes.ToString().PadLeft(2, '0')
    switch ($strAction) {
        "Script" {
            $strScriptFile = $DACPACSQLScripts + $dateReverse + "_" + $strInstance.Replace('\', '_') + "." + $strDatabase + ".sql"
            $strArguments = "/action:Script /TargetTrustServerCertificate:True /SourceFile:" + [char]34 + $strDACPAC + [char]34 + " /TargetDatabaseName:" + [char]34 + $strDatabase + [char]34 + " /TargetServerName:" + [char]34 + $strInstance + [char]34 + " /Profile:" + [char]34 + $strPublishProfile + [char]34 + " /OutputPath:" + [char]34 + $strScriptFile + [char]34 + $strVariables
            $output = $strScriptFile
        }
        "DeployReport" {
            $strReportFile = $DACPACSQLScripts + $dateReverse + "_" + $strInstance.Replace('\', '_') + "." + $strDatabase + ".xml"
            $strArguments = "/action:DeployReport /TargetTrustServerCertificate:True /SourceFile:" + [char]34 + $strDACPAC + [char]34 + " /TargetDatabaseName:" + [char]34 + $strDatabase + [char]34 + " /TargetServerName:" + [char]34 + $strInstance + [char]34 + " /Profile:" + [char]34 + $strPublishProfile + [char]34 + " /OutputPath:" + [char]34 + $strReportFile + [char]34 + $strVariables
            $output = $strReportFile
        }
        "Publish" {
            $strArguments = "/action:Publish /TargetTrustServerCertificate:True /SourceFile:" + [char]34 + $strDACPAC + [char]34 + " /TargetDatabaseName:" + [char]34 + $strDatabase + [char]34 + " /TargetServerName:" + [char]34 + $strInstance + [char]34 + " /Profile:" + [char]34 + $strPublishProfile + [char]34 + $strVariables
            $output = "Captured"
        }
        default {
            Write-Host "Invalid action passed..."
            throw ""
        }
    }
    Write-Host ""
    Write-Host "Action: " $strAction "****"
    Write-Host "DACPAC: " $strDACPAC
    Write-Host "Against: " $strInstance"."$strDatabase
    Write-Host "Publish Profile: " $strPublishProfile
    Write-Host "To: " $output
    Write-Host ""
    
    $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
    $ProcessInfo.FileName = $sqlPackagePath
    $ProcessInfo.RedirectStandardError = $true
    $ProcessInfo.RedirectStandardOutput = $true                
    $ProcessInfo.UseShellExecute = $false
    $ProcessInfo.Arguments = $strArguments    
    $Process = New-Object System.Diagnostics.Process
    $Process.StartInfo = $ProcessInfo
    $oStdOutEvent = Register-ObjectEvent -InputObject $Process -EventName OutputDataReceived -Action {Write-Host "StdOut: " $Event.SourceEventArgs.Data}
    $oStdErrEvent = Register-ObjectEvent -InputObject $Process -EventName ErrorDataReceived -Action {Write-Host "StdERR: " $Event.SourceEventArgs.Data}
    $Process.Start() | Out-Null
    $Process.BeginOutputReadLine()
    $Process.BeginErrorReadLine()
    $Process.WaitForExit()
    Unregister-Event -SourceIdentifier $oStdOutEvent.Name
    Unregister-Event -SourceIdentifier $oStdErrEvent.Name
    $intProcExit = $Process.ExitCode
    $ProcessInfo = $null
    $Process = $null    
    #Write-Host "Exit code: " $intProcExit
    if ($intProcExit -eq 0) {
        if ($strAction -eq "Publish") {
            $bolReturn = $true
        }
        else {
            Write-Host "Checking: " $output
            if ($strBlackList.Length -gt 0) {
                $bolBlackStatements = $false
                $intLineCount = 0
                Write-Host "Blacklist " $strBlackList
                $oContent = Get-Content $output
                foreach ($strLine in $oContent) {
                    $intLineCount++
                    if ($strBlackList.Contains(";")) {
                        foreach ($strEntry in $strBlackList.Split("`;")) {
                            if ($strLine.ToLower().Contains($strEntry.Tolower())) {
                                Write-Host "BLACKLISTED: " $strLine
                                $bolBlackStatements = $true
                            }
                        }
                    }
                    else {
                        if ($strLine.ToLower().Contains($strBlackList)) {
                            Write-Host "BLACKLISTED: " $strLine
                            $bolBlackStatements = $true
                        }
                    }
                }
                Write-Host "Lines checked: " $intLineCount
                if (!$bolBlackStatements) {
                    $bolReturn = $true
                }
            }
            else {
                Write-Host " Nothing to check, blacklist is empty..."
                $bolReturn = $true
            }
        }
    }    
    return $bolReturn
}

function CheckAndApplySQL([string] $strInstance, [string] $strDatabase, [string] $strSQLFile, [string] $strBlackList) {
    $bolReturn = $false
    $bolBlackStatements = $false    
    if ($strBlackList.Length -gt 0) {
        $bolBlackStatements = $false
        $intLineCount = 0
        Write-Host "Blacklist " $strBlackList
        $oContent = Get-Content $strSQLFile
        foreach ($strLine in $oContent) {
            $intLineCount++
            if ($strBlackList.Contains(";")) {
                foreach ($strEntry in $strBlackList.Split("`;")) {
                    if ($strLine.ToLower().Contains($strEntry.Tolower())) {
                        Write-Host "BLACKLISTED: " + $strLine
                        $bolBlackStatements = $true
                    }
                }
            }
            else {
                if ($strLine.ToLower().Contains($strBlackList)) {
                    Write-Host "BLACKLISTED: " + $strLine
                    $bolBlackStatements = $true
                }
            }
        }
        Write-Host "Lines checked: " $intLineCount
        if (!$bolBlackStatements) {
            Write-Host "Applying: " $strSQLFile
            try {
                Invoke-Sqlcmd -ServerInstance $strInstance -Database $strDatabase -InputFile $strSQLFile  -QueryTimeout 3600 -TrustServerCertificate -ErrorAction 'Stop' -Verbose 
                $bolReturn = $true
            }
            catch {
                Write-Host "Error:" ($_) " applying:" $strSQLFile
                throw "Aborting because of an error applying SQL script..."
            }
        }
    }
    else {
        Write-Host " Nothing to check, blacklist is empty..."
        $bolReturn = $true
    }
    return $bolReturn
}

function Merge-Tokens() {
    [CmdletBinding()] 
    param (
        [Parameter(Mandatory = $True, ValueFromPipeline = $true)]
        [AllowEmptyString()]
        [String] $template,

        [Parameter(Mandatory = $true)]
        [HashTable] $tokens
    ) 

    begin { Write-Verbose "$($MyInvocation.MyCommand.Name)::Begin" }

    process {
        Write-Verbose "$($MyInvocation.MyCommand.Name)::Process" 

        # https://github.com/craibuc/PsTokens
        # adapted based on this Stackoverflow answer: http://stackoverflow.com/a/29041580/134367
        try {

            [regex]::Replace( $template, '__(?<tokenName>[\w\.]+)__', {
                    # __TOKEN__
                    param($match)

                    $tokenName = $match.Groups['tokenName'].Value              
                    $tokenValue = Invoke-Expression "`$tokens.$tokenName"
                    Write-Host " " $tokenName "=" $tokenValue

                    if ($tokenValue) {
                        # there was a value; return it
                        return $tokenValue
                    } 
                    else {
                        # non-matching token; return token
                        return $match
                    }
                })

        }
        catch {
            write-host "Error in Merge-Tokens" $_
            throw ""
        }
    }
    end { Write-Verbose "$($MyInvocation.MyCommand.Name)::End" }
} 

function CheckFeature([string] $strComputerName, [string] $strFeature) {
    Write-Host "Checking for:" $strFeature "on:" $strComputerName
    $session = New-PSSession -ComputerName $strComputerName
    Invoke-Command -session $session {Import-Module ServerManager}    
    Invoke-Command -session $session {$ProgressPreference = "SilentlyContinue"}   
    [bool]$state = invoke-command -session $session {$resState = Get-WindowsFeature -Name $($args[0]); return [bool]$resState.Installed} -ArgumentList $strFeature
    Remove-PSSession $session
    return $state
}

function InstFeature([string] $strComputerName, [string] $strFeature, [bool] $bolAllSubFeatures, [string] $strSourceSxSFolder, [string] $strDeploymentServiceAccount, [string] $strDeploymentServiceAccountPassword) {
    # Required because deployment nodes don't support -ComputerName on current powershell version
    $bolReturn = $false
    Write-Host "Checking:" $strComputerName":"$strFeature
    $session = New-PSSession -ComputerName $strComputerName
    if (CheckFeature $strComputerName $strFeature) {
        Write-Host " Already installed..."
        $bolReturn = $true
    }
    else {
        Write-Host " Installing:" $strFeature
        if (-not($session)) { Write-Host " Unable to start session" }
        Invoke-Command -session $session {$ProgressPreference = "SilentlyContinue"}
        Invoke-Command -session $session {net use * $($args[0]) $($args[1]) /user:$($args[2]) 2>&1>null} -ArgumentList $strSourceSxSFolder, $DeploymentServiceAccountPassword, $DeploymentServiceAccount
        if ($bolAllSubFeatures) {    
            if (((Get-WmiObject Win32_OperatingSystem -ComputerName $strComputerName).Name).Contains("2012")) {
                Invoke-Command -session $session {Install-WindowsFeature -Name $($args[0]) -IncludeAllSubFeature -Source $($args[1]) | Out-Null} -ArgumentList $strFeature, $strSourceSxSFolder
            }
            else {
                Invoke-Command -session $session {Import-Module ServerManager; Add-WindowsFeature -Name $($args[0]) -IncludeAllSubFeature | Out-Null; Remove-Module ServerManager} -ArgumentList $strFeature
            }
        }
        else {
            if (((Get-WmiObject Win32_OperatingSystem -ComputerName $strComputerName).Name).Contains("2012")) {
                Invoke-Command -session $session {Install-WindowsFeature -Name $($args[0]) -Source $($args[1]) | Out-Null} -ArgumentList $strFeature, $strSourceSxSFolder
            }
            else {                
                Invoke-Command -session $session {Import-Module ServerManager; Add-WindowsFeature -Name $($args[0]) | Out-Null; Remove-Module ServerManager} -ArgumentList $strFeature
            }
        }
        Invoke-Command -session $session {net use * /d /y 2>&1>null}
        Remove-PSSession $session
        $bolReturn = CheckFeature $strComputerName $strFeature
    }
    return $bolReturn
}

function UnInstFeature([string] $strComputerName, [string] $strFeature, [bool] $bolRestartBeforeCheck) {
    # Required because deployment nodes don't support -ComputerName on current powershell version
    $bolReturn = $false
    Write-Host "Checking:" $strComputerName":"$strFeature
    $session = New-PSSession -ComputerName $strComputerName
    if (CheckFeature $strComputerName $strFeature) {
        Write-Host " Uninstalling:" $strFeature
        Invoke-Command -session $session {$ProgressPreference = "SilentlyContinue"}
        if ( ((Get-WmiObject Win32_OperatingSystem -ComputerName $strComputerName).Name).Contains("2012")) {
            Invoke-Command -session $session {Uninstall-WindowsFeature -Name $($args[0]) | Out-Null} -ArgumentList $strFeature
        }
        else {
            Invoke-Command -session $session {Import-Module ServerManager; Remove-WindowsFeature -Name $($args[0]) | Out-Null; Remove-Module ServerManager} -ArgumentList $strFeature
        }
        Remove-PSSession $session
        if ($bolRestartBeforeCheck) {
            Restart-Servers $strComputerName
        }
        $tmp = CheckFeature $strComputerName $strFeature
        if (-not($tmp)) {$bolReturn = $true}
    }
    else {
        Write-Host " Not installed, won't remove..."
        $bolReturn = $true
    }
    return $bolReturn
}

function CreateOleConnectionString([string] $provider, [string] $server, [string] $database, [bool] $mars = $false) {
    if ($mars) {
        [string]::Format("Provider={0};Data Source={1};Integrated Security=SSPI;Initial Catalog={2};Packet Size=32767;MARS Connection=True", $provider, $server, $database)
    }
    else {
        [string]::Format("Provider={0};Data Source={1};Integrated Security=SSPI;Initial Catalog={2};Packet Size=32767", $provider, $server, $database)
    }
}

Function Test-RequiredProperties {
    <#
        .SYNOPSIS
            Take a array of required property names and test if the all are defined, throw an error if 1 or more are not

        .DESCRIPTION
            Take a array of required property names and test if the all are defined, throw an error if 1 or more are not

        .PARAMETER Properties
            Array of property names
    #>

    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$Properties,
        [Switch]$AllowEmptyValues
    )

    $missingProperties = $false

    foreach ($propName in $Properties) {
        
        try {
            $prop = Get-Variable -Name $propName -ErrorAction Stop

            $propValue = Get-Variable -Name $propName -ValueOnly
    
                    
            if ([string]::IsNullOrEmpty($propValue)) {
                if ($AllowEmptyValues) {
                    Write-Host "WARNING: Property $($propName): has a null/empty value"
                } 
                else {
                    Write-Host "****Property $($propName): has a null/empty value****"
                    $missingProperties = $true
                }
                
            }
        }
        catch {
            Write-Host "****Property $($propName): Not defined****"
            $missingProperties = $true
        }
    }
    if ($missingProperties) {
        Write-Host ""
        throw "1 or more properties undefined"
    }    
}

Function DeleteFolderonSSRSPortal
(
    [Parameter(Position = 0, Mandatory = $true)]
    [string]$ReportingServerUri,
 
    [Parameter(Position = 1, Mandatory = $true)]
    [string]$SSRSReportDestinationFolder
) {
    Write-Host "Deleting Folder: " $SSRSReportDestinationFolder # deleting Report folder on site
    try {
        Remove-SsrsItem -Url $ReportingServerURI -ItemPath $SSRSReportDestinationFolder -erroraction SilentlyContinue 
    }
    catch [System.Web.Services.Protocols.SoapException] {
        Write-Host "Folder Does not Exist"
    }
}

Function CreateFolderonSSRSPortal
(
    [Parameter(Position = 0, Mandatory = $true)]
    [string]$ReportingServerUri,
 
    [Parameter(Position = 1, Mandatory = $true)]
    [string]$SSRSReportDestinationFolder
) {
    Write-Host "Creating folder:" $SSRSReportDestinationFolder #Recreating report folder on site
    New-SsrsFolder -Url $ReportingServerURI -Name "$SSRSReportDestinationFolder" -Verbose
}

Function CreateDataSourceonSSRSPortal
(
    [Parameter(Position = 0, Mandatory = $true)]
    [string]$ReportingServerUri,
 
    [Parameter(Position = 1, Mandatory = $true)]
    [string]$SSRSDataSourceName,
         
    [Parameter(Position = 2, Mandatory = $true)]
    [string]$SSRSDataSourceDestinationFolder,
    
    [Parameter(Position = 3, Mandatory = $true)]
    [string]$Description,
     
    [Parameter(Position = 4, Mandatory = $true)]
    [string]$ConnectionString,

    [Parameter(Position = 5, Mandatory = $true)]
    [string]$Username,
    
    [Parameter(Position = 6, Mandatory = $true)]
    [string]$Password
) {
    Write-Host "Creating DataSource: $SSRSDataSourceDestinationFolder/$SSRSDataSourceName"
    try {   
        New-SsrsDataSource -Url $ReportingServerURI -Name "$SSRSDataSourceName" -Folder "$SSRSDataSourceDestinationFolder" -Description "$Description" -ConnectString "$ConnectionString"   -Extension "SQL" -CredentialRetrieval ([Internal.DOrcSSrsModule.SsrsReportsService.CredentialRetrievalEnum]::Store) -UserName $UserName -Password $Password -WindowsCredentials $true
    }
    catch [Exception] {    
        if ($_.Exception.Message.ToString() -match "already exists") {
            Write-Host "Datasource $SSRSDataSourceName already exists"
        }
        else {
            Throw $_.Exception.Message
        }
    }
}

Function UploadSSRSFiles
(
    [Parameter(Position = 0, Mandatory = $true)]
    [string]$ReportingServerURI,

    [Parameter(Position = 1, Mandatory = $true)]
    [string]$SSRSSourceFolder,
 
    [Parameter(Position = 2, Mandatory = $true)]
    [string]$SSRSReportDestinationFolder,

    [Parameter(Position = 3, Mandatory = $true)]
    [string]$SSRSDataSourceDestinationFolder,
    
    [Parameter(Position = 4, Mandatory = $false)]
    [string]$Environment
) {
    Write-Host ""
    $SSRSFiles = Get-ChildItem -Path $SSRSSourceFolder -Recurse -Filter "*.rdl" # pick up all ssrs files

    if ($SSRSFiles.count -eq 0) {   
        throw ".rdl files not found in $SSRSSourceFolder"
    }
    else {
        Write-Host "Uploading" $SSRSFiles.Count"reports..."
    }

    foreach ($SSRSFile in $SSRSFiles) {
        $FullReportFileName = Join-Path $SSRSSourceFolder $SSRSFile #full path name to SSRS file in dropfolder
        $ReportName = $SSRSFile -replace ".rdl", ""
        $ReportWithPath = $SSRSReportDestinationFolder + "/" + $ReportName #full ssrs report path on SSRS site
        Write-Host ""
        Write-Host "Uploading: " $FullReportFileName "As: " $ReportWithPath
        New-SsrsReport -Url $ReportingServerURI -File $FullReportFileName -Folder $SSRSReportDestinationFolder -Description $ReportName -Overwrite #create the new SSRS report
        #create Data Source from finding data source from .rdl file
        $SSRSDataSourceNames = GetDataSource $FullReportFileName

        $DataSourceNamesCommaSep = ""
        $DataSourcePathsCommaSep = ""
        $separator = ""
        foreach ($SSRSDataSourceName in $SSRSDataSourceNames) {

            $DataSourceReference = $SSRSDataSourceName.DataSourceReference
            $ConnectionString = "SSRS_" + $DataSourceReference + "_ConnectionString"
            $ConnectionString = Get-Variable $ConnectionString -ValueOnly -ErrorAction SilentlyContinue
            if ($ConnectionString -eq $null) {
                $DatabaseServer = Get-Variable "DbServer_$DataSourceReference" -ValueOnly -ErrorAction SilentlyContinue
                $DatabaseName = Get-Variable "DbName_$DataSourceReference" -ValueOnly -ErrorAction SilentlyContinue
                if (($DatabaseServer -eq $null) -or ($DatabaseName -eq $null)) {
                    throw "Can't find connection string in DOrc Database for $DataSourceReference"
                }
                else {
                    Write-Host ("Generating connection string from Environment Management Database for database type $DataSourceReference")
                    $ConnectionString = "Data Source=$DatabaseServer;Initial Catalog=$DatabaseName;Integrated Security=True"
                }
            }
            else {
                Write-Host ("Using DOrc connection string property SSRS_$DataSourceReference" + "_ConnectionString")
            }
            $Username = "SSRS_" + $DataSourceReference + "_Username"
            $Username = Get-Variable $Username -ValueOnly -ErrorAction Stop
            $Password = "SSRS_" + $DataSourceReference + "_Password"
            $Password = Get-Variable $Password -ValueOnly -ErrorAction Stop
            $SSRSDataSourceDestinationFullPath = $SSRSDataSourceDestinationFolder + "/" + $DataSourceReference
            CreateDataSourceonSSRSPortal $ReportingServerURI $DataSourceReference $SSRSDataSourceDestinationFolder $DataSourceReference $Connectionstring $Username $Password

            $DataSourceNamesCommaSep = $DataSourceNamesCommaSep + $separator + $SSRSDataSourceName.Name
            $DataSourcePathsCommaSep = $DataSourcePathsCommaSep + $separator + $SSRSDataSourceDestinationFullPath
            $separator = ","
        }
        Write-Host "Associating: " $FullReportFileName "With: " $SSRSDataSourceDestinationFullPath
        Set-SsrsDataSourceReference -Url $ReportingServerURI -ReportPath $ReportWithPath -DataSourcePath $DataSourcePathsCommaSep -DataSourceName $DataSourceNamesCommaSep # associate the new SSRS report to the datasource


        if ([string]::IsNullOrEmpty($Environment)) {    
            Write-Host "'Environment' Property is null or empty, therefore skipping creation of file subscription"
        }
        else {
            $ScheduleDefPath = Join-Path $SSRSSourceFolder ($ReportName + ".xml")
            if (Test-Path $ScheduleDefPath) {
                Write-Host "File Subscription: $ScheduleDefPath found so adding file subscription based on the supplied definition"
                Add-SSRSFileSubscription -ReportServerURI $ReportingServerURI -XMLFile $ScheduleDefPath -Env $Environment -ServiceUserName $Username -ServicePassword $Password -EnvironmentSSRSReportPath $ReportWithPath
            }
            else {
                Write-Host "No Schedule Definition XML file found so not adding file subscription"
            }
        }
    }
}

Function GetDataSource
(
    [Parameter(Position = 0, Mandatory = $true)]
    [string]$SSRSFilePath
) {
    $RDLFile = $SSRSFilePath
    [xml]$xmlFile = Get-Content $RDLFile
    $DataSource = $xmlfile.Report.DataSources.DataSource
    return $DataSource
} 

Function Add-SSRSFileSubscription
(
    [Parameter(Position = 0, Mandatory = $true)]
    [string]$ReportServerUri,
 
    [Parameter(Position = 1, Mandatory = $true)]
    [string]$XMLFile,
         
    [Parameter(Position = 2, Mandatory = $true)]
    [string]$Env,
    
    [Parameter(Position = 3, Mandatory = $true)]
    [string]$ServiceUserName,
     
    [Parameter(Position = 4, Mandatory = $true)]
    [string]$ServicePassword,
    
    [Parameter(Position = 5, Mandatory = $true)]
    [string]$EnvironmentSSRSReportPath
) {
    $ssrsProxy = New-WebServiceProxy -Uri $ReportServerUri -UseDefaultCredential ;
    [xml]$config = Get-Content $XMLFile
    $ReportName = $config.FirstChild.ReportName
    $ReportPath = $EnvironmentSSRSReportPath
    $DropFileName = $config.FirstChild.FileName
    
    $DropPaths = $config.FirstChild.OutputFilePath
    $DropPath = $DropPaths.ChildNodes | Where-Object {$_.LocalName -eq $Env}
    If ($DropPath.Length -lt 1) {$DropPath = ($DropPaths.Default) + "\" + $Env}
    Else {$DropPath = $DropPaths.$Env}
    
    #Write-Host "Drop path = " $DropPath
    
    $OutputFormat = $config.FirstChild.OutputFormat
    $OutputWriteMode = $config.FirstChild.OutputWriteMode
    $OutputFileName = $config.FirstChild.OutputFileName
    $EventType = $config.FirstChild.EventType
    $SubscriptionDescription = $config.FirstChild.SubscriptionDescription
    [string]$ScheduledefinintionXML = $config.FirstChild.ScheduleDefifintionXML.InnerXML

    $FullPath = $ReportPath

    $type = $ssrsProxy.GetType().Namespace
    $ExtensionSettingsDataType = ($type + '.ExtensionSettings')
    $ActiveSettingsDataType = ($type + '.ActiveState')
    $ParameterValueType = ($type + '.ParameterValue')

    $extensionSettings = New-Object ($ExtensionSettingsDataType)

    $extensionSettings.Extension = 'Report Server FileShare'

    $ParameterValueOrFieldReferenceType = ($type + '.ParameterValueOrFieldReference[]')

    $extensionParams = New-Object $ParameterValueOrFieldReferenceType 6

    #Only used for description
    $subscriptionType = '_File_Subscription'

    $Path = New-Object ($type + '.ParameterValue')
    $Path.Name = 'PATH'
    $Path.Value = $DropPath
    $extensionParams[0] = $Path

    $FileName = New-Object ($type + '.ParameterValue')
    $FileName.Name = 'FILENAME'
    $FileName.Value = $DropFileName
    $extensionParams[1] = $FileName

    $FileExtension = New-Object ($type + '.ParameterValue')
    $FileExtension.Name = 'FILEEXTN'
    $FileExtension.Value = 'TRUE'
    $extensionParams[2] = $FileExtension

    $SubUserName = New-Object ($type + '.ParameterValue')
    $SubUserName.Name = 'USERNAME'
    $SubUserName.Value = $ServiceUserName
    $extensionParams[3] = $SubUserName

    $RenderFormat = New-Object ($type + '.ParameterValue')
    $RenderFormat.Name = 'RENDER_FORMAT'
    $RenderFormat.Value = $OutputFormat
    $extensionParams[4] = $RenderFormat

    $WriteMode = New-Object ($type + '.ParameterValue')
    $WriteMode.Name = 'WRITEMODE'
    $WriteMode.Value = $OutputWriteMode
    $extensionParams[5] = $WriteMode

    $Password = New-Object ($type + '.ParameterValue')
    $Password.Name = 'PASSWORD'
    $Password.Value = $ServicePassword
    $extensionParams[5] = $Password
            
    $Report_Subscr_Description = 'Automatically generated supscription defined by the XML in the report drop folder'

    $extensionSettings.ParameterValues = $extensionParams
    
    Return $ssrsProxy.CreateSubscription($FullPath, $extensionSettings, $Report_Subscr_Description, $EventType, $ScheduledefinintionXML, $null)
}

Function Move-ClusterRole {
    <#
            .SYNOPSIS
                Moves cluster role to the another node
            .DESCRIPTION
                Takes role name, node name and moves a role to a provided node.
        #>

    param(
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$RoleName,
        [Parameter(Position = 1, Mandatory = $true)]
        [string]$ToNodeName
    )
    $cluster = Get-DbaWsfcCluster $ToNodeName
    if (!$cluster){ return}
    $role = Get-DbaWsfcRole -ComputerName $ToNodeName | where {$_.Name -eq $RoleName}
    if ($role.OwnerNode -eq $ToNodeName){return}
    $output=Invoke-Command -ComputerName $ToNodeName -ScriptBlock {
        param($Cluster_Role_Name,$Node_Name)
        Write-host "Switching $Cluster_Role_Name to $Node_Name node"
        try {
            $info = Move-ClusterGroup -Name $Cluster_Role_Name -Node $Node_Name -ErrorAction Stop -Errorvariable isError
            Write-Host "`nSuccess!"
            Write-Host "The $($info.Name) role is switched to: $($info.OwnerNode)`nCluster status: $($info.State)"
            Write-host "`nCheck if cluster resources are offline, and try to run each one:"
            Start-ClusterGroup $Cluster_Role_Name -Verbose -ErrorAction Stop -Errorvariable isError
        }
        catch {
            foreach ($exception in $IsError) {
                Write-Error $($Exception.Exception.Message)
            }
        }
    } -ArgumentList @($RoleName, $ToNodeName)
    $output
}
Function Update-ClusterRoleOwners{
    param(
        [Parameter(Position = 0, Mandatory = $true)]
        [string]$FromNode,
        [Parameter(Position = 1, Mandatory = $true)]
        [string]$ToNode
    )
    $roles = Get-DbaWsfcRole -ComputerName $FromNode | where {!($_.Name -eq "Cluster Group") -and !($_.Name -eq "Available Storage")}
    write-host "Updating Cluster Role Owners"
    foreach ($role in $roles){
        if ($role.OwnerNode -eq $ToNode){continue}
        write-host "move $($role.Name) to $ToNode"
        Move-ClusterRole -RoleName $role.Name -ToNodeName $ToNode
    }
}
Function Restore-ClusterRoleOwners{
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        $StateBefore
    )
    $roles = Get-DbaWsfcRole -ComputerName $StateBefore[0].OwnerNode | where {!($_.Name -eq "Cluster Group") -and !($_.Name -eq "Available Storage")}
    write-host "Restoring Cluster Role Owners"
    foreach ($item in $StateBefore) {
        $role = $roles | where {$_.Name -eq $item.Name}
        if ($item.OwnerNode -eq $role.OwnerNode){continue}
        write-host "move $($role.Name) to $($item.OwnerNode)"
        Move-ClusterRole -RoleName $role.Name -ToNodeName $item.OwnerNode
    }
}

function Get-HostsOfType {
    param(
        [Parameter(Position = 0, Mandatory = $true)]
        $ServerInfo
    )
    $servers = @()
    foreach ($Row in $ServerInfo) {
        $serverName = $Row.Server_Name.Trim()
        $serverType = $Row.Application_Server_Name.Trim()
        $isCluster = Get-DbaWsfcCluster $serverName
        if ($isCluster) {
            $nodes = Get-DbaWsfcNode $isCluster.Name
            foreach ($node in $nodes) {
                $servers+=[pscustomobject]@{
                    ServerName = $node.Name;
                    ServerType = $serverType 
                }
            }
        }
        else{
            $servers+=[pscustomobject]@{
                ServerName = $serverName;
                ServerType = $serverType 
            }
        }
    }
    return $servers
}


#region Function DeployMSI
Function DeployMSI {
    <#
        .SYNOPSIS
            Deploys the namd MSI from the DropFolder
            
        .DESCRIPTION
            Takes the name of an MSI to be deployed and looks for a named JSON to determine
            the properties and other attributes

        .PARAMETER MSIFile
            The name and optionally relative drop folder path of the msi to install,
            if the file is not found it will be searched for from the drop folder.
            Only one must exist, any more and error will be thrown

        .PARAMETER DropFolder
            The root folder where to find the MSI files, including sub-folders

        .PARAMETER ProductNames
            An array of product names to uninstall before the MSI is installed

        .PARAMETER ServerTag
            ServerTag defines which servers msi will be deployed to. Optional parametr can be setup in project's json. Otherwise this property is defined in MSI's json
        .PARAMETER NoLogoffUsers
            Switch if present will not log off users RPC sessions on target servers
    #>

    [CmdletBinding()]
    [OutputType([Bool])]
    Param(
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]$MSIFile,        
        [Parameter(Position = 1, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$DropFolder,
        [Parameter(Position = 2, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String[]]$ProductNames,
        [Parameter(Position = 3, Mandatory = $false)]
        [System.String[]]$ServerTag,
        
        [Switch]$NoLogoffUsers
    )
    try {
           Start-Sleep -s 10

        $msiFiles = Get-ChildItem -Path $DropFolder -Include $MSIFile -Recurse        
        if ($msiFiles.Count -eq 0) {
            throw "[ERROR][DeployMSI][$(Get-Date)] Could not find any files named $($MSIFile) in $DropFolder"
        }
        
        if ($msiFiles.Count -gt 1) {
            throw "[ERROR][DeployMSI][$(Get-Date)] Found $($msiFiles.Count) files named $($MSIFile) in $DropFolder"
        }
        
        $msiFullPath = $msiFiles[0].FullName
        $settings = LoadMsiSettings "$($msiFullPath).json"

        $props = $settings.GetDeployPropertyNames()
        if ($props -ne $null -and $props.Count -gt 0) {
            $missingProperties = $false
            $invalidProperties = $false
            foreach ($propName in $props) {
                try {
                    $prop = Get-Variable -Name $propName -ErrorAction Stop
                    $propValue = Get-Variable -Name $propName -ValueOnly                        
                    if ($propValue -match '["]') {
                        Write-Host "****WARNING: Property $($propName)'s value has characters that require escaping****"
                        $invalidProperties = $true
                    }            
                }
                catch {
                    Write-Host "****WARNING: Property $($propName) not defined. Skipped.****"
                    $missingProperties = $true
                }
            }
            if ($invalidProperties) {throw "`nOne or more properties' values have characters that require escaping which prevents the msi installation.`nPlease replace them with escape sequence."} 
            if ($missingProperties) {throw "`nOne or more properties undefined"}                
        }
        
        if ([string]::IsNullOrEmpty($settings.ServerType)) 
            {
            If ($ServerTag) 
                {
                $ServerTag = @($ServerTag)
                $settings = $settings | Add-Member -NotePropertyMembers @{ServerType=@($ServerTag)} -PassThru
                Write-Host "[DeployMSI][$(Get-Date)] ServerTag from project's json file has been used to define ServerType"
                }
            Else
                {
                throw "[ERROR][DeployMSI][$(Get-Date)] No ServerType type found in $($settings.FileName)"
                }
            }
        
        #set the var for further use within start-job scriptblock - it must be set to something to support "$using:"
        if ($GenericScript_StartServicesInEnvironment -ne "true") {
            $GenericScript_StartServicesInEnvironment = "false"
        }
        
        $ServerInfo = new-object "System.Data.DataTable"        
        $ServerInfo = GetServersOfType_V2 ($settings.ServerType)        
        if ($ServerInfo.Rows.Count -gt 0) {
            foreach ($Row in $ServerInfo) {
                $serverName = $Row.Server_Name.Trim()
                $serverType = $Row.Application_Server_Name.Trim()
                $msiArgs = $settings.GetPropertiesAsArrayList()
                    
                    # this is where we split workload to run in parallel
                    # first we want to cleanup any previous stale jobs for this env
                    Get-Job -Name "DeployMSI-$serverName" -ErrorAction SilentlyContinue | Where-Object { $_.State -ne "Running" } | Remove-Job -ErrorAction SilentlyContinue

                    # Here we need to establish which DOrc Runtime Vars exist in this Scope
                    $RuntimeVars = @{
                        BuildNumber                      = $BuildNumber
                        DeploymentLogDir                 = $DeploymentLogDir
                        ScriptRoot                       = $ScriptRoot
                        DropFolder                       = $DropFolder
                        EnvironmentName                  = $EnvironmentName
                        IsProd                           = $IsProd
                        DORC_ProdDeployUsername          = $DORC_ProdDeployUsername
                        DORC_ProdDeployPassword          = $DORC_ProdDeployPassword
                        DORC_NonProdDeployUsername       = $DORC_NonProdDeployUsername
                        DORC_NonProdDeployPassword       = $DORC_NonProdDeployPassword
                        DeploymentServiceAccount         = $DeploymentServiceAccount
                        DeploymentServiceAccountPassword = $DeploymentServiceAccountPassword
                        DBPermsOutput                    = $DBPermsOutput
                        MSILogsRoot                      = $MSILogsRoot
                        WiLogUtlPath                     = $WiLogUtlPath
                        DOrcSupportEmailSMTPServer       = $DOrcSupportEmailSMTPServer
                        DOrcSupportEmailFrom             = $DOrcSupportEmailFrom
                        DOrcSupportEmailTo               = $DOrcSupportEmailTo
                    }
                    
                    foreach ($RuntimeVar in $RuntimeVars.keys) {
                        if (-not (Get-Variable $RuntimeVar -ErrorAction SilentlyContinue)) {
                            Write-Verbose  -message "[VERBOSE][DeployMSI][$(Get-Date)] Setting $RuntimeVar with empty value, as it does not exist."
                            try { 
                                New-Variable -Name $RuntimeVar
                            }
                            catch {
                                Throw "[ERROR][DeployMSI][$(Get-Date)] New-Variable failed, full exception: `n$_"
                            }
                        }
                    }

                    Write-Host "[DeployMSI] [$(Get-Date)] Creating Separate Start-Job session for parallel execution for [$serverName]..."
                    
                    Start-Job -ScriptBlock {
                        # Create DOrc Runtime variables in this scope
                        $RuntimeVars = $using:RuntimeVars
                        
                        foreach ($RuntimeVarName in $RuntimeVars.keys) {
                            try {
                                Write-Verbose -Message "[VERBOSE][DeployMSI][$(Get-Date)] Creating DOrc Runtime Variable [$RuntimeVarName] with a value of [$($RuntimeVars[$RuntimeVarName])]"
                                New-Variable -Name $RuntimeVarName -Value $RuntimeVars[$RuntimeVarName] -Force
                            }
                            catch {
                                Throw "[ERROR][DeployMSI][$(Get-Date)] New-Variable Failed, full exception: `n$_"
                            }
                        }

                        if (Test-Connection $using:serverName -Count 1 -quiet) {

                            Write-Host "[INFO][DeployMSI][$(Get-Date)]" $using:serverName "is of type" $using:serverType "is alive and will be deployed to..."
                            
                            if ($using:NoLogoffUsers) {
                                Write-Host "[INFO][DeployMSI][$(Get-Date)] LogOffUsers skipped..."
                            }
                            elseif ((Check-IsCitrixServer -compName $using:serverName)) {
                                Write-Host "[INFO][DeployMSI][$(Get-Date)] LogOffUsers skipped because the target is Citrix..."
                            }
                            else {
                                LogOffUsers $using:serverName
                            }
                            
                            $settings = LoadMsiSettings "$($using:msiFullPath).json"
                            
                            if (($settings.ServicesToStop).Count -ne 0) {
                                foreach ($service in $settings.ServicesToStop) {
                                    Stop-Services $service $using:serverName
                                }
                            }

                            try {
                                foreach ($product in $using:ProductNames) {
                                    if ((DOrcDeployModule\RemoveMSI $using:serverName $using:msiFullPath $product)) {
                                        Write-Host "[INFO][DeployMSI][$(Get-Date)] Uninstalled $product from $($using:serverName)"
                                    }
                                    else {
                                        throw "[ERROR][DeployMSI][$(Get-Date)] Uninstall of $product from $($using:serverName)"
                                    }
                                }
                                
                                if (DOrcDeployModule\InstallMSI $using:serverName $using:msiFullPath $using:msiArgs) {
                                    Write-Host "[INFO][DeployMSI][$(Get-Date)] Install ok..."
                                }
                                else {
                                    throw "[ERROR][DeployMSI][$(Get-Date)] Problem installing the MSI..."
                                }

                                if ($using:GenericScript_StartServicesInEnvironment -eq "true") {
                                    if (($settings.ServicesToStart).Count -ne 0) {
                                        foreach ($service in $settings.ServicesToStart) {
                                            StartServices $service $using:serverName
                                        }
                                    }
                                }
                            }
                            catch {
                                Throw "[ERROR][DeployMSI][$(Get-Date)] `n$_"
                            }
                        }
                        else {
                            Throw "[ERROR][DeployMSI][$(Get-Date)] $using:ServerName is not online..."
                        }
                    } -Name "DeployMSI-$ServerName" | Out-Null
                }#foreach
            
                $jobs = @()
                foreach ($Row in $ServerInfo) {
                    $serverName = $Row.Server_Name.Trim()
                    $jobs += Get-Job -Name "DeployMSI-$serverName"
                }

                $ErrorMsg = $null
                foreach ($job in $jobs) {
                    Receive-Job -Job $job -Wait
                    if ($job.State -eq 'Failed') {
                        $errorMsg += "[$($job.Name)] $($job.ChildJobs[0].JobStateInfo.Reason.Message)`n"
                        Write-Host "[INFO][DeployMSI][$(Get-Date)] Removing PowerShell Job session for [$($job.Name)]..."
                        $job | Remove-Job
                    }
                    else {
                        Write-Host "[INFO][DeployMSI][$(Get-Date)] Removing PowerShell Job session for [$($job.Name)]..."
                        $job | Remove-Job
                    }
                }
                if ($ErrorMsg) {
                    Throw $errorMsg
                }
        }
        else {
            throw "[ERROR][DeployMSI][$(Get-Date)] No servers of type $($settings.ServerType) on $EnvironmentName to target ..."
        }
    }
    catch {
        throw
    }
}
#endregion Function DeployMSI

#region Function LoadJSONFromFile
Function LoadJSONFromFile {
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Path
    )
    try {
        return (Get-Content $Path) -join "`n" | ConvertFrom-Json
    }
    catch {
        throw
    }
}
#endregion Function LoadJSONFromFile

#region Function LoadMsiSettings
Function LoadMsiSettings {
    Param
    (
        [String]$SettingsFile
    )
    try {
        if (-not (Test-Path -Path $SettingsFile -PathType Leaf)) {
            throw "[LoadMsiSettings] Cannot find $SettingsFile"
        }
        else {
            $settings = LoadJSONFromFile -Path $SettingsFile
        }
        Add-Member -InputObject $settings -MemberType NoteProperty -Name FileName -Value $SettingsFile

        
        # Add methods to settings object
        $getParameterValue = {
            Param([String]$Name)
            
            $varName = ($this.Parameters | Where-Object {$_.MSIParameter -eq $Name} ).DeployProperty
            return (Get-Variable -Name $varName -ValueOnly)
        }
        Add-Member -InputObject $settings -MemberType ScriptMethod -Name GetParameterValue -Value $getParameterValue        

        $getDeployPropertyNames = {
            
            $varNames = @()
            foreach ($param in $this.Parameters) {
                $varNames += $param.DeployProperty
            }
            return $varNames
        }
        Add-Member -InputObject $settings -MemberType ScriptMethod -Name GetDeployPropertyNames -Value $getDeployPropertyNames        
        $getPropertiesAsArrayList = {
            $arrParameters = New-Object System.Collections.ArrayList($null)
            foreach ($param in $this.Parameters) {
                [Void]$arrParameters.add("$($param.MSIParameter)=" + [char]34 + "$($this.GetParameterValue($param.MSIParameter))" + [char]34)
            }
            
            return $arrParameters
        }
        Add-Member -InputObject $settings -MemberType ScriptMethod -Name GetPropertiesAsArrayList -Value $getPropertiesAsArrayList

        return $settings
    }
    catch {
        throw
    }
}
#endregion Function LoadMsiSettings

function WasDbSnapped([string] $strInstance, [string] $strDatabase) {
    $bolSnapped = $false
    $connection = New-Object System.Data.SqlClient.SqlConnection
    $connection.ConnectionString = "Server=" + $strInstance + ";Database=master;Integrated Security=true"
    $connection.Open()
    $command = $connection.CreateCommand()
    $command.CommandText = "select m.physical_name from sys.master_files m inner join sys.databases d on (m.database_id = d.database_id) where d.name = '" + $strDatabase + "' and m.physical_name like '%.mdf%'"
    $table = new-object "System.Data.DataTable"
    $table.Load($command.ExecuteReader())
    $connection.Close()
    if ($table.Rows.Count -gt 0) {
        foreach ($Row in $table.Rows) {
            if ($Row.Item("physical_name").Contains("__")) {
                $bolSnapped = $true
            }
        }
    }
    $table = $null
    return $bolSnapped
}

function GetSQLServerName([string] $strInstance) {
    $strSQLServerName = ""
    $connection = New-Object System.Data.SqlClient.SqlConnection
    $connection.ConnectionString = "Server=" + $strInstance + ";Database=master;Integrated Security=true"
    $connection.Open()
    $command = $connection.CreateCommand()
    $command.CommandText = "SELECT SERVERPROPERTY('ServerName')"
    $table = new-object "System.Data.DataTable"
    $table.Load($command.ExecuteReader())
    $connection.Close()
    if ($table.Rows.Count -gt 0) {
        foreach ($Row in $table.Rows) {
            $strSQLServerName = $Row[0]
        }
    }
    $table = $null
    return $strSQLServerName
}

function Invoke-RemoteProcess([string] $serverName, [string] $strExecutable, [string] $strParams = "", [string] $strUser = "", [string] $strPassword = "", [switch]$IgnoreStdErr) {
    $bolResult = $true
    $strExecutableChild = $null
    # Test for UNC to executable then check for user / password
    $bolCredsOK = $false
    if ($strExecutable.StartsWith("\\")) {
        write-host " UNC path has been detected, checking for username / password parameters..."
        if ([String]::IsNullOrEmpty($strUser) -or [String]::IsNullOrEmpty($strPassword)) {
            Write-Host " Cannot continue as remote machine will be unable to connect out without credentials..."
            $bolResult = $false
        }
        else {
            $uncShare =  $strExecutable.SubString(0, $strExecutable.LastIndexOf("\"))
            $strExecutable = $strExecutable.Replace($uncShare, "")
            $uncShare = [char]34 + $uncShare + [char]34
            write-host " UNC Share: " $uncShare
            write-host " Executable:" $strExecutable
            $bolCredsOK = $true
        }
    }
    else {
        $bolCredsOK = $true
    }
    if ($bolCredsOK) {
        $session = New-PSSession -ComputerName $ServerName
        if ($strExecutable.Contains(";")) {
            $strExecutableChild = $strExecutable.Split(";")[1]
            $strExecutable = $strExecutable.Split(";")[0]
        }
        if ($strExecutable.StartsWith("\")) {
            # Need to connect out...
            Invoke-Command -session $session {net use * /d /y 2>&1>null}
            Invoke-Command -session $session {net use S: $($args[0]) $($args[1]) /user:$($args[2]) 2>&1>null} -ArgumentList $uncShare, $strPassword, $strUser
            $strExecutable = "S:" + $strExecutable
            write-host " Executable:" $strExecutable
        }
        $result = Invoke-Command -session $session {$result = $false; if (Test-Path -Path $($args[0]) -PathType Leaf) { $result = $true }; return $result} -ArgumentList $strExecutable
        if ($result) {
            write-host "Executing:" $strExecutable $strParams "on:" $ServerName
            Invoke-Command -session $session { $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo }
            Invoke-Command -session $session { $ProcessInfo.FileName = $($args[0]) } -ArgumentList $strExecutable
            Invoke-Command -session $session { $ProcessInfo.RedirectStandardError = $true }
            Invoke-Command -session $session { $ProcessInfo.RedirectStandardOutput = $true }
            Invoke-Command -session $session { $ProcessInfo.UseShellExecute = $false }
            Invoke-Command -session $session { $ProcessInfo.Arguments = $($args[0])} -ArgumentList $strParams
            Invoke-Command -session $session { $Process = New-Object System.Diagnostics.Process }
            Invoke-Command -session $session { $Process.StartInfo = $ProcessInfo }
            Invoke-Command -session $session { $Process.Start()}
            Invoke-Command -session $session { try { $out = $Process.StandardOutput.ReadToEndAsync() } catch {} }
            Invoke-Command -session $session { try { $outErr = $Process.StandardError.ReadToEndAsync() } catch {} }
            Invoke-Command -session $session { $Process.WaitForExit() }
            if (![String]::IsNullOrEmpty($strExecutableChild)) {
                do {
                    start-sleep 2
                    write-host " Checking for child process:" $strExecutableChild
                    $remProcs = Invoke-Command -session $session { $result = Get-Process $($args[0]) -ErrorAction SilentlyContinue | measure ; return $result } -ArgumentList $strExecutableChild
                    $remProcs.Count
                } while ($remProcs.Count -gt 0)
                $remProcs = $null
            }
            $execResult = Invoke-Command -session $session { return $Process.ExitCode }
            write-host " Remote execution complete, exit code:" $execResult
            if ($execResult -ne 0) { $bolResult = $false }
            $stdOut = Invoke-Command -session $session { return $out.Result}
            $stdErr = Invoke-Command -session $session { return $outErr.Result }
            Invoke-Command -session $session { $outErr, $out, $Process, $ProcessInfo = $null }
            if ($stdOut.Length -gt 0) {
                if ($stdOut.Contains([char]10)) {
                    foreach ($strLine in $stdOut.Split([char]10)) {
                        write-host " Stdout:" $strLine
                    }
                }
                else { write-host " Stdout:" $stdOut }
                write-host ""

            }
            if ($stdErr.Length -gt 0) {
                if (!$IgnoreStdErr) {$bolResult = $false}
                if ($stdErr.Contains([char]10)) {
                    foreach ($strLine in $stdErr.Split([char]10)) {
                        write-host " StdERR:" $strLine
                    }
                }
                else { write-host " StdERR:" $stdErr }
                write-host ""
            }
            $stdOut, $stdErr = $null
        }
        else {
            write-host " Unable to find:" $strExecutable "from" $ServerName
        }
        if ($strExecutable.StartsWith("\\")) {Invoke-Command -session $session {net use * /d /y 2>&1>null}}
        Remove-PSSession $session
    }
    write-host "bolResult:" $bolResult
    return $bolResult
}

function Find-RemoteNSIS([string] $serverName, [string] $productString) {
    $strUninstallString = "None"
    $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $serverName)
    $RegKey_x32 = $Reg.OpenSubKey("SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
    $RegKey_x64 = $Reg.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
    foreach ($subKeyName in $RegKey_x32.GetSubKeyNames()) {
        if ($subKeyName.ToString().Contains($productString)) {
            write-host "Found:" $subKeyName.ToString()
            $erlangKey = $RegKey_x32.OpenSubKey($subKeyName.ToString())
            $strUninstallString = $erlangKey.GetValue("UninstallString")
        }
    }
    foreach ($subKeyName in $RegKey_x64.GetSubKeyNames()) {
        if ($subKeyName.ToString().Contains($productString)) {
            write-host "Found:" $subKeyName.ToString()
            $erlangKey = $RegKey_x64.OpenSubKey($subKeyName.ToString())
            $strUninstallString = $erlangKey.GetValue("UninstallString")
        }
    }
    $erlangKey, $RegKey_x32, $RegKey_x64, $Reg = $null
    return $strUninstallString
}

function Remove-NSISErlang([string] $serverName) {
    $arrDirectories = New-Object System.Collections.ArrayList($null) 
    $remErlang = Find-RemoteNSIS $serverName "Erlang"
    do {
        if ($remErlang -ne "None") {
            write-host " Uninstall string:" $remErlang
            $remErlang += ";Au_"
            $bolResult = (Invoke-RemoteProcess $serverName $remErlang "/S")[0]
            write-host "[Remove-NSISErlang] Result:" $bolResult
            if (!$bolResult) { throw " Failed to remove..."}
            [Void]$arrDirectories.add("\\" + $serverName + "\" + $remErlang.SubString(0, $remErlang.LastIndexOf("\")).Replace(":", "$"))
            $remErlang = Find-RemoteNSIS $serverName "Erlang"
        }
    } while ($remErlang -ne "None")
    foreach ($remDir in $arrDirectories) {
        write-host " Checking:" $remDir
        if (Test-Path -Path $remDir -PathType Container) {
            write-host " Removing:" $remDir
            remove-item $remDir -recurse -force -verbose
        }
    }
    $remErlang, $bolResult, $arrDirectories = $null
}

function Remove-NSISRabbitMQ([string] $serverName) {
    $arrDirectories = New-Object System.Collections.ArrayList($null) 
    $remRabbit = Find-RemoteNSIS $serverName "RabbitMQ"
    do {
        if ($remRabbit -ne "None") {
            write-host " Uninstall string:" $remRabbit
            $remRabbit += ";Au_"
            $bolResult = (Invoke-RemoteProcess $serverName $remRabbit "/S")[0]
            write-host "[Remove-NSISRabbitMQ] Result:" $bolResult
            if (!$bolResult) { throw " Failed to remove..."}
            [Void]$arrDirectories.add("\\" + $serverName + "\" + $remRabbit.SubString(0, $remRabbit.LastIndexOf("\")).Replace(":", "$"))
            $remRabbit = Find-RemoteNSIS $serverName "RabbitMQ"
        }
    } while ($remRabbit -ne "None")
    foreach ($remDir in $arrDirectories) {
        write-host " Checking:" $remDir
        if (Test-Path -Path $remDir -PathType Container) {
            write-host " Removing:" $remDir
            remove-item $remDir -recurse -force -verbose
        }
    }
    $remRabbit, $bolResult, $arrDirectories = $null
}

function Set-RemoteSystemVar {
    param (
    [string] $serverName, 
    [string] $varName, 
    [string] $value,
    [switch] $Add
    )
    
    try {
        $session = New-PSSession -ComputerName $serverName
    }
    catch {
        Remove-PSSession $session
        Throw "Failed to establish remote PS session to [$serverName]`n"
    }
    
    if ($add) {
        try {
            $existingValue = Invoke-Command -session $session { [System.Environment]::GetEnvironmentVariable($($args[0]), "Machine")} -ArgumentList $varName
        }
        catch {
            Remove-PSSession $session
            Throw "Failed to get existing value of System Variable [$varName]`n"
        }       
        Write-host "[DEBUG] Existing value of System Variable [$varName] `"$existingValue`"`n"
        #now add it to the value variable for later use
        $value = $existingValue + ";" + $value
    }
    
    try {
        Write-host "[VERBOSE] Setting System Variable [$varName] to `"$value`"`n"
        Invoke-Command -session $session { [System.Environment]::SetEnvironmentVariable($($args[0]), $($args[1]), "Machine")} -ArgumentList $varName, $value
    }
    catch {
        Remove-PSSession $session
        Throw "Failed to set System Variable [$varName] to [$value]`n"
    }
    
    Remove-PSSession $session
}

function Get-EndurDbVersion
(
  [Parameter(Position = 0, Mandatory = $true)] [string] $endurDbServer,
  [Parameter(Position = 1, Mandatory = $true)] [string] $endurDb
)
{
    $dataTable = New-Object System.Data.DataTable
    $sqlConnection = New-Object System.Data.SQLClient.SQLConnection
    $sqlQuery = "SELECT TOP 1 [major],[minor],[revision],[build] FROM [" + $endurDb + "].[dbo].[version_number]"
    $sqlConnection.ConnectionString = "Server=$endurDbServer;Database=$endurDb;Integrated Security=True"
    $sqlConnection.Open()
    $command = New-Object System.Data.SQLClient.SQLCommand
    $command.Connection = $sqlConnection
    $command.CommandText = $sqlQuery
    $reader = $command.ExecuteReader()
    $dataTable.Load($reader)
    $sqlConnection.Close()
    $ver = $dataTable.Rows[0][0].ToString() + "." + $dataTable.Rows[0][1].ToString() + "." + $dataTable.Rows[0][2].ToString() + "." + $dataTable.Rows[0][3].ToString()
    $reader, $command, $sqlConnection, $dataTable, $sqlQuery = $null
    return $ver
}

function GetDbInfoByTypeForEnvWithArray([string] $strEnvironment, [string] $strType) {
    $securePassword = ConvertTo-SecureString $DorcApiAccessPassword -AsPlainText -Force
    $credentials = New-Object System.Management.Automation.PSCredential ($DorcApiAccessAccount, $securePassword)
    $uri=$RefDataApiUrl + 'RefDataEnvironments?env=' + $strEnvironment
    write-host "GetDbInfoByTypeForEnvWithArray Uri is:" $uri
    $EnvId=(Invoke-RestMethod -Uri $uri -method Get -Credential $credentials -ContentType 'application/json').EnvironmentId
    write-host "EnvId" $EnvId 
    $uri=$RefDataApiUrl + 'RefDataEnvironmentsDetails/' + $EnvId
    write-host "EnvironmentDetails Uri is:" $uri
    $table=Invoke-RestMethod -Uri $uri -method Get -Credential $credentials -ContentType 'application/json'
    $table=$table.DbServers | where {$_.Type -eq $strType} | Select Name, ServerName, ArrayName
    $strResult="Invalid"
    if ($table.Name.count -eq 0) {
        Write-Host "No entries returned for" $strEnvironment $strType 
    }
    elseif ($table.Name.Count -eq 1) {
            $strResult = $table.ServerName + ":" + $table.Name + ":" + $table.ArrayName
    }
    else {
        throw "Too many entries for $strEnvironment $strType"
    }
    return $strResult
}

function Snap-Database 
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $dropFolder,
    [Parameter(Position = 1, Mandatory = $true)] [string] $dacpacName,
    [Parameter(Position = 2, Mandatory = $true)] [string] $envMgtDBServer, 
    [Parameter(Position = 3, Mandatory = $true)] [string] $envMgtDBName,
    [Parameter(Position = 4, Mandatory = $true)] [string] $restoreMode,
    [Parameter(Position = 5, Mandatory = $true)] [string] $restoreSource
)
{
    $result = $true
    $rows = (Get-ChildItem -Path $dropFolder -Recurse)
    $dacpacFile = $null
    foreach ($row in $rows) {
        if ($row.Name.Equals($dacpacName)) { $dacpacFile = $row.FullName }   
    }
    if ([String]::IsNullOrEmpty($dacpacFile)) { write-host "Couldn't locate" $dacpacName "in" $dropFolder ; $result = $false }
    else {
        $jsonFile = $dacpacFile.TrimEnd("dacpac") + "restore.json"
        $postRestoreFile = $dacpacFile.TrimEnd("dacpac") + "postrestore.sql"
        If (!(Test-Path $dacpacFile)) { write-host "Cannot find:" $dacpacName ; $result = $false }
        If (!(Test-Path $jsonFile)) { write-host "Cannot find JSON " $dacpacName; $result = $false }
        write-host "JSON settings:" $jsonFile
        $jsonParams = LoadJSONFromFile $jsonFile        
        switch ($RestoreSource.ToLower()) {
            "prod" { $SourceInfo = (GetDbInfoByTypeForEnvWithArray $jsonParams.SourceEnvProd $jsonParams.DatabaseType) }
            "staging" { $SourceInfo = (GetDbInfoByTypeForEnvWithArray $jsonParams.SourceEnvStaging $jsonParams.DatabaseType) }
            default { write-host "RestoreSource should be prod or staging..." $result = $false }
        }
        if (!($SourceInfo.Contains(":"))) { write-host "Source database information could not be retrived for db type:" $jsonParams.DatabaseType "in" $jsonParams.SourceEnvProd ; $result = $false }
        $strEnvMgtConnString = "Server=" + $EnvMgtDBServer + ";Database=" + $EnvMgtDBName + ";Integrated Security=true"
        $TargetInfo = (GetDbInfoByTypeForEnvWithArray $EnvironmentName $jsonParams.DatabaseType)
        if (!($TargetInfo.Contains(":"))) { write-host "Target database information could not be retrived for db type:" $jsonParams.DatabaseType "in" $EnvironmentName ; $result = $false }
        if ($TargetInfo -eq $SourceInfo) { write-host "Source and target the same!" ; $result = $false }
        $TargetDB = ($TargetInfo.Split(":"))[1]
        $TargetInstance = GetSQLServerName ($TargetInfo.Split(":"))[0]
        $SourceDB = ($SourceInfo.Split(":"))[1]
        $SourceInstance = GetSQLServerName ($SourceInfo.Split(":"))[0]
        $array =  ($SourceInfo.Split(":"))[2]
        write-host ""
        write-host "Source:" $SourceInstance"."$SourceDB
        write-host "Target:" $TargetInstance"."$TargetDB
        write-host "Array: " $array
        write-host ""

        if ([string]::IsNullOrEmpty($array)) { write-host "Cannot continue as the Array information is not valid..." ; $result = $false } 
        if ($result) {
            # Restore
            Switch ($RestoreMode.ToLower()) {
                "latest" { Invoke-PureSQLSnapshotCopy -SourceDatabase $SourceDB -SourceSQLServer $SourceInstance -DestinationDatabase $TargetDB -DestinationSQLServer $TargetInstance -PureArrayName $array }
            }

            # Permissions
            $TargetInstance = ($TargetInfo.Split(":"))[0]
            Apply-DatabasePermissions -instance $TargetInstance -database $TargetDB

            # Simple Recovery Model
 # if ($SimpleRecoveryModel)
 # {
                Write-Host "Setting Simple Recovery Model for " $TargetInstance"."$TargetDB
                $RecoveryModelresult = Invoke-Sqlcmd -ServerInstance $TargetInstance -Query "ALTER DATABASE $TargetDB SET RECOVERY SIMPLE" -QueryTimeout 600 -TrustServerCertificate -ErrorAction 'Stop' -OutputSqlErrors $true 
                $RecoveryModelresult
  # }

            # Post restore SQL
            if (Test-Path $postRestoreFile)
            {
                Write-Host "Applying:" $postRestoreFile "on" $TargetInstance"."$TargetDB
                $postRestoreresult = Invoke-Sqlcmd -ServerInstance "$TargetInstance" -Database "$TargetDB" -InputFile $postRestoreFile -QueryTimeout 600 -TrustServerCertificate -ErrorAction 'Stop' -OutputSqlErrors $true 
                $postRestoreresult
            }
            else {write-host "$postRestoreFile not found..."}
        }
    }
    return $result
}

function Check-ProductInstalled
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $serverName,
    [Parameter(Position = 1, Mandatory = $true)] [string] $productName
)
{
    $installed = $false
    $products = Get-WmiObject Win32_Product -ComputerName $serverName
    foreach ($product in $products) { if ($product.Name.ToLower() -eq $productName.ToLower()) { $installed = $true } }
    $products = $null
    return $installed
}

function Install-WindowsFeaturesDorc
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $targetServerType,
    [Parameter(Position = 1, Mandatory = $true)] [string] $baseBuildFeatures,
    [Parameter(Position = 2, Mandatory = $true)] [string] $deploymentServiceAccount,
    [Parameter(Position = 3, Mandatory = $true)] [string] $deploymentServiceAccountPassword,
    [Parameter(Position = 4, Mandatory = $true)] [string] $coreCodeSxSFolder
)
{
    $ProgressPreference = 'SilentlyContinue'
    $ServerInfo = new-object "System.Data.DataTable"
    $ServerInfo = GetServersOfType $EnvironmentName $targetServerType
    if ($ServerInfo.Rows.Count -gt 0) {
        foreach ($Row in $ServerInfo) {
            $serverName = $Row.Server_Name.Trim()
            $serverType = $Row.Application_Server_Name.Trim()
            $remOS = Get-WmiObject Win32_OperatingSystem -ComputerName $serverName
            if ($remOS.Name -match "2008") {
                write-host "[Install-WindowsFeaturesDorc] Target server O/S is 2008, skipping:" $serverName
            }
            elseif (Test-Connection $serverName -Count 1 -quiet) {
                Write-Host "[Install-WindowsFeaturesDorc]" $serverName "is of type" $serverType "is alive and will be deployed to..."
                if ($BaseBuildFeatures.Contains(";")) {
                    $features = $BaseBuildFeatures.Split("`;")
                }
                else {
                    $features = $BaseBuildFeatures
                }
                if ($BaseBuildFeatures -match "Web-Server") {
                    write-host "[Install-WindowsFeaturesDorc] Removing IIS..."
                    Uninstall-WindowsFeature -Name "Web-Server" -ComputerName $serverName -WarningAction SilentlyContinue
                    Restart-Servers $serverName
                }
                $session = New-PSSession -ComputerName $serverName
                Invoke-Command -session $session {$ProgressPreference ='SilentlyContinue'}
                Invoke-Command -session $session {net use * /d /y 2>&1>null}
                Invoke-Command -session $session {net use * $($args[0]) $($args[1]) /user:$($args[2]) 2>&1>null} -ArgumentList $coreCodeSxSFolder, $DeploymentServiceAccountPassword, $DeploymentServiceAccount
                foreach ($feature in $features) {
                    $result = Get-WindowsFeature -Name $feature -ComputerName $serverName
                    if ($result.Installed){
                        write-host "[Install-WindowsFeaturesDorc] Already installed:" $feature
                    }
                    else {
                        write-host "[Install-WindowsFeaturesDorc] Installing:" $feature
                        try {
                            Invoke-Command -session $session {Install-WindowsFeature -Name $($args[0]) -Source $($args[1]) -WarningAction SilentlyContinue -ErrorAction Stop} -ArgumentList $feature, $coreCodeSxSFolder -ErrorAction Stop
                        }
                        catch {
                            Write-Error -Message "Exectution of this command failed: `nInstall-WindowsFeature -Name $feature -Source $coreCodeSxSFolder; Exception: `n$_"
                        }
                    }
                }
                Invoke-Command -session $session {net use * /d /y 2>&1>null}
                Remove-PSSession $session
                Restart-Servers $ServerName
            }
            else {
                throw "$ServerName is not online..."
            }
            Write-Host ""
        }
    }
    else {
        write-host "No servers to target..."
    }
}

function Apply-DatabasePermissions 
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $instance,
    [Parameter(Position = 1, Mandatory = $true)] [string] $database
)
{
        write-host ""
        write-host "[Apply-DatabasePermissions] Processing:" $instance"."$database
        $strStatus = GetDBStatus $instance $database 
        if ($strStatus -eq "Normal") {
            Write-Host "[Apply-DatabasePermissions] Processing:" $instance"."$database "is" $strStatus
            $outputFolder = $DBPermsOutput + "\" + (GetDateReverse) + "-" + $instance.Replace("\", "_") + "." + $database
            Write-Host "[Apply-DatabasePermissions] Output folder:" $outputFolder
            Get-DBPermissions -server $instance -database $database -outputFilepath $outputFolder
            if ((Test-Path $outputFolder -PathType Container) -and ((Get-Childitem $outputFolder -Recurse -File -Filter *.sql | Measure-Object | ForEach-Object{$_.Count}) -gt 0)) {
                write-host "[Apply-DatabasePermissions] Permissions extract does exist...:" $instance"."$database
                write-host "[Apply-DatabasePermissions] Applying permissions to:" $instance"."$database                
                write-host "[Apply-DatabasePermissions] Using:" $ApplyPermissionsExePath
                $args = "/instance:$instance /database:$database /prod:$IsProd"
                $tmpOutput = New-TemporaryFile
                start-process -FilePath $ApplyPermissionsExePath -ArgumentList $args -RedirectStandardOutput $tmpOutput -Wait
                $permsOutput = Get-Content -Path $tmpOutput
                foreach ($line in $permsOutput) { write-host "[Apply-DatabasePermissions]" $line}
                Remove-Item $tmpOutput.FullName -Force
            } else { write-host "[Apply-DatabasePermissions] SQL extract has not succeeded to permissions will not be reset..." }
        } else { write-host "[Apply-DatabasePermissions] Database is not in the normal state..." }
}

function Check-ProductInstalledReg
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $serverName,
    [Parameter(Position = 1, Mandatory = $true)] [string] $productName
)
{
    $installed = $false
    $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine",$serverName)
    $uninstKey = "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
    $subkey = $reg.OpenSubKey($uninstKey)
    $subs = $subkey.GetSubKeyNames()
    foreach ($key in $subs) {
        $productKey = $uninstKey + "\" + $key
        $prodKey = $reg.OpenSubKey($productKey)
        $dispName = $prodKey.GetValue("DisplayName")
        if (!([String]::IsNullOrEmpty($dispName))) { if ($dispName.ToLower() -eq $productName.ToLower()) { $installed = $true } }
    }
    return $installed
}

function Add-ToRemoteSystemPath (
    [Parameter(Position = 0, Mandatory = $true)] [string] $comp,
    [Parameter(Position = 1, Mandatory = $true)] [string] $newPath
)
{
    $remPath = Invoke-Command -ComputerName $comp { [Environment]::GetEnvironmentVariable("Path","Machine") }
    if ($remPath.ToLower().Contains($newPath.ToLower())) { write-host $newPath "is already in the path on $comp" }
    else {
        $newFullPath = $remPath.TrimEnd(";") + ";" + $newPath
        write-host "Setting path to:" $newFullPath
        $session = New-PSSession -ComputerName $comp
        Invoke-Command -session $session { Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $($args[0]) -Force } -ArgumentList $newFullPath
        Remove-PSSession -Session $session
    }
}

function Add-ADO2LocalGroup
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $comp,
    [Parameter(Position = 1, Mandatory = $true)] [string] $localGroup,
    [Parameter(Position = 2, Mandatory = $true)] [string] $adoItem,
    [Parameter(Position = 3, Mandatory = $true)] [string] $user,
    [Parameter(Position = 4, Mandatory = $true)] [string] $pass
)
{
    if (Test-Connection $comp) {
        if ($adoItem.ToLower().Contains($DomainAdoItem)) { $adoItem = $adoItem.ToLower().Replace($DomainAdoItem, "") }
        $ado = Get-ADObject -Filter {(SamAccountName -eq $adoItem)}
        $adoClass = $ado.ObjectClass
        if (($adoClass -eq "user") -or ($adoClass -eq "group")) {
            $passwd = ConvertTo-SecureString $pass -AsPlainText -Force
            $cred = New-Object System.Management.Automation.PSCredential ($user, $passwd)
            $session = New-PSSession -ComputerName $serverName -Credential $cred
            write-host "[Add-ADO2LocalGroup] Adding: $adoItem ($adoClass) to $localGroup on: $serverName"
            Invoke-Command -session $session { $group = [ADSI]("WinNT://"+$env:COMPUTERNAME+"/$($args[0]),group") } -ArgumentList $localGroup
            Invoke-Command -session $session { $group.add("WinNT://$env:USERDOMAIN/$($args[0]),$($args[1])") } -ArgumentList $adoItem, $adoClass
            Remove-PSSession -Session $session
        } else { write-host "[Add-ADO2LocalGroup] INVALID: $adoItem" }
    } else { write-host "[Add-ADO2LocalGroup] FAIL: Could not ping $comp" }
}

function Setup-VMAccess 
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $env,
    [Parameter(Position = 1, Mandatory = $true)] [string] $serverTypeFilter,
    [Parameter(Position = 2, Mandatory = $true)] [string] $recUser,
    [Parameter(Position = 3, Mandatory = $true)] [string] $recPassword,
    [Parameter(Position = 4, Mandatory = $true)] [string] $dorcUser,
    [Parameter(Position = 5, Mandatory = $true)] [string] $remoters,
    [Parameter(Position = 6, Mandatory = $true)] [string] $localAdmins,
    [Parameter(Position = 7, Mandatory = $true)] [string] $logonAsService
)
{
    $ServerInfo = new-object "System.Data.DataTable"
    $ServerInfo = GetServersOfType $EnvironmentName $serverTypeFilter
    foreach ($Row in $ServerInfo) {
        $serverName = $Row.Server_Name.Trim()
        if (Test-Connection $serverName) {
            write-host "[Setup-VMAccess] Processing: $serverName"
            Add-ADO2LocalGroup -comp $serverName -localGroup "administrators" -adoItem $dorcUser -user $recUser -pass $recPassword
            if ($localAdmins -eq "null"){
                write-host "[Setup-VMAccess] No local admins to add..."
            } else {
                if (($EnvironmentName -match "dv") -or ($EnvironmentName -match "dev")) {
                    write-host "[Setup-VMAccess] DV Server detected..."
                    if ($localAdmins -match ";") { $localAdminsAll = $localAdmins.Split(";") } else { $localAdminsAll = $localAdmins }
                    foreach ($localAdmin in $localAdminsAll) {
                        Add-ADO2LocalGroup -comp $serverName -localGroup "administrators" -adoItem $localAdmin -user $recUser -pass $recPassword
                    }
                }
            }
            if ($remoters -eq "null"){
                write-host "[Setup-VMAccess] No remoters to add..."
            } else {
                if ($remoters -match ";") { $remotersAll = $remoters.Split(";") } else { $remotersAll = $remoters }
                foreach ($remoter in $remotersAll) {
                    Add-ADO2LocalGroup -comp $serverName -localGroup "Remote Desktop Users" -adoItem $remoter -user $recUser -pass $recPassword
                    Add-ADO2LocalGroup -comp $serverName -localGroup "Event Log Readers" -adoItem $remoter -user $recUser -pass $recPassword
                }
            }
            if ($logonAsService -eq "null"){
                write-host "[Setup-VMAccess] No Logon As Service rights to add..."
            } else {
                if ($logonAsService -match ";") { $logonAsServiceAll = $logonAsService.Split(";") } else { $logonAsServiceAll = $logonAsService }
                foreach ($las in $logonAsServiceAll) {
                    write-host "[Setup-VMAccess] Granting Logon As Service to: $las"
                    Grant-UserRight -Account $las -Right SeServiceLogonRight -Computer $ServerName
                }
            }
        } else { write-host "[Setup-VMAccess] FAIL: Could not ping $serverName" }
    }
}

function Get-ServerIDs 
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $environment,
    [Parameter(Position = 1, Mandatory = $true)] [string] $serverType
)
{
    $ServerInfo = new-object "System.Data.DataTable"
    $ServerInfo = GetServersOfType $environment $serverType
    if ($ServerInfo.Rows.Count -gt 0) {
        foreach ($Row in $ServerInfo) {
            $serverName = $Row.Server_Name.Trim()
            $serverType = $Row.Application_Server_Name.Trim()
            try {
                [guid]$id = icm $serverName {(get-wmiobject Win32_ComputerSystemProduct).UUID} -ErrorAction Stop
                $result += $serverName.ToUpper() + ":" + $id + ";"
            }
            catch {
                Write-Host "[Get-ServerIDs] Server $ServerName not reachable. This is expected for a new build."
                $result += $serverName.ToUpper() + ":" + "Not Reachable" + ";"
            }
            
        }
    } else { write-host "[Get-ServerIDs] No servers..." }
    return $result
}

function Check-ServerIDsDifferent
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $environment,
    [Parameter(Position = 1, Mandatory = $true)] [string] $serverType,
    [Parameter(Position = 2, Mandatory = $true)] [string] $previousIDs
)
{
    $result = $true
    $ServerInfo = new-object "System.Data.DataTable"
    $ServerInfo = GetServersOfType $environment $serverType
    if ($ServerInfo.Rows.Count -gt 0) {
        foreach ($Row in $ServerInfo) {
            $serverName = $Row.Server_Name.Trim()
            $serverType = $Row.Application_Server_Name.Trim()
            try {
                [guid]$id = icm $serverName {(get-wmiobject Win32_ComputerSystemProduct).UUID} -ErrorAction Stop
            }
            catch {
                Write-Host "[Get-ServerIDs] Server $ServerName not reachable. This is expected for a new build."
                continue
            }
            $nowID = $serverName.ToUpper() + ":" + $id
            if ($previousIDs -match $nowID) {
                write-host "[Check-ServerIDsDifferent] FAIL:" $serverName "has the same ID:" $id
                $result = $false
            } else { write-host "[Check-ServerIDsDifferent] Pass:" $serverName "has a new ID:" $id }
        }
    } else { write-host "No servers..." }
    return $result
}

function Deploy-SSISPackages
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $environment,
    [Parameter(Position = 1, Mandatory = $true)] [string] $packageFolderRoot,
    [Parameter(Position = 2, Mandatory = $true)] [string] $ssisServer,
    [Parameter(Position = 2, Mandatory = $true)] [string] $ssisDb
)
{
    Try {
        $result = $false
        [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices")
        $ISNamespace = "Microsoft.SqlServer.Management.IntegrationServices"
        $sqlConnectionString = "Data Source=$ssisServer;Initial Catalog=master;Integrated Security=SSPI;"
        Write-Host "Cxn String: " + $sqlConnectionString
        $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnectionString
        $integrationServices = New-Object "$ISNamespace.IntegrationServices" $sqlConnection
        $catalog = $integrationServices.Catalogs.get_Item($ssisDb)
        if ($catalog.Name.ToLower() -eq $ssisDb.ToLower()) {
            Write-Host "[Deploy-SSISPackages] Checking for folder: $environment"
            $ispacs = Get-Childitem $packageFolderRoot -Recurse -File -Filter *.ispac
            $folder = $catalog.Folders[$environment]
            if (!$folder) {
                Write-Host "[Deploy-SSISPackages] Creating Folder: $environment"
                $folder = New-Object "$ISNamespace.CatalogFolder" ($catalog, $environment, $environment)            
                $folder.Create()  
            }
            foreach ($ispac in $ispacs) {
                $exists = $false
                foreach ($project in $folder.Projects) {
                    if ($ispac.Name.ToLower() -match $project.Name.ToLower()) {
                        $exists = $true
                        $projectToRemove = $project
                    }
                }
                if ($exists) {
                    $removeText = $environment + "\Projects\" + $project.Name
                    Write-Host "[Deploy-SSISPackages] Removing: " $removeText
                    $folder.Projects.Remove($projectToRemove)
                    $folder.Alter()
                }
                Write-Host "[Deploy-SSISPackages] Deploying:" $ispac.FullName
                [byte[]] $projectFile = [System.IO.File]::ReadAllBytes($ispac.FullName)            
                $folder.DeployProject($ispac.Name.Replace(".ispac", ""), $projectFile)
            }
            $integrationServices, $catalog, $folder = $null
            $integrationServices = New-Object "$ISNamespace.IntegrationServices" $sqlConnection
            $catalog = $integrationServices.Catalogs.get_Item($ssisDb)
            $folder = $catalog.Folders[$environment]
            foreach ($project in $folder.Projects) {
                if ($ispac.Name.ToLower() -match $project.Name.ToLower()) {
                    write-host "[Deploy-SSISPackages] Verified: " $project.IdentityKey
                    $result = $true
                }
            }
            $integrationServices, $catalog, $folder = $null
        } else { Write-Host "[Deploy-SSISPackages] Failed to connect to SSIS server $ssisServer.$ssisDb" }
        return $result
    }
    Catch {
        Write-Host "Error during deploy of ssis package:"
        Write-Host $_
        throw
    }
    Finally {
        
    }
}

function Set-SSISPackageParamters
(
    [Parameter(Position = 0, Mandatory = $true)] [string] $environment,
    [Parameter(Position = 1, Mandatory = $true)] [string] $packageFolderRoot,
    [Parameter(Position = 2, Mandatory = $true)] [string] $ssisServer,
    [Parameter(Position = 2, Mandatory = $true)] [string] $ssisDb
)
{
    Try {
    $result = $false
    $mask = "*******************************************************************************************************************************************************"
    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Management.IntegrationServices")
    $ISNamespace = "Microsoft.SqlServer.Management.IntegrationServices"
    $sqlConnectionString = "Data Source=$ssisServer;Initial Catalog=master;Integrated Security=SSPI;"
    $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnectionString
    $integrationServices = New-Object "$ISNamespace.IntegrationServices" $sqlConnection
    $catalog = $integrationServices.Catalogs.get_Item($ssisDb)
    $folder = $catalog.Folders[$environment]
    $envVarsSet = $false
    if ($catalog.Name.ToLower() -eq $ssisDb.ToLower()) {
        $isConfigs = Get-Childitem $packageFolderRoot -Recurse -File -Filter *.json
        if ($isConfigs.Count -gt 0) {
            write-host "[Set-SSISPackageParamters] JSON files have been found..."
            foreach ($project in $folder.Projects) {
                foreach ($package in $project.Packages) {
                    foreach ($isConfig in $isConfigs) {
                        if ($isConfig.Name.ToLower() -match $package.Name.ToLower()) {
                            $settingText = $project.Name + "\" + $package.Name
                            write-host "[Set-SSISPackageParamters] Setting paramters for:" $settingText "using" $isConfig.FullName
                            $jsonConfig = Get-Content -Raw -Path $isConfig.FullName | ConvertFrom-Json
                            foreach ($jsonParameter in $jsonConfig.Parameters) {
                                $isSecure = $false
                                try {
                                    $value = (get-variable $jsonParameter.DeployProperty -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).Value
                                    $url = $DORC_PropertiesUrl #+ $jsonParameter.DeployProperty
                                    $ProgressPreference = "SilentlyContinue"
                                    $propInfo = Invoke-WebRequest -UseDefaultCredentials $url | ConvertFrom-Json
                                    $propInfo = $propInfo|  where {$_.Name -eq $jsonParameter.DeployProperty}
                                    $ProgressPreference = "Continue"
                                    $isSecure = $propInfo.Secure #IsSecured
                                } catch { $value = "<<< Not Defined in DOrc >>>" }
                                if ([string]::IsNullOrEmpty($value)) { $value = "<<< Not Defined in DOrc >>>" }
                                elseif ($isSecure) { $outputValue = $mask.Substring(0, $value.Length) } 
                                else { $outputValue = $value }
                                $output = "[Set-SSISPackageParamters] " + $jsonParameter.DeployProperty + " = " + $outputValue + " --> " + $jsonParameter.SSISParameter
                                write-host $output
                                foreach ($packageParameter in $package.Parameters){
                                    if ($packageParameter.Name -eq $jsonParameter.SSISParameter) {
                                        $output =  "[Set-SSISPackageParamters] Updating: " + $packageParameter.Name + " = " + $outputValue
                                        write-host $output
                                        $packageParameter.Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Literal, $value.ToString())
                                        $project.Alter()
                                    }
                                }
                            }
                        } elseif (($isConfig.Name.ToLower() -match "environmentvariables") -and !($envVarsSet)) {
                            $varEnv = $folder.Environments["Variables"]
                            if (!($varEnv)){
                                write-host "[Set-SSISPackageParamters] Creating Environment: Variables"
                                $env = New-Object $ISNamespace".EnvironmentInfo" ($folder, "Variables", "Variables")
                                $env.Create()
                            } else {
                                write-host "[Set-SSISPackageParamters] Removing Environment: Variables"
                                $folder.Environments.Remove($varEnv)
                                $folder.Alter()
                                write-host "[Set-SSISPackageParamters] Creating Environment: Variables"
                                $env = New-Object $ISNamespace".EnvironmentInfo" ($folder, "Variables", "Variables")
                                $env.Create()    
                            }
                            $varEnv = $folder.Environments["Variables"]
                            write-host "[Set-SSISPackageParamters] Setting Environment paramters for:" $project.Name "using" $isConfig.FullName
                            $jsonConfig = Get-Content -Raw -Path $isConfig.FullName | ConvertFrom-Json
                            foreach ($jsonParameter in $jsonConfig.Parameters) {
                                $isSecure = $false
                                try {
                                    $value = (get-variable $jsonParameter.DeployProperty -ErrorAction SilentlyContinue -WarningAction SilentlyContinue).Value
                                    $url = $DORC_PropertiesUrl #+ $jsonParameter.DeployProperty
                                    $ProgressPreference = "SilentlyContinue"
                                    $propInfo = Invoke-WebRequest -UseDefaultCredentials $url | ConvertFrom-Json
                                    $propInfo = $propInfo|  where {$_.Name -eq $jsonParameter.DeployProperty}
                                    $ProgressPreference = "Continue"
                                    $isSecure = $propInfo.Secure #IsSecured
                                } catch { $value = "<<< Not Defined in DOrc >>>" }
                                if ([string]::IsNullOrEmpty($value)) { $value = "<<< Not Defined in DOrc >>>" }
                                elseif ($isSecure) { $outputValue = $mask.Substring(0, $value.Length) } 
                                else { $outputValue = $value }
                                $output = "[Set-SSISPackageParamters] Environment: " + $jsonParameter.DeployProperty + " = " + $outputValue + " --> " + $jsonParameter.SSISParameter
                                write-host $output
                                $description = "Set by DOrc:" + $jsonParameter.DeployProperty
                                $varEnv.Variables.Add($jsonParameter.SSISParameter, [System.TypeCode]::String, $value, $isSecure, $description)
                                $varEnv.Alter()
                            }
                            $envVarsSet = $true
                        }
                    }
                }
            }
            $result = $true
        } else { Write-Host "[Deploy-SSISPackages] No JSON files found..." ; $result = $true }
    } else { Write-Host "[Set-SSISPackageParamters] Failed to connect to SSIS server $ssisServer.$ssisDb" ; $result = $false }
    $packageParameters, $integrationServices, $catalog, $folder, $isConfigs = $null
    return $result
    }
    Catch {
        Write-Host "Error during deploy of ssis params:"
        Write-Host $_
        throw
    }
    Finally {
        
    }
}
function CheckBackup ([string] $SourceInstance, [string] $SourceDB, [string] $RestoreMode){ 
    if (($RestoreMode -eq "latest") -or ($RestoreMode -eq "now") -or ($RestoreMode -eq "pit")) {write-host "restore mode $RestoreMode"}
        else {throw "wrong RestoreMode, expected: latest/now/pit"}
    $inc = "msdb..backupset.type = 'D' OR backupset.type = 'I' OR backupset.type = 'L'"
    $full = "msdb..backupset.type = 'D'"
    $type = $full
    Write-Host ">>> SourceInstance: $SourceInstance"
    Write-Host ">>> SourceDB: $SourceDB"

    $realSQLNameQueryResult = Invoke-Sqlcmd -Query "SELECT @@SERVERNAME" -ServerInstance "$SourceInstance" -TrustServerCertificate 
    $realSQLName = $realSQLNameQueryResult.Column1

    $fullbackpath =  Invoke-Sqlcmd -Query "USE master SELECT top(1)
        msdb.dbo.backupmediafamily.physical_device_name as path
        FROM msdb.dbo.backupmediafamily
        INNER JOIN msdb.dbo.backupset ON msdb.dbo.backupmediafamily.media_set_id = msdb.dbo.backupset.media_set_id
        WHERE msdb.dbo.backupset.database_name = '$SourceDB'
        AND (CONVERT(datetime, msdb.dbo.backupset.backup_start_date, 102) >= GETDATE() - 60)
        AND ($type) AND msdb.dbo.backupset.server_name = '$realSQLName'
        ORDER BY
        msdb.dbo.backupset.backup_finish_date desc"
 -ServerInstance "$SourceInstance" -TrustServerCertificate
    Write-Host "CheckBackup full path:" $fullbackpath.path
    if (!([string]::IsNullOrEmpty($fullbackpath))) {
        if ($RestoreMode -eq "latest"){
            if (Test-Path $fullbackpath.path) { return $true }
                else {return $false}
        }
    }
    else {return $false}
    if ($RestoreMode -eq "now" -or $RestoreMode -eq "pit"){
        $type = $inc
        $incbackpath =  Invoke-Sqlcmd -Query "USE master SELECT top(1)
            msdb.dbo.backupmediafamily.physical_device_name as path
            FROM msdb.dbo.backupmediafamily
            INNER JOIN msdb.dbo.backupset ON msdb.dbo.backupmediafamily.media_set_id = msdb.dbo.backupset.media_set_id
            WHERE msdb.dbo.backupset.database_name = '$SourceDB'
            AND (CONVERT(datetime, msdb.dbo.backupset.backup_start_date, 102) >= GETDATE() - 60)
            AND ($type) AND msdb.dbo.backupset.server_name = '$realSQLName'
            ORDER BY
            msdb.dbo.backupset.backup_finish_date desc"
 -ServerInstance "$SourceInstance" -TrustServerCertificate
        Write-Host "CheckBackup inc path:" $incbackpath.path
        if (!([string]::IsNullOrEmpty($incbackpath))) {
            if ((Test-Path $fullbackpath.path) -and (Test-Path $incbackpath.path)) { return $true }
                else {return $false}
        }
        else {return $false}
    }
}

Function Get-MSHotfix  {  
    <#
    .SYNOPSIS
    Get list of installed Hotfixes. This is essentially a wrapper for "wmic qfe list".
    
    .DESCRIPTION
    Get list of installed Hotfixes. Note, the term Hotfix is incorrectly used interchangeably with Windows Update or Patch.
    
    .PARAMETER ComputerName
    ComputerName
    
    .PARAMETER Credential
    Credential object
        
    .EXAMPLE
    $admcred = Get-Credential
    $Hotfixes = Get-MSHotfix -ComputerName SERVER1 -Credential $admcred 2>$null
    
    Stores list of installed hotfixes on SERVER1 in variable $Hotfixes. Error stream is ignored.

    .EXAMPLE
    $admcred = Get-Credential
    Get-MSHotfix -ComputerName SERVER1 -Credential $admcred | sort InstalledOn | ft HotFixID, InstalledOn, InstalledBy
    
    Produces table with list of installed hotfixes sorted by date. Oldest on the top.

    #>

    param (
        $ComputerName = $env:COMPUTERNAME,
        [pscredential]$Credential
    )
    
    $params = @{}

    if ($ComputerName) {
        $params."ComputerName" = $ComputerName;
    }
    if ($Credential) {
        $params."Credential" = $Credential;
    }

    $outputs = Invoke-Command -ScriptBlock {Invoke-Expression "wmic qfe list"} @params
    $outputs = $outputs[1..($outputs.length)]
      
      
    foreach ($output in $Outputs) {  
        if ($output) {  
            $output = $output -replace 'Security Update','Security-Update'
            $output = $output -replace 'Service Pack','Service-Pack'
            $output = $output -replace 'NT AUTHORITY','NT-AUTHORITY'  
            $output = $output -replace '\s+',' '  
            $parts = $output -split ' ' 
            $InstalledBy = $parts[4]
            if ($parts[5] -like "*/*/*") {  
                $Dateis = [datetime]::ParseExact($parts[5], '%M/%d/yyyy',[Globalization.cultureinfo]::GetCultureInfo("en-US").DateTimeFormat)
            }
            elseif (($parts[4] -like "*/*/*")) {
                $Dateis = [datetime]::ParseExact($parts[4], '%M/%d/yyyy',[Globalization.cultureinfo]::GetCultureInfo("en-US").DateTimeFormat)
                $InstalledBy = "n/a"
            }
            else {
                #catch parsing errors
                try {
                    $Dateis = get-date([DateTime][Convert]::ToInt64("$parts[5]", 16)) -Format '%M/%d/yyyy'
                }
                catch {
                    #unknown date format
                    $Dateis = Get-Date 1901
                }
            }
            New-Object -Type PSObject -Property @{  
                KBArticle = [string]$parts[0]  
                Computername = [string]$parts[1]  
                Description = [string]$parts[2]  
                FixComments = [string]$parts[6]  
                HotFixID = [string]$parts[3]  
                InstalledOn = [datetime]$Dateis.Date #to make is sortable!
                InstalledBy = $InstalledBy
                InstallDate = [string]$parts[7]  
                Name = [string]$parts[8]  
                ServicePackInEffect = [string]$parts[9]  
                Status = [string]$parts[10]  
            }
        }
    }
}

Function Get-ServerOSVersion {
    param (
        $ComputerName = $env:COMPUTERNAME,
        [pscredential]$Credential
    )

    $params = @{}
    if ($PSBoundParameters['ComputerName']) {
        $params."ComputerName" = $ComputerName
    }
    if ($PSBoundParameters['Credential']) {
        $params."Credential" = $Credential
    }
    
    $buildNumber = Invoke-Command {(Get-CimInstance Win32_OperatingSystem).BuildNumber} @params

    switch ($buildNumber) {
        6001 {$OS = "Windows Server 2008"}
        7600 {$OS = "Windows Server 2008 R2"}
        7601 {$OS = "Windows Server 2008 R2 SP1"}    
        9200 {$OS = "Windows Server 2012"}
        9600 {$OS = "Windows Server 2012 R2"}
        14393 {$OS = "Windows Server 2016 1607"}
        15063 {$OS = "Windows Server 2016 1703"}
        16229 {$OS = "Windows Server 2016 1709"}
        17134 {$OS = "Windows Server 2019 1803"}
        17763 {$OS = "Windows Server 2019 1809"}
        18363 {$OS = "Windows Server 2019 1909"}
        19041 {$OS = "Windows Server 2019 2004"}
        default { $OS = "n/a"}
    }
    return $OS
}

Function Get-DorcCredSSPStatus {
    <#
    .SYNOPSIS
    Evaluates whether CredSSP is supported for establishing Powershell session to a remote host from local machine.
     
    .DESCRIPTION
    This function will validate number of things.
    - Is credential delegation enabled in WSMAN settings on local machine (Client)
    - Is credential delegation enabled in WSMAN settings on remote machine (Server)
    - Are any CredSSP vulnerability hotfixes installed on either local or remote machine,
      see https://support.microsoft.com/en-gb/help/4295591/credssp-encryption-oracle-remediation-error-when-to-rdp-to-azure-vm
    - Are any registry changes in place to work around the above hotfixes
    - Attempts to establish CredSSP PS session to the remote computer
    
    .PARAMETER ComputerName
    Remote Computer Name
    
    .PARAMETER Credential
    Credential Object
    
    .PARAMETER Test
    Attempts to establish CredSSP PS session to the remote computer.
    
    .EXAMPLE
    $admcred = Get-Credential
    Get-DorcCredSSPStatus -ComputerName SERVER1 -Credential $admcred -Test
    
    Validates whether local and remote machine can establish PS CredSSP session. The result is an output object looking like this:
    
    LocalComputerName : CLIENT1
    RemoteComputerName : SERVER1
    LocalOS : 10.0.17134
    RemoteOS : 10.0.14393
    LocalCredSSPEnabled : True
    RemoteCredSSPEnabled : True
    LocalPatchInstalled : False
    RemotePatchInstalled : False
    LocalHotFixWorkaroundInPlace : False
    RemoteHotFixWorkaroundInPlace : False
    CredSSPWorks : True
    
    .LINK
    https://blogs.technet.microsoft.com/ashleymcglone/2016/08/30/powershell-remoting-kerberos-double-hop-solved-securely/
    https://support.microsoft.com/en-gb/help/4295591/credssp-encryption-oracle-remediation-error-when-to-rdp-to-azure-vm
    #>

    
    
    #requires -RunAsAdministrator
        
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [String]        
        $ComputerName,
        
        [Parameter(Mandatory = $true)]
        [pscredential]
        $Credential,

        [Parameter(Mandatory = $false)]
        [switch]
        $Test
    )
    
    $params = @{}

    if ($PSBoundParameters['ComputerName']) {
        $params."ComputerName" = $ComputerName
    }
    if ($PSBoundParameters['Credential']) {
        $params."Credential" = $Credential
    }
    
    #Verify local computer has CredSSP enabled on Local machie (Client)
    try {
        $GetWSMAN = Get-WSManCredSSP -ErrorAction Stop
        #Expected output should look like this:
            # "The machine is configured to allow delegating fresh credentials to the following target(s): wsman/*.domain.name,
            # This computer is not configured to receive credentials from a remote client computer."
    }
    catch {
        Throw $_
    }
    if (($GetWSMAN -match "The machine is configured to allow delegating fresh credentials")`
        -and (($GetWSMAN -match "$ComputerName") -or ($GetWSMAN -match "\*")) ) {
        $LocalCredSSPEnabled = $true
    }
    else {
        $LocalCredSSPEnabled = $false
    }

    #Get operating system version
    try {
        $RemoteOS = Get-ServerOSVersion @params
    }
    catch {
        Write-Warning -message "Failed to get OS version of $ComputerName!"
    }

    try {
        $LocalOS = Get-ServerOSVersion
    }
    catch {
        Write-Warning -message "Failed to get OS version of $Env:COMPUTERNAME!"
    }
    

    #Check if CredSSP is enabled on the remote computer
    try {
        $GetWSMANRemote = Invoke-Command {Get-WSManCredSSP} @params -ErrorAction Stop
    }
    catch {
        Throw "Failed to connect to [$ComputerName] with Invoke-Command to obtain WSMAN config"
    }
    if ($GetWSMANRemote -match "This computer is configured to receive credentials from a remote client computer") {
        $RemoteCredSSPEnabled = $true
    }
    elseif ($GetWSMANRemote -match "This computer is not configured to receive credentials from a remote client computer") {
        $RemoteCredSSPEnabled = $false
    }
    
    #Check if CredSSP hotfixes are present
    $CredSSPHotfixes = @(
        [pscustomobject]@{os = "Windows Server 2008 R2"; hotfixes = @("KB4088875", "KB4088878")},
        [pscustomobject]@{os = "Windows Server 2012"; hotfixes = @("KB4103730", "KB4103726")},
        [pscustomobject]@{os = "Windows Server 2012 R2"; hotfixes = @("KB4103725", "KB4103715")},
        [pscustomobject]@{os = "Windows Server 2016 1607"; hotfixes = @("KB4103723")},
        [pscustomobject]@{os = "Windows Server 2016 1703"; hotfixes = @("KB4103731")},
        [pscustomobject]@{os = "Windows Server 2016 1709"; hotfixes = @("KB4103727")}  
    )
    
    $RemoteApplicableHotfixes = ($CredSSPHotfixes | Where-Object {Invoke-Command {$using:_.os -eq $using:RemoteOS} @params }).hotfixes
    $LocalApplicableHotfixes = ($CredSSPHotfixes | Where-Object { $_.os -eq $LocalOS}).hotfixes

    if ($RemoteApplicableHotfixes) {
        $RemoteHotfixes = Get-MSHotfix @params 2>$null | Where-Object {$RemoteApplicableHotfixes -contains $_.HotFixID}  #is expected to throw errors
    }
    
    if ($LocalApplicableHotfixes) {
        $LocalHotfixes = Get-MSHotfix -ComputerName $Env:COMPUTERNAME 2>$null | Where-Object {$LocalApplicableHotfixes -contains $_.HotFixID}
    }

    if (($RemoteHotfixes | Measure-Object).Count -ge 1) {
        $RemotePatchInstalled = $true
    }
    else {
        $RemotePatchInstalled = $false
    }

    if (($LocalHotfixes | Measure-Object).Count -ge 1) {
        $LocalPatchInstalled = $true
    }
    else {
        $LocalPatchInstalled = $false
    }

    #Check whether registry workaround is in place (https://support.microsoft.com/en-gb/help/4093492/credssp-updates-for-cve-2018-0886-march-13-2018)
    try {
        $LocalAllowEncryptionOracleValue = (Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Name AllowEncryptionOracle -ErrorAction Stop).AllowEncryptionOracle
    }
    catch {
        Write-Verbose -Message "[VERBOSE] Local AllowEncryptionOracleValue registry key either does not exist or is not accessible on [$Env:COMPUTERNAME]"
    }
    try {
        $RemoteAllowEncryptionOracleValue = Invoke-Command {(Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Name AllowEncryptionOracle).AllowEncryptionOracle} @params -ErrorAction Stop
    }
    catch {
        Write-Verbose -Message "[VERBOSE] Remote AllowEncryptionOracleValue registry key either does not exist or is not accessible on [$ComputerName]"
    }
    if ($RemoteAllowEncryptionOracleValue -eq 2) {
        $RemoteHotFixWorkaroundInPlace = $true
    }
    else {
        $RemoteHotFixWorkaroundInPlace = $false
    }

    if ($LocalAllowEncryptionOracleValue -eq 2) {
        $LocalHotFixWorkaroundInPlace = $true
    }
    else {
        $LocalHotFixWorkaroundInPlace = $false
    }
    
    #Test CredSSP
    if ($PSBoundParameters['Test']) {
        try {
            $Session = New-PSSession @params -Authentication Credssp -ErrorAction Stop
            $CredSSPWorks = $true
        }
        catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
            if ($_ -match "server name cannot be resolved") {
                Throw "[ERROR] Server name [$ComputerName] cannot be resolved!"
            }
            else {
                $CredSSPWorks = $false
            }
        }
        catch {
            $CredSSPWorks = $false
            Throw $_
        }
    }
    else {
        $CredSSPWorks = "N/A"
    }
    
    #Build Custom Object for output
    $properties = @{
        "LocalComputerName"               = $Env:COMPUTERNAME
        "RemoteComputerName"              = $ComputerName
        "LocalOS"                         = $LocalOS
        "RemoteOS"                        = $RemoteOS
        "LocalCredSSPEnabled"             = $LocalCredSSPEnabled
        "RemoteCredSSPEnabled"            = $RemoteCredSSPEnabled
        "LocalPatchInstalled"             = $LocalPatchInstalled
        "RemotePatchInstalled"            = $RemotePatchInstalled
        "LocalHotFixWorkaroundInPlace"    = $LocalHotFixWorkaroundInPlace
        "RemoteHotFixWorkaroundInPlace"   = $RemoteHotFixWorkaroundInPlace
        "CredSSPWorks"                    = $CredSSPWorks
    }
    
    if ($session) {
        Remove-PSSession $session
    }
    
    $output = New-Object -TypeName psobject -Property $properties
    Return $output | Select-Object LocalComputerName, RemoteComputerName, LocalOS, RemoteOS, LocalCredSSPEnabled, RemoteCredSSPEnabled, LocalPatchInstalled, RemotePatchInstalled, LocalHotFixWorkaroundInPlace, RemoteHotFixWorkaroundInPlace, CredSSPWorks
}

Function Enable-DorcCredSSP {
    <#
    .SYNOPSIS
    Enables CredSSP for remote PS session on the local computer (where this is executed) and a remote machine
    
    .DESCRIPTION
    Enables CredSSP for remote PS session on the local computer (where this is executed) and a remote machine
    
    .PARAMETER ComputerName
    ComputerName
    
    .PARAMETER Credential
    Credential object
    
    .PARAMETER Force
    Forces creation of registry values for patched systems.
    
    .EXAMPLE
    Enable-DorcCredSSP -ComputerName SERVER1 -credential $admcred -Force -verbose
    
    Enables CredSSP and shows detailed verbose output.
    #>

    
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $true)]
        [String]        
        $ComputerName,
        
        [Parameter(Mandatory = $true)]
        [pscredential]
        $Credential,

        [Parameter(Mandatory = $false)]
        [switch]
        $Force
    )
    
    $params = @{}

    if ($PSBoundParameters['ComputerName']) {
        $params."ComputerName" = $ComputerName
    }
    if ($PSBoundParameters['Credential']) {
        $params."Credential" = $Credential
    }
    
    $CredSSPStatus = Get-DorcCredSSPStatus @params -Test
    Write-Verbose -Message "[VERBOSE] `n$($CredSSPStatus | out-string)"
    if ($CredSSPStatus.CredSSPWorks -eq $true) {
        Return "[INFO] CredSSP has already been enabled between [$Env:COMPUTERNAME] and [$ComputerName]"
    }
    
    if ($CredSSPStatus.LocalCredSSPEnabled -eq $false) {
        Write-Verbose -Message "Configuring Local WSMAN for CredSSP"
        try {
            Enable-WSManCredSSP -Role Client -DelegateComputer $ComputerName -Force -ErrorAction Stop
        }
        catch {
            Throw "[ERROR] Failed to enable CredSSP on local machine [$Env:COMPUTERNAME]: Full Exception: `n$_"
        }
    }

    if ($CredSSPStatus.RemoteCredSSPEnabled -eq $false) {
        Write-Verbose -Message "Configuring Remote WSMAN for CredSSP"
        try {
            Invoke-Command {Enable-WSManCredSSP -Role Server -Force -ErrorAction Stop} @params -ErrorAction Stop
        }
        catch {
            Throw "[ERROR] Failed to enable CredSSP on remote machine [$Env:COMPUTERNAME]: Full Exception: `n$_"
        }
    }
 
    if ($PSBoundParameters['Force']) {
        Write-Warning -Message "[WARN] Putting a registry override in. This will make system vulnerable."
        try  {
            New-Item HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Force -ErrorAction Stop | Out-Null
            New-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Name AllowEncryptionOracle -Value 2 -Force -ErrorAction Stop | Out-Null
            Write-Warning -Message "[WARN] System [$Env:COMPUTERNAME] may need to be rebooted for the changes to kick in"
        }
        catch {
            Throw "[ERROR] Failed to put a registry override on [$Env:COMPUTERNAME]"
        }
        try { 
            Invoke-Command {
                New-Item HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Force -ErrorAction Stop | Out-Null;
                New-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System\CredSSP\Parameters -Name AllowEncryptionOracle -Value 2 -Force -ErrorAction Stop | Out-Null
            } @params -ErrorAction Stop
        }
        catch {
            Throw "[ERROR] Failed to put a registry override on [$ComputerName]"
        }
    }

    #Validate CredSSP works
    $CredSSPStatus = Get-DorcCredSSPStatus @params -Test
    Write-Verbose -Message "[VERBOSE] `n$($CredSSPStatus | out-string)"
    if ($CredSSPStatus.CredSSPWorks -eq $true) {
        Return "[INFO] CredSSP Successfully enabled between [$ENV:ComputerName] and [$ComputerName]"
    }
    else {
        Throw "[ERROR] Failed to enable CredSSP, please investigate. You may need to use the -Force parameter to add a registry workaround for patched systems."
    }
}

Function Check-IsCitrixServer {
    <#
    .SYNOPSIS
    Checks to see if the specified computer is a Citrix server
    
    .DESCRIPTION
    Checks to see if the specified computer is a Citrix server
    
    .PARAMETER ComputerName
    ComputerName
    
    .EXAMPLE
    Check-IsCitrixServer -ComputerName "SSDV-CTXAPP01"

    #>

    
    param (
        [Parameter(Mandatory = $true)]
        [String]        
        $compName
    )
    
    $result = $false

    Try {
        $compOS = "unknown"
        $os = Get-CimInstance -ComputerName $compName -ClassName Win32_OperatingSystem -Property *
        $compOS = $os.Caption
        $os = $null
    } catch { }
    if ($compOS -eq "unknown") {
        write-host "[Check-IsCitrixServer] Unable to identify O/S on: $compName"
    } elseif ($compOS.ToLower() -match "server"){
        write-host "[Check-IsCitrixServer] $compName is $compOS"
        $rkCitrixValueCount = icm -ComputerName $compName { $rkCitrix = get-item HKLM:\SOFTWARE\Citrix -ErrorAction SilentlyContinue ; return $rkCitrix.ValueCount }
        if ($rkCitrixValueCount -gt 0) {
            write-host "[Check-IsCitrixServer] $compName is a Citrix Server..."
            $result = $true
        }
    }
    return $result
}

Function Stop-CitrixApp {
    <#
    .SYNOPSIS
    Terminates an application on a Citrix server for all users
    
    .DESCRIPTION
    Terminates an application on a Citrix server for all users
    
    .EXAMPLE
     Stop-CitrixApp -compName $serverName -procName "client" -pathText $envForPath -warningMessage "Client will terminate in 30 seconds, please save any work and exit..."
    #>

    
    param (
        [Parameter(Mandatory = $true)]
        [String]        
        $compName,
        
        [Parameter(Mandatory = $true)]
        [string]
        $procName,

        [Parameter(Mandatory = $true)]
        [string]
        $pathText,

        [Parameter(Mandatory = $true)]
        [string]
        $warningMessage
    )
    
    if (Test-Connection -ComputerName $compName){
        $validProcesses = Invoke-Command -ComputerName $compName { Get-Process -IncludeUserName } | Where-Object {$_.ProcessName -match $procName -and $_.Path -match $pathText}
        foreach($validProcess in $validProcesses) {
            Write-Host "[Stop-CitrixApp] Messaging:" $validProcess.UserName
            Invoke-Command -ComputerName $compName {msg $($args[0]) /TIME:30 "$($args[1])"} -ArgumentList $validProcess.UserName.Split("\")[1], $warningMessage
        }
        start-sleep -Seconds 30
        $validProcesses = Invoke-Command -ComputerName $compName { Get-Process -IncludeUserName } | Where-Object {$_.ProcessName -match $procName -and $_.Path -match $pathText}
        foreach($validProcess in $validProcesses) {
            $info = $compName + "." + $validProcess.UserName
            Write-Host "[Stop-CitrixApp] Terminating: [$info]" $validProcess.Path
            Invoke-Command -ComputerName $compName {Get-Process -IncludeUserName | Where-Object UserName -eq $($args[0]) | Stop-Process -Force -ErrorAction SilentlyContinue} -ArgumentList $validProcess.UserName
        }
        $validProcesses, $info = $null
    } else { write-host "[Stop-CitrixApp] Cannot connect to:" $compName }
}

function Test-HaveAdminAccess {
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $serverName
    )

    $result = $false
    $isAdmin = Invoke-Command $serverName { ([Security.Principal.WindowsPrincipal] `
        [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
    if ($isAdmin.GetType().fullname -eq "System.Boolean") { $result = $isAdmin }
    return $result
}


function Get-GACUtilPath {
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $serverName
    )

    # Saves having to have DOrc properties for this exe
    $GACUtilPath = "C:\Windows\Microsoft.NET\Framework\v1.1.4322\gacutil.exe"
    if (Test-Path ("\\" + $serverName + "\C$" + "\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\x64\gacutil.exe")) {
        $GACUtilPath = "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\x64\gacutil.exe"
    } elseif (Test-Path ("\\" + $serverName + "\C$" + "\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\gacutil.exe")) {
        $GACUtilPath = "C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools\gacutil.exe"
    } elseif (Test-Path ("\\" + $serverName + "\C$" + "\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\x64\gacutil.exe")) {
        $GACUtilPath = "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\x64\gacutil.exe"
    }
    return $GACUtilPath
}
function Get-LibsInGAC {
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $serverName,

        [Parameter(Position = 1, Mandatory = $true)]
        [string] $GACUtilPath
    )
    return (Invoke-Command -ComputerName $serverName { & $($args[0]) /l } -ArgumentList $GACUtilPath)
}

function UnRegister-FromGAC {
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $serverName,

        [Parameter(Position = 1, Mandatory = $true)]
        [string] $library,

        [Parameter(Position = 2, Mandatory = $true)]
        [string] $GACUtilPath
    )
    Invoke-RemoteProcess -serverName $serverName -strExecutable $GACUtilPath -strParams "/uf $library" 
}

function Register-ToGAC {
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $serverName,

        [Parameter(Position = 1, Mandatory = $true)]
        [string] $library,

        [Parameter(Position = 2, Mandatory = $true)]
        [string] $GACUtilPath
    )

    $tempDest = "\\" + $serverName + "\c$\installs"
    if (Test-Path $tempDest) {
        Copy-Item $library -Destination $tempDest -Force
        $lib = "C:\Installs" + ($library.Substring($library.LastIndexOf("\") , ($library.Length - $library.LastIndexOf("\"))))
        Invoke-RemoteProcess -serverName $serverName -strExecutable $GACUtilPath -strParams "/i $lib"
        Invoke-Command -ComputerName $serverName { Remove-Item $($args[0]) -Force } -ArgumentList $lib
    } else { throw "[Register-FromNuGetToGAC] Expected C:\Installs not found" }
}

function Register-FromNuGetToGAC {
    param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $packageLocation,
        
        [Parameter(Position = 1, Mandatory = $true)]
        [string] $packageName,

        [Parameter(Position = 2, Mandatory = $true)]
        [string] $packageVersion,

        [Parameter(Position = 3, Mandatory = $true)]
        [string] $serverType,
        
        [Parameter(Position = 4, Mandatory = $false)]
        [string] $packageSubfolder
    )

    $ProgressPreference = 'SilentlyContinue'
    $targetServerType = $serverType
    $ServerInfo = new-object "System.Data.DataTable"
    $ServerInfo = GetServersOfType $EnvironmentName $targetServerType
    
    if ($ServerInfo.Rows.Count -gt 0)
    {
        
        $tmpSource = [System.IO.Path]::GetRandomFileName().Replace(".", "")
        Register-PackageSource -Name $tmpSource -Location $packageLocation -ProviderName NuGet

        $tempFolder = ($env:temp + "\" + [System.IO.Path]::GetRandomFileName().Replace(".", ""))

        $nupkgName = $tempFolder + "\" + $packageName + "." + $packageVersion + ".nupkg"
        $extractFolder = Join-Path $tempFolder "Temp"
        New-Item -ItemType Directory -Path $extractFolder

        write-host "[Register-FromNuGetToGAC] Downloading NuPkg..."
        $ProgressPreference = "SilentlyContinue"
        Save-Package -Name $packageName -RequiredVersion $packageVersion -Path $tempFolder -Source $tmpSource
        Rename-Item -Path $nupkgName -NewName $nupkgName.Replace(".nupkg", ".zip")
        Expand-Archive -Path $nupkgName.Replace(".nupkg", ".zip") -DestinationPath $extractFolder
        $ProgressPreference = "Continue"

        if ($packageSubfolder) {
            $packageSubfolder= $packageSubfolder.TrimStart("\").Trim()
            $extractFolder = $extractFolder+"\"+$packageSubfolder
            if (!(Test-Path $extractFolder)) {throw "$packageSubfolder was not found in the package"}
        }

        $dlls = Get-Childitem $extractFolder -Recurse -File -Filter *.dll
        write-host "[Register-FromNuGetToGAC] Libraries found:" $dlls.Count "in $extractFolder"
        foreach ($Row in $ServerInfo)
        {
            $serverName = $Row.Server_Name.Trim()
            $serverType = $Row.Application_Server_Name.Trim()
            if (Test-Connection $serverName -Count 1 -quiet)
            {
                write-host "[Register-FromNuGetToGAC] $serverName is alive..."
                if ((Test-HaveAdminAccess -serverName $serverName)) {
                    write-host "[Register-FromNuGetToGAC] Admin access confirmed, attempting to register libraries in $nupkgName on $serverName..."
                    $GACUtilPath = Get-GACUtilPath -serverName $serverName
                    write-host "[Register-FromNuGetToGAC] Using: $GACUtilPath"
                    foreach ($dll in $dlls) { 
                        Write-Host "[Register-FromNuGetToGAC] $dll"
                        $libName = $dll.Name.Replace(".dll", "")
                        $remLibs = Get-LibsInGAC -serverName $serverName -GACUtilPath $GACUtilPath
                        $count = $remLibs.Count
                        write-host "[Register-FromNuGetToGAC] Found $count libraries registered in GAC on $serverName"
                        if ($remLibs -match $libName) {
                            Write-Host "[Register-FromNuGetToGAC] $libName will be unregistered..."
                            UnRegister-FromGAC -serverName $serverName -library $libName -GACUtilPath $GACUtilPath
                            $remLibs = Get-LibsInGAC -serverName $serverName -GACUtilPath $GACUtilPath
                            if ($remLibs -match $libName) { 
                                throw "[Register-FromNuGetToGAC] unregister failed..." 
                            } else { Write-Host "[Register-FromNuGetToGAC] Success, $libName has been unregistered..."}
                        }
                        Write-Host "[Register-FromNuGetToGAC] Registering $libName from " $dll.FullName
                        Register-ToGAC -serverName $serverName -library $dll.FullName -GACUtilPath $GACUtilPath
                        $remLibs = Get-LibsInGAC -serverName $serverName -GACUtilPath $GACUtilPath
                        if ($remLibs -match $libName) { Write-Host "[Register-FromNuGetToGAC] Success, $libName has been registered..." ; Write-Host "" }
                    }    
                } else { Throw "[Register-FromNuGetToGAC] Don't have admin access on $serverName" }
            }
        }
        Remove-Item $tempFolder -Recurse -Force
        UnRegister-PackageSource -Name $tmpSource -Force
    } else { Throw "[Register-FromNuGetToGAC] No servers of type $serverType" }   
}

function Grant-LogonAsAService {
    <#
    .Synopsis
      Grant logon as a service right to the defined user.
    .Parameter computerName
      Defines the name of the computer where the user right should be granted.
      Default is the local computer on which the script is run.
    .Parameter username
      Defines the username under which the service should run.
      Use the format domain\username or username.
      Default is the user under which the script is run.
    .Example
      Usage:
      .\GrantSeServiceLogonRight.ps1 -computerName hostname.domain.com -username "domain\username"
    #>

    param(
      [string] $computerName = ("{0}.{1}" -f $env:COMPUTERNAME.ToLower(), $env:USERDNSDOMAIN.ToLower()),
      [string] $username = ("{0}\{1}" -f $env:USERDOMAIN, $env:USERNAME)
    )
    Invoke-Command -ComputerName $computerName -Script {
      param([string] $username)
      $tempPath = [System.IO.Path]::GetTempPath()
      $import = Join-Path -Path $tempPath -ChildPath "import.inf"
      if(Test-Path $import) { Remove-Item -Path $import -Force }
      $export = Join-Path -Path $tempPath -ChildPath "export.inf"
      if(Test-Path $export) { Remove-Item -Path $export -Force }
      $secedt = Join-Path -Path $tempPath -ChildPath "secedt.sdb"
      if(Test-Path $secedt) { Remove-Item -Path $secedt -Force }
      try {
        Write-Host ("Granting SeServiceLogonRight to user account: {0} on host." -f $username)
        $sid = ((New-Object System.Security.Principal.NTAccount($username)).Translate([System.Security.Principal.SecurityIdentifier])).Value
        secedit /export /cfg $export
        $sids = (Select-String $export -Pattern "SeServiceLogonRight").Line
        foreach ($line in @("[Unicode]", "Unicode=yes", "[System Access]", "[Event Audit]", "[Registry Values]", "[Version]", "signature=`"`$CHICAGO$`"", "Revision=1", "[Profile Description]", "Description=GrantLogOnAsAService security template", "[Privilege Rights]", "$sids,*$sid")){
         Add-Content $import $line
        }
        secedit /import /db $secedt /cfg $import
        secedit /configure /db $secedt
        gpupdate /force
        Remove-Item -Path $import -Force
        Remove-Item -Path $export -Force
        Remove-Item -Path $secedt -Force
      } catch {
        Write-Host ("Failed to grant SeServiceLogonRight to user account: {0} on host: {1}." -f $username, $computerName)
        $error[0]
      }
    } -ArgumentList $username
}


function Get-AzAccessTokenToResource {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ClientID,

        [Parameter(Mandatory = $true)]
        [string]$ClientSecret,

        [Parameter(Mandatory = $true)]
        [string]$TenantDomain,

        [Parameter(Mandatory = $true)]
        [string]$ResourceUrl
    )
    
    Import-Module -Name Az


    $SecureClientSecret = ConvertTo-SecureString $ClientSecret -AsPlainText -Force
    $Credential = New-Object System.Management.Automation.PSCredential($ClientID, $SecureClientSecret)

    Connect-AzAccount -TenantId $TenantDomain -Credential $Credential -ServicePrincipal

    $AccessTokenToResource = Get-AzAccessToken -ResourceUrl $ResourceUrl
    return $AccessTokenToResource
}
function DeployDACPACToAzureSQL {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$TargetServerName, 
        [Parameter(Mandatory = $true)]  
        [string]$TargetDatabaseName, 
        [Parameter(Mandatory = $true)]
        [string]$dacpacPath,         
        [Parameter(Mandatory = $true)]
        [string]$sqlPackagePath,     
        [Parameter(Mandatory = $true)]
        [string]$AccessToken         
    )


    Write-Host "Deploying DACPAC to Azure SQL Server..."
    & "$sqlPackagePath" /Action:Publish /SourceFile:$dacpacPath /TargetServerName:$TargetServerName /TargetDatabaseName:$TargetDatabaseName /AccessToken:$AccessToken /Quiet

    if ($?) {
        Write-Host "DACPAC deployment to $ServerName database $DatabaseName completed successfully."
    } else {
        Write-Host "DACPAC deployment failed."
        throw "Deployment failed"
    }
}