DSC Resource Kit – Anniversary Release

The latest DSC Resource Kit (all your favorite DSC Resources in one handy pack) is available now. It is one mighty release with all sorts of awesomeness included! I strongly recommend picking it up if you’re doing DSC automation, as it has something for everyone.

Happy automating!

 

cDFS is dead, long live xDFS

The xDFS DSC resource module has been officially released to the PowerShell Gallery thanks to the awesome review efforts of the Microsoft PowerShell Team. The cDFS DSC Resource has now been unlisted from the PowerShell Gallery. So now is the time to update any DSC configuration scripts to use xDFS.

ss_xdfs_releasepsgallery

Important: There were some minor changes to xDFS when it was converted from cDFS. For information on what you’ll need to change to convert to xDFS see my earlier post.

 

cDFS moving to the PowerShell Team

Just a Friday afternoon heads up – if you’re using the cDFS DSC Resource I created to manage Windows Server Distributed File System (Replication and Namespaces), it has now been accepted into the PowerShell Community resources and will be under the control of the PowerShell Team.

This means that the GitHub source code repository will be moving over to the PowerShell organization in the next few days. This also means that any future releases of this resource module won’t be provided by me as cDFS, but will be released by the PowerShell team as xDFS.

So I recommend that when this happens you switch over to using the xDFS resource. I will put another post up here when the change over officially occurs. The first official release version under the new xDFS name will be 3.0.0.x. I won’t make any further changes or bug fixes to the cDFS resources.

It is also worth noting that as part of this move some minor changes were made to the DSC Resource modules. These are breaking changes and you will most likely need to update any DSC Configurations depending on this, but you would have to do this anyway because of the name change.

The changes are:

  • Resource xDFSRepGroup renamed to xDFSReplicationGroup
  • Resource xDFSRepGroupConnection renamed to xDFSReplicationGroupConnection
  • Resource xDFSRepGroupFolder renamed to xDFSReplicationGroupFolder
  • Resource xDFSRepGroupMembership renamed to xDFSReplicationGroupMembership
  • xDFSReplicationGroupConnection:
    • Changed DisableConnection parameter to EnsureEnabled.
    • Changed DisableRDC parameter to EnsureRDCEnabled.

These changes should only require minor changes to your configuration scripts to implement.

Thanks for reading and have a great Friday~

 

 

Install Jenkins using DSC – Part 2

In my previous post I showed how to create a PowerShell script that would install a Jenkins CI Master server onto a Windows Server Core installation. The obvious next step for such a script was to convert it into a DSC configuration file.

In this post I’m assuming WMF 5.0 is installed onto the server that will be converted into a Jenkins Master. You could manage this without WMF 5.0, but you’d need to manually install the DSC Resource modules that the configuration will use.

Once again, the full DSC Configuration script can be found at the end of the post.

Requirements

You’ll need:

  • A physical or virtual machine running Windows Server 2012 R2 Core (or Full) – it should be a completely clean install with WMF 5.0 installed on it.
  • An administrator login to the server.
  • An internet connection to the server.

Resource Modules

This DSC Configuration requires the use of three DSC Resources:

  • cChoco – this community resource is used to install Chocolatey and Jenkins.
  • xNetworking – this resource is used to configure the networking on the server if required.
  • PSDesiredStateConfiguration – this resource comes with PowerShell by default and is used to provide the Script resource.

The easiest way to install these resource modules is by executing these commands on the Jenkins server:

# Make sure the DSC Resource modules are downloaded
Install-Module -Name cChoco -Force
Install-Module -Name xNetworking -Force

However, if you’re using a Pull server or compiling the DSC MOF on a development machine (rather than the Jenkins node) you would need to use other methods of ensuring the modules are available.

The Configuration Components

The DSC Configuration needs to do the following things:

  • Configure Networking (optional)
  • Install .NET 3.5 Framework
  • Install Chocolatey
  • Install JDK 8
  • Install Jenkins
  • Configure Jenkins Port (optional)

Configure Networking

I like to use the xNetwoking DSC resource to configure the IPv4 and IPv6 settings on the Network adapter to have a static configuration. However, you won’t need to do this if you’re using DHCP or manual configuration. Note, in my case my adapter was called “Ethernet”.

xIPAddress IPv4_1 {
    InterfaceAlias = 'Ethernet'
    AddressFamily  = 'IPv4'
    IPAddress      = '192.168.128.20'
    SubnetMask     = '24'
}
xDefaultGatewayAddress IPv4G_1 {
    InterfaceAlias = 'Ethernet'
    AddressFamily  = 'IPv4'
    Address        = '192.168.128.19'
}
xDnsServerAddress IPv4D_1 {
    InterfaceAlias = 'Ethernet'
    AddressFamily  = 'IPv4'
    Address        = '192.168.128.10'
}
xIPAddress IPv6_1 {
    InterfaceAlias = 'Ethernet'
    AddressFamily  = 'IPv6'
    IPAddress      = 'fd53:ccc5:895a:bc00::14'
    SubnetMask     = '64'
}
xDefaultGatewayAddress IPv6G_1 {
    InterfaceAlias = 'Ethernet'
    AddressFamily  = 'IPv6'
    Address        = 'fd53:ccc5:895a:bc00::13'
}
xDnsServerAddress IPv6D_1 {
    InterfaceAlias = 'Ethernet'
    AddressFamily  = 'IPv6'
    Address        = 'fd53:ccc5:895a:bc00::a'
}

Install .NET 3.5 Framework

Jenkins requires the .NET 3.5 Framework, so I’m going to use the WindowsFeature DSC Resource to install it:

WindowsFeature NetFrameworkCore
{
    Ensure    = "Present"
    Name      = "NET-Framework-Core"
}

Install Chocolatey

Next up, I’m going to use the cChocoInstaller resource in the cChoco resource module (available on PowerShell Gallery here) to install the Chocolatey package manager:

# Install Chocolatey
cChocoInstaller installChoco
{
    InstallDir = "c:\choco"
    DependsOn = "[WindowsFeature]NetFrameworkCore"
}

Install JDK 8 and Jenkins

The cChocoPackageInstaller resource module is the used to install JDK 8 and Jenkins

# Install JDK8
cChocoPackageInstaller installJdk8
{
    Name = "jdk8"
    DependsOn = "[cChocoInstaller]installChoco"
}

# Install Jenkins
cChocoPackageInstaller installJenkins
{
    Name = "Jenkins"
    DependsOn = "[cChocoInstaller]installChoco"
}

Configure Jenkins Port

The last step of the configuration is optional. By default Jenkins is configured to listen on port 8080, however I want to change it to 80. So this next part uses the Script resource to change the “–httpPort” setting in the Jenkins.xml file. I use Regex to do this:

