Wednesday, June 3, 2015

PowerShell 5.0 - what's new for the DSC guy - part 2

PowerShell 5.0 class based DSC resources simplifies the development of custom resources by making use of the concepts from object oriented programming like class, properties, methods, inheritance, method overriding etc. With the support for inheritance now it’s much easier to create a composite resource or a resource that can make use of the functionality of an existing resource to do something extra.

We'll see how we can use this functionality to build a SharePoint content database that makes use of the xDatabase resource to further extend the database properties based on the specifications. The resources are both saved in the same file and the base resource must at least include one key property and the Get, Set and Test method in the class defined resource or the base resource.
To create the folder structure, modules and manifest files, we use the code snippet from the previous post.

$resourcePath = "C:\Program Files\WindowsPowerShell\Modules\xSPContentDb"

#Create the folder in PSModulePath
New-Item -ItemType Directory -Path "$resourcePath"

#Create a module manifest for the DSC resource
New-ModuleManifest  -Path "$resourcePath\xSPContentDb.psd1" `
                    -Author "Prajeesh Prathap" `
                    -CompanyName "Prowareness" `
                    -Guid ([System.Guid]::NewGuid()) `
                    -RootModule "xSPContentDb.psm1" `
                    -Description "DSC resource sample" `
                    -PowerShellVersion 5.0 `
                    -DscResourcesToExport @('xSPContentDb', 'xDatabase') `
                    -ModuleVersion 1.0 `
                    -Copyright '(c) 2015 Prajeesh Prathap. All rights reserved'

PSEDIT "$resourcePath\xSPContentDb.psd1"

#Create the module files
New-Item -ItemType File -Path "$resourcePath\xSPContentDb.psm1"
PSEDIT "$resourcePath\xSPContentDb.psm1"

The base resource (xDatabase) is defined as given below. As you can see, we’ve implemented the Get, Set and Test methods in the base resource. When we create the resource for the Content database, we’ll see how we can override some of these methods/ properties and add some additional properties to the resource.

[DscResource()]
class xDatabase
{
    [DsCProperty(Key)]
    [System.String] $SQLServerInstance

    [DsCProperty(Key)]
    [System.String] $DBName

    [DsCProperty(Mandatory)]
    [System.String]  $DataFileLocation

    [DsCProperty(Mandatory)]
    [System.String]  $LogFileLocation

    [DsCProperty(Mandatory)]
    [System.Double]  $InitialDataSizeInMB

    [DsCProperty(Mandatory)]
    [System.Double]  $InitialLogSizeInMB

    [DsCProperty(Mandatory)]
    [System.Double]  $DataGrowFactor

    [DsCProperty(Mandatory)]
    [System.Double]  $LogGrowFactor

    [DsCProperty(Mandatory)]
    [System.String]  $Collation

    [DsCProperty(Mandatory)]
    [Ensure] $Ensure

    [DsCProperty(NotConfigurable)]
    [Boolean] $IsValid

    [xDatabase] Get()
    {
        [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
        $sqlServer = new-object Microsoft.SqlServer.Management.Smo.Server($this.SQLServerInstance)
        $this.IsValid = $false

        foreach ($_ in $sqlServer.Databases)
        {
            if ($_.Name -eq $this.DBName)
            {
                $this.IsValid = $true
            }
        }
        return $this   
    }

    [void] Set()
    {
        [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
        $sqlServer = new-object Microsoft.SqlServer.Management.Smo.Server($this.SQLServerInstance)                
       
        if($this.Ensure -eq [Ensure]::Present)
        {       
            Write-Verbose "Creating database $($this.DBName)"
            $db = new-object Microsoft.SqlServer.Management.Smo.Database ($sqlServer, $this.DBName)
            if($this.Collation -ne $null)
            {
                $db.Collation = $this.Collation                   
            }

            ## Create a new primary filegroup
            $dbPrimaryFileGroup = new-object ('Microsoft.SqlServer.Management.Smo.FileGroup') ($db, 'PRIMARY')
            $db.FileGroups.Add($dbPrimaryFileGroup)

            ## Create the data file and add it to the primary filegroup
            $dbDataFile = new-object ('Microsoft.SqlServer.Management.Smo.DataFile') ($dbPrimaryFileGroup, $this.DBName)
            $dbPrimaryFileGroup.Files.Add($dbDataFile)

            $dbDataFile.FileName = $this.DataFileLocation + '\' + $this.DBName + '.mdf'
            $dbDataFile.Size = $this.InitialDataSizeInMB * 1024
            $dbDataFile.GrowthType = "KB"
            $dbDataFile.Growth = $this.DataGrowFactor * 1024
            $dbDataFile.IsPrimaryFile = "True"

            ## Create the log file
            $dbLogFilename = $this.DBName + "_Log"
            $dbLogFile = new-object ('Microsoft.SqlServer.Management.Smo.LogFile') ($db, $dbLogFilename)
            $db.LogFiles.Add($dbLogFile)
            $dbLogFile.FileName = $this.LogFileLocation + '\' + $dbLogFilename + '.ldf'
            $dbLogFile.Size = $this.InitialLogSizeInMB * 1024
            $dbLogFile.GrowthType ="KB"
            $dbLogFile.Growth = $this.LogGrowFactor * 1024

            $db.Create()
        }
        else
        {
            $db = new-object Microsoft.SqlServer.Management.Smo.Database
            $db = $sqlServer.Databases[$this.DBName]
            if($db -ne $null)
            {
                Write-Verbose "Dropping database $($this.DBName)"
                $db.Drop()
            }
        }
    }

    [bool] Test()
    {
        $result = $this.Get()

        if($this.Ensure -eq [Ensure]::Present)
        {       
            if($result.IsValid)
            {
                Write-Verbose "Database $($this.DBName) already exists on SQL Server: $($this.SQLServerInstance)"     
                return $true
            }
            else
            {
                Write-Verbose "Database $($this.DBName) does not exist on SQL Server : $($this.SQLServerInstance), the database will be created"
                return $false                     
            }
        }
        else
        {
            if($this.IsValid)
            {
                Write-Verbose "Database $($this.DBName) exists on SQL Server : $($this.SQLServerInstance), the database will be removed"
                return $false
            }
            else
            {
                Write-Verbose "Database $($this.DBName) does not exist on SQL Server: $($this.SQLServerInstance)"
                return $true
            }
        }
    } 
}


The xSPContentDb resource extends the xDatabase resource and for this example, I’ve added some default values to the location to manage the primary and log files etc. The additional property to mention the RecoveryModel on the database is configured at the xSPContentDb resource object. Also I’ve decided to override the Test and Set methods to add additional checks and alterations on the database.
[DscResource()]
class xSPContentDb : xDatabase
{
    [DsCProperty(NotConfigurable)]
       [System.String]      $DataFileLocation = "D:\SQLDataFiles"

    [DsCProperty(NotConfigurable)]
       [System.String]      $LogFileLocation = "D:\SQLLogFile"

    [DsCProperty(NotConfigurable)]
       [System.Double]      $InitialDataSizeInMB = 20

    [DsCProperty(NotConfigurable)]
       [System.Double]      $InitialLogSizeInMB = 5

    [DsCProperty(NotConfigurable)]
       [System.Double]      $DataGrowFactor = 5

    [DsCProperty(NotConfigurable)]
       [System.Double]      $LogGrowFactor = 2  

    [DsCProperty(Mandatory)]
       [System.String]      $RecoveryMode

    [void] Set()
    {
        $isValid = ([xDatabase]$this).Test()
        if($isValid)
        {
            Write-Verbose "Setting recovery mode on database $($this.DBName)"
            [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
               $sqlServer = new-object Microsoft.SqlServer.Management.Smo.Server($this.SQLServerInstance)      
            $db = new-object Microsoft.SqlServer.Management.Smo.Database
            $db = $sqlServer.Databases[$this.DBName]
            $db.RecoveryModel = $this.RecoveryMode
            $db.Alter()
        }
        else
        {
            ([xDatabase]$this).Set()
        }
    }

    [bool] Test()
    {
        $isValid = ([xDatabase]$this).Test()
        if($isValid)
        {
            [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
               $sqlServer = new-object Microsoft.SqlServer.Management.Smo.Server($this.SQLServerInstance)      
            $db = new-object Microsoft.SqlServer.Management.Smo.Database
            $db = $sqlServer.Databases[$this.DBName]
            if($db.RecoveryModel -eq "Simple")
            {
                if($this.RecoveryMode -eq "Simple")
                {
                    Write-Verbose "$($this.DBName) already has recovery mode set to Simple"
                    return $true
                }
                else
                {
                    Write-Verbose "$($this.DBName) already does not have recovery mode set to Simple. Will be set to simple"
                    return $false
                }
            }
            else
            {
                if($this.RecoveryMode -eq "Simple")
                {
                    Write-Verbose "$($this.DBName) already does not have recovery mode set to Simple. Will be set to Full"
                    return $false
                }
                else
                {
                    Write-Verbose "$($this.DBName) already has recovery mode set to Full"
                    return $true
                }
            }
        }
        return $false
    }
}

Note that some of the properties that are mandatory in the base resource are not mandatory in the xSPContentDb class. Also the ([xDatabase]$this).Test() syntax is used to call the methods on the base class.

Now you can create a configuration for creating a SharePoint content database like.
Configuration SPContentDBConfig
{
    Import-Dscresource -ModuleName xSPContentDb    
   
    xSPContentDb SPContentDB
    {
        SQLServerInstance = MYServer'
        DBName = 'PS5DSCDemoDB'
        Collation = "Latin1_General_CI_AS_KS_WS"
        RecoveryMode = "Full"
        Ensure = "Present"      
    }       
}

SPContentDBConfig  


PS E:\PSDemo5> Start-DscConfiguration -Wait -Force -Path E:\PSDemo5\SPContentDBConfig -Verbose -Debug
VERBOSE: Perform operation 'Invoke CimMethod' with following parameters, ''methodName' = SendConfigurationApply,'className' = MSFT_DSCLocalConfigurationManager,'namespaceName' = root/Microsoft/Windo
ws/DesiredStateConfiguration'.
VERBOSE: An LCM method call arrived from computer PRA-DEV with user sid S-1-5-21-1229272821-1801674531-839522115-133922.
VERBOSE: [PRA-DEV]: LCM:  [ Start  Set      ]
VERBOSE: [PRA-DEV]: LCM:  [ Start  Resource ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]: LCM:  [ Start  Test     ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]:                            [[xSPContentDb]SPContentDB] Database PS5DSCDemoDB does not exist on SQL Server : PRA-DEV, the database will be created
VERBOSE: [PRA-DEV]: LCM:  [ End    Test     ]  [[xSPContentDb]SPContentDB]  in 0.7500 seconds.
VERBOSE: [PRA-DEV]: LCM:  [ Start  Set      ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]:                            [[xSPContentDb]SPContentDB] Database PS5DSCDemoDB does not exist on SQL Server : PRA-DEV, the database will be created
VERBOSE: [PRA-DEV]:                            [[xSPContentDb]SPContentDB] Creating database PS5DSCDemoDB
VERBOSE: [PRA-DEV]: LCM:  [ End    Set      ]  [[xSPContentDb]SPContentDB]  in 8.6090 seconds.
VERBOSE: [PRA-DEV]: LCM:  [ End    Resource ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]: LCM:  [ End    Set      ]
VERBOSE: [PRA-DEV]: LCM:  [ End    Set      ]    in  10.5460 seconds.
VERBOSE: Operation 'Invoke CimMethod' complete.
VERBOSE: Time taken for configuration job to complete is 10.66 seconds

PS E:\PSDemo5> E:\PSDemo5\SPContentDBConfig.ps1


    Directory: E:\PSDemo5\SPContentDBConfig


Mode                LastWriteTime         Length Name                                                                                                                                                
----                -------------         ------ ----                                                                                                                                                
-a----         3-6-2015     16:49           2082 localhost.mof                                                                                                                                      



PS E:\PSDemo5> Start-DscConfiguration -Wait -Force -Path E:\PSDemo5\SPContentDBConfig -Verbose -Debug
VERBOSE: Perform operation 'Invoke CimMethod' with following parameters, ''methodName' = SendConfigurationApply,'className' = MSFT_DSCLocalConfigurationManager,'namespaceName' = root/Microsoft/Windo
ws/DesiredStateConfiguration'.
VERBOSE: An LCM method call arrived from computer PRA-DEV with user sid S-1-5-21-1229272821-1801674531-839522115-133922.
VERBOSE: [PRA-DEV]: LCM:  [ Start  Set      ]
VERBOSE: [PRA-DEV]: LCM:  [ Start  Resource ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]: LCM:  [ Start  Test     ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]:                            [[xSPContentDb]SPContentDB] Database PS5DSCDemoDB already exists on SQL Server: PRA-DEV
VERBOSE: [PRA-DEV]:                            [[xSPContentDb]SPContentDB] PS5DSCDemoDB already has recovery mode set to Full
VERBOSE: [PRA-DEV]: LCM:  [ End    Test     ]  [[xSPContentDb]SPContentDB]  in 0.8910 seconds.
VERBOSE: [PRA-DEV]: LCM:  [ Skip   Set      ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]: LCM:  [ End    Resource ]  [[xSPContentDb]SPContentDB]
VERBOSE: [PRA-DEV]: LCM:  [ End    Set      ]
VERBOSE: [PRA-DEV]: LCM:  [ End    Set      ]    in  1.9210 seconds.
VERBOSE: Operation 'Invoke CimMethod' complete.
VERBOSE: Time taken for configuration job to complete is 2.003 seconds


PS E:\PSDemo5> Test-DscConfiguration -Path E:\PSDemo5\SPContentDBConfig | fl


InDesiredState             : True
ResourcesInDesiredState    : {[xSPContentDb]SPContentDB}
ResourcesNotInDesiredState :
ReturnValue                : 0
PSComputerName             : localhost



No comments: