DSCResources/VDD_WemDatabase/VDD_WemDatabase.psm1

Import-LocalizedData -BindingVariable localizedData -FileName VDD_WemDatabase.Resources.psd1;

function Get-TargetResource {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSDSCUseVerboseMessageInDSCResource', '')]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseServer,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseFilesFolder,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $VuemUserSqlPassword,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $WemInfrastructureServiceAccount,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DefaultAdministratorsGroup,

        [Parameter()]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()] [ValidateSet('Present','Absent')]
        [System.String] $Ensure = 'Present'
    )
    begin {

        #Test if SqlServer module if available
        if (-not (Get-Module -ListAvailable -Name SqlServer)) {
            ThrowInvalidProgramException -ErrorId 'SqlServerModuleNotFoundError' -ErrorMessage $localizedData.SqlServerModuleNotFoundError;
        }

    } #end begin
    process {

        $targetResource = @{
            DatabaseServer = $DatabaseServer;
            DatabaseName = '';
            DatabaseFilesFolder = '';
            VuemUserSqlPassword = '';
            WemInfrastructureServiceAccount = '';
            DefaultAdministratorsGroup = '';
            Ensure = '';
        }

        #if ($PSBoundParameters.ContainsKey('Credential')) {
            Import-Module -Name SqlServer;

            #Check if database $DatabaseName exist
            if (TestMSSQLDatabase -DatabaseServer $DatabaseServer -DatabaseName $DatabaseName) {
                $targetResource['DatabaseName'] = $DatabaseName;
                $targetResource['Ensure'] = 'Present';

                #Check WEM Default Administrator Group
                #Only the Group SID is stored in dbo.VUEMAdministrators table
                $AdObj = New-Object System.Security.Principal.NTAccount($DefaultAdministratorsGroup)
                $strSID = $AdObj.Translate([System.Security.Principal.SecurityIdentifier])
                $DefaultAdministratorsGroupSID = $strSID.Value
                $checkDefaultAdministratorsGroup = Invoke-Sqlcmd -Query "SELECT * FROM dbo.VUEMAdministrators WHERE Name = '$DefaultAdministratorsGroupSID'" -ServerInstance $DatabaseServer -Database $DatabaseName
                if ($null -ne $checkDefaultAdministratorsGroup ) {
                    $targetResource['DefaultAdministratorsGroup'] = $DefaultAdministratorsGroup;
                }

                #Check Database files folder
                $checkDatabaseFilesFolder = Invoke-Sqlcmd -Query "SELECT name, physical_name AS current_file_location FROM sys.master_files WHERE name LIKE '%$DatabaseName%'" -ServerInstance $DatabaseServer;
                if ($null -ne $checkDatabaseFilesFolder.current_file_location[0] ) {
                    $targetResource['DatabaseFilesFolder'] = Split-Path -Path $checkDatabaseFilesFolder.current_file_location[0];
                }

                #Check VuemUserSqlPassword
                $checkVuemUserSqlPassword = New-Object System.Data.DataTable
                $checkVuemUserSqlPassword = Invoke-Sqlcmd -Query "SELECT * FROM master.dbo.syslogins WHERE name = 'VuemUser' and PWDCOMPARE('$VuemUserSqlPassword', password) = 1" -ServerInstance $DatabaseServer;
                if ($checkVuemUserSqlPassword.name -eq 'VuemUser'){
                    $targetResource['VuemUserSqlPassword'] = $VuemUserSqlPassword;
                }


                #Check if Wem Infrastructure Service account is allowed to use the database
                $databasePermissionsQuery= "SELECT Us.name AS username, Obj.name AS object, dp.permission_name AS permission FROM sys.database_permissions dp JOIN sys.sysusers Us ON dp.grantee_principal_id = Us.uid AND Us.name = '$WemInfrastructureServiceAccount' JOIN sys.sysobjects Obj ON dp.major_id = Obj.id";
                $databasePermissions = Invoke-Sqlcmd -Query $databasePermissionsQuery -ServerInstance $DatabaseServer -Database $DatabaseName;
                if ($null -ne $databasePermissions ) {
                    $targetResource['WemInfrastructureServiceAccount'] = $WemInfrastructureServiceAccount;
                }

            }
            else {
                $targetResource['Ensure'] = 'Absent';
            }

        #}

        #else {
        #}

        return $targetResource;

    } #end process
} #end function Get-TargetResource