# Set the Jenkins Port
Script SetJenkinsPort
{
	SetScript = {
		Write-Verbose -Verbose "Setting Jenkins Port to $Using:JenkinsPort"
		$Config = Get-Content `
			-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
		$NewConfig = $Config `
			-replace '--httpPort=[0-9]*\s',"--httpPort=$Using:JenkinsPort "
		Set-Content `
			-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml" `
			-Value $NewConfig `
			-Force
		Write-Verbose -Verbose "Restarting Jenkins"
		Restart-Service `
			-Name Jenkins
	}
	GetScript = {
		$Config = Get-Content `
			-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
		$Matches = @([regex]::matches($Config, "--httpPort=([0-9]*)\s", 'IgnoreCase'))
		$CurrentPort = $Matches.Groups[1].Value
		Return @{
			'JenkinsPort' = $CurrentPort
		}
	}
	TestScript = {
		$Config = Get-Content `
			-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
		$Matches = @([regex]::matches($Config, "--httpPort=([0-9]*)\s", 'IgnoreCase'))
		$CurrentPort = $Matches.Groups[1].Value

		If ($Using:JenkinsPort -ne $CurrentPort) {
			# Jenkins port must be changed
			Return $False
		}
		# Jenkins is already on correct port
		Return $True
	}
	DependsOn = "[cChocoPackageInstaller]installJenkins"
}

Create the MOF

The final thing to do is download the cChoco and xNetworking DSC Resources,create the MOF and then ask the LCM to apply it:

$ConfigData = @{
    AllNodes =
    @(
        @{
            NodeName = "LocalHost"
        }
    )
}

JENKINS_CI -JenkinsPort 80 -ConfigurationData $ConfigData

Start-DscConfiguration -Path .\JENKINS_CI -Wait -Verbose

The Complete DSC Configuration

Here is the complete DSC Configuration file. You just need to copy it to the Server and run it. It will compile the configuration into a MOF and tell the LCM to apply it. Just remember to ensure required DSC Resource modules are installed.

Configuration JENKINS_CI
{
param (
$JenkinsPort = 8080
)
Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
Import-DscResource -ModuleName 'cChoco'
Import-DscResource -ModuleName 'xNetworking'
Node $AllNodes.NodeName {
# Configure networking (optional)
xIPAddress IPv4_1 {
InterfaceAlias = 'Ethernet'
AddressFamily = 'IPv4'
IPAddress = '192.168.128.20'
SubnetMask = '24'
}
xDefaultGatewayAddress IPv4G_1 {
InterfaceAlias = 'Ethernet'
AddressFamily = 'IPv4'
Address = '192.168.128.19'
}
xDnsServerAddress IPv4D_1 {
InterfaceAlias = 'Ethernet'
AddressFamily = 'IPv4'
Address = '192.168.128.10'
}
xIPAddress IPv6_1 {
InterfaceAlias = 'Ethernet'
AddressFamily = 'IPv6'
IPAddress = 'fd53:ccc5:895a:bc00::14'
SubnetMask = '64'
}
xDefaultGatewayAddress IPv6G_1 {
InterfaceAlias = 'Ethernet'
AddressFamily = 'IPv6'
Address = 'fd53:ccc5:895a:bc00::13'
}
xDnsServerAddress IPv6D_1 {
InterfaceAlias = 'Ethernet'
AddressFamily = 'IPv6'
Address = 'fd53:ccc5:895a:bc00::a'
}
# Install .NET 3.5
WindowsFeature NetFrameworkCore
{
Ensure = "Present"
Name = "NET-Framework-Core"
}
# Install Chocolatey
cChocoInstaller installChoco
{
InstallDir = "c:\choco"
DependsOn = "[WindowsFeature]NetFrameworkCore"
}
# Install JDK8
cChocoPackageInstaller installJdk8
{
Name = "jdk8"
DependsOn = "[cChocoInstaller]installChoco"
}
# Install Jenkins
cChocoPackageInstaller installJenkins
{
Name = "Jenkins"
DependsOn = "[cChocoInstaller]installChoco"
}
# Set the Jenkins Port
Script SetJenkinsPort
{
SetScript = {
Write-Verbose -Verbose "Setting Jenkins Port to $Using:JenkinsPort"
$Config = Get-Content `
-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
$NewConfig = $Config `
-replace '--httpPort=[0-9]*\s',"--httpPort=$Using:JenkinsPort "
Set-Content `
-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml" `
-Value $NewConfig `
-Force
Write-Verbose -Verbose "Restarting Jenkins"
Restart-Service `
-Name Jenkins
}
GetScript = {
$Config = Get-Content `
-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
$Matches = @([regex]::matches($Config, "--httpPort=([0-9]*)\s", 'IgnoreCase'))
$CurrentPort = $Matches.Groups[1].Value
Return @{
'JenkinsPort' = $CurrentPort
}
}
TestScript = {
$Config = Get-Content `
-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
$Matches = @([regex]::matches($Config, "--httpPort=([0-9]*)\s", 'IgnoreCase'))
$CurrentPort = $Matches.Groups[1].Value
If ($Using:JenkinsPort -ne $CurrentPort) {
# Jenkins port must be changed
Return $False
}
# Jenkins is already on correct port
Return $True
}
DependsOn = "[cChocoPackageInstaller]installJenkins"
}
}
}
$ConfigData = @{
AllNodes =
@(
@{
NodeName = "LocalHost"
}
)
}
JENKINS_CI -JenkinsPort 80 -ConfigurationData $ConfigData
Start-DscConfiguration -Path .\JENKINS_CI -Wait -Verbose

Within five to ten minutes the Jenkins server will be configured and ready to go.

Install Jenkins on Windows Server Core – Part 1

I’ll admit it- I love Windows Server Core and I use it whenever possible. I think everyone should try and do the same. However, I know not everyone is a PowerShell expert or has any desire to be one.

So for this blog post I’m going to show how I created a simple script that will install Jenkins CI on a Windows Server Core system to be a Jenkins Master server. Feel free to just skip down to the end and use the completed script if you want to. I did this on a Windows Server 2012 R2 Core system, but this would probably work on Windows Server 2016 TP4 (the currently available version). You could of course use this on a Windows Server Full system as well.

Note: Installing a Windows Server Core as a Jenkins Slave server is a similar process but there is no need to install the Jenkins Server software or service. I won’t cover the process of installing a Windows Jenkins Slave in this post.

This is post is part one of a two part post. In the part two I’ll convert the process over to a DSC configuration that can be applied to one or more nodes to make the process even easier and ensure your Jenkins servers maintain their state.

Requirements

You’ll need:

  • A physical or virtual machine running Windows Server 2012 R2 Core (or Full) – it should be a completely clean install with nothing already installed.
  • An administrator login to the server.
  • An internet connection to the server.

The Script

The first thing I like to do is get all the variables into one place so I can easily see what options I might want to set. In this case the only thing I care about is setting a static IP Address details of the server and also the port Jenkins will be assigned to:

 

# Configure the settings to use to setup this Jenkins Executor
$Port = 80
$IPAddress = '192.168.1.96'
$SubnetPrefixLength = 24
$DNSServers = @('192.168.1.1')
$DefaultGateway = '192.168.1.1'

The next thing I need to do is ensure the .NET Framework v3.5 is installed (required by Jenkins on Windows):

# Install .NET Framework 3.5
Install-WindowsFeature -Name NET-Framework-Core

For this installation I’m actually going to let the Chocolatey package manager do most of the heavy lifting of actually downloading and installing the Jenkins bits. So I need to install Chocolatey:

# Install Chocolatey
iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))

Next up, I use Chocolatey to install both the Oracle JDK 8 and the Jenkins bits. These will be downloaded off the internet so may take a little while depending on your connection. The -y parameter forces the install to occur without prompting:

# Install Chocolatey
# Install JDK 8
choco install jdk8 -y

