Conceptualize Desired State Configuration: Part 5


Summary: Microsoft MVP, Will Anderson, continues his series by talking about importing DSC resources and adding your scripts.

Good day! Ed Wilson here to welcome back MVP, Will Anderson, as my guest blogger this week. Will is sharing Part 5 in a series about Desired State Configuration (DSC).

  Note   This is a seven-part series that includes the following posts:

The code used in this series can be downloaded from the Script Center Repository:
Conceptualize Desired State Configuration - Reference Script

Now that we've managed to push our Desired State Configuration to a target machine, we're going to start working to find other DSC resources to further build the configuration. In my previous post, I mentioned that I didn't have RPC open on my servers by default, so I need to add some Windows Firewall rules to my systems.

Furthermore, I need to add some additional ports on my distribution points to make sure they communicate back to my SCCM primary without issue. When I’m done with that, I’m going to add a script into the mix to see how the Script resource works, and show why you should look at building a custom DSC resource instead.

Warning   I'm intentionally trying to keep these posts bite-sized so you don't have an aneurysm trying to follow along with this series. Admittedly, this post is a little long, but I wanted to make sure we accomplish something more significant than adding another resource. Feel free to take a break before the Script section.

Finding DSC resources

PowerShellGallery.com is an awesome site that's been provided by the Windows PowerShell team. You can find scripts and modules that have been submitted by the team and the community. Most of these resources are also available on GitHub, and everything is searchable with your preferred Internet search provider. The PowerShell Gallery is a great place to start searching for what we need.

Searching for anything related to firewall, I found a number of resources that I can use, but I'm going to go with one called xNetworking.

Image of menu

Installing DSC resources

If you're using WMF 4.0, I've got bad news for you: You're going to have to manually download and install the module. You can do this by grabbing the source files from GitHub (or wherever you find the binaries). If you're using WMF 5.0, however, you'll have a much easier time installing the module by using Find-Module and Install-Module:

PS C:\Windows\system32> Find-Module xNetworking

Version Name        Repository   Description                           

------- ----        ----------   -----------                           

2.5.0.0 xNetworking       PSGallery   Module with DSC Resources for Networking area                  

PS C:\Windows\system32> Find-Module xNetworking | Install-Module

You can also use Save-Module in lieu of Install-Module if you'd like to download the files and review the code before installing. I tested this module previously and trust it, so I've downloaded and installed it on my source machine. Now I can verify that the module is installed.

PS C:\Windows\system32> Get-DscResource -Module xNetworking

ImplementedAs Name      ModuleName      Version Properties         

------------- ----      ----------      ------- ----------         