function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseServer,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseFilesFolder,

        #Specific password for the WEM vuemUser SQL user account. Leave empty to create a default password.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $VuemUserSqlPassword,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $WemInfrastructureServiceAccount,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DefaultAdministratorsGroup,

        [Parameter()]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()] [ValidateSet('Present','Absent')]
        [System.String] $Ensure = 'Present'
    )
    process {
        #Get the data from target node
        $targetResource = Get-TargetResource @PSBoundParameters;

        #Normalize DatabaseFilesFolder to prepare the test
        $targetDatabaseFilesFolder = ''
        if ($targetResource.DatabaseFilesFolder) {
            $targetDatabaseFilesFolder = Join-Path $targetResource.DatabaseFilesFolder "";
        }

        $desiredDatabaseFilesFolder = Join-Path $DatabaseFilesFolder "";

        if (($targetResource.Ensure -eq $Ensure) -and 
            ($targetResource.DatabaseName -eq $DatabaseName) -and 
            ($targetDatabaseFilesFolder -eq $desiredDatabaseFilesFolder) -and 
            ($targetResource.VuemUserSqlPassword -eq $VuemUserSqlPassword) -and
            ($targetResource.WemInfrastructureServiceAccount -eq $WemInfrastructureServiceAccount) -and 
            ($targetResource.DefaultAdministratorsGroup -eq $DefaultAdministratorsGroup)) {

            Write-Verbose ($localizedData.DatabaseDoesExist -f $DatabaseName, $DatabaseServer);
            Write-Verbose ($localizedData.ResourceInDesiredState -f $DatabaseName);
            $inDesiredState = $true;

        }
        else {
            $inDesiredState = $false;
        }

        if ($inDesiredState) {

            Write-Verbose ($localizedData.ResourceInDesiredState);
            return $true;
        }
        else {

            Write-Verbose ($localizedData.ResourceNotInDesiredState);
            return $false;
        }

    } #end process
} #end function Test-TargetResource


