Wednesday, May 4, 2016

How to parameterize a basic DSC configuration

After reading my two previous posts (here and here) you should have a good understanding of how DSC Configurations work.

At this point, it may be useful to know that within a Configuration block you can use parameters just like you would do in Functions:

Configuration TestConfig
{
    Param (
 [string[]]$ComputerName
 )
    Node $Computername

    {
        WindowsFeature FileAndStorage
        {
            Name = 'FileAndStorage-Services'
            Ensure = 'Present'
        }
    }
}
The aim of this Configuration is to enable the File and Storage Services feature on a given node. If we query the help for it we get:

Man TestConfig -Parameter * | Format-Table Name,IsDynamic,Required,Position,Aliases

name              isDynamic required position aliases
----              --------- -------- -------- -------
ComputerName      false     false    4        None   
ConfigurationData false     false    3        None   
DependsOn         false     false    1        None   
InstanceName      false     false    0        None   
OutputPath        false     false    2        None
There are here four parameters that automatically come with the Configuration keyword plus one parameter that I explicitely define in the Param () section of the script: ComputerName.

Man TestConfig -Parameter ComputerName

-ComputerName 
    
    Required?                    false
    Position?                    4
    Accept pipeline input?       false
    Parameter set name           (All)
    Aliases                      None
    Dynamic?                     false
Running the Configuration will generate a MOF file for the server specified in $ComputerName:

TestConfig -ComputerName 'srv1'

WARNING: The configuration 'TestConfig' is loading one or more built-in resources without explicitly importing associated modules. Add Import-DscResource –ModuleName 'PSDesiredStateConfiguration' to your configuration to avoid this message.


    Directory: E:\dsc\TestConfig


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----         5/4/2016  12:58 PM           1974 srv1.mof

We can pass more than one server to the ComputerName parameter and each will get its customized MOF file:

TestConfig -ComputerName 'srv1','srv2'
WARNING: The configuration 'TestConfig' is loading one or more built-in resources without explicitly importing associated modules. Add Import-DscResource –ModuleName 'PSDesiredStateConfiguration' to your configuration to avoid this message.


    Directory: E:\dsc\TestConfig


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----         5/4/2016   1:00 PM           1974 srv1.mof
-a----         5/4/2016   1:00 PM           1962 srv2.mof
Inside each MOF you'll find a @TargetNode:

Get-Content .\TestConfig\* | Select-String Target

@TargetNode='srv1'
@TargetNode='srv2'
Let's try now to add a second parameter in the form of a Feature that we'd like to be installed on the target servers. Before we proceed, note here that the Name property of the WindowsFeature resource takes in the Name of the feature as it is returned by Get-WindowsFeature, not its Display Name otherwise you'll get a ugly red error when running Start-DscConfiguration:

PowerShell DSC resource MSFT_RoleResource  failed to execute Test-TargetResource functionality with error message: The requested feature File and Storage Services is not found on the target machine. 
    + CategoryInfo          : InvalidOperation: (:) [], CimException
    + FullyQualifiedErrorId : ProviderOperationExecutionFailure
    + PSComputerName        : srv1
So, here's my Configuration block with two parameters:

Configuration TestConfig
{
    Param (
    [string[]]$ComputerName,
    [string]$Feature
    )
    Node $Computername

    {
        WindowsFeature $Feature
        {
            Name = $Feature
            Ensure = 'Present'
        }
    }
}

TestConfig -ComputerName 'srv1' -Feature 'FileAndStorage-Services'

    Directory: E:\dsc\TestConfig


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----         5/4/2016   1:09 PM           1998 srv1.mof

That is working pretty cool!

Now if I want to have more than one service running, the question is: is it possible to do a foreach loop in the WindowsFeature block?

The answer is yes: just change the parameter type to String[] so that it can accept multiple values and add the foreach loop around the WindowsFeature bock.

Configuration TestConfig
{
    Param (
    [string[]]$ComputerName,
    [string[]]$Feature
    )
    Node $Computername

    {

    foreach($FeatureName in $Feature) {

        WindowsFeature $FeatureName

            {

            Name = $FeatureName

            }

    }
        
    }
}

TestConfig -ComputerName 'srv1','srv2' -Feature 'FileAndStorage-Services','FS-SMB1'

man TestConfig -Parameter * | Format-Table Name,parameterValue -auto

name              parameterValue
----              --------------
ComputerName      string[]      
ConfigurationData hashtable     
DependsOn         string[]      
Feature           string[]      
InstanceName      string        
OutputPath        string
That's a good achievement for today. Let me just finish this post beautyfing the output of this script and remove the orange warning:

