Creating a Class based DSC Resource using PowerShell

Scenario: I would like to think I am fairly competent at creating DSC Resources using .MOF files. But with the upcoming release of Windows Management Framework 5.0 we can now write DSC Resources using Classes. I have never done anything with a Class. I am going to attempt to figure out how to write a simple Class based DSC Resource.

I am going to start with this TechNet Article, because it was the first thing that came up when I Googled “Create Class based DSC Resource”.

I am going to create a DSC Resource called MyTestClassResource. The first thing I need to do is create the appropriate folder structure, which I do by running the following two commands:

New-Item -ItemType Directory -Path 'C:\Program Files\WindowsPowerShell\Modules\MyClassResources'
New-Item -ItemType Directory -Path 'C:\Program Files\WindowsPowerShell\Modules\MyClassResources\MyTestClassResource'

Next, I need to create the Module Manifest.

$Path = 'C:\Program Files\WindowsPowerShell\Modules\MyClassResources\MyTestClassResource'
New-ModuleManifest -Path $Path\MyTestClassResource.psd1 -Author 'Jacob Benson' -ModuleVersion 1.0 -Description 'Module for Testing Class Based DSC Resources'

The .PSM1 file is where I define and create my Class based resource.

New-Item -ItemType File -Path $Path\MyTestClassResource.psm1
PSEdit -filenames 'C:\Program Files\WindowsPowerShell\Modules\MyClassResources\MyTestClassResource\MyTestClassResource.psm1'

Now, let’s get to creating this Resource. I am going to go super simple here and create a Resource that will just ensure that a Folder exists. Yes, I know the File DSC Resource already does this, that’s not the point :). After fiddling around for a little bit here is what my Class DSC Resource “Skeleton” looks like.

enum Ensure 
{ 
    Absent 
    Present 
}

[DscResource()]
class MyTestClassResource{

    [DscProperty(Key)]
    [string]$Path

    
    [DscProperty(Mandatory)]
    [Ensure] $Ensure

    [DscProperty(Mandatory)]
    [ValidateSet("Directory","File")]
    [string]$ItemType

    #Replaces Get-TargetResource
    [MyTestClassResource] Get() 
    {

    }

    #Replaces Test-TargetResource
    [bool] Test()
    {

    }

    #Replaces Set-TargetResource
    [void] Set()
    {

    }


}

The rest of this should be pretty straightforward. Right? The first thing I try to do in the Get section is to test and see if the path exists.

$Item = Test-Path $Path

This doesn’t work however because “Variable is not assigned in the method”, whatever the hell that means. Looking at the article it uses a $This object (I have no idea if that’s even the right word) with the variables to do things, so let’s try a different tactic.

$Item = Test-Path $This.Path

And that works fine, so clearly $This is some special thing I need to be using going forward. This should be fun :). Now that I got that working, the rest of this Method was pretty straight forward.

    #Replaces Get-TargetResource
    [MyTestClassResource] Get() 
    {
        $Item = Test-Path $This.Path

        If($Item -eq $True)
        {
            $This.Ensure = [Ensure]::Present
        }
        Else
        {
            $This.Ensure = [Ensure]::Absent
        }

        Return $This

    }

After that, the Test Method is pretty easy as well. However, I have no idea what is going on with the whole Return -not $Item part, I am just following along from the example and hoping that it works.

    #Replaces Test-TargetResource
    [bool] Test()
    {
        $Item = Test-Path $This.Path

        If($This.Ensure -eq [Ensure]::Present)
        {
            Return $Item
        }
        Else
        {
            Return -not $Item
        }

    }

Onto the Set Method. One thing I noticed when creating this is that I don’t need an Else block with an If statement, which is nice.

    #Replaces Set-TargetResource
    [void] Set()
    {
        $Item = Test-Path $This.Path
        If($This.Ensure -eq [Ensure]::Present)
        {
            If(-not $Item)
            {
                Write-Verbose "Creating Folder"
                New-Item -ItemType Directory -Path $This.Path
            }
        }
        #If [Ensure]::Absent
        Else
        {
            If($Item)
            {
                Write-Verbose "File exists and should be absent.  Deleting file"
                Remove-Item -Path $This.Path
            }
        }

    }

With that done I need to re-create the Module Manifest that I made earlier with some important information.

$Path = 'C:\Program Files\WindowsPowerShell\Modules\MyClassResources\MyTestClassResource'
New-ModuleManifest -Path $Path\MyTestClassResource.psd1 -Author 'Jacob Benson' -ModuleVersion 1.0 -Description 'Module for Testing Class Based DSC Resources' -RootModule 'MyTestClassResource.psm1' -DscResourcesToExport *

At this point while trying to run Get-DSCResource I realized that my folder structure I created at the beginning was not correct, because I was used to the way I had been doing it when working with .MOF Files. I actually need only this:

$Path = 'C:\Program Files\WindowsPowerShell\Modules\'
New-Item $Path\MyTestClassResource -ItemType Directory

And then I moved the .psm1 and .psd1 files into that folder. No extra sub-folder required. This is a win in my book.

