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). Don’t miss the previous posts in the series:
- Desired State Configuration: Part 1
- Desired State Configuration: Part 2
- Desired State Configuration: Part 3
- Desired State Configuration: Part 4
Note 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.
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.
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