NTS.Tools.MSSQL.psm1

function Get-SQLISO {
    <#
        .Description
        downloads sql iso from microsoft
 
        .Parameter Version
        version of the iso
 
        .Parameter Outpath
        path where the iso is stored
 
        .Example
        Get-SQLISO -Outpath $Outpath
 
        .NOTES
        downloads the sql enterprise eval setup and with it the iso
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2019", "2022")]
        [string]
        $Version,

        [Parameter(Mandatory = $true)]
        [string]
        $Outpath
    )

    switch ($Version) {
        "2019" { $DownloadURL = "https://download.microsoft.com/download/4/8/6/486005eb-7aa8-4128-aac0-6569782b37b0/SQL2019-SSEI-Eval.exe" }
        "2022" { $DownloadURL = "https://download.microsoft.com/download/4/1/b/41b9a8c3-c2b4-4fcc-a3d5-62feed9e6885/SQL2022-SSEI-Eval.exe" }
        Default { throw "no version was selected or not supported" }
    }

    New-ItemIfNotExists -Path $Outpath -ItemType Directory
    $Outpath = (Get-Item -Path $Outpath).FullName
    $SQLEvalSetupPath = "$($Version)-SSEI-Eval.exe"
    $SQLEvalSetupFullPath = "$($Outpath)\$($SQLEvalSetupPath)"

    # downloading eval setup
    try {
        Start-FileDownload -DownloadURL $DownloadURL -FileOutPath $SQLEvalSetupFullPath
    }
    catch {
        throw "error downloading eval setup: $($PSItem.Exception.Message)"
    }

    # downloading iso
    try {
        $ISOPath = (Get-ChildItem -Path $Outpath | Where-Object -FilterScript { $PSItem.Name -like "SQL*.iso" }).FullName
        if ($null -ne $ISOPath -and (Test-Path -Path $ISOPath) -eq $true) {
            if ((Get-Item $ISOPath).LastWriteTime -gt (Get-Date).AddHours(-2)) {
                # juenger als zwei stunden
                # do nothing
                Write-Output "found sql iso at $($ISOPath), will use it"
            } 
            else {
                Write-Output "found sql iso at $($ISOPath), removing the files because too old"
                Remove-Item -Path $ISOPath -Recurse -Force | Out-Null
            }            
        }
        Write-Output "downloading sql iso with eval setup"
        $Arguments = "/ACTION=Download /MEDIAPATH=$($Outpath)\ /MEDIATYPE=ISO /LANGUAGE=en-US /QUIET"
        Start-Process $SQLEvalSetupFullPath -ArgumentList $Arguments -Wait -NoNewWindow

        Remove-Item -Path $SQLEvalSetupFullPath -Force
    }
    catch {
        throw "error downloading iso: $($PSItem.Exception.Message)"
    }
    Write-Output "finished download - check folder $($Outpath)"
}

function Initialize-SQLSetup {
    <#
        .Description
        prepares the sql installation files for i
 
        .Parameter Outpath
        path where the data should be cached
 
        .Example
        Initialize-SQLSetup -Outpath $TempFolderForSQL
 
        .NOTES
        downloads the sql install files and stores them extracted to the outpath
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateSet("2019", "2022")]
        [string]
        $Version = "2022",

        [Parameter(Mandatory = $true)]
        [string]
        $Outpath
    )
    
    New-ItemIfNotExists -Path $Outpath -ItemType Directory
    $Outpath = (Get-Item -Path $Outpath).FullName

    # checks
    $SQLFileFolder = "$($Outpath)\SQLFiles\"
    if ((Test-Path -Path $SQLFileFolder) -eq $true ) {
        throw "folder $($SQLFileFolder) already exits, please cleanup or skip"
    }

    # downloading iso
    try {
        Get-SQLISO -Version $Version -Outpath $Outpath
    }
    catch {
        throw "error during download - $($PSItem.Exception.Message)"
    }
    
    # copying files
    try {
        if ((Test-Path -Path $SQLFileFolder) -eq $true) {
            if ((Get-Item $SQLFileFolder).LastWriteTime -gt (Get-Date).AddHours(-2)) {
                # juenger als zwei stunden
                # do nothing
                Write-Output "found setup files at $($SQLFileFolder) will use it"
            } 
            else {
                Write-Output "found setup files at $($SQLFileFolder), removing the files because too old"
                Remove-Item -Path $SQLFileFolder -Recurse -Force | Out-Null
            }
        }
        Write-Output "mounting iso"
        $ISO = Get-ChildItem -Path $Outpath | Where-Object -FilterScript { $PSItem.Name -like "SQL*.iso" }
        $MountedISOs = Mount-DiskImage -PassThru -ImagePath $ISO.FullName
        $Volume = Get-Volume -DiskImage $MountedISOs
        Write-Output "copy files from iso to $($SQLFileFolder)"
        Copy-Item -Path "$($Volume.DriveLetter):\" -Destination $SQLFileFolder -Recurse -Force
        Write-Output "finished copy job"
        Dismount-DiskImage $MountedISOs.ImagePath | Out-Null
        Write-Output "iso was dismounted"
    }
    catch {
        throw "error during mount or copy - $($PSItem.Exception.Message)"
    }
}