WARNING: The configuration 'TestConfig' is loading one or more built-in resources without explicitly importing associated modules. Add Import-DscResource –ModuleName 'PSDesiredStateConfiguration' to your configuration to avoid this message.
To do so just add the Import-DscResource magic word between the Param () section and the Node keyword:


Feel free to share!

Monday, May 2, 2016

From monolithic to split DSC configurations

In the previous post I briefly introduced the PowerShell Configuration keyword which is used to describe desired states for your nodes.

Let’s have today a deeper look at its basic logic and see how Microsoft offered us a way to improve its usability in real-word environments.

1. DSC SNIPPETS

To ease our learning process, we can take advantage of a great PowerShell ISE functionality which is called Snippets. These are reusable sections of code that you can insert into your scripts, just like templates.

The default set of snippets that comes with your ISE comprehends five DSC-related snippets:


2. MONOLITHIC CONFIGURATIONS

In the simplest case, we will use the first snippet: DSC Configuration (simple)

configuration Name
{
    # One can evaluate expressions to get the node list
    # E.g: $AllNodes.Where("Role -eq Web").NodeName
    node ("Node1","Node2","Node3")
    {
        # Call Resource Provider
        # E.g: WindowsFeature, File
        WindowsFeature FriendlyName
        {
           Ensure = "Present"
           Name   = "Feature Name"
        }

        File FriendlyName
        {
            Ensure          = "Present"
            SourcePath      = $SourcePath
            DestinationPath = $DestinationPath
            Type            = "Directory"
            DependsOn       = "[WindowsFeature]FriendlyName"
        }
    }
}
As you can see the structure you see above matches the scripting rules I explained in the previous post, and contains two sections:
  • the list of nodes I am applying a configuration to (node1, node2 and node3)
  • the configuration to apply (a feature and a file in this example)
In my case I want to adapt this example to configure UAC and IEESC, so, after having installed the xSystemSecurity module (Install-Module or manually download from GitHub), I can easily modify the snippet to suit my needs:

configuration DisableUacIEEsc
{
    # Importing the required modules
    Import-DSCResource -Module xSystemSecurity -Name xUac
    Import-DSCResource -Module xSystemSecurity -Name xIEEsc

    node ("Node1","Node2","Node3")
    {
        xUAC NeverNotifyAndDisableAll
        {
           Setting = "NeverNotifyAndDisableAll"
        }
        xIEEsc DisableIEEsc
        {
            IsEnabled = $false
            UserRole = "Administrators"
        }       
    }
}

DisableUacIEEsc
The last line is the actual call to the Configuration, which generates one MOF file for each target computer in a subfolder named as the configuration itself under the current path:

E:\DSC\uacieesc.ps1
WARNING: The configuration 'DisableUacIEEsc' is loading one or more built-in resources without explicitly importing associated modules. Add Import-DscResource –ModuleName 'PSDesiredStateConfiguration' to your configuration to avoid this message.


    Directory: E:\DSC\DisableUacIEEsc


Mode       LastWriteTime         Length Name
----       -------------         ------ ----
-a----     4/28/2016  1:31 PM    6102 Node1.mof
-a----     4/28/2016  1:31 PM    6102 Node2.mof
-a----     4/28/2016  1:31 PM    6102 Node3.mof
Those MOF files are text files whose format has been standardized by the DMTF and that are passed to the Start-DscConfiguration cmdlet for execution:
Start-DscConfiguration -path .\DisableUacIEEsc -Wait -ComputerName Node1

A couple of notes here:
  • we are not passing the name of a single MOF but the name of folder that contains configuration settings files
  • the –Wait parameter is particularly useful here to follow the configuration application in real-time. In this case the appropriate registry keys are added to the target server. In the example below here’s how the registry keys for IEEsc appear after running Start-DscConfiguration.

Now that was the simplest case, which is perfect for the novice to understand how the Configuration keyword works.

3. SPLIT CONFIGURATIONS

I want to show you now how you can adopt a less monolithic approach and separate the configurations that you want to apply from the group of target computers you want to apply them to. There are a few major evident reasons for doing so, starting from the fact that we don’t want to hardcode the name of the servers in the same file used for keeping the desired configurations.
Let’s exercise a bit and convert the previous example to something more modular and agile.
A DSC Configuration like the one we built here above as a ConfigurationData parameter that accepts an optional hashtable which contains the information on the target computers:
Get-Command DisableUacIEEsc -Syntax

DisableUacIEEsc [[-InstanceName] ] [[-DependsOn] ] [[-OutputPath] ] [[-ConfigurationData] ] []
The called hashtable must contain at least the keyword AllNodes:

$MyEnvironment = 
@{
    AllNodes = @();
    NonNodeData = “”   
} 

$MyEnvironment

Name                           Value
----                           -----
AllNodes                       {}
NonNodeData
Inside the AllNodes you have to put a list of hashtables (as simple as @{ propertyname = value }) like in the following example:
$MyEnvironment = @{
    AllNodes = @(
        @{
        NodeName = "Node1"
        },
        @{
        NodeName = "Node1"
        },
        @{
        NodeName = "Node1"
        }
    )
}
Now I want to explicitly declare some of my nodes as DEV nodes, and others as PRODUCTION nodes:
$MyEnvironment = @{
    AllNodes = @(
        @{
        NodeName = "Node1"
        Role = 'PROD'
        },
        @{
        NodeName = "Node1"
        Role = 'DEV'
        },
        @{
        NodeName = "Node1"
        Role = 'DEV'
        }
    )
}
 $MyEnvironment.AllNodes

Name                           Value
----                           -----
NodeName                       Node1
Role                           PROD
NodeName                       Node1
Role                           DEV
NodeName                       Node1
Role                           DEV
When you save this file, remember to use the .psd1 extension, which is the same used for module manifests, and to remove the variable name at the beginning, otherwise you’ll get the following error:
DisableUacIEEsc : Cannot process argument transformation on parameter 'ConfigurationData'. Failed to load the PowerShell data file 'myenv.psd1' with the following error:
At E:\DSC\myenv.psd1:1 char:1
+ $MyEnvironment = @{
+ ~~~~~~~~~~~~~~~~~~~~
Assignment statements are not allowed in restricted language mode or a Data section.
At E:\DSC\myenv.psd1:1 char:1
+ $MyEnvironment = @{
+ ~~~~~~~~~~~~~~~
A variable that cannot be referenced in restricted language mode or a Data section is being referenced. Variables that can be referenced include the following: 
$PSCulture, $PSUICulture, $true, $false, and  $null.
At E:\DSC\uacieesc2.ps1:19 char:36
+ DisableUacIEEsc -ConfigurationData myenv.psd1
+                                    ~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [DisableUacIEEsc], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,DisableUacIEEsc
Once you have defined your environment, it is time to move to rewrite the configuration definition file as follow:
configuration DisableUacIEEsc
{
    Import-DSCResource -Module xSystemSecurity -Name xUac
    Import-DSCResource -Module xSystemSecurity -Name xIEEsc
    node $AllNodes.Where{$_.Role -eq “DEV”}.NodeName
    {
        xUAC NeverNotifyAndDisableAll
        {
           Setting = "NeverNotifyAndDisableAll"
        }
        xIEEsc DisableIEEsc
        {
            IsEnabled = $false
            UserRole = "Administrators"
        }       
    }
}

DisableUacIEEsc -ConfigurationData myenv.psd1
In my case I want to disable Uac and IEEsc only on DEV servers, so I replaced the hardcoded node names with the $AllNodes automatic variable and its Where() method.

4. SUMMIN' IT UP

In the end you have two files for a split DSC configuration against one file in case of a monolithic approach:

5. SNIPPETS AGAIN

To finish with, if you like the idea (you should) of separating your environmental configuration from the configuration itself, and you are scared of so much typing, remember that two ISE snippets are available for split DSC configurations:

DSC Configuration Data:
@{
    AllNodes = @(
        @{
            NodeName = "Node1"
            Role = "WebServer"
            },
        @{
            NodeName = "Node2"
            Role = "SQLServer"
            },
        @{
            NodeName = "Node3"
            Role = "WebServer"
            }
    )
}
# Save ConfigurationData in a file with .psd1 file extension


...and DSC Configuration (using ConfigurationData):
configuration ConfigurationName
{
    # One can evaluate expressions to get the node list
    # E.g: $AllNodes.Where("Role -eq Web").NodeName
    node $AllNodes.Where{$_.Role -eq "WebServer"}.NodeName
    {
        # Call Resource Provider
        # E.g: WindowsFeature, File
        WindowsFeature FriendlyName
        {
           Ensure = "Present"
           Name   = "Feature Name"
        }

        File FriendlyName
        {
            Ensure          = "Present"
            SourcePath      = $SourcePath
            DestinationPath = $DestinationPath
            Type            = "Directory"
            DependsOn       = "[WindowsFeature]FriendlyName"
        }       
    }
}

# ConfigurationName -configurationData 

In a next posts we will move the automation slider a bit further. Stay tuned.
Related Posts Plugin for WordPress, Blogger...