# Install Jenkins using Chocolatey
choco install Jenkins -y

What I’m going to do next is configure the port Jenkins should run on. This is done by changing the –httpPort setting in the c:\program files (x86)\Jenkins\Jenkins.xml file. I’ll use a simple RegEx to do this. Also, because the Jenkins Service is already running at this point I’ll need to restart it before the changed setting will be read:

# Set the port Jenkins uses
$Config = Get-Content `
  -Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
$NewConfig = $Config `
  -replace '--httpPort=[0-9]*\s',"--httpPort=$Port "
Set-Content `
  -Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml" `
  -Value $NewConfig `
  -Force
Restart-Service `
  -Name Jenkins

The Chocolatey Jenkins package automatically configures a firewall rule named “Jenkins” that allows inbound traffic to the Java.exe application. This means that external machines will be able to connect to this Jenkins server. You may want to change this by removing the “Jenkins” firewall rule and replace it with something more specific to your needs, however I didn’t do this in my script.

The final section is optional – it just configures the network connection on the machine to use a static IP address. You could omit this section completely if you were using DHCP or some other method of configuring the network connection:

# Set a static IP Address - optional
New-NetIPAddress `
 -IPAddress $IPAddress `
 -InterfaceAlias Ethernet `
 -DefaultGateway $DefaultGateway `
 -AddressFamily IPv4 `
 -PrefixLength $SubnetPrefixLength
Set-DnsClientServerAddress `
 -InterfaceAlias Ethernet `
 -Addresses $DNSServers

That’s all there is to it.

The Complete Script

Here is the complete script. You can just fire up PowerShell on the Core server and copy/paste this directly into the PowerShell console, or use some other method of running it:

# Configure the settings to use to setup this Jenkins Executor
$Port = 80
$IPAddress = '192.168.1.96'
$SubnetPrefixLength = 24
$DNSServers = @('192.168.1.1')
$DefaultGateway = '192.168.1.1'
# Install .NET Framework 3.5
Install-WindowsFeature -Name NET-Framework-Core
# Install Chocolatey
iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))
# Install JDK 8
choco install jdk8 -y
# Install Jenkins using Chocolatey
choco install Jenkins -y
# Set the port Jenkins uses
$Config = Get-Content `
-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml"
$NewConfig = $Config `
-replace '--httpPort=[0-9]*\s',"--httpPort=$Port "
Set-Content `
-Path "${ENV:ProgramFiles(x86)}\Jenkins\Jenkins.xml" `
-Value $NewConfig `
-Force
Restart-Service `
-Name Jenkins
# Set a static IP Address - optional
New-NetIPAddress `
-IPAddress $IPAddress `
-InterfaceAlias Ethernet `
-DefaultGateway $DefaultGateway `
-AddressFamily IPv4 `
-PrefixLength $SubnetPrefixLength
Set-DnsClientServerAddress `
-InterfaceAlias Ethernet `
-Addresses $DNSServers

Tomorrow I’ll improve on this process by converting it into a DSC configuration, which will ensure the Jenkins Server maintains it’s state and makes provisioning them even easier.

Thanks for reading.

 

 

Creating Professional DSC Resources – Part 7

The purpose of this series of articles is to try and document a few of the lessons I learned while releasing new DSC resources as well as contributing to the existing Microsoft Community DSC resources. These articles are not intended to tell you how to write DSC resources from a programming perspective, but to give you some ideas on what might be expected of a DSC resource you’re releasing to the public. For example, unit and integration tests (don’t worry if you aren’t familiar with those terms).

These articles are also not intended to tell you what you must do to release your resource, but more document what will help your resource be easier to use and extend by other people. Some of these these things are obvious for people who have come from the development community, but may be quite new to operations people.

If you missed any previous articles you can find them here:

 

Recap

In the last couple of articles I covered the importance of automated testing with unit testing in particular. I had covered creating new unit tests using the unit test templates that are now available here. I also covered how to complete the Pester Test Initialization and the Get-TargetResource, Set-TargetResource and Test-TargetResource function areas of the unit test.

 

Integration Testing

Integration testing is a great way of catching many errors that can’t be easily picked up by Unit testing. It effectively tests your DSC Resource by actually using it in a DSC configuration file and applying it to a computer and checking the results. So this is as close to real-life testing as you can get.

Integration testing of a PowerShell DSC resource should be performed after unit testing. When a PowerShell DSC Resource is integration tested the following process occurs:

  1. A DSC configuration file using the DSC resource to be integration tested is compiled into a MOF.
  2. The MOF file is applied to the test machine.
  3. The parameters current DSC Configuration of this DSC Resource on the test machine is obtained.
  4. The parameters of the current DSC Configuration are compared with what was set in the DSC configuration file in step 1.

Just like unit testing we use Pester to test the above steps and ensure that errors don’t occur and the output is as expected.

 

 

Sometimes Integration Tests are not Possible

Integration testing is not always possible on a resource. Some resources may rely on external servers being available or they might be destructive to the machine performing the tests.

For example, integration tests could not be implemented for the MSFT_xIPAddress resource in the xNetworking DSC Resource module because it would have caused the network to disconnect during testing which would have resulted in a failure of the AppVeyor CI machine running the tests.

But, if there is a reasonable way of implementing integration tests for a resource in a non-destructive manor, then I’d strongly recommend it – especially as it is usually really easy.

 

Don’t Be Destructive!

Unlike unit testing, integration testing actually changes configuration on the machine performing the tests. If you’re using a continuous integration service like AppVeyor to perform your tests then this isn’t such a problem as the test machine is “destroyed” after your tests are run.

However, many people also run any integration tests on their local machines before committing code, therefore, your integration tests should always leave the machine in the state that it was before running them. This means that any changes that will be made applying the integration tests should be undone at the completion of your integration tests script.

 

Integration Test Files

Integration tests for a DSC resource actually consist of two different files:

ss_dsc_inttestfiles

  1. *.config.ps1 – The DSC Configuration file that will use the DSC Resource being tested.
  2. *.Integration.Tests.ps1 – The Integration Test script file containing the Pester tests.

These files should be stored in the Tests\Integration folder in the DSC Resource module:

ss_dsc_inttestfolders

You must also ensure that the names of these files exactly matches the name of the resource itself. For example, if your DSC Resource is called BMD_MyResource then these files must be called:

 

  1. BMD_MyResource.config.ps1
  2. BMD_MyResource.Integration.Tests.ps1

 

 

Creating a New Integration Test

Luckily, a good amount of the work in implementing integration tests is already done for you. Like unit tests, templates for the two integration files are available in the DscResources repository in GitHub:

ss_dsc_inttesttemplatesrepo

You need to copy the integration test template files and rename them to match your DSC Resource.

The easiest way to do this is to clone the repository containing the test template files and copy the integration_template.ps1 and integration_config_template.ps1 files to your Tests/Integration folder:

git clone https://github.com/PowerShell/DSCResources.git
Copy-Item .\DSCResources\Tests.Template\integration_config_template.ps1 .\ciSCSI\Tests\Integration\BMD_ciSCSIVirtualDisk.config.ps1
Copy-Item .\DSCResources\Tests.Template\integration_template.ps1 .\ciSCSI\Tests\Integration\BMD_ciSCSIVirtualDisk.Integration.Tests.ps1