Now, when I run Get-Module -ListAvailable, the MyTestClassResource Module is listed. However, when I run Get-DSCResource -Module MyTestClassResource I get a lot of nothing. Weird. Next I try to Import-Module -Name MyTestClassResource and I get this giant bundle of joy.

$> Import-Module -Name MyTestClassResource -Verbose
VERBOSE: Loading module from path 'C:\Program Files\WindowsPowerShell\Modules\MyTestClassResource\MyTestClassResource.psd1'.
Import-Module : The given assembly name or codebase was invalid. (Exception from HRESULT: 0x80131047)
At line:1 char:1
+ Import-Module -Name MyTestClassResource -Verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Import-Module], FileLoadException
    + FullyQualifiedErrorId : System.IO.FileLoadException,Microsoft.PowerShell.Commands.ImportModuleCommand

Uhhh…What? Thinking quickly I decide that it’s probably not going to work to name everything exactly the same. I rename the MyTestClassResource folder to MyTestClassResources and leave everything else the same. And well, I will spare all the error text but that didn’t work at all either. I am not sure how much time I spent trying to figure out what the hell I was doing wrong, but to say it was an exercise in frustration is a massive understatement. No matter what I did I couldn’t get it to recognize a valid Module, let alone DSC Resource. I lost track of all the troubleshooting that I did but here is what my folder structure looks like now that it is working. Note that I renamed my Class based Resource from MyTestClassResource to MyFolderResource.
ClassTest

$> Get-DscResource -Module MyFolderResource

ImplementedAs   Name                      ModuleName                     Version    Properties                                        
-------------   ----                      ----------                     -------    ----------                                        
PowerShell      MyFolderResource          MyFolderResource               1.0        {Ensure, Path, DependsOn, PsDscRunAsCredential}   



$> Get-DscResource -Module MyFolderResource -Syntax
MyFolderResource [String] #ResourceName
{
    Ensure = [string]{ Absent | Present }
    Path = [string]
    [DependsOn = [string[]]]
    [PsDscRunAsCredential = [PSCredential]]
}

Now, let’s test a Configuration!

Configuration TestClass
{
    Import-DSCResource -ModuleName MyFolderResource
    MyFolderResource Test
    {
        Ensure = "Present"
        Path =   "C:\Scripts\Blah"
    }


}

TestClass -OutputPath C:\Scripts\TestClass
Start-DSCConfiguration -Wait -Force -Verbose -Path C:\Scripts\TestClass

$> Start-DSCConfiguration -Wait -Force -Verbose -Path C:\Scripts\TestClass
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 VALHALLA with user sid S-1-5-21-3449200860-990092898-4058043841-1001.
VERBOSE: [VALHALLA]: LCM:  [ Start  Set      ]
VERBOSE: [VALHALLA]: LCM:  [ Start  Resource ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]: LCM:  [ Start  Test     ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]: LCM:  [ End    Test     ]  [[MyFolderResource]Test]  in 0.2180 seconds.
VERBOSE: [VALHALLA]: LCM:  [ Start  Set      ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] Exporting function 'Get-FileHash'.
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] Exporting function 'Get-SerializedCommand'.
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] Exporting function 'New-TemporaryFile'.
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] Creating Folder
VERBOSE: [VALHALLA]: LCM:  [ End    Set      ]  [[MyFolderResource]Test]  in 0.1970 seconds.
VERBOSE: [VALHALLA]: LCM:  [ End    Resource ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]: LCM:  [ End    Set      ]
VERBOSE: [VALHALLA]: LCM:  [ End    Set      ]    in  0.8620 seconds.
VERBOSE: Operation 'Invoke CimMethod' complete.
VERBOSE: Time taken for configuration job to complete is 1.004 seconds

And just to make sure it’s working, let’s set it to Absent

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 VALHALLA with user sid S-1-5-21-3449200860-990092898-4058043841-1001.
VERBOSE: [VALHALLA]: LCM:  [ Start  Set      ]
VERBOSE: [VALHALLA]: LCM:  [ Start  Resource ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]: LCM:  [ Start  Test     ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]: LCM:  [ End    Test     ]  [[MyFolderResource]Test]  in 0.1840 seconds.
VERBOSE: [VALHALLA]: LCM:  [ Start  Set      ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] Exporting function 'Get-FileHash'.
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] Exporting function 'Get-SerializedCommand'.
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] Exporting function 'New-TemporaryFile'.
VERBOSE: [VALHALLA]:                            [[MyFolderResource]Test] File exists and should be absent.  Deleting file
VERBOSE: [VALHALLA]: LCM:  [ End    Set      ]  [[MyFolderResource]Test]  in 0.1590 seconds.
VERBOSE: [VALHALLA]: LCM:  [ End    Resource ]  [[MyFolderResource]Test]
VERBOSE: [VALHALLA]: LCM:  [ End    Set      ]
VERBOSE: [VALHALLA]: LCM:  [ End    Set      ]    in  0.6720 seconds.
VERBOSE: Operation 'Invoke CimMethod' complete.
VERBOSE: Time taken for configuration job to complete is 0.771 seconds

One thought on “Creating a Class based DSC Resource using PowerShell

Leave a Reply