function Install-SQLADServiceAccount {
    <#
        .Description
        adds the local server to a group with permission to the serviec account and then installs it
 
        .Parameter SQLServiceAccount
        name of the group managed service account
 
        .Parameter GroupWithPermissions
        name of the group with permissions to retrieve the password of the group managed service account
 
        .Example
        Install-SQLADServiceAccount -SQLServiceAccount $SQLEngine.Name -GroupWithPermissions $SQLEngine.GroupWithPermissions
 
        .NOTES
        - the server needs to be member of a domain
        - the group managed service account must exit
        - the user which runs this command must have permissions to add the local device to the group
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $SQLServiceAccount,

        [Parameter(Mandatory = $true)]
        [string]
        $GroupWithPermissions
    )

    if ((Get-CimInstance -ClassName Win32_ComputerSystem).PartofDomain -eq $false) {
        throw "not a member of a domain"
    }

    Install-WindowsFeature -Name RSAT-AD-PowerShell | Out-Null

    if ($null -eq (Get-ADServiceAccount -Identity $SQLServiceAccount -ErrorAction SilentlyContinue)) {
        throw "service account $($SQLServiceAccount)$ could not be found"
    }

    # adding device to group
    try {
        Write-Output "adding to group $($GroupWithPermissions)"
        $Self = Get-ADComputer -Identity $env:COMPUTERNAME
        Add-ADGroupMember -Identity $GroupWithPermissions -Members $Self
    }
    catch {
        throw "not able to add to group $($GroupWithPermissions): $($PSItem.Exception.Message)"
    }
    
    # installing and testing service account
    try {
        Write-Output "installing $($SQLServiceAccount)"
        Start-Process -FilePath klist -ArgumentList "purge -lh 0 -li 0x3e7" -NoNewWindow -Wait
        Start-Sleep -Seconds 5
        Start-Process -FilePath klist -ArgumentList "purge -lh 0 -li 0x3e7" -NoNewWindow -Wait
        Start-Sleep -Seconds 10
        Install-ADServiceAccount -Identity ($SQLServiceAccount + "$")
        if ((Test-ADServiceAccount -Identity ($SQLServiceAccount + "$") -WarningAction SilentlyContinue) -eq $false) {
            throw "service account $($SQLServiceAccount)$ is not installed, please verify"
        }
    }
    catch {
        throw "could not install $($SQLServiceAccount) - $($PSItem.Exception.Message)"
    }
}