ss_dsc_createnewinttestfromtemplate

You’ll now have two new integration test files that you can open in your PowerShell editor of choice.

 

Modifying the Config File

The first file I usually edit is the *.config.ps1 file:

ss_dsc_newinttestconfigtemplate

Next, you’ll want to change any <ResourceName> occurrences in this file to the name of your resource. I also like to remove the #TODO bits at the same time so I know what I’ve completed:

configuration 'BMD_ciSCSIVirtualDisk_config' {
Import-DscResource -Name 'BMD_ciSCSIVirtualDisk'
node localhost {
BMD_ciSCSIVirtualDisk Integration_Test {
# TODO: Fill Configuration Code Here
}
}
}

Next, we need to configure the config file with the parameters we want to use as tests of the resource.

The best way of doing this is actually to create a hash table object at the beginning of the file with the parameters that we’re going to set. This is so that we can use this hash table object in the other integration file (*.Integration.Tests.ps1) when we’re comparing the values that are expected to be set.

$VirtualDisk = @{
Path = Join-Path -Path $ENV:Temp -ChildPath 'TestiSCSIVirtualDisk.vhdx'
Ensure = 'Present'
DiskType = 'Dynamic'
Size = 100MB
Description = 'Integration Test iSCSI Virtual Disk'
}
Configuration BMD_ciSCSIVirtualDisk_Config {
Import-DscResource -Name BMD_ciSCSIVirtualDisk_Config
node localhost {
BMD_ciSCSIVirtualDis Ikntegration_Test {
Path = $VirtualDisk.Path
Ensure = $VirtualDisk.Ensure
DiskType = $VirtualDisk.DiskType
SizeBytes = $VirtualDisk.Size
Description = $VirtualDisk.Description
}
}
}

As you can see in the example above, I create a $VirtualDisk hash table that contains all the parameters and values that will be used to test this DSC Resource. The $VirtualDisk object is then also accessible in the *.Integration.Tests.ps1 file.

 

Modifying the Integration Tests File

Now that the integration tests config file has been completed it is time to move on to the integration test script (*.Integration.Tests.ps1) itself, so open it in your editor of choice:

ss_dsc_newinttesttemplate

Next, customize the TODO area in the header with the your DSC Resource Name and DSC Module Name:

ss_dsc_inttestsheader

Feel free to remove the TODO comments if you want (I always do).

 

Initialization Code

After customizing the header we need to add any code that might be required to set this machine up to actually perform these integration tests. The first thing I like to do is add code to check that these integration tests can actually be performed on this machine. In my example resource, the iSCSI Virtual Disk resource will require the iSCSI Target Server feature to be installed, which also means the OS must be a Server OS. So, first thing in the try/catch block I add these checks:

# Ensure that the tests can be performed on this computer
$ProductType = (Get-CimInstance Win32_OperatingSystem).ProductType
Describe 'Environment' {
Context 'Operating System' {
It 'Should be a Server OS' {
$ProductType | Should Be 3
}
}
}
if ($ProductType -ne 3)
{
Break
}
$Installed = (Get-WindowsFeature -Name FS-iSCSITarget-Server).Installed
Describe 'Environment' {
Context 'Windows Features' {
It 'Should have the iSCSI Target Feature Installed' {
$Installed | Should Be $true
}
}
}
if ($Installed -eq $false)
{
Break
}

This will cause the try/catch block to be exited straight away if these tests can’t actually be performed on this machine.

Note: The cleanup code in the finally block will still be called if we exit with a break command.

After this you might also need to add code to configure anything that these integration tests might depend on. For example, if you were implementing integration tests for testing an iSCSI Server Target, you’d need to make sure that there was an iSCSI Virtual Disk available to use, so you’d need to create one at this point. However, in the integration tests for the iSCSI Virtual Disk resource I don’t need anything else.

 

Testing the Resource Was Applied

Next, we need to add the tests that check that after the DSC Configuration has been applied to the machine that the changes have actually been made and that the parameters match those set by the Configuration:

To do this, we complete this section:

ss_dsc_inttestsvalidate

In this case, I’ve changed it to:

It 'Should have set the resource and all the parameters should match' {
# Get the Rule details
$virtualDiskNew = Get-iSCSIVirtualDisk -Path $VirtualDisk.Path
$VirtualDisk.Path | Should Be $virtualDiskNew.Path
$VirtualDisk.DiskType | Should Be $virtualDiskNew.DiskType
$VirtualDisk.Size | Should Be $virtualDiskNew.Size
$VirtualDisk.Description | Should Be $virtualDiskNew.Description
}

What this code does is gets the iSCSI Virtual Disk that is at the path specified in the $VirtualDisk.path into a variable $VirtualDiskNew.

The parameters in $VirtualDiskNew are then matched to ensure they are the same as those in the $VirtualDisk hash table object that was created in the DSC Configuration script (*.config.ps1).

 

Cleaning Up

It is important that after the tests have been run that any changes that were made to the testing computer are reverted. So, after the end of the last test I add any clean up code. In my case, I want to remove the iSCSI Virtual Disk that was created:

# Clean up
Remove-iSCSIVirtualDisk `
-Path $VirtualDisk.Path
Remove-Item `
-Path $VirtualDisk.Path `
-Force

The above code just removes the iSCSI Virtual Disk and then also makes sure that the VHD file was also deleted. This is also very important because if the clean up does not occur and the tests are run again on the same computer they may fail.

 

And We’re Done!

Now, that may all seem like quite a bit of work, but it becomes second nature after creating a few of them. They will also save you far more time in addressing future issues with the resource every time you make a simple change to the MOF (but forget to change the resource code). These tests will give users and other maintainers much more confidence in your resources as well.

This series actually ended up being a bit longer than I intended, but hopefully you’ve stuck with it and it has helped in some small way. If you’ve got this far and you’re wanting to know what to do next, why not head over to the PowerShell DSCResources GitHub repository and see if you could help out on some resources. You could start off adding some small but useful parameter to an existing resource, fixing a bug or contribute an entire new resource to an existing module. There are numerous issues that need to be addressed on these resources, many of which are requests for new features or resources.

If you have an idea for a new resource in an existing module, raise an issue in the DSC Resource Module repository and offer to create the new resource. You may find that someone is already working on one, but if not, then this is a great opportunity to get started. It is quite a rewarding feeling the first time one of your contributions gets published in the official community DSC Resources!

So, thanks again for reading.

 

 

Creating Professional DSC Resources – Part 6

The purpose of this series of articles is to try and document a few of the lessons I learned while releasing new DSC resources as well as contributing to the existing Microsoft Community DSC resources. These articles are not intended to tell you how to write DSC resources from a programming perspective, but to give you some ideas on what might be expected of a DSC resource you’re releasing to the public. For example, unit and integration tests (don’t worry if you aren’t familiar with those terms).

These articles are also not intended to tell you what you must do to release your resource, but more document what will help your resource be easier to use and extend by other people. Some of these these things are obvious for people who have come from the development community, but may be quite new to operations people.

If you missed any previous articles you can find them here:

 

Recap

In the last couple of articles I covered the importance of automated testing and covered unit testing in particular (I’ll get to integration testing later). I had covered creating new unit tests using the unit test templates that are available here (although they will probably move here). I also covered how to complete the Pester Test Initialization and the Get-TargetResource and Set-TargetResource function areas of the unit test.

 

Unit Testing Completion

The final task in completing the unit tests is to complete the Set-TargetResource  in tests and also optionally tests for any other supporting functions your DSC Resource may have required.

In these unit tests I am using a DSC Resource for creating iSCSI Virtual Disks to illustrate the process. You don’t need to know anything about iSCSI Virtual Disks to understand these articles or resources, but if you’re interested to know the cmdlets I’m using for these, see this page. I’m using the *_iSCSIVirtualDisk cmdlets in this DSC Resource.

Function Test-TargetResource

This area will contain the actual Pester tests that test the Test-TargetResource function. These are fairly similar to the Set-TargetResource except we will be checking these two things:

  1. The output of the Test-TargetFunction is correct. E.g. it returns false if changes are required, which will cause Set-TargetFunction to be called.
  2. The expected Mocks are called by the Function.

This area may contain a large number of tests depending on the complexity of your DSC Resource. In most cases, you should expect there to create the tests from the following list, but often you will need even more for 100% code coverage:

  • Does the function return false when the resource being configured does exist and should, but one of the configured parameters does not match the current values? This test is usually repeated for each parameter in the DSC Resource.
  • Does the function return true when the resource being configured does exist and should, and all the configured parameters match the current values?
  • Does the function return false when the resource being configured does not exist but should?
  • Does the function return false when the resource being configured does exist but should not?
  • Does the function return true when the resource being configured does not exist and should not?

The bottom four of these tests are very similar. So I’ll only show examples of the top two contexts here.

Context ‘Virtual Disk exists and should but has a different …’

In this scenario we Mock the Get-iSCSIVirtualDisk cmdlet to return the object we defined in the Pester Test Initialization section. This is the behavior we’d expect if the resource being configured does exist:

Context 'Virtual Disk exists and should but has a different Description' {
Mock Get-iSCSIVirtualDisk -MockWith { return @($MockVirtualDisk) }
It 'should return false' {
{
$Splat = $TestVirtualDisk.Clone()
$Splat.Description = 'Different'
Test-TargetResource @Splat | Should Be $False
} | Should Not Throw
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
}
}

This context will perform two tests:

  1. Should return false – The Test-TargetResource should return false because we are changing the Description parameter so that the resource will require changes (e.g. Set-TargetResource should be called).
  2. Should call the expected mocks – The Test-TargetResource should call the mocked cmdlets the expected number of times. In all contexts in this function this will always be just once.

The purpose of cloning the $TestVirtualDisk object is so we can modify the properties to simulate a property difference without modifying the $TestVirtualDisk object.

You should expect to repeat this context for each parameter that might be different.

 

Context ‘Virtual Disk does not exist but should’

In this scenario we Mock the Get-iSCSIVirtualDisk cmdlet to return nothing. This is the behavior we’d expect if the resource being configured does not exist:

Context 'Virtual Disk does not exist but should' {
Mock Get-iSCSIVirtualDisk
It 'should return false' {
$Splat = $TestVirtualDisk.Clone()
Test-TargetResource @Splat | Should Be $False
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
}
}

As you can see, there is not too much different with these tests and you shouldn’t have any problems figuring out the remaining ones. Just remember, the goal is always to get 100% code coverage.

 

Unit Testing Supporting Functions

It is quite common that you might have implemented some supporting functions in your DSC Resource. These supporting functions are usually called by your standard *-TargetResource functions. If that is the case there are two important things you should do:

  1. Write unit tests that cover all code paths in your supporting functions.
  2. Add mocks to your *-TargetResource unit tests that prevent any constructive/destructive cmdlets that exist in your supporting functions from being called.

The first item is fairly self explanatory. For example, I often implement a get-* function in my DSC Resources which is used to pull the actual objects that will be used by the *-TargetResource functions (e.g. Get-VirtualDisk):

Function Get-VirtualDisk {
param
(
[parameter(Mandatory = $true)]
[System.String]
$Path
)
try
{
$VirtualDisk = Get-iSCSIVirtualDisk `
-Path $Path `
-ErrorAction Stop
}
catch [Microsoft.Iscsi.Target.Commands.IscsiCmdException]
{
$VirtualDisk = $null
}
catch
{
Throw $_
}
Return $VirtualDisk
}

To unit test this function I’d write unit tests that tested the following contexts:

  1. Context ‘Virtual Disk does not exist’
  2. Context ‘Virtual Disk does exist’

For example:

Describe "$($Global:DSCResourceName)\Get-VirtualDisk" {
Context 'Virtual Disk does not exist' {
Mock Get-iSCSIVirtualDisk
It 'should return null' {
$Splat = $TestVirtualDisk.Clone()
$Result = Get-VirtualDisk -Path $Splat.Path
$Result | Should Be $null
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
}
}
Context 'Virtual Disk does exist' {
Mock Get-iSCSIVirtualDisk -MockWith { return @($MockVirtualDisk) }
It 'should return expected parameters' {
$Splat = $TestVirtualDisk.Clone()
$Result = Get-VirtualDisk -Path $Splat.Path
$Result.Path | Should Be $MockVirtualDisk.Path
$Result.DiskType | Should Be $MockVirtualDisk.DiskType
$Result.Size | Should Be $MockVirtualDisk.Size
$Result.Description | Should Be $MockVirtualDisk.Description
$Result.ParentPath | Should Be $MockVirtualDisk.ParentPath
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
}
}
}

As you can see, there isn’t much to it.

Earn a Chocolate Fish: If you look at the above supporting function and unit tests carefully, you’ll notice that I haven’t got 100% code coverage on it!

To get 100% code coverage I would have had to implement a unit test that covered the situation where the Get-iSCSIVirtualDisk function threw an exception that wasn’t a [Microsoft.Iscsi.Target.Commands.IscsiCmdException] exception.

In case you’re wondering, the Get-iSCSIVirtualDisk function throws a [Microsoft.Iscsi.Target.Commands.IscsiCmdException] when the cmdlet is called with the path parameter set to a path that does not contain a valid iSCSI Virtual Hard Disk file.

 

Unit Testing Exceptions

When creating unit tests you’ll often need to test a scenario where the function that is being tested is expected to throw an exception. If you read the Pester documentation, you’d might write a test for an exception like this:

Context 'Virtual Disk exists and should but has a different ParentPath' {
Mock Get-iSCSIVirtualDisk -MockWith { return @($MockVirtualDisk) }
It 'should throw an exception' {
{ Test-TargetResource @Splat } | Should Throw
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
}
}

This would of course will work. It will ensure that the code throws an exception in this situation. The problem is we aren’t really sure if it is the exception that we expected it to throw. It could have been thrown by some other part of our code.

So to improve on this we need to do things:

  1. Customize the exception that is thrown.
  2. Change the unit test so that it checks for the customized exception.

 

Customize the Exception

To create a custom exception we need to create a new exception object containing our custom error message. The exception object is then used to create a custom Error Record:

$errorId = 'iSCSIVirtualDiskRequiresRecreateError'
$errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
$errorMessage = $($LocalizedData.iSCSIVirtualDiskRequiresRecreateError) -f $Path
$exception = New-Object -TypeName System.InvalidOperationException `
-ArgumentList $errorMessage
$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
-ArgumentList $exception, $errorId, $errorCategory, $null
$PSCmdlet.ThrowTerminatingError($errorRecord)