PowerShell  xDefaultGatewayAddress xNetworking     2.5.0.0 {AddressFamily, InterfaceAlias, Address, Depend...

PowerShell  xDnsConnectionSuffix  xNetworking     2.5.0.0 {ConnectionSpecificSuffix, InterfaceAlias, Depe...

PowerShell  xDNSServerAddress   xNetworking     2.5.0.0 {Address, AddressFamily, InterfaceAlias, Depend...

PowerShell  xFirewall     xNetworking     2.5.0.0 {Name, Action, Authentication, DependsOn...} 

PowerShell  xIPAddress    xNetworking     2.5.0.0 {InterfaceAlias, IPAddress, AddressFamily, Depe...

PowerShell  xNetConnectionProfile  xNetworking     2.5.0.0 {InterfaceAlias, DependsOn, IPv4Connectivity, I...

And there we see our xFirewall DSC resource. So first let's add the module to our configuration file:

configuration CMDPConfig

{

 Import-DscResource -ModuleName @{ModuleName = 'PSDesiredStateConfiguration'; ModuleVersion = '1.1' }

 Import-DscResource -ModuleName @{ModuleName = 'xNetworking'; ModuleVersion = '2.5.0.0'}

 Node ("LWINCM02")

Let's grab the template for the xFirewall DSC resource like we did with the WindowsFeature resource:

xFirewall [String] #ResourceName

{

 Name = [string]

 [Action = [string]{ Allow | Block | NotConfigured }]

 [Authentication = [string]{ NoEncap | NotRequired | Required }]

 [DependsOn = [string[]]]

 [Description = [string]]

 [Direction = [string]{ Inbound | Outbound }]

 [DisplayName = [string]]

 [Enabled = [string]{ False | True }]

 [Encryption = [string]{ Dynamic | NotRequired | Required }]

 [Ensure = [string]{ Absent | Present }]

 [Group = [string]]

 [InterfaceAlias = [string[]]]

 [InterfaceType = [string]{ Any | RemoteAccess | Wired | Wireless }]

 [LocalAddress = [string[]]]

 [LocalPort = [string[]]]

 [LocalUser = [string]]

 [Package = [string]]

 [Platform = [string[]]]

 [Profile = [string[]]]

 [Program = [string]]

 [Protocol = [string]]

 [PsDscRunAsCredential = [PSCredential]]

 [RemoteAddress = [string[]]]

 [RemoteMachine = [string]]

 [RemotePort = [string[]]]

 [RemoteUser = [string]]

 [Service = [string]]

}

Now that's a lot of options! So I'm going to create a few rules here for SCCM. Here’s what I’ll need:

  • My TCP ports of 443(HTTPS), 8531(WSUS HTTPS), 445 (SMB), 135 (RPC endpoint)
  • 135 UDP (requires a separate rule)
  • The ephemeral 49152-65535 for RPC (requires a separate rule)
  • Rules for inbound and outbound access
  • 5986 TCP (I’m adding this to my TCP rules to make sure that we can document PSRemoting ports as open)

Here's the script:

xFirewall PSRemoteAndSCCRulesInboundTCP

{

 Name = "PSRemoting and SCCM Inbound Rules TCP"

 Ensure = "Present"

 DependsOn = "[WindowsFeature]BITSRSAT"

 Direction = "Inbound"

 Description = "PSRemoting and SCCM Inbound Rules TCP"

 Profile = "Domain"

 Protocol = "TCP"

 LocalPort = ("443","1723","8531","445","135","5986")

 Action = "Allow"

 Enabled = "True"

}

xFirewall PSRemoteAndSCCRulesOutboundTCP

{

 Name = "PSRemoting and SCCM Outbound Rules TCP"

 Ensure = "Present"

 DependsOn = "[xFireWall]PSRemoteAndSCCRulesInboundTCP"

 Direction = "Outbound"

 Description = "PSRemoting and SCCM Outbound Rules TCP"

 Profile = "Domain"

 Protocol = "TCP"

 LocalPort = ("443","1723","8531","445","135","5986")

 Action = "Allow"

 Enabled = "True"

}

xFirewall PSRemoteAndSCCRulesInboundUDP

{

 Name = "PSRemoting and SCCM Inbound Rules UDP"

 Ensure = "Present"

 DependsOn = "[xFireWall]PSRemoteAndSCCRulesOutboundTCP"

 Direction = "Inbound"

 Description = "PSRemoting and SCCM Inbound Rules UDP"

 Profile = "Domain"

 Protocol = "UDP"

 LocalPort = ("135")

 Action = "Allow"

 Enabled = "True"

}

xFirewall PSRemoteAndSCCRulesOutboundUDP

{

 Name = "PSRemoting and SCCM Outbound Rules UDP"

 Ensure = "Present"

 DependsOn = "[xFireWall]PSRemoteAndSCCRulesInboundUDP"

 Direction = "Outbound"

 Description = "PSRemoting and SCCM Outbound Rules UDP"

 Profile = "Domain"

 Protocol = "UDP"

 LocalPort = ("135")

 Action = "Allow"

 Enabled = "True"

}

xFirewall PSRemoteAndSCCRulesOutboundTCPEphemeral

{

 Name = "PSRemoting and SCCM Outbound Rules TCP Ephemeral"

 Ensure = "Present"

 DependsOn = "[xFireWall]PSRemoteAndSCCRulesOutboundUDP"

 Direction = "Outbound"

 Description = "PSRemoting and SCCM Outbound Rules TCP Ephemeral"

 Profile = "Domain"

 Protocol = "TCP"

 LocalPort = ("49152-65535")

 Action = "Allow"

 Enabled = "True"

}

xFirewall PSRemoteAndSCCRulesInboundTCPEphemeral

{

 Name = "PSRemoting and SCCM Inbound Rules TCP Ephemeral"

 Ensure = "Present"

 DependsOn = "[xFireWall]PSRemoteAndSCCRulesOutboundUDP"

 Direction = "Inbound"

 Description = "PSRemoting and SCCM Inbound Rules TCP Ephemeral"

 Profile = "Domain"

 Protocol = "TCP"

 LocalPort = ("49152-65535")

 Action = "Allow"

 Enabled = "True"

}

Copy and push DSC resources to target machine

Now we'll get ready to again push our configuration to the target test machine. But first, because we're currently using the push method to deliver the configurations to the test machine, we'll need to manually copy and install the xNetworking module on the target system.

Note   This is one of the issues with using the push method (for more information, read Pushing Configurations in the DSC Book). The DSC Book is a great place to learn more about key differences between the push and pull methods. It is available free on PowerShell.org: The DSC Book.

We've copied the xNetworking module to the target machine. Now that we've added the firewall rules, let's update the .mof file to make sure that it generates correctly:

PS C:\Windows\system32> C:\Scripts\Configs\CMDPConfig.ps1

 Directory: C:\Windows\system32\CMDPConfig

Mode    LastWriteTime   Length Name                                 

----    -------------   ------ ----                                  

-a----   1/7/2016 6:00 PM   20208 LWINCM02.mof                               

-a----   1/7/2016 6:00 PM   1080 LWINCM02.meta.mof                              

PS C:\Windows\system32> Start-DscConfiguration -Path .\CMDPConfig\ -ComputerName lwincm02 -Force -Verbose

Id  Name   PSJobTypeName State   HasMoreData  Location    Command    

--  ----   ------------- -----   -----------  --------    -------    

15  Job15   Configuratio... Running  True   lwincm02    Start-DscConfiguration...

VERBOSE: Time taken for configuration job to complete is 0.214 seconds

To verify that the target is running a configuration, run:

PS C:\Windows\system32> Get-DscLocalConfigurationManager -CimSession lwincm02

ActionAfterReboot    : ContinueConfiguration

AgentId      : FA621241-B2F2-11E5-80C0-000D3A331A74

AllowModuleOverWrite   : False

CertificateID     :

ConfigurationDownloadManagers : {}

ConfigurationID    :

ConfigurationMode    : ApplyAndAutoCorrect

ConfigurationModeFrequencyMins : 15

Credential      :

DebugMode      : {NONE}

DownloadManagerCustomData  :

DownloadManagerName   :

LCMCompatibleVersions   : {1.0, 2.0}

LCMState      : Busy

LCMStateDetail     : LCM is applying a new configuration.

LCMVersion      : 2.0

StatusRetentionTimeInDays  : 10

PartialConfigurations   :

RebootNodeIfNeeded    : True

RefreshFrequencyMins   : 30

RefreshMode     : PUSH

ReportManagers     : {}

ResourceModuleManagers   : {}

PSComputerName     : lwincm02

PSComputerName     : lwincm02

We'll wait a few minutes for the system to finish processing. If you're an SCCM guy like me, you know all about the “hurry up and wait!” scenario. After a few minutes, we'll take a look at the DscConfigurationStatus...

PS C:\Windows\system32> Get-DscConfigurationStatus -CimSession lwincm02

Status  StartDate     Type   Mode RebootRequested  NumberOfResources    PSComputerName               

------  ---------     ----   ---- ---------------  -----------------    --------------               

Success 1/7/2016 6:00:36 PM  Initial   PUSH False    14        lwincm02

Let's check for the firewall rules:

PS C:\Windows\system32> Get-NetFirewallRule -CimSession lwincm02 | Where-Object Name -Like '*SCCM*' | Select-Object PSComputerName,Name,Enabled,Profile,Direction | Format-Table

PSComputerName Name            Enabled Profile Direction

-------------- ----            ------- ------- ---------

lwincm02  PSRemoting and SCCM Inbound Rules TCP    True Domain Inbound

lwincm02  PSRemoting and SCCM Outbound Rules TCP    True Domain Outbound

lwincm02  PSRemoting and SCCM Inbound Rules UDP    True Domain Inbound

lwincm02  PSRemoting and SCCM Outbound Rules UDP    True Domain Outbound

lwincm02  PSRemoting and SCCM Outbound Rules TCP Ephemeral True Domain Outbound

lwincm02  PSRemoting and SCCM Inbound Rules TCP Ephemeral  True Domain Inbound

So we have added an additional DSC resource, deployed it, and configured our firewall steps.

Using Script resource to add your scripts

One of the most common things I get asked is, “How can I add custom scripts to a configuration file in DSC?”

My first response is always to create your own custom DSC resource. However, some people (myself included) sometimes need to experience the sloppy method before being turned to the Light Side.

The PSDesiredStateConfiguration module contains a DSC resource called Script. This resource allows you to insert custom scripts into your configurations. So let's take a look at it for a moment.

PS C:\Windows\system32> Get-DscResource Script -Syntax

Script [String] #ResourceName

{

 GetScript = [string]

 SetScript = [string]

 TestScript = [string]

 [Credential = [PSCredential]]

 [DependsOn = [string[]]]

 [PsDscRunAsCredential = [PSCredential]]

}

Much like building a DSC resource (for more information, see Writing a DSC Resource), there are three mandatory functions that you have to insert into the Script DSC resource provider: Get, Set, and Test.

To show you how this resource can be used, I’m going to skip down in my example script (#Apply Firewall Rules for SCCM Communication) to the insertion of the NO_SMS_ON_DRIVE.sms file on drives that I don't want SMS packages to be installed on.

GetScript, which invokes Get-DscConfiguration (Get-TargetResource) must return a hash table. You'll find in many cases on the Internet, that most people actually don't use this portion, and you'll see it commented like this:

GetScript = {

 #needs to return hashtable.

}#EndGetScript

This often begets the question, “If I don't have to use it, why bother?”

This seems to be a way to validate the input that's being given to the script block. For example, if we decided to have an $ExcludeDrive parameter to specify what drive we don't want to insert, we would do something like this:

GetScript = {

 @{'ExcludeDrive' = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne ($ExcludeDrive + ':')})}

}#EndGetScript

This should give us our drive outputs in a hash table format. I'm sure you can see where this is headed, but I'll keep going. I'm also going to keep drive E as the drive I want to exclude because this is a static configuration for now. We'll get into parameterization a little later.

Script InstallNoSMSOnDrive

  {

 GetScript = {

  @{'ExcludeDrive' = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})}

 }#EndGetScript

}#EndScript

TestScript leverages the Start-DscConfiguration command and will determine if the SetScript block needs to run. This is a Yes or No block, so you need to return a boolean True or False. Now here's the challenge, if you have an array of objects, you need to return a single True or False. Here's the problem when dealing with an array of disks that you're checking to see if the file exists:

PS C:\Windows\system32> $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

ForEach($Drive in $LogicalDisk){

     $Disk = ($Drive.DeviceID + '\')

     Test-Path ($Disk + 'NO_SMS_ON_DRIVE.sms')

     }#ForEach

True

True

False

If I run my script and it returns an array of True/False statements, only the last output will be acknowledged. So as this script stands, we have to take that array of outputs, and turn it into a single out. Here's how we do this:

  • Declare our array as a variable
  • Add an if statement at the end of our ForEach block to return a value of False if any output from the block returns false

If we execute our code, we'll get the following if there's one False statement:

PS C:\Windows\system32> $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

$DiskCheck = ForEach($Drive in $LogicalDisk){

 $Disk = ($Drive.DeviceID + '\')

 $Output = Test-Path ($Disk + 'NO_SMS_ON_DRIVE.sms')

 If ($Output -eq $false){Return $false}

 }#ForEach

False

If all of the outputs equal True, we'll get a null output like this:

PS C:\Windows\system32> $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

$DiskCheck = ForEach($Drive in $LogicalDisk){

 $Disk = ($Drive.DeviceID + '\')

 $Output = Test-Path ($Disk + 'NO_SMS_ON_DRIVE.sms')

 If ($Output -eq $false){Return $false}

 }#ForEach

PS C:\Windows\system32>

To manage this, we call the DiskCheck variable we created in an if statement that says if the output is null, then return True:

PS C:\Windows\system32> $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

$DiskCheck = ForEach($Drive in $LogicalDisk){

 $Disk = ($Drive.DeviceID + '\')

 $Output = Test-Path ($Disk + 'NO_SMS_ON_DRIVE.sms')

 If ($Output -eq $false){Return $false}

 }#ForEach

 If($DiskCheck -eq $null){Return $True}

True

And now we have our TestScript!

Script InstallNoSMSOnDrive

  {

 GetScript = {

  @{'ExcludeDrive' = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})}

 }#EndGetScript

 TestScript = {

  $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

  $DiskCheck = ForEach($Drive in $LogicalDisk){

   $Disk = ($Drive.DeviceID + '\')

   $Output = Test-Path ($Disk + 'NO_SMS_ON_DRIVE.sms')

   If ($Output -eq $false){Return $false}

   }#ForEach

   If($DiskCheck -eq $null){Return $True}

 }#EndTestScript

}#EndScript

Finally, to SetScript. This is exceptionally easy because it's basically adding what your original script was:

SetScript = {

 $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

 ForEach($Drive in $LogicalDisk){

 $Disk = ($Drive.DeviceID + '\')   

  If(!(Get-Item -Path ($Disk + 'NO_SMS_ON_DRIVE.sms') -ErrorAction SilentlyContinue)){      

  New-Item -Name 'NO_SMS_ON_DRIVE.sms' -Path $Disk -ItemType File  

  }#EndIf

 }#EndForeach

}#EndSetScript

It's pretty straightforward. I'm reviewing any logical disk that isn't assigned to drive E. Each drive is checked to see if the NO_SMS_ON_DRIVE.sms file exists. If it doesn't, make the file. So here's our completed Script resource provider script:

Script InstallNoSMSOnDrive {

GetScript = {

  @{'ExcludeDrive' = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})}

 }#EndGetScript

 TestScript = {

  $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

  $DiskCheck = ForEach($Drive in $LogicalDisk){

   $Disk = ($Drive.DeviceID + '\')

   $Output = Test-Path ($Disk + 'NO_SMS_ON_DRIVE.sms')

   If ($Output -eq $false){Return $false}

   }#ForEach

   If($DiskCheck -eq $null){Return $True}

 }#EndTestScript

 SetScript = {

  $LogicalDisk = (Get-CimInstance -ClassName Win32_LogicalDisk).where({$PSItem.DriveType -eq '3' -and $PSItem.DeviceID -ne 'E:'})

  ForEach($Drive in $LogicalDisk){

  $Disk = ($Drive.DeviceID + '\')      

   If(!(Get-Item -Path ($Disk + 'NO_SMS_ON_DRIVE.sms') -ErrorAction SilentlyContinue)){     

   New-Item -Name 'NO_SMS_ON_DRIVE.sms' -Path $Disk -ItemType File  

   }#EndIf

  }#EndForeach

 }#EndSetScript

 DependsOn = "[xFirewall]PSRemoteAndSCCRulesInboundTCPEphemeral"

}#EndScript

So let's see what this resource provider looks like in comparison to the others. It's a mess! Not only that, but you've basically done three-quarters of the work needed to make this an actual DSC resource. So we're going to explore that in our next exciting episode!

In the meantime, I've checked my target system, and it looks like our Script resource did the trick.

Image of command output

Phew! What a doozy! Well I'm sure everyone's brain needs to do a little percolating on this before we continue. To recap, here's what we've accomplished in this series:

  • Learned the basic ins an outs of a configuration in DSC, including format.
  • Created a basic configuration with the DSC resources and pushed it to a target machine.
  • Added a new DSC resource from the PowerShell Gallery, imported it into our configuration, and pushed the module and updated configuration to our target machine.
  • Added a custom script to the configuration, and discussed why Get, Test, and Set are required.

Next time, we'll be looking at the DSC Resource Designer, and creating our first DSC resource! Stay tuned!

~Will

Thanks, Will, for sharing your time and knowledge about DSC. I am looking forward to Part 6 tomorrow.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. Also check out my Microsoft Operations Management Suite Blog. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Comments (0)

Skip to main content