function Install-SQLInstance {
    <#
        .Description
        adds the local server to a group with permission to the serviec account and then installs it
 
        .Parameter Name
        name of group managed service account
         
        .Parameter UseLocalAccount
        specify if local accounts are used
 
        .Parameter UseGmSA
        specify if group managed service accounts are used
         
        .Parameter EngineAccountName
        service account name for sql engine
 
        .Parameter AgentAccountName
        service account name for sql agent
 
        .Parameter UseMixedAuth
        when used, the instance will use sql and windows authentication
 
        .Parameter SAPWD
        sa pwd
         
        .Parameter INSTALLSQLDATADIR
        specifies the data directory for SQL Server data files
         
        .Parameter INSTANCEDIR
        specifies nondefault installation directory for instance-specific components
         
        .Parameter SQLBACKUPDIR
        backup file path for the sql database
         
        .Parameter SQLUSERDBDIR
        user file path for the sql database
 
        .Parameter SQLUSERDBLOGDIR
        user log file path for the sql database
         
        .Parameter SQLTEMPDBDIR
        temp file path for the sql database
         
        .Parameter SQLTEMPDBLOGDIR
        temp log file path for the sql database
 
        .Parameter Features
        features that should be installed, like 'SQLENGINE,FULLTEXT,CONN'
         
        .Parameter SQLSYSADMINACCOUNTS
        accounts that should be admin on that instance
 
        .Parameter SQLMinRAM
        minimum ram for instance
 
        .Parameter SQLMaxRAM
        maximum ram for instance
 
        .Example
        Install-SQLInstance -Name "PB1" `
            -Features "SQLENGINE" `
            -SQLSYSADMINACCOUNTS ('"' + "IC\T1-CMAdmin" + '" "' + "IC\T1-CMAdmins" + '"') `
            -SQLBACKUPDIR "S:\SQL\PB1\BACKUP\DATA" `
            -SQLUSERDBDIR "S:\SQL\PB1\USERDATA\DATA" `
            -SQLUSERDBLOGDIR "S:\SQL\PB1\USERLOG\LOG" `
            -SQLTEMPDBDIR "S:\SQL\PB1\TEMPDATA\DATA" `
            -SQLTEMPDBLOGDIR "S:\SQL\PB1\TEMPLOG\LOG" `
            -UseGmSA `
            -EngineAccountName $SQLEngine `
            -AgentAccountName $SQLAgent
 
        .Example
        Install-SQLInstance -Name "INSTANCE" -UseLocalAccount
 
        .NOTES
         
    #>


    [CmdletBinding(DefaultParameterSetName = 'local')]
    param (
        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $SetupPath = ".\SQLFiles\setup.exe",

        [Parameter(ParameterSetName = 'local', Mandatory = $true)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'local')]
        [switch]
        $UseLocalAccount,

        [Parameter(ParameterSetName = 'gmsa')]
        [switch]
        $UseGmSA,

        [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)]
        [string]
        $EngineAccountName,

        [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)]
        [string]
        $AgentAccountName,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [switch]
        $UseMixedAuth,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SAPWD,

        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $INSTALLSQLDATADIR,
        
        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $INSTANCEDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLBACKUPDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLUSERDBDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLUSERDBLOGDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLTEMPDBDIR,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLTEMPDBLOGDIR,

        [Parameter(ParameterSetName = 'local', Mandatory = $false)]
        [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)]
        [string]
        $Features,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLSYSADMINACCOUNTS,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLMinRAM,

        [Parameter(ParameterSetName = 'local')]
        [Parameter(ParameterSetName = 'gmsa')]
        [string]
        $SQLMaxRAM
    )

    # define arguments
    if ($UseLocalAccount -eq $false -and $UseGmSA -eq $false) {
        throw "you have to specify 'UseLocalAccount' or 'UseGmSA'"
    }
    if ($UseMixedAuth -eq $false -and $null -eq $SAPWD) {
        throw "you have to specify 'SAPWD' when 'UseMixedAuth' is used"
    }
    if ($uselocalaccount) {
        $AGTSVCACCOUNT = 'NT Service\SQLAgent$' + $Name
        $SQLSVCACCOUNT = 'NT Service\MSSQL$' + $Name
    }
    elseif ($UseGmSA) {
        $AGTSVCACCOUNT = $env:USERDOMAIN + "\" + (Get-ADServiceAccount -Identity $AgentAccountName).SamAccountName
        $SQLSVCACCOUNT = $env:USERDOMAIN + "\" + (Get-ADServiceAccount -Identity $EngineAccountName).SamAccountName
    }
    $SQLTELSVCACCT = 'NT Service\SQLTELEMETRY$' + $Name
    if ($Features -eq "") {
        $Features = "SQLENGINE"
    }
    if ($null -eq $SQLBACKUPDIR) {
        $SQLBACKUPDIR = $INSTALLSQLDATADIR + "\BACKUP\DATA"
    }
    if ($null -eq $SQLUSERDBDIR) {
        $SQLUSERDBDIR = $INSTALLSQLDATADIR + "\USER\DATA"
    }
    if ($null -eq $SQLUSERDBLOGDIR) {
        $SQLUSERDBLOGDIR = $INSTALLSQLDATADIR + "\USERLOG\LOG"
    }
    if ($null -eq $SQLTEMPDBDIR) {
        $SQLTEMPDBDIR = $INSTALLSQLDATADIR + "\TEMP\DATA"
    }
    if ($null -eq $SQLTEMPDBLOGDIR) {
        $SQLTEMPDBLOGDIR = $INSTALLSQLDATADIR + "\TEMPLOG\LOG"
    }
    if ($SQLSYSADMINACCOUNTS -eq "") {
        $SQLSYSADMINACCOUNTS = "`"$($env:COMPUTERNAME)\Administrator`""
    }

    # check dependencies
    try {
        if ($UseGmSA) {
            if ($env:USERDOMAIN -eq $env:COMPUTERNAME) {
                throw "you are logged in with a local user, domain user required"
            }
            if ((Test-ADServiceAccount -Identity "$($EngineAccountName)$") -eq $false) {
                throw "gmsa $($EngineAccountName) not installed"
            }
            if ((Test-ADServiceAccount -Identity "$($AgentAccountName)$") -eq $false) {
                throw "gmsa $($AgentAccountName) not installed"
            }
        }
        $InstalledInstances = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server' -ErrorAction SilentlyContinue).InstalledInstances
        if ($InstalledInstances -contains $Name) {
            throw "SQL instance is already installed $($Name)"
        }
    }
    catch {
        throw "error during prerequesits check : $($Psitem.Exception.Message)"
    }

    # start installation
    try {
        Write-Output "starting install of instance $($Name)"
        $Arguments = @(
            '/IACCEPTSQLSERVERLICENSETERMS="True"'
            '/IACCEPTPYTHONLICENSETERMS="False"'
            '/ACTION="Install"'
            '/ENU="True"'
            '/IACCEPTROPENLICENSETERMS="False"'
            '/SUPPRESSPRIVACYSTATEMENTNOTICE="False"'
            '/QUIET="True"'
            '/QUIETSIMPLE="False"'
            '/UpdateEnabled="True"'
            '/USEMICROSOFTUPDATE="False"'
            '/SUPPRESSPAIDEDITIONNOTICE="False"'
            '/UpdateSource="MU"'
            ('/FEATURES=' + $Features)
            ('/INSTANCENAME="' + $Name + '"')
            ('/INSTALLSHAREDDIR="' + $env:ProgramFiles + '\Microsoft SQL Server"')
            ('/INSTALLSHAREDWOWDIR="' + ${env:ProgramFiles(x86)} + '\Microsoft SQL Server"')
            ('/INSTANCEID="' + $Name + '"')
            ('/SQLTELSVCACCT="' + $SQLTELSVCACCT + '"')
            '/SQLTELSVCSTARTUPTYPE="Automatic"'            
            ('/AGTSVCACCOUNT="' + $AGTSVCACCOUNT + '"')
            '/AGTSVCSTARTUPTYPE="Automatic"'
            '/SQLSVCSTARTUPTYPE="Automatic"'
            '/SQLCOLLATION="SQL_Latin1_General_CP1_CI_AS"'
            ('/SQLSVCACCOUNT="' + $SQLSVCACCOUNT + '"')
            ('/SQLSYSADMINACCOUNTS=' + $SQLSYSADMINACCOUNTS)
            ('/SQLBACKUPDIR="' + $SQLBACKUPDIR + '"')
            ('/SQLUSERDBDIR="' + $SQLUSERDBDIR + '"')
            ('/SQLUSERDBLOGDIR="' + $SQLUSERDBLOGDIR + '"')
            ('/SQLTEMPDBDIR="' + $SQLTEMPDBDIR + '"')
            ('/SQLTEMPDBLOGDIR="' + $SQLTEMPDBLOGDIR + '"')
            '/TCPENABLED="1"'
            '/NPENABLED="0"'
            '/BROWSERSVCSTARTUPTYPE="Automatic"'
        )
        if ($UseMixedAuth -eq $true) {
            $SAArguments = @(
                '/SECURITYMODE="SQL"'
                '/SAPWD="' + $SAPWD + '"'
            )
            $Arguments = $Arguments + $SAArguments
        }
        if ($INSTALLSQLDATADIR -ne "") {
            $INSTALLSQLDATADIR_Arguments = @(
                ('/INSTALLSQLDATADIR="' + $INSTALLSQLDATADIR + '"')
            )
            $Arguments = $Arguments + $INSTALLSQLDATADIR_Arguments
        }
        if ($INSTANCEDIR -ne "") {
            $INSTANCEDIR_Arguments = @(
                ('/INSTANCEDIR="' + $INSTANCEDIR + '"')
            )
            $Arguments = $Arguments + $INSTANCEDIR_Arguments
        }
        if ($SQLMinRAM -ne "") {
            $MinRAMArguments = @(
                ('/SQLMINMEMORY="' + $SQLMinRAM + '"')
            )
            $Arguments = $Arguments + $MinRAMArguments
        }
        if ($SQLMaxRAM -ne "") {
            $MaxRAMArguments = @(
                ('/SQLMAXMEMORY="' + $SQLMaxRAM + '"')
            )
            $Arguments = $Arguments + $MaxRAMArguments
        }

        $Process = Start-Process $SetupPath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
        if ($Process.ExitCode -ne 0 -and $Process.ExitCode -ne 3010) {
            $Message = Search-SQLSummaryLog -SearchString "Exit message"
            throw "check logs of sql setup - $($env:ProgramFiles)\Microsoft SQL Server\*\Setup Bootstrap\Log `nexit code: $($Process.ExitCode)`nmessage found: $($Message)"
        }
        elseif ($Process.ExitCode -eq 3010) {
            if (Test-RebootPending) {
                Write-Warning "A reboot is pending, please reboot before procceeding"
            }
        }
    }
    catch {
        throw "error during installation of sql instance $($Name): $($Psitem.EXception.Message)"
    }
    # service customization
    try {
        if ($UseGmSA) {
            Write-Output "configuring SQLAgent`$$($Name) service to autostart delayed"
            $Agent = (Get-Service -Name "SQLAgent`$$($Name)").Name
            Start-Process "sc" -ArgumentList ('config "' + $($Agent) + '" start=delayed-auto') -NoNewWindow

            Write-Output "configuring MSSQL`$$($Name) service to autostart delayed"
            $Engine = (Get-Service -Name "MSSQL`$$($Name)").Name    
            Start-Process "sc" -ArgumentList ('config "' + $($Engine) + '" start=delayed-auto') -NoNewWindow
        }
    }
    catch {
        throw "error during service customization : $($Psitem.EXception.Message)"
    }
    Write-Output "finished install of instance $($Name)"
}

function Set-SQLInstanceStaticPort {
    <#
        .Description
        configures the sql instance to use a static tcp port for all ips
 
        .Parameter InstanceName
        sql instance name
         
        .Parameter StaticPort
        port which should be used
 
        .Example
        Set-SQLInstanceStaticPort -InstanceName "INSTANCE" -StaticPort "1503"
 
        .NOTES
        Support is only available for SQL 2019 Standard / Enterprise
    #>

    
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $InstanceName,

        [Parameter(Mandatory = $true)]
        [string]
        $StaticPort
    )

    $RegPath = (Resolve-Path -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL*$($InstanceName)\MSSQLServer\SuperSocketNetLib\Tcp\IPAll\").Path
    
    if (Test-Path -Path $RegPath) {
        try {
            $SQLSettings = Get-ItemProperty -Path $RegPath
            if ($SQLSettings.TcpDynamicPorts -ne $StaticPort) {
                Write-Output "set tcp port of $($InstanceName) to $($StaticPort)"
                Set-ItemProperty -Path $RegPath -Name TcpPort -Value $StaticPort
                Set-ItemProperty -Path $RegPath -Name TcpDynamicPorts -Value ""
            }
            Write-Output "restarting $($InstanceName) to apply change"
            Get-Service "*$($InstanceName)*" | Restart-Service -Force -WarningAction SilentlyContinue
        }
        catch {
            throw "could not configure the static port on $($InstanceName) - $($PSItem.Exception.Message)"
        }
    }
    else {
        throw "could not find the registry path for the instance"
    }
}

function Test-SQLDatabaseConnection {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True)] 
        [string] 
        $Server,

        [Parameter(Position = 1, Mandatory = $false)] 
        [string] 
        $Database = "master",

        [Parameter(Position = 2, Mandatory = $True, ParameterSetName = "SQLAuth")] 
        [pscredential] 
        $SACredential,

        [Parameter(Position = 2, Mandatory = $True, ParameterSetName = "WindowsAuth")] 
        [switch] 
        $UseWindowsAuthentication
    )

    if ($Server -notlike "*\*") {
        $Server = "$($env:COMPUTERNAME)\$($Server)"
    }

    # connect to the database, then immediatly close the connection. If an exception occurrs it indicates the conneciton was not successful.
    $dbConnection = New-Object System.Data.SqlClient.SqlConnection
    if (!$UseWindowsAuthentication) {
        $dbConnection.ConnectionString = "Data Source=$($Server); uid=$($SACredential.UserName); pwd=$($SACredential.GetNetworkCredential().Password); Database=$($Database);Integrated Security=False"
    }
    else {
        $dbConnection.ConnectionString = "Data Source=$($Server); Database=$($Database);Integrated Security=True;"
    }
    try {
        Measure-Command { $dbConnection.Open() } | Out-Null
        $Success = $true
    }
    # exceptions will be raised if the database connection failed.
    catch {
        $Success = $false
    }
    Finally {
        # close the database connection
        $dbConnection.Close()
    }

    return $Success
}

function Get-SQLCU {
    <#
        .Description
        downloads sql cu from microsoft
 
        .Parameter Version
        version of the cu
 
        .Parameter Outpath
        path where the cu is stored
 
        .Example
        Get-SQLISO -Version 2019_latest -Outpath $Outpath
 
        .NOTES
        downloads the sql enterprise eval setup and with it the iso
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2019_latest", "2022_latest")]
        [string]
        $Version,

        [Parameter(Mandatory = $true)]
        [string]
        $Outpath
    )

    $ErrorActionPreference = 'Stop'
    if ($Outpath[-1] -eq "\") {
        $Outpath = $Outpath.Substring(0, $Outpath.Length - 1)
    }
    $SQLCUFilePath = "$($Outpath)\SQL-$($Version)-cu.exe"

    switch ($Version) {
        "2019_latest" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=100809" }
        "2022_latest" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=105013" }
        Default { throw "no version was selected or not supported" }
    }

    try {
        $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL
        $UpdateLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href

        Start-FileDownload -DownloadURL $UpdateLink -FileOutPath $SQLCUFilePath
    }
    catch {
        throw "error getting sql cu files - $($PSItem.Exception.Message)"
    }

    Write-Output "finished download - check folder $($Outpath)"
}

function Install-SQLCU {
    <#
        .Description
        installs sql 2019 latest cu to all local instances
 
        .Parameter Version
        version string of sql cu, like 2019_latest
 
        .Parameter TempFolder
        folder, where files will be stored temporally
 
        .Example
        Install-SQLCU -Version "2019_latest"
 
        .NOTES
 
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2019_latest", "2022_latest")]
        [string]
        $Version,

        [Parameter(Mandatory = $false)]
        [string]
        $TempFolder = "$($env:ProgramData)\NTS\SQL\CU"
    )

    $ErrorActionPreference = 'Stop'
    $SQLCUFilePath = "$($TempFolder)\SQL-$($Version)-cu.exe"
    $SQLCUUnpackedFilePath = "$($TempFolder)\$($Version)-cu\"

    try {
        Get-SQLCU -Version $Version -Outpath $TempFolder

        # unpacking sql cu to file system
        if ((Test-Path -Path $SQLCUFilePath) -eq $false) {
            throw "cannot find cu package file"
        }
        Write-Output "unpacking sql cu"
        $Arguments = "/X:$($SQLCUUnpackedFilePath)"
        $Process = Start-Process $SQLCUFilePath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
        if ($Process.ExitCode -ne 0) {
            throw "there was an error unpacking cu files"
        }
    }
    catch {
        throw "error getting sql cu files - $($PSItem.Exception.Message)"
    }
    
    try {
        # installation
        Write-Output "installing sql cu"
        $Arguments = "/ACTION=Patch /ALLINSTANCES /QUIET /IACCEPTSQLSERVERLICENSETERMS /ENU"
        $Process = Start-Process ($SQLCUUnpackedFilePath + "setup.exe") -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
        if ($Process.ExitCode -ne 0) {
            $FinalResult = Search-SQLSummaryLog -SearchString "Final result"
            $ExitMessage = Search-SQLSummaryLog -SearchString "Exit message"

            if ($ExitMessage -like "*No features were updated during the setup execution. The requested features may not be installed or features are already at a higher patch level*") {
                Write-Output "no updates were applied, because already at a higher or equal patch level"
            }
            elseif ($FinalResult -like "*Passed but reboot required, see logs for details*") {
                Write-Output "sql cu install but reboot required"
            }
            else {
                throw "there was an error installing sql cu - $($ExitMessage)"
            }
        }
    
        # cleanup
        Start-FolderCleanUp -FolderToRemove $TempFolder
    }
    catch {
        throw "error sql cu installation - $($PSItem.Exception.Message) - see logs at C:\Program Files\Microsoft SQL Server\*\Setup Bootstrap\Log\"
    }
}

function Install-SQLSSMS {
    <#
        .Description
        this installs the latest version of sql sql management studio
 
        .Example
        Install-SQLSSMS
 
        .NOTES
        always installs the latest version from https://aka.ms/ssmsfullsetup
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [ValidateSet("Latest","19.1","19.0.2","19.0.1","18.12.1","17.9.1")]
        [string]
        $Version = "Latest"
    )

    switch ($Version) {
        "Latest" { $SetupURL = "https://aka.ms/ssmsfullsetup" }
        "19.1" { $SetupURL = "https://download.microsoft.com/download/a/c/a/aca4e29f-6925-4d50-a06b-5576c6ea629f/SSMS-Setup-ENU.exe" }
        "19.0.2" { $SetupURL = "https://download.microsoft.com/download/9/f/8/9f8197f4-0f71-42a3-8717-b2817c77b820/SSMS-Setup-ENU.exe" }
        "19.0.1" { $SetupURL = "https://download.microsoft.com/download/a/3/2/a32ae99f-b6bf-4a49-a076-e66503ccb925/SSMS-Setup-ENU.exe" }
        "18.12.1" { $SetupURL = "https://download.microsoft.com/download/8/a/8/8a8073d2-2e00-472b-9a18-88361d105915/SSMS-Setup-ENU.exe" }
        "17.9.1" { $SetupURL = "https://download.microsoft.com/download/D/D/4/DD495084-ADA7-4827-ADD3-FC566EC05B90/SSMS-Setup-ENU.exe" }
        Default { throw "no version was specified or not supported" }
    }
    
    # static vars
    $ErrorActionPreference = 'Stop'

    # install
    try {
        Confirm-RunningAsAdministrator
        Install-GenericApplication -Name "SSMS-$($Version)" -InstallerURL $SetupURL -SetupParameter "/quiet /norestart" -CleanUpInstallFiles
    }
    catch {
        throw "error installing - $($PSItem.Exception.Message)"
    }
}

function Get-SQLSSRSSetup {
    <#
        .Description
        this downloads the sql reporting services setup
 
        .Parameter Version
        version of sql reporting services setup
 
        .Parameter Outpath
        where should the setup file be stored
 
        .Example
        Get-SQLSSRSSetup -TempFolder $SSRSTempFolder
 
        .NOTES
        setup file name will be "$($TempFolder)\SQL$($Version)_ReportingServices-latest.exe"
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2022","2019")]
        [string]
        $Version,

        [Parameter(Mandatory = $false)]
        [string]
        $Outpath = "$($env:ProgramData)\NTS\SQL\ReportingServices"
    )
    
    try {
        switch ($Version) {
            "2022" { $DownloadURL = "https://download.microsoft.com/download/8/3/2/832616ff-af64-42b5-a0b1-5eb07f71dec9/SQLServerReportingServices.exe" }
            "2019" { $DownloadURL = "https://download.microsoft.com/download/1/a/a/1aaa9177-3578-4931-b8f3-373b24f63342/SQLServerReportingServices.exe" }
            Default { throw "no version was specified or not supported" }
        }

        $SQL_RPServices_FilePath = "$($Outpath)\SQL$($Version)_ReportingServices-latest.exe"
        New-ItemIfNotExists -Path $Outpath -ItemType Directory

        # $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL
        # $SetupLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href
        Write-Output "downloading sql reporting services"
        Start-FileDownload -DownloadURL $DownloadURL -FileOutPath $SQL_RPServices_FilePath
    }
    catch {
        throw "$($PSItem.Exception.Message)"
    }
}

function Install-SQLSSRS {
    <#
        .Description
        this will install sql reporting services
 
        .Parameter Version
        version of sql reporting services setup
 
        .Parameter SetupFilePath
        path to setup file
 
        .Example
        Install-SQLSSRS -Version 2019
 
        .NOTES
        setup file name must be at "$($TempFolder)\SQL$($Version)_ReportingServices-latest.exe"
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2022","2019")]
        [string]
        $Version,

        [Parameter(Mandatory = $false)]
        [string]
        $SetupFilePath = "$($env:ProgramData)\NTS\SQL\ReportingServices\SQL$($Version)_ReportingServices-latest.exe",

        [Parameter(Mandatory = $false)]
        [string]
        $SQL_RPServices_LogPath = "$($env:ProgramData)\NTS\SQL\ReportingServices\install.log"
    )

    try {
        if (Test-Path -Path $SetupFilePath) {
            Write-Output "installing sql reporting services"
            $Arguments = "/quiet /IAcceptLicenseTerms /Edition=Eval /norestart /log $($SQL_RPServices_LogPath)"
            $Process = Start-Process $SetupFilePath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru
            
            if ($Process.ExitCode -eq 3010) {
                if (Test-RebootPending) {
                    Write-Warning "A reboot is pending, please reboot before procceeding"
                }
            }
            elseif ($Process.ExitCode -ne 0 -and $Process.ExitCode -ne 3010) {
                throw "error installing ssrs - exit code $($Process.ExitCode) `ncheck logs at $($SQL_RPServices_LogPath)"
            }
        }
        else {
            throw "setup file not found at $($SetupFilePath)"
        }
    }
    catch {
        throw "- $($PSItem.Exception.Message)"
    }
}

function Initialize-SQLSSRS {
    <#
        .Description
        this will configure the sql reporting services
 
        .Parameter Version
        version of sql reporting services setup
 
        .Parameter SQL_Instance
        mssql instance
 
        .Parameter SSRS_ServiceAccountCredentials
        credentials for the service account of ssrs, must be an domain user
 
        .Parameter SQL_DB_Name
        name of ssrs db
 
        .Example
        Initialize-SQLSSRS -SQL_Instance "$($env:COMPUTERNAME)\$($using:CM_SQL_RPT_InstanceName)" -SSRS_ServiceAccountCredentials $using:CM_SQL_SSRS_ServiceAccountCredentials -TempFolder $SSRSTempFolder
 
        .NOTES
        https://blog.aelterman.com/2018/01/03/complete-automated-configuration-of-sql-server-2017-reporting-services/
        https://github.com/mrsquish/AutomationScripts/blob/main/ConfigureSSRS.ps1
        https://gist.github.com/SvenAelterman/f2fd058bf3a8aa6f37ac69e5d5dd2511
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("2022","2019", "2017")]
        [string]
        $Version,

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

        [Parameter(Mandatory = $true)]
        [pscredential]
        $SSRS_ServiceAccountCredentials,

        [Parameter(Mandatory = $false)]
        [string]
        $SQL_DB_Name = "ReportServer"
    )

    switch ($Version) {
        "2022" { $WMI_Namespace = "root\Microsoft\SqlServer\ReportServer\RS_SSRS\v16\Admin" } #?????
        "2019" { $WMI_Namespace = "root\Microsoft\SqlServer\ReportServer\RS_SSRS\v15\Admin" }
        "2017" { $WMI_Namespace = "root\Microsoft\SqlServer\ReportServer\RS_SSRS\v14\Admin" }
        Default { throw "no version was selected or the version is unsupported" }
    }

    if ($SQL_Instance -notlike "*\*") {
        $SQL_Instance = "$($env:COMPUTERNAME)\$($SQL_Instance)"
    }

    # test sql instance connection
    if ((Test-SQLDatabaseConnection -Server $SQL_Instance -UseWindowsAuthentication) -eq $false) {
        throw "could not connect to sql instance $($SQL_Instance) using integrated security"
    }

    try {
        $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace
        If ($RSConfig.IsInitialized -eq $true) {
            throw "ssrs is already initialized, stopping"
        }
        Write-Output "configuring sql reporting services"
    
        # set service account
        $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace
        $useBuiltInServiceAccount = $false
        Write-Output "configuring $($SSRS_ServiceAccountCredentials.UserName) as service account for ssrs"
        $RSConfig.SetWindowsServiceIdentity($useBuiltInServiceAccount, $($SSRS_ServiceAccountCredentials.UserName), $($SSRS_ServiceAccountCredentials.GetNetworkCredential().Password)) | out-null
    
        # need to reset the URLs for domain service account to work
        Write-Output "configuring http urls"
        $HTTPport = 80
        $RSConfig.RemoveURL("ReportServerWebService", "http://+:$($HTTPport)", 1033) | out-null
        $RSConfig.RemoveURL("ReportServerWebApp", "http://+:$($HTTPport)", 1033) | out-null
        $RSConfig.SetVirtualDirectory("ReportServerWebService", "ReportServer", 1033) | out-null
        $RSConfig.SetVirtualDirectory("ReportServerWebApp", "Reports", 1033) | out-null
        $RSConfig.ReserveURL("ReportServerWebService", "http://+:$($HTTPport)", 1033) | out-null
        $RSConfig.ReserveURL("ReportServerWebApp", "http://+:$($HTTPport)", 1033) | out-null
     
        # restart SSRS service for changes to take effect
        # Restart-Service -Name $RSConfig.ServiceName -Force
    
        # retrieve the current configuration
        $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace
        # get the ReportServer and ReportServerTempDB creation script
        [string]$dbscript = $RSConfig.GenerateDatabaseCreationScript($SQL_DB_Name, 1033, $false).Script
    
        # import the SQL Server PowerShell module
        Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force -Scope Process
        Import-Module "sqlps" -DisableNameChecking | Out-Null

        # establish a connection to the database server
        # $ErrorActionPreference = 'SilentlyContinue'
        # $RunCount = 0
        # do {
        Write-Output "connecting to instance $($SQL_Instance)"
        $conn = New-Object Microsoft.SqlServer.Management.Common.ServerConnection -ArgumentList $SQL_Instance
        $conn.ApplicationName = "SSRS Configuration Script"
        $conn.StatementTimeout = 0
        $conn.Connect()
        # Start-Sleep -Seconds 5
        # $RunCount++
        # }
        # while ($conn.IsOpen -eq $false -and $RunCount -lt 6)
        # $ErrorActionPreference = 'Continue'

        if ($conn.IsOpen -ne $true) {
            throw "could not connect to $($SQL_Instance)"
        }
        $smo = New-Object Microsoft.SqlServer.Management.Smo.Server -ArgumentList $conn

        # create the ReportServer and ReportServerTempDB databases
        Write-Output "generating databases in instance $($SQL_Instance)"
        $db = $smo.Databases["master"]
        $db.ExecuteNonQuery($dbscript)
    
        # set permissions for the databases
        $dbscript = $RSConfig.GenerateDatabaseRightsScript($RSConfig.WindowsServiceIdentityConfigured, $SQL_DB_Name, $false, $true).Script
        $db.ExecuteNonQuery($dbscript)
    
        # set the database connection info # check hresult auf wert 0
        Write-Output "configuring db connection for ssrs to instance $($SQL_Instance) with db $($SQL_DB_Name)"
        $RSConfig.SetDatabaseConnection($SQL_Instance, $SQL_DB_Name, 2, "", "") | Out-Null
        $RSConfig.InitializeReportServer($RSConfig.InstallationID) | Out-Null
    
        # restart services # check hresult auf wert 0
        $RSConfig.SetServiceState($false, $false, $false) | Out-Null
        Restart-Service $RSConfig.ServiceName
        $RSConfig.SetServiceState($true, $true, $true) | Out-Null
    }
    catch {
        throw "$($PSItem.Exception.Message)"
    }
}

function Search-SQLSummaryLog {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $SearchString
    )
    
    try {
        $SummaryLogFilePath = (Get-Item -Path "$($env:ProgramFiles)\Microsoft SQL Server\*\Setup Bootstrap\Log\Summary.txt" | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1).FullName
        $Content = Get-Content -Path $SummaryLogFilePath

        if ($null -eq $Content) {
            throw "file could not be found"
        }
        else {
            $FilteredContent = $Content | Select-String -Pattern $SearchString
            if ($null -eq $FilteredContent) {
                return "no message found with selected search pattern"
            }
            else {
                $Message = $FilteredContent[0].Tostring().Replace($SearchString, "").Trim()
                if ($Message -like "*:*") {
                    $Message = $Message.Replace(":", "").Trim()
                }
                return $Message
            }
        }
    }
    catch {
        throw $PSItem.Exception.Message
    }
}

function Add-SQLDBRole {
    <#
        .Description
        this function can be used to add a role to db for an sql login
 
        .Parameter InstanceName
        name of the instance
 
        .Parameter SQLogin
        sql login name
 
        .Parameter DBName
        database name
 
        .Parameter DBRole
        role that should be granted to the sql login
 
        .Example
        Add-SQLDBRole -InstanceName "$($env:COMPUTERNAME)\$using:CM_SQL_WSUS_InstanceName" -SQLogin "NT AUTHORITY\SYSTEM" -DBName "SUSDB" -DBRole 'db_owner'
 
        .NOTES
        https://www.sqlservercentral.com/blogs/use-powershell-to-add-a-login-to-a-database-role-in-all-databases
    #>

    
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $InstanceName,

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

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

        [Parameter(Mandatory = $true)]
        [string]
        $DBRole
    )
    
    try {
        # Load the SMO assembly
        [System.Reflection.Assembly]::LoadWithPartialName( 'Microsoft.SqlServer.SMO') | Out-Null

        # Connect to the instance using SMO
        $SQLServer = new-object ('Microsoft.SqlServer.Management.Smo.Server') $InstanceName

        # Get the defined login - if it doesn't exist it's an error
        $SQLoginObject = $SQLServer.Logins[$SQLogin]
        if ($null -eq $SQLoginObject) {
            throw "$($SQLogin) is not a valid SQL Server Login on this instance."
        }

        $SQLLoginName = $SQLoginObject.Name
        $SQLDatabaseObject = $SQLServer.Databases[$DBName]

        # Check to see if the login is a user in this database
        $SQLUserObject = $SQLDatabaseObject.Users[$SQLLoginName]
        if ($null -eq $SQLUserObject) {
            # Not present, so add it
            $SQLUserObject = New-Object ('Microsoft.SqlServer.Management.Smo.User') ($SQLDatabaseObject, $SQLLoginName)
            $SQLUserObject.Login = $SQLLoginName
            $SQLUserObject.Create()
        }

        # Check to see if the user is a member of the db_owner role
        if ($SQLUserObject.IsMember($DBRole) -ne $True) {
            # Not a member, so add that role
            $SQLConnection = new-object system.data.SqlClient.SqlConnection("Data Source=$($InstanceName);Integrated Security=SSPI;Initial Catalog=$($DBName)");
            $SQLConnection.Open()
            $SQLQuery = "EXEC sp_addrolemember @rolename = N'$($DBRole)', @membername = N'$($SQLLoginName)'"
            $SQLCommand = new-object "System.Data.SqlClient.SqlCommand" ($SQLQuery, $SQLConnection)
            Write-Output "adding db role $($DBRole) to $($SQLogin) on instance $($InstanceName) for db $($DBName)"
            $SQLCommand.ExecuteNonQuery() | out-null
            $SQLConnection.Close()
        }    
    }
    catch {
        throw "could not grant sql db role - $($PSItem.Exception.Message)"
    }
}

function Add-SQLMountPoint {
    <#
        .Description
        this function creates a mountpoint
 
        .Parameter DiskNumber
        number of disk which should be configured
 
        .Parameter SQLBasePath
        where should be mountpoint be created
 
        .Parameter DiskLabel
        label for the volume and the underlaying folders
 
        .Example
        Confirm-HyperV
 
        .NOTES
         
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $DiskNumber,

        [Parameter(Mandatory = $false)]
        [string]
        $SQLBasePath = "C:\SQL\",

        [Parameter(Mandatory = $true)]
        [string]
        $DiskLabel
    )
    $MountPath = $SQLBasePath + $DiskLabel
    Write-Output "formating disk for $($DiskLabel)"
    Set-Disk -Number $DiskNumber -IsOffline $false
    Set-Disk -Number $DiskNumber -IsReadOnly $false
    Get-Disk -Number $DiskNumber | Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -UseMaximumSize | Format-Volume -NewFileSystemLabel $DiskLabel -FileSystem ReFS -AllocationUnitSize 65536 -SetIntegrityStreams $false | Out-Null
    New-Item -ItemType Directory -Path $MountPath | Out-Null
    Get-Partition -DiskNumber $DiskNumber | Add-PartitionAccessPath -AccessPath $MountPath -ErrorAction SilentlyContinue

    if ($DiskLabel -like "*LOG*") {
        New-Item -ItemType Directory -Path "$($SQLBasePath)\$($DiskLabel)\LOG" -ErrorAction SilentlyContinue | Out-Null
    }
    else {
        New-Item -ItemType Directory -Path "$($SQLBasePath)\$($DiskLabel)\DATA" -ErrorAction SilentlyContinue | Out-Null
    }
    Start-Sleep -Seconds 2
}

function Add-VirtualSQLDiskVolume {
    <#
        .Description
        this function creates a virtual disk on a storage pool and configures a volume on it
 
        .Parameter DiskName
        friendly name of the virtual disk
         
        .Parameter PoolName
        friendly name of the storage pool
 
        .Parameter Size
        size of the virtual disk
 
        .Parameter DriveLetter
        volume drive letter
 
        .Parameter ResiliencySettingName
        raid level of virtual disk
 
        .Parameter AllocationUnitSize
        volume allocation unit size
 
        .Example
        Add-VirtualSQLDiskVolume -DiskName "VDISK1" -PoolName "POOL01" -Size 20Gb -DriveLetter "K" -VolumeLabel "DATA"
 
        .NOTES
         
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $DiskName,

        [Parameter(Mandatory = $true)]
        $PoolName,

        [Parameter(Mandatory = $true)]
        $Size,

        [Parameter(Mandatory = $true)]
        $DriveLetter,

        [Parameter(Mandatory = $true)]
        $VolumeLabel,

        [Parameter(Mandatory = $false)]
        $ResiliencySettingName = "Simple",

        [Parameter(Mandatory = $false)]
        $AllocationUnitSize = 65536
    )

    try {
        Write-Output "creating disk $($DiskName) for $($VolumeLabel)"
        New-VirtualDisk -FriendlyName $DiskName -StoragePoolFriendlyName $PoolName -Size 25GB -ResiliencySettingName $ResiliencySettingName | Out-Null
        Get-Disk -FriendlyName $DiskName | New-Volume -FileSystem NTFS -AllocationUnitSize $AllocationUnitSize -DriveLetter $DriveLetter -FriendlyName $VolumeLabel | Out-Null
    }
    catch {
        throw "error creating disk $($DiskName): $($PSItem.Exception.Message)"
    }
}