function Set-TargetResource {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseServer,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseName,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseFilesFolder,

        #Specific password for the WEM vuemUser SQL user account. Leave empty to create a default password.
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $VuemUserSqlPassword,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $WemInfrastructureServiceAccount,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DefaultAdministratorsGroup,

        [Parameter()]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential,

        [Parameter()] [ValidateSet('Present','Absent')]
        [System.String] $Ensure = 'Present'
    )
    begin {

        #Test if WEM SDK module is available
        if (-not (Test-Path -Path "${Env:ProgramFiles(x86)}\Norskale\Norskale Infrastructure Services\SDK\WemDatabaseConfiguration\WemDatabaseConfiguration.psd1" -PathType leaf)) {
            ThrowInvalidProgramException -ErrorId 'WEMSdkNotFound' -ErrorMessage $localizedData.WEMSDKNotFoundError;
        }

        #Test if SqlServer module if available
        if (-not (Get-Module -ListAvailable -Name SqlServer)) {
            ThrowInvalidProgramException -ErrorId 'SqlServerModuleNotFoundError' -ErrorMessage $localizedData.SqlServerModuleNotFoundError;
        }

    } #end begin
    process {

        $scriptBlock = {
            #Import Citrix WEM SDK Powershell module
            Import-Module "${Env:ProgramFiles(x86)}\Norskale\Norskale Infrastructure Services\SDK\WemDatabaseConfiguration\WemDatabaseConfiguration.psd1" -Verbose:$false;
            Import-Module -Name SqlServer;

            #GET data
            $targetResource = Get-TargetResource @PSBoundParameters;

            if ($Ensure -eq 'Present') {
                #Normalize DatabaseFilesFolder to prepare the DatabaseFileFolder test
                $targetDatabaseFilesFolder = ''
                if ($targetResource.DatabaseFilesFolder) {
                    $targetDatabaseFilesFolder = Join-Path $targetResource.DatabaseFilesFolder "";
                }
                $desiredDatabaseFilesFolder = Join-Path $DatabaseFilesFolder "";

                #Convert plain-text VuemUserSqlPassword to Secure-String
                $VuemUserSqlPasswordSecureString = ConvertTo-SecureString -AsPlainText $VuemUserSqlPassword -Force


                #If database does not exist : create database
                if (-not ($targetResource.DatabaseName -eq $DatabaseName)) {
                    $databaseFileName = Join-Path $DatabaseFilesFolder $DatabaseName;
                    New-WemDatabase -DatabaseServerInstance $DatabaseServer -DatabaseName $DatabaseName -DataFilePath($databaseFileName+"_Data.mdf") -LogFilePath($databaseFileName+"_Log.ldf") -DefaultAdministratorsGroup $DefaultAdministratorsGroup -VuemUserSqlPassword $VuemUserSqlPasswordSecureString -WindowsAccount $WemInfrastructureServiceAccount;
                    Write-Verbose ($using:localizedData.CreatingWEMDatabase -f $using:DatabaseName, $using:DatabaseServer);
                }
                else {
                    #If default administator group is wrong : Configure Default Administrators Group
                    if (-not ($targetResource.DefaultAdministratorsGroup -eq $DefaultAdministratorsGroup)) {
                        $AdObj = New-Object System.Security.Principal.NTAccount($DefaultAdministratorsGroup)
                        $strSID = $AdObj.Translate([System.Security.Principal.SecurityIdentifier])
                        $DefaultAdministratorsGroupSID = $strSID.Value

                        $null = Invoke-Sqlcmd -Query "INSERT INTO dbo.VUEMAdministrators ([Name], [Description], [State], [Type], [Permissions], [RevisionId]) VALUES ('$DefaultAdministratorsGroupSID', NULL, 1, 2, '<?xml version=`"1.0`" encoding=`"utf-8`"?><ArrayOfVUEMAdminPermission xmlns:xsd=`"http://www.w3.org/2001/XMLSchema`" xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`"><VUEMAdminPermission><idSite>0</idSite><AuthorizationLevel>FullAccess</AuthorizationLevel></VUEMAdminPermission></ArrayOfVUEMAdminPermission>', 1)" -ServerInstance $DatabaseServer -Database $DatabaseName

                    }

                    #If database files folder is wrong, move database files to the correct directory
                    if (-not ( $targetDatabaseFilesFolder -eq $desiredDatabaseFilesFolder)) {
                        #Get the logical name of the data and log files associated with the database by typing the following:
                        #USE master SELECT name, physical_name FROM sys.master_files WHERE database_id = DB_ID("Personnel");
                        $databaseFiles = Invoke-Sqlcmd -Query "SELECT name, physical_name AS current_file_location FROM sys.master_files WHERE name LIKE '%$DatabaseName%'" -ServerInstance $DatabaseServer;

                        #Take the database you want to work with offline
                        $null = Invoke-Sqlcmd -Query "ALTER DATABASE $DatabaseName SET offline WITH ROLLBACK IMMEDIATE" -ServerInstance $DatabaseServer

                        #Move one file at a time to the new location
                        foreach ($databaseFile in $databaseFiles) {
                            $fileName = $databaseFile.name
                            $file = Split-Path -Path $databaseFile.current_file_location  -Leaf
                            $newDatabaseFilePath = Join-Path $DatabaseFilesFolder $file
                            $null = Invoke-Sqlcmd -Query "ALTER DATABASE $DatabaseName MODIFY FILE ( NAME = $fileName, FILENAME = `"$newDatabaseFilePath`")" -ServerInstance $DatabaseServer
                        }

                        #Put the database back online
                        $null = Invoke-Sqlcmd -Query "ALTER DATABASE $DatabaseName SET online" -ServerInstance $DatabaseServer
                    }

                    #If VuemUserSqlPassword is wrong, reset it to the desired value
                    if (-not ($targetResource.VuemUserSqlPassword -eq $VuemUserSqlPassword)) {
                        $null = Invoke-Sqlcmd -Query "ALTER LOGIN vuemUser WITH PASSWORD = '$VuemUserSqlPassword'" -ServerInstance $DatabaseServer
                    }

                    #If WemInfrastructureServiceAccount is wrong, add the correct user login and permissions. Remove wrog user permissions.
                    if (-not ($targetResource.WemInfrastructureServiceAccount -eq $WemInfrastructureServiceAccount)) {
                        # Add the user into SQL.
                        [string]$err = Invoke-Sqlcmd -Query "CREATE LOGIN `[$WemInfrastructureServiceAccount`] FROM WINDOWS WITH DEFAULT_DATABASE=`[MASTER`], DEFAULT_LANGUAGE=`[us_english`]" -ServerInstance $DatabaseServer
                        
                        # This variable will become populated if an error occurred; else it will remain blank.
                        if (-not $err) {
                            #Assign permissions to WemInfrastructureServiceAccount on WEM DatabaseName
                            [string]$err = Invoke-Sqlcmd -Query "GRANT CONNECT TO `[$WemInfrastructureServiceAccount`] AS `[dbo`]" -ServerInstance $DatabaseServer -Database $DatabaseName;
                            [string]$err = Invoke-Sqlcmd -Query "GRANT CREATE PROCEDURE TO `[$WemInfrastructureServiceAccount`] AS `[dbo`]" -ServerInstance $DatabaseServer -Database $DatabaseName;
                            [string]$err = Invoke-Sqlcmd -Query "GRANT CREATE QUEUE TO `[$WemInfrastructureServiceAccount`] AS `[dbo`]" -ServerInstance $DatabaseServer -Database $DatabaseName;
                            [string]$err = Invoke-Sqlcmd -Query "GRANT CREATE SERVICE TO `[$WemInfrastructureServiceAccount`] AS `[dbo`]" -ServerInstance $DatabaseServer -Database $DatabaseName;
                            [string]$err = Invoke-Sqlcmd -Query "GRANT SUBSCRIBE QUERY NOTIFICATIONS TO `[$WemInfrastructureServiceAccount`] AS `[dbo`]" -ServerInstance $DatabaseServer -Database $DatabaseName;
                            
                            if ($err) {
                                Write-Verbose("Error Assigning Permisssions to $WemInfrastructureServiceAccount : $err");
                            }
                        
                        }
                        else {
                            Write-Verbose("The following error occurred while creating SQL User: $err ");
                        }
                    }

                }
            }
            #If ensure eq Absent, drop the existing database
            else {
                $null = Invoke-Sqlcmd -Query "DROP DATABASE $DatabaseName" -ServerInstance $DatabaseServer
            }


        } #end scriptBlock

        $invokeCommandParams = @{
            ScriptBlock = $scriptBlock;
            ErrorAction = 'Stop';
        }

        #if ($Credential) {
        # AddInvokeScriptBlockCredentials -Hashtable $invokeCommandParams -Credential $Credential;
        #}
        #else {
            $invokeCommandParams['ScriptBlock'] = [System.Management.Automation.ScriptBlock]::Create($scriptBlock.ToString().Replace('$using:','$'));
        #}

        #$scriptBlockParams = @($Credential, $SiteName, $DatabaseServer, $DataStore, $DatabaseName);
        #Write-Verbose ($localizedData.InvokingScriptBlockWithParams -f [System.String]::Join("','", $scriptBlockParams));

        [ref] $null = Invoke-Command @invokeCommandParams;

    } #end process
} #end function Set-TargetResource

#region Private Functions

function TestMSSQLDatabase {
    <#
    .SYNOPSIS
        Tests for the presence of a MS SQL Server database.
    .NOTES
        This function requires CredSSP to be enabled on the local machine to communicate with the MS SQL Server.
    #>

    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseServer,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [System.String] $DatabaseName,

        [Parameter()]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.CredentialAttribute()]
        $Credential
    )
    process {

        $scriptBlock = {

            $sqlConnection = New-Object -TypeName 'System.Data.SqlClient.SqlConnection';
            $sqlConnection.ConnectionString = 'Server="{0}";Integrated Security=SSPI;' -f $using:DatabaseServer;
            $sqlCommand = $sqlConnection.CreateCommand();
            $sqlCommand.CommandText = "SELECT name FROM master.sys.databases WHERE name = N'$using:DatabaseName'";
            $sqlDataAdapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter -ArgumentList $sqlCommand;
            $dataSet = New-Object -TypeName System.Data.DataSet;

            try {

                [ref] $null = $sqlDataAdapter.Fill($dataSet);
                if ($dataSet.Tables.Name) { return $true; } else { return $false; }
            }
            catch [System.Data.SqlClient.SqlException] {

                Write-Verbose $_;
                return $false;
            }
            finally {

                $sqlCommand.Dispose();
                $sqlConnection.Close();
            }

        } #end scriptblock

        $invokeCommandParams = @{
            ScriptBlock = $scriptBlock;
            ErrorAction = 'Stop';
        }

        if ($Credential) {
            AddInvokeScriptBlockCredentials -Hashtable $invokeCommandParams -Credential $Credential;
        }
        else {
            $invokeCommandParams['ScriptBlock'] = [System.Management.Automation.ScriptBlock]::Create($scriptBlock.ToString().Replace('$using:','$'));
        }

        Write-Verbose ($localizedData.InvokingScriptBlockWithParams -f [System.String]::Join("','", @($DatabaseServer, $DatabaseName)));

        return Invoke-Command @invokeCommandParams;

    } #end process
} #end function TestMSSQLDatabase

function ThrowInvalidProgramException {
<#
    .SYNOPSIS
        Throws terminating error of category NotInstalled with specified errorId and errorMessage.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.String] $ErrorId,

        [Parameter(Mandatory)]
        [System.String] $ErrorMessage
    )
    process {

        $errorCategory = [System.Management.Automation.ErrorCategory]::NotInstalled;
        $exception = New-Object -TypeName 'System.InvalidProgramException' -ArgumentList $ErrorMessage;
        $errorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList $exception, $ErrorId, $errorCategory, $null;
        throw $errorRecord;

    } #end process
} #end function ThrowInvalidProgramException

#endregion Private Functions

$moduleRoot = Split-Path -Path $MyInvocation.MyCommand.Path -Parent;

## Import the XD7Common library functions
$moduleParent = Split-Path -Path $moduleRoot -Parent;
#Import-Module (Join-Path -Path $moduleParent -ChildPath 'VE_XD7Common');

Export-ModuleMember -Function *-TargetResource;