In the above code, you just need to customize the $errorId and $errorMessage variables. The $errorId should just contain a simply string identifier for this particular type of error, but the $errorMessage can contain a full description of the error, including related parameters.

Once you’ve created the $errorRecord object you can call the ThrowTerminatingError method of the $PSCmdLet object, passing the $errorRecord object as the parameter.

Important: the $PSCmdLet object is only available in Functions that include the [CmdletBinding()] function attribute. So ensure your *-TargetResource and supporting functions include this attribute if you want to be able to access this object.

 

Test for the Customized Exception

To test for the custom exception object we need to create an identical object in the unit test and test for it:

Context 'Virtual Disk exists and should but has a different ParentPath' {
Mock Get-iSCSIVirtualDisk -MockWith { return @($MockVirtualDisk) }
It 'should throw an iSCSIVirtualDiskRequiresRecreateError exception' {
$Splat = $TestVirtualDisk.Clone()
$Splat.ParentPath = 'c:\NewParent.vhdx'
$errorId = 'iSCSIVirtualDiskRequiresRecreateError'
$errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
$errorMessage = $($LocalizedData.iSCSIVirtualDiskRequiresRecreateError) -f $Splat.Path
$exception = New-Object -TypeName System.InvalidOperationException `
-ArgumentList $errorMessage
$errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
-ArgumentList $exception, $errorId, $errorCategory, $null
{ Test-TargetResource @Splat } | Should Throw $errorRecord
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
}
}

The above code creates an identical exception object to the one produced by the exception in our DSC Resource code. The exception object can then be passed to the should throw cmdlet. If a different exception is thrown by the code then the test will fail – it will only pass if the exception object is exactly the same.

Important: Make sure both the $errorId and $errorMessage variables are exactly the same as what would be produced by the code when your unit test calls it. This includes ensuring that if your $errorMessage contains any parameters that the unit test $errorMessage contains the same parameter values.

 

That about completes creating unit tests. After you’ve implemented a few unit tests you’ll no doubt come up with your own method of implementing them, but hopefully this has given you a place to start.

 

Up Next – Integration Tests

In the next article, I’ll cover the integration tests. There are often the most difficult to implement, but if you can take the time to implement them then your DSC Resources are guaranteed to be extremely robust and bugs are far less likely to slip through.

Further parts in this series:

Creating Professional DSC Resources – Part 5

The purpose of this series of articles is to try and document a few of the lessons I learned while releasing new DSC resources as well as contributing to the existing Microsoft Community DSC resources. These articles are not intended to tell you how to write DSC resources from a programming perspective, but to give you some ideas on what might be expected of a DSC resource you’re releasing to the public. For example, unit and integration tests (don’t worry if you aren’t familiar with those terms).

These articles are also not intended to tell you what you must do to release your resource, but more document what will help your resource be easier to use and extend by other people. Some of these these things are obvious for people who have come from the development community, but may be quite new to operations people.

If you missed any previous articles you can find them here:

 

Recap

Yesterday I talked about  the importance of automated testing and covered unit testing in particular (I’ll get to integration testing later). I had covered creating new unit tests using the unit test templates that are available here (although they will probably move here). I also covered how to complete the Pester Test Initialization and the Function Get-TargetResource areas of the unit test.

 

Unit Testing Continued

The next task in completing the unit tests is to complete the Set-TargetResource area in the unit test.

In these unit tests I am using a DSC Resource for creating iSCSI Virtual Disks to illustrate the process. You don’t need to know anything about iSCSI Virtual Disks to understand these articles or resources, but if you’re interested to know the cmdlets I’m using for these, see this page. I’m using the *_iSCSIVirtualDisk cmdlets in this DSC Resource.

Function Set-TargetResource

This area will contain the actual Pester tests that test the Set-TargetResource function. Unlike the Get-TargetResource this area may contain a large number of tests depending on the complexity of your DSC Resource. In most cases, you should expect there to create the tests from the following list, but often you will need even more for 100% code coverage:

  • Does each parameter get set/updated correctly when the resource being configured does exist and should? This test is usually repeated for each parameter in the DSC Resource.
  • Does it work when the resource being configured does not exist but should?
  • Does it work when the resource being configured does exist but should not?
  • Does it work when the resource being configured does not exist and should not?

Context ‘Virtual Disk exists and should but has a different …’

In this scenario we Mock the Get-iSCSIVirtualDisk cmdlet to return the object we defined in the Pester Test Initialization section. This is the behavior we’d expect if the resource being configured does exist.

We are also going to Mock the Set-iSCSIVirtualDisk, New-iSCSIVirtualDisk and Remove-iSCSIVirtualDisk. This is so we can ensure the expected cmdlets are called as well as preventing the real cmdlets from being run:

Context 'Virtual Disk exists and should but has a different Description' {
Mock Get-iSCSIVirtualDisk -MockWith { return @($MockVirtualDisk) }
Mock New-iSCSIVirtualDisk
Mock Set-iSCSIVirtualDisk
Mock Remove-iSCSIVirtualDisk
It 'should not throw error' {
{
$Splat = $TestVirtualDisk.Clone()
$Splat.Description = 'Different'
Set-TargetResource @Splat
} | Should Not Throw
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
Assert-MockCalled -commandName New-iSCSIVirtualDisk -Exactly 0
Assert-MockCalled -commandName Set-iSCSIVirtualDisk -Exactly 1
Assert-MockCalled -commandName Remove-iSCSIVirtualDisk -Exactly 0
}
}

This context will perform two tests:

  1. Should not throw error – The Set-TargetResource should not throw an error when called in this context.
  2. Should call the expected mocks – The Set-TargetResource should call the mocked cmdlets the expected number of times.

The purpose of cloning the $TestVirtualDisk object is so we can modify the properties to simulate a property difference without modifying the $TestVirtualDisk object.

It is also important to ensure that we are not only checking that the expected cmdlets are called, but also that the other cmdlets in this function are not called. This is why we are checking the New-iSCSIVirtualDisk and Remove-iSCSIVirtualDisk are being called zero times.

You should expect to repeat this context for each parameter that might be updated.

Note: It is possible that updating some parameters may not be possible because of limitations in the underlying cmdlets. In this case I like to throw an exception so that the user is made aware that they are configuring a scenarios that can not be performed. In that case the test would be to ensure the correct exception occurs. I’ll cover testing exceptions in a later article.

 

Context ‘Virtual Disk does not exist but should’

In this scenario we Mock the Get-iSCSIVirtualDisk cmdlet to return nothing. This is the behavior we’d expect if the resource being configured does not exist.

We are also going to Mock the Set-iSCSIVirtualDisk, New-iSCSIVirtualDisk and Remove-iSCSIVirtualDisk. This is so we can ensure the expected cmdlets are called as well as preventing the real cmdlets from being run:

Context 'Virtual Disk does not exist but should' {
Mock Get-iSCSIVirtualDisk
Mock New-iSCSIVirtualDisk
Mock Set-iSCSIVirtualDisk
Mock Remove-iSCSIVirtualDisk
It 'should not throw error' {
{
$Splat = $TestVirtualDisk.Clone()
Set-TargetResource @Splat
} | Should Not Throw
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
Assert-MockCalled -commandName New-iSCSIVirtualDisk -Exactly 1
Assert-MockCalled -commandName Set-iSCSIVirtualDisk -Exactly 0
Assert-MockCalled -commandName Remove-iSCSIVirtualDisk -Exactly 0
}
}

The context tests are very similar to all the other tests so I won’t go into detail on them here. It is important to note that the expected Mocks will be different.

 

Context ‘Virtual Disk exists but should not’

In this scenario we Mock the Get-iSCSIVirtualDisk cmdlet to return the object we defined in the Pester Test Initialization section. This is the behavior we’d expect if the resource being configured does exist.

We are also going to Mock the Set-iSCSIVirtualDisk, New-iSCSIVirtualDisk and Remove-iSCSIVirtualDisk. This is so we can ensure the expected cmdlets are called as well as preventing the real cmdlets from being run:

Context 'Virtual Disk exists but should not' {
Mock Get-iSCSIVirtualDisk -MockWith { return @($MockVirtualDisk) }
Mock New-iSCSIVirtualDisk
Mock Set-iSCSIVirtualDisk
Mock Remove-iSCSIVirtualDisk
It 'should not throw error' {
{
$Splat = $TestVirtualDisk.Clone()
$Splat.Ensure = 'Absent'
Set-TargetResource @Splat
} | Should Not Throw
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
Assert-MockCalled -commandName New-iSCSIVirtualDisk -Exactly 0
Assert-MockCalled -commandName Set-iSCSIVirtualDisk -Exactly 0
Assert-MockCalled -commandName Remove-iSCSIVirtualDisk -Exactly 1
}
}

The context tests are very similar to all the other tests so I won’t go into detail on them here. It is important to note that the expected Mocks will be different.

 

Context ‘Virtual Disk does not exist and should not’

In this scenario we Mock the Get-iSCSIVirtualDisk cmdlet to return the object we defined in the Pester Test Initialization section. This is the behavior we’d expect if the resource being configured does not exist.

We are also going to Mock the Set-iSCSIVirtualDisk, New-iSCSIVirtualDisk and Remove-iSCSIVirtualDisk. This is so we can ensure the expected cmdlets are called as well as preventing the real cmdlets from being run:

Context 'Virtual Disk does not exist and should not' {
Mock Get-iSCSIVirtualDisk
Mock New-iSCSIVirtualDisk
Mock Set-iSCSIVirtualDisk
Mock Remove-iSCSIVirtualDisk
It 'should not throw error' {
{
$Splat = $TestVirtualDisk.Clone()
$Splat.Ensure = 'Absent'
Set-TargetResource @Splat
} | Should Not Throw
}
It 'should call expected Mocks' {
Assert-MockCalled -commandName Get-iSCSIVirtualDisk -Exactly 1
Assert-MockCalled -commandName New-iSCSIVirtualDisk -Exactly 0
Assert-MockCalled -commandName Set-iSCSIVirtualDisk -Exactly 0
Assert-MockCalled -commandName Remove-iSCSIVirtualDisk -Exactly 0
}
}

The context tests are very similar to all the other tests so I won’t go into detail on them here. It is important to note that the expected Mocks will be different.

 

Unit Tests to be Continued…

In the next article, I’ll cover the unit tests for Get-TargetResource as well as unit testing any additional functions. Thanks again for reading and I hope it is useful.

Further parts in this series:

 

Creating Professional DSC Resources – Part 3

The purpose of this series of articles is to try and document a few of the lessons I learned while releasing new DSC resources as well as contributing to the existing Microsoft Community DSC resources. These articles are not intended to tell you how to write DSC resources from a programming perspective, but to give you some ideas on what might be expected of a DSC resource you’re releasing to the public. For example, unit and integration tests (don’t worry if you aren’t familiar with those terms).

These articles are also not intended to tell you what you must do to release your resource, but more document what will help your resource be easier to use and extend by other people. Some of these these things are obvious for people who have come from the development community, but may be quite new to operations people.

If you missed any previous articles you can find them here:

 

Coding Style

Everyone has there own preferences of how they like their code to look. Just take a look at all the PowerShell repositories on GitHub and you’d see a lot of different coding styles.

When I refer to coding style, this covers a many different things, for example:

  • Variable name format
  • Tab format (spaces or tabs?)
  • Function name format
  • Maximum line length
  • Comments
  • Curly braces allowed at the end of a line
  • Allow more than one blank line in a row

If you’re writing code that only you will look at and maintain, it doesn’t much matter what your coding style is – as long as you can understand it 6 months later. However, if you would like other people to use and possibly  contribute and maintain your code or if you are contributing to community project, you’ll want to adopt a standard coding style.

The best reason for adopting a standard coding style is to ensure consistency in your code – especially across DSC resources within the same module. This makes it much easier for people to become familiar with your coding style and therefore easier for them to read and understand.

If you don’t have a particular coding style you have adopted, Microsoft has released a simple style guidelines document for DSC Resources that you can adopt quite easily.

Tip: Using the Microsoft style guidelines document is a requirement for contributing code to the Microsoft Community DSC Resources. So if you’re planning on contributing code or even entire resource modules to this project, I’d recommend you adopt the Microsoft style guidelines.

 

Message Localization

Adding support for language localization to your DSC Resources is trivial, but it is often over looked. Supporting data localization is much easier to add when you are creating your DSC Resource rather than adding it later on. This doesn’t mean you have to provide the language files of course, just provide support so that someone else could contribute them should they want to.

To ensure language support, create a new file in the same folder as your DSC Resource with the same name as the DSC Resource but with a psd1 extension. In that file  create a Data section named LocalizedData containing the messages for your default culture. For example:

data LocalizedData
{
# culture="en-US"
ConvertFrom-StringData -StringData @'
GettingiSCSIVirtualDiskMessage=Getting iSCSI Virtual Disk "{0}".
iSCSIVirtualDiskExistsMessage=iSCSI Virtual Disk "{0}" exists.
iSCSIVirtualDiskDoesNotExistMessage=iSCSI Virtual Disk "{0}" does not exist.
SettingiSCSIVirtualDiskMessage=Setting iSCSI Virtual Disk "{0}".
EnsureiSCSIVirtualDiskExistsMessage=Ensuring iSCSI Virtual Disk "{0}" exists.
EnsureiSCSIVirtualDiskDoesNotExistMessage=Ensuring iSCSI Virtual Disk "{0}" does not exist.
iSCSIVirtualDiskCreatedMessage=iSCSI Virtual Disk "{0}" has been created.
iSCSIVirtualDiskUpdatedMessage=iSCSI Virtual Disk "{0}" has been updated.
iSCSIVirtualDiskRemovedMessage=iSCSI Virtual Disk "{0}" has been removed.
TestingiSCSIVirtualDiskMessage=Testing iSCSI Virtual Disk "{0}".
iSCSIVirtualDiskParameterNeedsUpdateMessage=iSCSI Virtual Disk "{0}" {1} is different. Change required.
iSCSIVirtualDiskDoesNotExistAndShouldNotMessage=iSCSI Virtual Disk "{0}" does not exist and should not. Change not required.
iSCSIVirtualDiskRequiresRecreateError=iSCSI Virtual Disk "{0}" needs to be deleted and recreated. Please perform this manually.
'@
}

Each line contains a localized message – in this case for the culture en-us. You could of course use a different default culture if you wanted to.

At the beginning of your DSC Resource you would the following command to ensure the appropriate localization strings are imported:

Import-LocalizedData -BindingVariable LocalizedData -Filename BMD_cMyNewResource.psd1

Alternately, if you want to support Message Localization but don’t want to have to supply your default messages in a separate file, you can place the LocalizedData section for your default culture at the top of your DSC Resource and exclude the Import-LocalizedData command.

 

Using Localization Messages

Once you’ve got the messages in, using them is extremely easy:

Write-Verbose -Message ($LocalizedData.iSCSIVirtualDiskExistsMessage -f $Path)

You can of course consume the messages anyway you like, but all of your localized messages are just properties of the LocalizedData object.

 

Other Localization Files

You put LocalizedData for other languages in separate PowerShell files in sub-folders named after the culture the file contains. For example, you might create a sub-folder called en-uk and place a file called BMD_cMyNewResource.psd1 containing the en-uk messages.

 

Example DSC Configuration Files

Another element of any easy to use DSC Resource are example DSC Configuration files. These can usually be found in the Examples folder in the root of the DSC Module:

ss_dsc_examplesfolder

There should usually be a number of different DSC Configuration files in this folder, showing common scenarios for using your DSC Resources:

ss_dsc_examples

The file name of any example files should be prefixed with Sample or Example so that they can be easily identified and differentiated from types of DSC Module files. The summary of the purpose of the configuration should also be included in the file name. This is fairly obvious I realize, but I have seen public DSC Resources named Example_1, Example_2, Example_3 etc – which reduces usability of the examples.

Each example should contain a DSC Configuration in a form that it could be used without any modification to actually test the resource. This might include installing prerequisite Windows Features or starting services etc. This allows a potential user to test drive the resource without investing a whole lot of work trying to figure out how to use it.

For example:

configuration Sample_ciSCSIInitiator
{
Param
(
[String] $NodeName = 'LocalHost'
)
Import-DscResource -Module ciSCSI
Node $NodeName
{
Service iSCSIService
{
Name = 'MSiSCSI'
StartupType = 'Automatic'
State = 'Running'
}
ciSCSITargetPortal iSCSITargetPortal
{
Ensure = 'Present'
TargetPortalAddress = '192.168.128.10'
InitiatorPortalAddress = '192.168.128.20'
DependsOn = "[WindowsFeature]iSCSIService"
} # End of ciSCSITargetPortal Resource
ciSCSITarget iSCSITarget
{
Ensure = 'Present'
NodeAddress = 'iqn.1991-05.com.microsoft:fileserver01-cluster-target'
TargetPortalAddress = '192.168.128.10'
InitiatorPortalAddress = '192.168.128.20'
IsPersistent = $true
DependsOn = "[ciSCSITargetPortal]iSCSITargetPortal"
} # End of ciSCSITarget Resource
} # End of Node
} # End of Configuration

The above DSC Resource example will ensure the MSiSCSI service is running before configuring an iSCSI Initiator.

Tip: Make the name of the configuration the same as the sample configuration file (without the extension of course).

It is also a great idea to copy the content of any example DSC Configuration files into the Examples section of the Readme.md of your DSC Resource Module along with a brief description of what the configuration will produce. You’ll find that all Microsoft Community DSC Resources do this.

 

In The Next Article

I intended on covering creating unit and integration tests in this article, but as that is by far the most involved part of creating a community DSC resource I’ve decided I’ll dedicate an entire part to each. So, rest assured the next ones will contain this very important component of any public DSC Resource. Thank you for reading this far and I hope you’re finding it useful.

Further parts in this series:

Creating Professional DSC Resources – Part 2

The purpose of this series of articles is to try and document a few of the lessons I learned while releasing new DSC resources as well as contributing to the existing Microsoft Community DSC resources. These articles are not intended to tell you how to write DSC resources from a programming perspective, but to give you some ideas on what might be expected of a DSC resource you’re releasing to the public. For example, unit and integration tests (don’t worry if you aren’t familiar with those terms).

These articles are also not intended to tell you what you must do to release your resource, but more document what will help your resource be easier to use and extend by other people. Some of these these things are obvious for people who have come from the development community, but may be quite new to operations people.

If you missed any previous articles you can find them here:

Before You Start Coding

So, you have an idea or need for a set of super new DSC Resources. Before you write a single line of code you should first go and take a look at the documentation provided by the DSC Community. These guys (many of them from Microsoft) have been doing this stuff for a while and they’ve come up with a set of best practices and instructions on how to get started.

The above GitHub repository should be your first port of call and for DSC creation and it is worth keeping an eye on this repository by watching it:

ss_github_watch

This will cause you to be notified whenever any changes to this repository are made (which isn’t that often). So if the best practices are updated you’ll be kept in the loop!

This repository also contains some template files you can use to create a new DSC Resource module.

Creating the Resource Module

Sure, there is no reason why you can’t just jump straight in and knock out a PSD1 file and some PSM1/MOF files and be done with it. But creating a resource that other people can easily use, usually requires a few other files.

First, you need to decide on the name for the DSC Resource module folder. This is usually simple enough, but if you think your module may contain more than one resource it is worth naming it with a more generic name. For example, if you were creating a DSC Resource module that will contain resources for configuring iSCSI Targets and iSCSI Initiators you might name your folder ciSCSI.

Tip: Your resource folder should begin with a lower case c. This indicates it is a Community DSC resource. This is not a requirement, but it tells people that the resource was created by the community (in this case you). DSC Resource modules starting with an x indicate that this is a Microsoft Community DSC resource and maintained by the PowerShell team as well as the community, but are not built in to DSC.

Once you’ve created the folder to store your new DSC Resource module, you should make a copy of all the files in the GitHub repository folder found here to the root of your new DSC Resource folder:

ss_dsc_newresourcefolder

The easiest way to get a copy of these files is to use Git to clone the DSCResource repository on your computer and then copy the files from the DSCResource.Template folder to your new DSC module folder:

 

The DSCResource.Template Files

At the time of writing this the DSCResource.Template folder only contains two files:

  • Readme.md – tells people how to use your DSC Resource as well as containing usage examples and Version information. You should fill this in as soon as possible and keep it up-to-date everytime you change your DSC Resource.
  • AppVeyor.yml – this is a file that configures the AppVeyor Continuous Integration (CI) for your DSC Resource Module. I will cover AppVeyor CI later in the series. At the moment don’t worry about this file, just copy it to your module folder and leave it alone.

 

Markdown

The readme.md file (like most other files with an .md file extension) is text file in the Markdown format. Because this file is being made available up on GitHub you can use GitHub Flavored Markdown. Markdown is really easy to create straight in your editor of choice and you’ll find it in use all over GitHub, so it is a good idea to get somewhat familiar with it (it should only take you about 2 minutes to get a handle on it).

If you want an example of how your Readme.md file might be constructed, have a look at this example. Of course you are completely free to format it any way you like, but keeping to a standard makes it easy for users to know what they can expect.

 

In The Next Article

As I’m trying to keep these parts short I’ll break for today and continue tomorrow. In the next part I intend to cover code guidelines and examples, with unit and integration testing to follow. I hope you have found this interesting!

Further parts in this series: