Tag Archives: Powershell

Unit Testing with Pester : storing complex Mock objects in a JSON file

When unit testing with Pester, mocking is pretty much unavoidable, especially for code related to infrastructure, configuration, or deployment.
We don’t want our unit tests to touch files, a database, the registry, and not to mention the internet, do we ?

With Pester’s Mock function, we can isolate our code from this outside (hostile ?) world by faking commands and make them return whatever we want, even a custom object. Unfortunately, creating custom objects from within the Mock is not ideal when dealing with complex Mock objects with nested properties.

Let’s see an example to understand why.

We need to unit test a simple function (Get-ProcessModule) that lists all modules (DLLs) loaded in the process(es) specified by name :

Function Get-ProcessModule {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [string]$Name        
    )
    $Processes = Get-Process -Name $Name

    If ( $Processes ) {
        Foreach ( $Process in $Processes ) {
            $LoadedModules = $Process.Modules

            Foreach ( $LoadedModule in $LoadedModules ) {
                $CustomProps = @{'Name'= $LoadedModule.ModuleName
                                 'Version'= $LoadedModule.ProductVersion
                                 'PreRelease' = $LoadedModule.FileVersionInfo.IsPreRelease
                                }
            $CustomObj = New-Object -TypeName psobject -Property $CustomProps
            $CustomObj
            }
        }
    }
}

 
Nothing fancy here, but notice that we are looking at a property named IsPreRelease which is nested in the FileVersionInfo property which itself is nested within the Modules property of our Process objects.

When unit testing this function, we don’t know which process(es) are running or not, and which DLLs they have loaded. And we don’t want to start new processes just for the sake of testing. So, we will need to Mock Get-Process and return fake process objects with the properties we need, including the IsPreRelease nested property.

It would look like this :

 
While this does work, I’m not a big fan of cluttering the test file with 10 lines of code for every single Mock. Imagine if we had a dozen (or more) different Mock objects to create, this would add up pretty quickly and make the test file quite difficult to follow.

I think we should strive to keep our test files as concise and readable as possible, if we want to realize the benefits of Pester’s DSL : a fairly minimalist syntax that reads almost like a specification. Granted, it’s no Gherkin, but this might be coming

Also, because these Mock objects are just fake objects with fake property values, they should be considered more as test data than code. So, applying the “separation of concerns” principle, we should probably separate this data from the testing logic and store it in a distinct file.

Being a PowerShell kind of guy, my first choice was to use a standard PowerShell data file (.psd1). Let’s see how this works out :

@{
    Process1 =  [PSCustomObject]@{ 
        Modules = @( @{
            ModuleName = 'Module1FromProcess1';
            ProductVersion = '1.0.0.1';
            FileVersionInfo = @{
                IsPreRelease = $False
            }
        } );
    }
}

 
We have to specify the type PSCustomObject, otherwise it would be a hashtable when imported from the file back into PowerShell. Unfortunately, Import-PowerShellDataFile doesn’t like that :

 
This is because to safely import data, the cmdlet Import-PowerShellDataFile works in RestrictedLanguage mode. And in this mode, casting to a PSCustomObject (to any type, for that matter) is forbidden.

We could use Invoke-Expression instead of Import-PowerShellDataFile, but we’ve been told that Invoke-Expression is evil, so we should probably look for another option.

I heard that JSON is a nice and lightweight format to store data, so let’s try to use it to store our Mock objects. Here is the solution I came up with to represent Mock objects as JSON :

{
    "Get-Process": [
        {
            "1ProcessWithMatchingName": {
                "Modules": {
                    "ModuleName": "Module1FromProcess1",
                    "ProductVersion": "1.0.0.1",
                    "FileVersionInfo": {
                        "IsPreRelease": false
                    }
                }
            }
        },
        {
            "2ProcessesWithMatchingName": [
                {
                    "Modules": {
                        "ModuleName": "Module1FromProcess1",
                        "ProductVersion": "1.0.0.1",
                        "FileVersionInfo": {
                            "IsPreRelease": false
                        }
                    }
                },
                {
                    "Modules": {
                        "ModuleName": "Module1FromProcess2",
                        "ProductVersion": "2.0.0.1",
                        "FileVersionInfo": {
                            "IsPreRelease": true
                        }
                    }
                }
            ]
        }
    ]
}

 
NOTE : For “true” and “false” to be treated as proper boolean values, they have to be all lower case.

The data is organized hierarchically, as follow :

  1. The top level is the name of the mocked command (Get-Process in this case)
  2. The next level describes each scenario (or test case)
  3. The inner level is the actual object(s) that we want the mocked command to return, in this specific scenario.

As we can see above, the second scenario (labelled “2ProcessesWithMatchingName“) returns an array of 2 objects. We could make it return 3, or more, if we wanted to. We could also have multiple modules in some of our fake processes, but for illustration purposes, the above is enough.

We can import this data back into PowerShell with ConvertFrom-Json and explore the objects it contains, and their properties using what I call “dot-browsing” :

C:\> $JsonMockData = Get-Content .\TestData\MockObjects.json -Raw
C:\> $Mocks = ConvertFrom-Json $JsonMockData
C:\> $2ndTestCase = $Mocks.'Get-Process'.'2ProcessesWithMatchingName'
C:\> $2ndTestCase.Modules

ModuleName          ProductVersion FileVersionInfo
----------          -------------- ---------------
Module1FromProcess1 1.0.0.1        @{IsPreRelease=False}
Module1FromProcess2 2.0.0.1        @{IsPreRelease=True}


C:\> $2ndTestCase.Modules.FileVersionInfo.IsPreRelease
False
True
   

 
Now, let’s see how we can use this in our tests :

 
Within each Context block, we get the Mock for a specific scenario that we have defined in our JSON data and store it into the ContextMock variable. Then, to define our Mock, we just specify that its return value is our ContextMock variable.

We can even use the ContextMock variable to get the expected values for the Should assertions, like in the first 2 tests above.

You might be wondering why the hell I would filter ContextMock with : Where-Object { $_ }, in the second Context block. Well, this is because importing arrays from JSON to PowerShell has a tendency to add $Null items in the resulting array.

In this case, $ContextMock contained 3 objects : the 2 fake process objects, as expected, and a $Null element. Why ? I have no idea, but I was able to get rid of it with the Where-Object statement above.

As we can see, it makes the tests cleaner and allows to define Mocks in an expressive way, so overall, I think this is a nice solution to manage complex Mock data.

That said, unit testing is still a relatively new topic in the PowerShell community, and I haven’t heard or read anything on best practices around test data. So I’m curious, how do you guys handle Mock objects and more generally, test data ? Do you have any tips or techniques ?

Merging data from 2 PowerShell DSC configuration data files

As you probably already know, when writing a DSC configuration, separating the environmental data from the configuration logic is a best practice. So all the environment-specific data gets stored in separate (typically .psd1) files. If you work with PowerShell DSC at medium-to-large scale, you (hopefully) have separate configuration data files for each customer and each environment.

Something like this, for example :


C:\TEST                                                    
│   Common_ConfigDataServer.psd1                           
│                                                          
├───Customer A                                             
│   ├───Production                                         
│   │       ConfigDataServer.psd1                          
│   │                                                      
│   ├───Staging                                            
│   │       ConfigDataServer.psd1                          
│   │                                                      
│   └───Test                                               
│           ConfigDataServer.psd1                          
│                                                          
├───Customer B                                             
│   ├───Production                                         
│   │       ConfigDataServer.psd1                          
│   │                                                      
│   ├───Staging                                            
│   │       ConfigDataServer.psd1                          
│   │                                                      
│   └───Test                                               
│           ConfigDataServer.psd1                          
│                                                          
└───Customer C                                             
    ├───Production                                         
    │       ConfigDataServer.psd1                          
    │                                                      
    ├───Staging                                            
    │       ConfigDataServer.psd1                          
    │                                                      
    └───Test                                               
            ConfigDataServer.psd1                          
   

 

Now, imagine we add stuff to a DSC configuration which takes some values from additional settings in the configuration data files. Updating every configuration data files every time we add stuff to the DSC configuration would get very inefficient as the number of customers or environments grows.

A solution for that is to have a common configuration data file which contains the more common settings and their default values and which is shared across all customers/environments (Common_ConfigDataServer.psd1 in the example above). Then, we have a config data file for each environment, which contains only the data that is specific to a given customer or environment.

Finally, we merge the configuration data from the 2 files (the common one and the environment-specific one) before passing this to the ConfigurationData parameter of the DSC configuration. In this scenario, we need to ensure that the more specific data takes precedence over the common data. This means :

  • Data which is present in the environment-specific file and absent from the common file gets added to the merged configuration data
  • Data which is absent in the environment-specific file and present in the common file is preserved in the merged configuration data
  • Data which is present in both files gets the value from the environment-specific file in the merged configuration data

Let’s look at how to do this.
In the example we are going to work with, the content of the common configuration data file (Common_ConfigDataServer.psd1) is :

@{ 
    # Node specific data 
    AllNodes = @(
       @{ 
            NodeName = '*'
            PSDscAllowPlainTextPassword = $True
            ServicesEndpoint = 'http://localhost/Services/'
            TimeZone = 'Pacific Standard Time'
       }
    );
}
   

 
And we are going to merge/override it with the file for Customer A’s Test environment, which contains this :

@{ 
    # Node specific data 
    AllNodes = @( 
       @{ 
            NodeName = '*'
            TimeZone = 'GMT Standard Time'
            LocalAdministrators = 'MyLocalUser'
       },
       @{
            NodeName = 'Server1'
            Role = 'Primary'
       },
       @{
            NodeName = 'Server2'
            Role = 'Secondary'
       }
    );
}
   

 
As we can see, the environment-specific data contains :

  • Additional node entries : Server1 and server2
  • An additional setting in an existing node : “LocalAdministrators” in the “*” node entry
  • A different value for an existing setting in an existing node : TimeZone in the “*” node entry (because this specific customer is located in Ireland)

To take care of the merging, we are going to use a function I wrote, named Merge-DscConfigData. The module containing this function is available on GitHub and on the PowerShell Gallery.

NOTE : This function uses Invoke-Expression to convert the content of the configuration data files into PowerShell objects. This is to keep this function compatible with PowerShell 4.0, but be aware that using Invoke-Expression has security implications. If you can get away with being compatible only with PowerShell 5.0 and later, then you should use the much safer Import-PowerShellDataFile instead.

This function takes the path of the common configuration data file via its BaseConfigFilePath parameter and the environment-specific data file via its OverrideConfigFilePath parameter.
It outputs the merged data as a hashtable that can be directly consumed by a DSC configuration.

Here is what it looks like :


 
The function’s verbose output gives a pretty good idea of how it works.
Also, we can see that the output object is a hashtable. More accurately, it is a hashtable containing an array of nested hashtables (one per node entry). This is exactly what the ConfigurationData parameter of any DSC configuration expects.

Now, let’s verify we can use this output object in a DSC configuration and that running the configuration results in the expected MOF files.
For testing purposes, we are going to use the following DSC configuration :

Configuration ProvisionServers
{

    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName xTimeZone

     Node $AllNodes.NodeName
     {
        Registry ServicesEndpoint
        {
            Key = 'HKLM:\SOFTWARE\MyApp\Server\Config'
            ValueName = 'ServicesEndpoint'
            ValueData = $Node.ServicesEndpoint
            ValueType = 'String'
            Ensure = 'Present'
        }
        xTimeZone TimeZone
        {
            IsSingleInstance = 'Yes'
            TimeZone = $Node.TimeZone
        }
        If ( $Node.LocalAdministrators ) {
            Group LocalAdminUsers
            {
                GroupName = 'Administrators'
                MembersToInclude = $Node.LocalAdministrators
                Ensure = 'Present'
            }
        }
     }

    Node $AllNodes.Where{$_.Role -eq 'Primary'}.NodeName
    {
        File FolderForPrimaryServer
        {
            DestinationPath = 'C:\MyApp_Data'
            Ensure = 'Present'
            Type = 'Directory'
        }
    }
}
   

 
Then, we just invoke our configuration named ProvisionServers, passing our merged data to its ConfigurationData parameter, like so :


 
Now, let’s check the configuration documents which have been generated from this DSC configuration and data. Here is the content of Server1.mof :

/*
@TargetNode='Server1'
@GeneratedBy=mbuisson
@GenerationDate=01/06/2017 13:52:43
*/

instance of MSFT_RegistryResource as $MSFT_RegistryResource1ref
{
ResourceID = "[Registry]ServicesEndpoint";
 ValueName = "ServicesEndpoint";
 Key = "HKLM:\\SOFTWARE\\MyApp\\Server\\Config";
 Ensure = "Present";
 SourceInfo = "::9::9::Registry";
 ValueType = "String";
 ModuleName = "PSDesiredStateConfiguration";
 ValueData = {
    "http://localhost/Services/"
};

ModuleVersion = "1.0";

 ConfigurationName = "ProvisionServers";

};
instance of xTimeZone as $xTimeZone1ref
{
ResourceID = "[xTimeZone]TimeZone";
 SourceInfo = "::17::9::xTimeZone";
 TimeZone = "GMT Standard Time";
 IsSingleInstance = "Yes";
 ModuleName = "xTimeZone";
 ModuleVersion = "1.3.0.0";

 ConfigurationName = "ProvisionServers";

};
instance of MSFT_GroupResource as $MSFT_GroupResource1ref
{
ResourceID = "[Group]LocalAdminUsers";
 MembersToInclude = {
    "MyLocalUser"
};
 Ensure = "Present";
 SourceInfo = "::23::13::Group";
 GroupName = "Administrators";
 ModuleName = "PSDesiredStateConfiguration";

ModuleVersion = "1.0";

 ConfigurationName = "ProvisionServers";

};
instance of MSFT_FileDirectoryConfiguration as $MSFT_FileDirectoryConfiguration1ref
{
ResourceID = "[File]FolderForPrimaryServer";
 Type = "Directory";
 Ensure = "Present";
 DestinationPath = "C:\\MyApp_Data";
 ModuleName = "PSDesiredStateConfiguration";
 SourceInfo = "::34::9::File";

ModuleVersion = "1.0";

 ConfigurationName = "ProvisionServers";

};
instance of OMI_ConfigurationDocument


                    {
                        Version="2.0.0";
                        MinimumCompatibleVersion = "1.0.0";
                        CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"};
                        Author="mbuisson";
                        GenerationDate="01/06/2017 13:52:43";
                        Name="ProvisionServers";
                    };
   

 
First, the sole fact that we got a file named Server1.mof tells us one thing : the node entry with the NodeName “Server1” was indeed in the merged config data.

Also, we can see that the value of the setting ServicesEndpoint from the common data file was preserved and properly injected in the Registry resource entry of the DSC configuration.

Then, we see that the time zone value is “GMT Standard Time”, so this was overridden by the environment-specific data, as expected. The setting “LocalAdministrators” was not present in the common data file but it got added and its value is properly reflected in the Group resource entry.

Finally, the resource entry named “FolderForPrimaryServer” was processed, which means the “Role” settings had the value “Primary”. This is the expected value for Server1.

Now, we can verify the configuration document which has been generated for Server2 :

/*
@TargetNode='Server2'
@GeneratedBy=mbuisson
@GenerationDate=01/06/2017 13:52:43
*/

instance of MSFT_RegistryResource as $MSFT_RegistryResource1ref
{
ResourceID = "[Registry]ServicesEndpoint";
 ValueName = "ServicesEndpoint";
 Key = "HKLM:\\SOFTWARE\\MyApp\\Server\\Config";
 Ensure = "Present";
 SourceInfo = "::9::9::Registry";
 ValueType = "String";
 ModuleName = "PSDesiredStateConfiguration";
 ValueData = {
    "http://localhost/Services/"
};

ModuleVersion = "1.0";

 ConfigurationName = "ProvisionServers";

};
instance of xTimeZone as $xTimeZone1ref
{
ResourceID = "[xTimeZone]TimeZone";
 SourceInfo = "::17::9::xTimeZone";
 TimeZone = "GMT Standard Time";
 IsSingleInstance = "Yes";
 ModuleName = "xTimeZone";
 ModuleVersion = "1.3.0.0";

 ConfigurationName = "ProvisionServers";

};
instance of MSFT_GroupResource as $MSFT_GroupResource1ref
{
ResourceID = "[Group]LocalAdminUsers";
 MembersToInclude = {
    "MyLocalUser"
};
 Ensure = "Present";
 SourceInfo = "::23::13::Group";
 GroupName = "Administrators";
 ModuleName = "PSDesiredStateConfiguration";

ModuleVersion = "1.0";

 ConfigurationName = "ProvisionServers";

};
instance of OMI_ConfigurationDocument


                    {
                        Version="2.0.0";
                        MinimumCompatibleVersion = "1.0.0";
                        CompatibleVersionAdditionalProperties= {"Omi_BaseResource:ConfigurationName"};
                        Author="mbuisson";
                        GenerationDate="01/06/2017 13:52:43";                        
                        Name="ProvisionServers";
                    };
   

 
The value of the setting ServicesEndpoint from the common data file was preserved as well. The time zone value is “GMT Standard Time”, so this was overridden as well. The setting “LocalAdministrators” got added as well because it applied to all nodes in the environment-specific data file.

More interestingly, unlike the MOF file for Server1, the one for Server2 doesn’t have the resource entry named “FolderForPrimaryServer“. This tells us that in the merged configuration data, the Role value for Server2 was not “Primary”. This is expected because the value for this setting was “Secondary” in the environment-specific data file.

That’s all there is to using the Merge-DscConfigData function.

I am aware that some configuration management tools can make overriding configuration data easier, for example, attributes defined at a Chef cookbook level can be overridden at different levels. But for those of us using PowerShell DSC in production, this is a working alternative.

Searching code, repos, issues, pull requests and users on GitHub using PowerShell

Coding, especially in PowerShell, is not about remembering the exact syntax of how to do something, it is more about knowing how to try things out and how to find the information you need to do whatever you need to accomplish.

Quite often, even though we don’t remember the exact syntax, we know the thing we want to do is something we’ve already done before. So there is a good chance it’s already in the code base we have published on GitHub over the years.
If only we could use GitHub as an extension of our brain and, even better, directly from our PowerShell console …

This is why I wrote the PowerShell module PSGithubSearch, available on the PowerShell Gallery and (off course) on GitHub. It uses the extensive GitHub Search API to search the following items on GitHub.com :

  • Code
  • Repositories
  • Issues
  • Pull requests
  • Users
  • Organisations

Let’s have a look at a few examples to illustrate how we can use its 4 cmdlets.

Find-GitHubCode :

Maybe you are working on a new PowerShell function which could really use a -Confirm parameter, let’s say Invoke-NuclearRedButton.
You know that this requires the SupportsShouldProcess cmdletBinding attribute but you never ever remember exactly how to use it inside the function. Then, you could do that :

C:\> Find-GitHubCode -Keywords 'SupportsShouldProcess' -User $Me -Language 'PowerShell' | Select-Object -First 1


FileName               : Update-ChocolateyPackage.psm1
FilePath               : Update-ChocolateyPackage/Update-ChocolateyPackage.psm1
File Url               : https://github.com/MathieuBuisson/Powershell-Utility/blob/5dde8e9bb4fa6244953fee4042e2100acfc6
                         ad72/Update-ChocolateyPackage/Update-ChocolateyPackage.psm1
File Details Url       : https://api.github.com/repositories/28262426/contents/Update-ChocolateyPackage/Update-Chocolat
                         eyPackage.psm1?ref=5dde8e9bb4fa6244953fee4042e2100acfc6ad72
Repository Name        : MathieuBuisson/Powershell-Utility
Repository Description : Various generic tools (scripts or modules) which can be reused from other scripts or modules
Repository Url         : https://github.com/MathieuBuisson/Powershell-Utility

 

OK, but you might want to see the actual file content with the code. This is not built into the cmdlet because it requires an additional API call and the GitHub search API limits the number of unauthenticated requests to 10 per minute, so I tried to limit as much as possible the number of API requests. Anyway, here is how to get the actual code :

C:\> $FileUrl = (Find-GitHubCode -Keywords 'SupportsShouldProcess' -User $Me -Language 'PowerShell' | Select-Object -First 1).url
C:\> $Base64FileContent = (Invoke-RestMethod -Uri $FileUrl).Content
C:\> [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Base64FileContent)) -split '\n' |
 Where-Object { $_ -match 'ShouldProcess' }

    [CmdletBinding(SupportsShouldProcess)]
                If ($PSCmdlet.ShouldProcess($($CurrentPackage.Name), "Install-Package")) {

 
Yep, not that easy.
The original result provides us with a URL that we can query to get more information on that file.
This additional query gives us the content of the file but as a Base64 encoded string, that’s why we need to decode it as UTF8.
But then, it is still a single (large) string, that’s why we needed to split it on each new line character (\n).

This is not very practical for large files, so you can restrict the search to files of a certain size (in bytes), like so :

C:\> Find-GitHubCode -Repo "$Me/Powershell-Utility" -Keywords 'SupportsShouldProcess' -SizeBytes '<4000'


FileName               : Update-ChocolateyPackage.psm1
FilePath               : Update-ChocolateyPackage/Update-ChocolateyPackage.psm1
File Url               : https://github.com/MathieuBuisson/Powershell-Utility/blob/5dde8e9bb4fa6244953fee4042e2100acfc6
                         ad72/Update-ChocolateyPackage/Update-ChocolateyPackage.psm1
File Details Url       : https://api.github.com/repositories/28262426/contents/Update-ChocolateyPackage/Update-Chocolat
                         eyPackage.psm1?ref=5dde8e9bb4fa6244953fee4042e2100acfc6ad72
Repository Name        : MathieuBuisson/Powershell-Utility
Repository Description : Various generic tools (scripts or modules) which can be reused from other scripts or modules
Repository Url         : https://github.com/MathieuBuisson/Powershell-Utility

 

There are other parameters which can help narrow down the search to get exactly what we need. We’ll leave that as homework, because we have 3 other cmdlets to cover.

Find-GitHubRepository :

As an example, we might want to know what interesting project one of our favorite automation hero is currently working on :

C:\> $WarrenRepos = Find-GitHubRepository -Keywords 'PowerShell' -User 'RamblingCookieMonster'
C:\> $WarrenRepos | Sort-Object -Property pushed_at -Descending | Select-Object -First 1


Name         : PSDepend
Full Name    : RamblingCookieMonster/PSDepend
Owner        : RamblingCookieMonster
Url          : https://github.com/RamblingCookieMonster/PSDepend
Description  : PowerShell Dependency Handler
Fork         : False
Last Updated : 2016-08-28T15:48:52Z
Last Pushed  : 2016-09-29T16:06:04Z
Clone Url    : https://github.com/RamblingCookieMonster/PSDepend.git
Size (KB)    : 202
Stars        : 10
Language     : PowerShell
Forks        : 2

 
Warren has been working on this new project for a good month, and this project already has 10 stars and 2 forks. It definitely looks like an interesting project.

We can also sort the results by popularity (number of stars) or by activity (number of forks). For example, we could get the 5 most popular PowerShell projects related to deployment, like so :

C:\> $DeploymentProjects = Find-GitHubRepository -Keywords 'Deployment' -In description -Language 'PowerShell' -SortBy stars
C:\> $DeploymentProjects | Select-Object -First 5

Name         : AzureStack-QuickStart-Templates
Full Name    : Azure/AzureStack-QuickStart-Templates
Owner        : Azure
Url          : https://github.com/Azure/AzureStack-QuickStart-Templates
Description  : Quick start ARM templates that deploy on Microsoft Azure Stack
Fork         : False
Last Updated : 2016-10-01T19:16:20Z
Last Pushed  : 2016-09-28T22:03:44Z
Clone Url    : https://github.com/Azure/AzureStack-QuickStart-Templates.git
Size (KB)    : 11617
Stars        : 118
Language     : PowerShell
Forks        : 74

Name         : unfold
Full Name    : thomasvm/unfold
Owner        : thomasvm
Url          : https://github.com/thomasvm/unfold
Description  : Powershell-based deployment solution for .net web applications
Fork         : False
Last Updated : 2016-09-25T03:55:03Z
Last Pushed  : 2014-10-10T07:28:22Z
Clone Url    : https://github.com/thomasvm/unfold.git
Size (KB)    : 1023
Stars        : 107
Language     : PowerShell
Forks        : 13

Name         : AppRolla
Full Name    : appveyor/AppRolla
Owner        : appveyor
Url          : https://github.com/appveyor/AppRolla
Description  : PowerShell framework for deploying distributed .NET applications to multi-server environments inspired
               by Capistrano
Fork         : False
Last Updated : 2016-09-20T10:29:29Z
Last Pushed  : 2013-08-25T18:04:13Z
Clone Url    : https://github.com/appveyor/AppRolla.git
Size (KB)    : 546
Stars        : 91
Language     : PowerShell
Forks        : 10

Name         : SharePointDsc
Full Name    : PowerShell/SharePointDsc
Owner        : PowerShell
Url          : https://github.com/PowerShell/SharePointDsc
Description  : The SharePointDsc PowerShell module provides DSC resources that can be used to deploy and manage a
               SharePoint farm
Fork         : False
Last Updated : 2016-09-05T12:05:18Z
Last Pushed  : 2016-09-27T17:07:10Z
Clone Url    : https://github.com/PowerShell/SharePointDsc.git
Size (KB)    : 3576
Stars        : 80
Language     : PowerShell
Forks        : 48

Name         : PSDeploy
Full Name    : RamblingCookieMonster/PSDeploy
Owner        : RamblingCookieMonster
Url          : https://github.com/RamblingCookieMonster/PSDeploy
Description  : Simple PowerShell based deployments
Fork         : False
Last Updated : 2016-09-27T17:33:51Z
Last Pushed  : 2016-09-29T09:42:46Z
Clone Url    : https://github.com/RamblingCookieMonster/PSDeploy.git
Size (KB)    : 665
Stars        : 76
Language     : PowerShell
Forks        : 18

 
We used the In parameter to look for the “Deployment” keyword in the description field of repositories, we could look only in the repositories name or ReadMe, if we wanted to.

Now let’s dive into the heart of GitHub collaboration with issues and pull requests.

Find-GitHubIssue :

Maybe, we want to know the most commented open issue on the PowerShell project which hasn’t been assigned to anyone yet. This is pretty easy to do :

C:\> Find-GitHubIssue -Repo 'PowerShell/PowerShell' -Type issue -State open -No assignee -SortBy comments |
 Select-Object -First 1


Title        : Parameter binding problem with ValueFromRemainingArguments in PS functions
Number       : 2035
Id           : 172737066
Url          : https://github.com/PowerShell/PowerShell/issues/2035
Opened by    : dlwyatt
Labels       : {Area-Language, Issue-Bug, Issue-Discussion}
State        : open
Assignees    :
Comments     :
Created      : 2016-08-23T15:51:55Z
Last Updated : 2016-09-29T14:45:43Z
Closed       :
Body         : Steps to reproduce
               ------------------

               Define a PowerShell function with an array parameter using the ValueFromRemainingArguments property of
               the Parameter attribute.  Instead of sending multiple arguments, send that parameter a single array
               argument.

               ```posh
                & {
                    param(
                        [string]
                        [Parameter(Position=0)]
                        $Root,

                        [string[]]
                        [Parameter(Position=1, ValueFromRemainingArguments)]
                        $Extra)
                    $Extra.Count;
                    for ($i = 0; $i -lt $Extra.Count; $i++)
                    {
                       "${i}: $($Extra[$i])"
                    }
                } root aa,bb
               ```

               Expected behavior
               -----------------
               The array should be bound to the parameter just as you sent it, the same way it works for cmdlets.
               (The "ValueFromRemainingArguments" behavior isn't used, in this case, it should just bind like any
               other array parameter type.)  The output of the above script block should be:

               2
               0: aa
               1: bb

               Actual behavior
               ---------------
               PowerShell appears to be performing type conversion on the argument to treat the array as a single
               element of the parameter's array, instead of checking first to see if more arguments will be bound as
               "remaining arguments" first.  The output of the above script block is currently:

               1
               0: aa bb

               Additional information
               ------------------

               To demonstrate that the behavior of cmdlets is different, you can use this code:

               ```posh
               Add-Type -OutputAssembly $env:temp\testBinding.dll -TypeDefinition @'
                   using System;
                   using System.Management.Automation;

                   [Cmdlet("Test", "Binding")]
                   public class TestBindingCommand : PSCmdlet
                   {
                       [Parameter(Position = 0)]
                       public string Root { get; set; }

                       [Parameter(Position = 1, ValueFromRemainingArguments = true)]
                       public string[] Extra { get; set; }

                       protected override void ProcessRecord()
                       {
                           WriteObject(Extra.Length);
                           for (int i = 0; i < Extra.Length; i++)
                           {
                               WriteObject(String.Format("{0}: {1}", i, Extra[i]));
                           }
                       }
                   }
               '@

               Import-Module $env:temp\testBinding.dll

               Test-Binding root aa,bb
               ```

               Environment data
               ----------------

               <!-- provide the output of $PSVersionTable -->

               ```powershell
               > $PSVersionTable
               Name                           Value
               ----                           -----
               PSEdition                      Core
               PSVersion                      6.0.0-alpha
               PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
               WSManStackVersion              3.0
               GitCommitId                    v6.0.0-alpha.9-107-g203ace04c09dbbc1ac00d6b497849cb69cc919fb-dirty
               PSRemotingProtocolVersion      2.3
               CLRVersion
               SerializationVersion           1.1.0.1
               BuildVersion                   3.0.0.0
               ```

 
That is a pretty detailed and complex issue.

Now, we might wonder who is the top contributor (in terms of Pull requests) to the PowerShell documentation project :

C:\> $DocsPR = Find-GitHubIssue -Type pr -Repo 'PowerShell/PowerShell-Docs'
C:\> $DocsPR | Group-Object { $_.user.login } |
 Sort-Object -Property Count -Descending | Select-Object -First 10

Count Name                      Group
----- ----                      -----
  141 eslesar                   {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/650; repositor...
   81 HemantMahawar             {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/656; repositor...
   59 alexandair                {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/627; repositor...
   32 juanpablojofre            {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/657; repositor...
   28 neemas                    {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/107; repositor...
   26 joeyaiello                {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/554; repositor...
   20 Dan1el42                  {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/645; repositor...
   10 PlagueHO                  {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/330; repositor...
    9 JKeithB                   {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/605; repositor...
    8 saldana                   {@{url=https://api.github.com/repos/PowerShell/PowerShell-Docs/issues/455; repositor...
   

 
Wow, some people are really into documentation.

Find-GithubUser :

GitHub.com is the largest open-source software community, so it is a great place to find passionate and talented coders who are willing to share their craft with the community.

Let’s say you are a recruiter or a hiring manager and you are looking for a PowerShell talent in Ireland. The cmdlet Find-GithubUser can facilitate that search :

C:\> Find-GithubUser -Type user -Language 'PowerShell' -Location 'Ireland' | Where-Object { $_.Hireable }


UserName      : JunSmith
Full Name     : Jun Smith
Type          : User
Url           : https://github.com/JunSmith
Company       :
Blog          :
Location      : Ireland
Email Address :
Hireable      : True
Bio           :
Repos         : 12
Gists         : 0
Followers     : 7
Following     : 4
Joined        : 2015-01-17T13:27:24Z

UserName      : TheMasterPrawn
Full Name     : Rob Allen
Type          : User
Url           : https://github.com/TheMasterPrawn
Company       : Unity Technology Solutions IRL
Blog          :
Location      : Ireland
Email Address :
Hireable      : True
Bio           : I.T/Dev guy, gamer, geek living and working in Ireland.
Repos         : 3
Gists         : 0
Followers     : 0
Following     : 0
Joined        : 2014-06-10T11:09:20Z

 
2 users only. That’s helpful … It highlights the huge PowerShell skill gap we have in Ireland.

Let’s focus on UK and organizations, then. Organizations don’t have followers (or following) so we cannot filter them on the number of followers they have, but we can narrow down the search to those which have 5 or more repos :

C:\> Find-GithubUser -Type org -Language 'PowerShell' -Location 'UK' -Repos '>=5'


UserName      : SpottedHyenaUK
Full Name     : Spotted Hyena UK
Type          : Organization
Url           : https://github.com/SpottedHyenaUK
Company       :
Blog          : http://www.spottedhyena.co.uk
Location      : Leeds, UK
Email Address :
Hireable      :
Bio           :
Repos         : 5
Gists         : 0
Followers     : 0
Following     : 0
Joined        : 2015-02-03T14:38:16Z

UserName      : VirtualEngine
Full Name     : Virtual Engine
Type          : Organization
Url           : https://github.com/VirtualEngine
Company       :
Blog          : http://virtualengine.co.uk
Location      : UK
Email Address : info@virtualengine.co.uk
Hireable      :
Bio           :
Repos         : 17
Gists         : 0
Followers     : 0
Following     : 0
Joined        : 2014-03-21T14:51:14Z

 
That was just a few examples, but since each of these cmdlets have many parameters, this was just scratching the surface of what can be done with them.
For the full list of parameters (and how to use them), please refer to the README file on GitHub or use Get-Help.

Also, I’m aware it’s not perfect (yet) so issues and pull requests are very much welcome.

Getting the members of a type, without an instance of the type

I read yesterday’s article from Jeff Hicks on “Dancing on the table with PowerShell“. The content is really quite fascinating (go read it !), but I got hung up on something.

He creates a new object of the type [System.Data.DataTable], pipes it to Get-Member and shows that we can’t discover the members of this object type, like so :

C:\> $table = New-Object -TypeName 'System.Data.DataTable'
C:\> $table | Get-Member
Get-Member : You must specify an object for the Get-Member cmdlet.
At line:1 char:10
+ $table | Get-Member
+          ~~~~~~~~~~
    + CategoryInfo          : CloseError: (:) [Get-Member], InvalidOperationException
    + FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand

 
My understanding is that Get-Member is complaining because at this point, $Table is not a fully formed object. Even though it has a type :

C:\> $table.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     DataTable                                System.ComponentModel.MarshalByValueComponent

 
Anyway, my thought was :

“There has to be a way to get the members of a type, even without an instance of that type”

So, let’s see how we can do that.

First, we get a type object from our type name :

C:\> $TypeObj = 'System.Data.DataTable' -as [type]
    

 
Then, this [Type] object surely has a property or a method allowing us to view the members associated with that type. So we can try something like this :

C:\> $TypeObj | Get-Member -Name "*Members*"


   TypeName: System.RuntimeType

Name              MemberType Definition
----              ---------- ----------
FindMembers       Method     System.Reflection.MemberInfo[] FindMembers(System.Reflection.MemberTypes memberType, Sy...
GetDefaultMembers Method     System.Reflection.MemberInfo[] GetDefaultMembers(), System.Reflection.MemberInfo[] _Typ...
GetMembers        Method     System.Reflection.MemberInfo[] GetMembers(), System.Reflection.MemberInfo[] GetMembers(...

 
Yep, the GetMembers() method (if it does what it says) is exactly what we want.
Let’s try it :

C:\> $TypeObj.GetMembers() | Select-Object -Property Name,MemberType

Name                    MemberType
----                    ----------
get_CaseSensitive           Method
set_CaseSensitive           Method
get_IsInitialized           Method
set_RemotingFormat          Method
get_RemotingFormat          Method
get_ChildRelations          Method
get_Columns                 Method
get_Constraints             Method
get_DataSet                 Method
get_DefaultView             Method
set_DisplayExpression       Method
get_DisplayExpression       Method
get_ExtendedProperties      Method
get_HasErrors               Method
set_Locale                  Method
get_Locale                  Method
get_MinimumCapacity         Method
set_MinimumCapacity         Method
get_ParentRelations         Method
get_PrimaryKey              Method
set_PrimaryKey              Method
get_Rows                    Method
set_TableName               Method
get_TableName               Method
set_Namespace               Method
get_Namespace               Method
get_Prefix                  Method
set_Prefix                  Method
set_Site                    Method
get_Site                    Method
get_Container               Method
get_DesignMode              Method
GetObjectData               Method

# Ouput cut for brevity

C:\> ($TypeObj.GetMembers() | Select-Object -Property Name,MemberType).Count
159

 
There are a lot of members for this type. This method also allows us to see some members that we wouldn’t see by default using Get-Member, because it even gets non-public members. And there are some duplicates :

C:\> ($TypeObj.GetMembers() | Select-Object Name,MemberType -Unique).Count
120

 
By the way, in case we are only interested in the properties of our type, there is another method (aptly named GetProperties) which gets only the properties :

C:\> $TypeObj.GetProperties() | Select-Object -Property Name,MemberType

Name               MemberType
----               ----------
CaseSensitive        Property
IsInitialized        Property
RemotingFormat       Property
ChildRelations       Property
Columns              Property
Constraints          Property
DataSet              Property
DefaultView          Property
DisplayExpression    Property
ExtendedProperties   Property
HasErrors            Property
Locale               Property
MinimumCapacity      Property
ParentRelations      Property
PrimaryKey           Property
Rows                 Property
TableName            Property
Namespace            Property
Prefix               Property
Site                 Property
Container            Property
DesignMode           Property

 
That’s pretty much all we need to know to view the members associated with a type, without having to create an object of that type.
We can package this knowledge into a function, for convenient reuse, like so :

Function Get-MemberFromTypeName
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory=$True)]
        [string]$TypeName
    )
    $TypeObj = $TypeName -as [type]
    $RawMembers = $TypeObj.GetMembers()

    [System.Collections.ArrayList]$OutputMembers = @()

    Foreach ( $RawMember in $RawMembers ) {
        
        $OutputProps = [ordered]@{
                                'Name'= $RawMember.Name
			                    'MemberType'= $RawMember.MemberType
                        }

        $OutputMember = New-Object -TypeName psobject -Property $OutputProps
        $OutputMembers += $OutputMember
    }
    $OutputMembers | Select-Object -Property * -Unique
}

 

Using Pester to validate deployment readiness for a large number of machines

Recently, I had to roll out an upgrade of our software for a customer. The upgrade failed for about 80 client machines (out of around 400). There was a lot of head-scratching and quite a few “It was working in the Test environment !”. Because we couldn’t afford much more downtime for end-users, I had to develop an ugly workaround to allow machines to upgrade. But even so, this upgrade rollout went well over the planned maintenance window.

In short, it was a pain. And you know what ?
Pains are great learning opportunities and powerful incentives to take action. The first lesson was that there was a multitude of different causes which boiled down to misconfiguration, due to inconsistently managed client machines.

The second lesson was that we needed some kind of tool to validate the upgrade (or deployment) readiness of a bunch of machines to prevent this kind of mess in the future. This tool would allow to check whether all the machines meet the prerequisites for a new deployment or upgrade before rolling it out. This should also provide a nice, visual report so that non-technical stakeholders can see :

  • The overall number and percentage of machines not ready
  • Which machines are ready
  • Which ones are not ready
  • Which prerequisites (and prerequisite categories) are met
  • Which prerequisites (and prerequisite categories) are not met

The report should also allow technical stakeholders to drill down to see for a specific machine which prerequisite(s) were not met and why.

Knowing that Pester can be used to validate the operation of a system, I figured I could build a tool leveraging Pester tests to validate prerequisites. So I wrote the DeploymentReadinessChecker PowerShell module and made it available on the PowerShell Gallery. Yes, anyone can use it because it is designed as BYOPS (Bring Your Own Pester Script).

Regarding the HTML report, I didn’t reinvent the wheel, this is using a great utility named ReportUnit.

Basic usage :

First, we need :

  • PowerShell 4.0 (or later).
  • The Pester module should be installed on the machine from which we run DeploymentReadinessChecker.
  • A Pester script containing tests for the prerequisites we want to validate and optionally, “Describe” blocks to group tests in “prerequisite categories”.
  • If the above Pester script (validation script) takes parameters, the values for the parameters we need to pass to it.
  • A list of computer names for the machines we want to check the prerequisites against.
  • Credentials to connect to all the target machines

Now that we have everything we need, let’s get to it.

The module comes with an example validation script : Example.Tests.ps1 and that is what we are going to use here. For your own deployments or upgrades, you will need a validation script containing tests for your own prerequisites : hardware prerequisites, OS requirements, runtime or other software dependencies, network connectivity prerequisites… whatever you need.

Here are a few examples from the first 2 “Describe” blocks of Example.Tests.ps1 :

Describe 'Hardware prerequisites' -Tag 'Hardware' {
    
    It 'Has at least 4096 MB of total RAM' {

        Invoke-Command -Session $RemoteSession {
        (Get-CimInstance -ClassName Win32_PhysicalMemory).Capacity / 1MB } |
        Should Not BeLessThan 4096
    }
}
Describe 'Networking prerequisites' -Tag 'Networking' {

    It 'Can ping the Management server by name' {

        Invoke-Command -Session $RemoteSession { param($ManagementServerName)
        Test-Connection -ComputerName $ManagementServerName -Quiet } -ArgumentList $ManagementServerName |
        Should Be $True
    }
    It 'Can ping the Deployment server by name' {

        Invoke-Command -Session $RemoteSession { param($DeploymentServerName)
        Test-Connection -ComputerName $DeploymentServerName -Quiet } -ArgumentList $DeploymentServerName |
        Should Be $True
    }
    It 'Has connectivity to the Management server on TCP port 80' {

        Invoke-Command -Session $RemoteSession { param($ManagementServerName)
        (Test-NetConnection -ComputerName $ManagementServerName -CommonTCPPort HTTP).TcpTestSucceeded } -ArgumentList $ManagementServerName |
        Should Be $True
    }
    It 'Has the firewall profile set to "Domain" or "Private"' {

        Invoke-Command -Session $RemoteSession {
        $FirewallProfile = (Get-NetConnectionProfile)[0].NetworkCategory.ToString();
        $FirewallProfile -eq 'Domain' -or $FirewallProfile -eq 'Private' } |
        Should Be $True
    }
}

As we can see, it is the validation script’s responsibility to handle the remoting to the target machines.
The validation script should be located in $Module_Folder\ReadinessValidationScript\, for example : C:\Program Files\WindowsPowerShell\Modules\DeploymentReadinessChecker\1.0.0\ReadinessValidationScript\Example.Tests.ps1.

Also, its extension should be “.Tests.ps1” because that’s what Invoke-Pester looks for.

There is no support for multiple validation scripts, so before adding your own validation script in there, rename Example.Tests.ps1 by changing its extension to something else than “.Tests.ps1“. This is to ensure that the example script is ignored by Invoke-Pester.

UPDATE :
I added support for multiple validation scripts being present in $Module_Folder\ReadinessValidationScript\.
Test-DeploymentReadiness can only invoke one validation script at a time, but if there is more than one validation script present, a dynamic parameter named ValidationScript is made available (mandatory, even) to specify the name of the validation script.

It is highly recommended to group related tests into distinct and meaningful “Describe” blocks because, as we’ll see later on, some items in the report are displayed on a per-Describe block basis.

Optionally, “Describe” blocks can have tags and the tool can use these tags to include or exclude some tests, just like Invoke-Pester does.

The module contains a single cmdlet : Test-DeploymentReadiness.

Our computer names list can be fed to the -ComputerName parameter at the command line, from a file, or via pipeline input. For example, for a single computer, this could look like :

C:\> Test-DeploymentReadiness -ComputerName Computer1 -Credential $Cred -OutputPath $env:USERPROFILE\Desktop\Readiness\ |
Invoke-Item

Here is the console output :

Simple example with Invoke-Item

So we get the normal output from Invoke-Pester for each target machine specified via the -ComputerName parameter and a little bit more text at the end. All of this is just written to the console (using Write-Host) but it outputs a single object to the pipeline : a FileInfo object for the Index.html of the report. That way, if we want instant gratification, we can directly open the report in our default browser by piping the output of Test-DeploymentReadiness to Invoke-Item, as seen above.

Off course, it generates a bunch of files, as well. These are generated in the current directory by default, or in the directory specified via the -OutputPath parameter. Invoke-Pester generates one test result file (.xml) per target machine and ReportUnit.exe generates one HTML report per target machine and the overall report Index.html. To view the report, we only need to open the Index.html because it has the links to machine-specific files if we want to drill down to the per-machine reports.

Filtering the tests using tags :

As said earlier, all the Pester tests representing the prerequisites should be in a single validation script, so we can potentially end up with a script containing a very large number of tests. To make this more modular and flexible, we can group tests related to the same topic, purpose, or component into distinct “Describe” blocks and give these “Describe” blocks some tags.

Then, Test-DeploymentReadiness can include only the tests contained in the “Describe” blocks which have the tag(s) specified via the -Tag parameter. Let’s see what it looks like :

Simple example with tag

Similarly, we can exclude the tests contained in the “Describe” blocks which have the tag(s) specified via the -ExcludeTag parameter.

Passing parameters to the validation script :

It is more than likely that the Pester-based validation script takes parameters, especially since it remotes into the target machines, so it may need a -ComputerName and a -Credential parameter. If your validation script has parameter names matching “ComputerName” or “Credential“, then Test-DeploymentReadiness does a bit of work for you.

If the validation script has a ComputerName parameter, Test-DeploymentReadiness passes one computer at a time to its ComputerName parameter, via the Script parameter of Invoke-Pester.

If the validation script has a Credential parameter, the Test-DeploymentReadiness passes the value of its own Credential parameter to the validation script, via the Script parameter of Invoke-Pester.

Cool, but what about any other parameters ?
That’s where the -TestParameters parameter comes in. The parameter names and values can be passed as a hashtable to the -TestParameters parameter of Test-DeploymentReadiness. Then, Test-DeploymentReadiness passes these into the Script parameter of Invoke-Pester, when calling the validation script.

The example validation script Example.Tests.ps1 takes quite a few parameters, among them are DeploymentServerName and ManagementServerName . We can pass values to these 2 parameters, like so :

C:\> $TestParameters= @{ DeploymentServerName = 'DeplServer1'
                      ManagementServerName = 'Mgmtserver1'
                   }
C:\>
C:\> 'Computer1','Computer2','Computer3','Computer4','Computer5' |
Test-DeploymentReadiness -Credential $Cred -OutputPath $env:USERPROFILE\Desktop\Readiness\ -TestParameters $TestParameters
   

 

The Reports :

As mentioned earlier, we only need to open the generated Index.html and this opens the overview report. After running the above command, here is what this looks like :

Overview Report

Fixture summary” gives us the number of ready machines and not-so-ready machines whereas the “Pass percentage” gives us the percentage of machines which are ready.

We can see that Computer4 is the only machine which failed more than 1 prerequisite. We can see what’s going on with it in more detail by clicking on the link named “Computer4” :

Computer4 Report

We can clearly see 4 distinct prerequisite categories, which corresponds with “Describe” blocks in our validation script. Here, “Fixture summary” tells us which prerequisite categories contained at least one failed prerequisite(s). In this case, there were 2.

Let’s check which Networking prerequisite(s) were not met by clicking on “Networking prerequisites” :

Network Prerequisites Details

So now, we have can a good idea of what the issue is (the actual usefulness of the Pester error message will depend on how the test was written).

Pretty neat, huh ? I can see this saving me hours and hours of work, and considerably reduce the maintenance windows in future deployments and upgrades.

If this tool doesn’t exactly fit your needs or if you think of an enhancement, the code is on GitHub, feel free to submit an issue, or even better, to fork it and improve it.

How to create a custom rule for PSScriptAnalyzer

As you probably already know, PSScriptAnalyzer is a static code analysis tool, which checks PowerShell code against rules representing best practices and style guidelines. This is a fantastic tool to set coding style, consistency and quality standards, and if we want to, we can easily enforce these standards within a build pipeline.

The PowerShell community was very much involved in the definition of PSScriptAnalyzer rules, so these rules really make a lot of sense as general guidelines and they are widely accepted by the PowerShell community. However, a given company or project might have specific coding standards which may contain different or more specific rules. Or maybe, you feel like Silicon Valley’s Richard regarding Tabs vs Spaces.

Fortunately, PSScriptAnalyzer allows us to create and use custom rules. In this article, we are going to learn how to do that with a simple example. Let’s say we have coding standards which specifies that all variables names should follow a consistent capitalization style, in particular : PascalCasing. So we are going to write a PSScriptAnalyzer rule to check our code against that convention in the form of a function.

To write this function, our starting point should be this documentation page.
First, how are we going to name our function ? If we look at the CommunityAnalyzerRules module, we see that all the functions names use the verb “Measure“. Why ? I don’t know, but it seems like a sensible convention to follow. That way, if we have multiple rules stored in a single module, we can export all of of them by simply adding the following in the module :

Export-ModuleMember -Function Measure-*

 
So, given our rule is about PascalCasing, the function name “Measure-PascalCase” makes sense.

Next, we need a proper comment-based help for our function. This looks like this :

Function Measure-PascalCase {
<#
.SYNOPSIS
    The variables names should be in PascalCase.

.DESCRIPTION
    Variable names should use a consistent capitalization style, i.e. : PascalCase.
    In PascalCase, only the first letter is capitalized. Or, if the variable name is made of multiple concatenated words, only the first letter of each concatenated word is capitalized.
    To fix a violation of this rule, please consider using PascalCase for variable names.

.EXAMPLE
    Measure-PascalCase -ScriptBlockAst $ScriptBlockAst

.INPUTS
    [System.Management.Automation.Language.ScriptBlockAst]

.OUTPUTS
    [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]

.NOTES
    https://msdn.microsoft.com/en-us/library/dd878270(v=vs.85).aspx
    https://msdn.microsoft.com/en-us/library/ms229043(v=vs.110).aspx
#>

 
The DESCRIPTION part of the help is actually used by PSScriptAnalyzer so it is important. It should contain an explanation of the rule, as well as a brief explanation of how to remediate any violation of the rule. Here, we don’t want to assume that all users know what PascalCase means, so we give a succinct but (hopefully) clear definition of PascalCase.

In the INPUTS field, we tell the user that the only parameter for our function takes an object of the type : [System.Management.Automation.Language.ScriptBlockAst], but it could be other types of AST objects. But wait, What is AST ?

The short(ish) version is that PowerShell 3.0 introduced a new parser and that Parser relies on AST to expose various elements of the PowerShell language as objects. This facilitates parsing PowerShell code and extract objects corresponding to language elements like : variables, function definitions, parameter blocks, parameters, arrays, hashtables, Foreach statements, If statements, the list goes on and on … And PSScriptAnalyzer relies heavily on this AST-based parser.

In the OUTPUTS field, we explicitly tell the user that the function will return one or more objects of the type [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]]. But the actual user will be PSScriptAnalyzer, so this is really a contract between our function and PSScriptAnalyzer. This is more formally declared with the following function attribute :

[OutputType([Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord[]])]

 
But even with this declaration, PowerShell doesn’t enforce that. So it’s our responsibility to ensure our code doesn’t return anything else, otherwise, PSScriptAnalyzer will not be happy.

Now it is time to tackle the code inside our function. Looking at the CommunityAnalyzerRules module most functions have the same basic structure :

#region Define predicates to find ASTs.

[ScriptBlock]$Predicate = {
    Param ([System.Management.Automation.Language.Ast]$Ast)

    [bool]$ReturnValue = $False
    If ( ... ) {

        ...

    }
    return $ReturnValue
}
#endregion

#region Find ASTs that match the predicates.
[System.Management.Automation.Language.Ast[]]$Violations = $ScriptBlockAst.FindAll($Predicate, $True)

If ($Violations.Count -ne 0) {

    Foreach ($Violation in $Violations) {

        $Result = New-Object `
                -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" `
                -ArgumentList  ...
          
        $Results += $Result
    }
}
return $Results
#endregion

 
We don’t have to follow that structure but it is a very helpful scaffolding. As we can see above, the function is divided in 2 logical parts: the first one is where we define one or more predicates corresponding to our rule and the second one is where we actually use the predicate(s) against input (PowerShell code) to identify any violation(s) of our rule.

Defining predicates

First, what is a predicate ?
It is a scriptblock which returns $True or $False and it is used to filter objects. We have a bunch of objects that we feed to our predicate then, we keep the objects for which the predicate returned $True and we filter out the objects for which the predicate returned $False. Sounds complicated ? It’s not, and you are using predicates. All. The. Time :

C:\> $ThisIsAPredicate = { $_.Name -like "*.ps*1" }
C:\> Get-ChildItem -Recurse | Where-Object $ThisIsAPredicate

 
In the context of our PSScriptAnalyzer rule function, the predicate is used to identify violations of our rule. Any piece of PowerShell code which returns $True when fed to our predicate has a violation of our rule. We can use multiple methods to detect violations, so we can define multiple predicates if we need/want to. Here, this is a simple example so we are going to define a single predicate.

Our predicate should take input (pieces of PowerShell code) via a parameter. Here, the parameter is named Ast and it takes objects of the type [System.Management.Automation.Language.Ast]. This is the generic class for AST, this allows the predicate’s parameter to accept objects of child classes like [System.Management.Automation.Language.ScriptBlockAst], [System.Management.Automation.Language.StatementAst], etc…

            [ScriptBlock]$Predicate = {
                Param ([System.Management.Automation.Language.Ast]$Ast)

                ...

 
Our rule for PascalCasing relates only to variable names, so we first need to identify variables. What is most relevant for naming is when variables are defined, or assigned a value, not really when they are referenced. So the arguably best way to identify variables for our particular purpose is to identify variable assignments, like so :

If ($Ast -is [System.Management.Automation.Language.AssignmentStatementAst]) {

    ...

 
Next, we need to identify any variable names which don’t follow PascalCasing. For that, we’ll use the comparison operator -cnotmatch and a regex. As you probably know, PowerShell is not case sensitive. But our rule is all about casing, it is case hypersensitive. This makes the “c” in -cnotmatch crucial for our predicate to work :

[System.Management.Automation.Language.AssignmentStatementAst]$VariableAst = $Ast
    If ($VariableAst.Left.VariablePath.UserPath -cnotmatch '^([A-Z][a-z]+)+$') {
        $ReturnValue = $True
    }

 
To extract only the variable names from our variable assignment objects, we take their “Left” property (what’s on the left side of the assignment operator), then the “VariablePath” property and then the “UserPath” nested property. This gives us only the variable name as a [string]. If that string doesn’t match our regular expression, the predicate returns $True, which means there is a violation.

A brief explanation of the regex used above ([A-Z][a-z]+) :
this means one upper case letter followed by one or more lower case letter(s). This particular pattern can be repeated so we put it between parenthesis and append a “+”. And all this should strictly between the beginning of the string “^” and the end of the string “$”.

Off course, this detection method is limited because there is no intelligence to detect words of the English language (or any language) which may be concatenated to form the variable name :

PS C:\> "FirstwordSecondword" -cmatch '^([A-Z][a-z]+)+$'
True

PS C:\> "FirstwoRdsecoNdword" -cmatch '^([A-Z][a-z]+)+$'
True

 
Also, I’m not a big fan of using digits in variable names but if you want the rule to allow that, you can use the following regex :

PS C:\> "Word1Word2" -cmatch '^([A-Z]\w+)+$'
True

 

Using the predicate to detect violations

Now, we can use our predicate against whatever PowerShell code is fed to our Measure-PascalCase function via its $ScriptBlockAst parameter. The input PowerShell code is an object of the type [System.Management.Automation.Language.ScriptBlockAst], so like most AST objects, it has a FindAll() method which we can use to find all the elements within that object which match a predicate.

[System.Management.Automation.Language.Ast[]]$Violations = $ScriptBlockAst.FindAll($Predicate, $True)

 
The second parameter of the FindAll() method ($True) tells it to search recursively in nested elements.

Now, for any violation of our rule, we need to create an object of the type [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord], because PSScriptAnalyzer expects our function to return an array of object(s) of that specific type :

Foreach ($Violation in $Violations) {

    $Result = New-Object `
            -Typename "Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord" `
            -ArgumentList "$((Get-Help $MyInvocation.MyCommand.Name).Description.Text)",$Violation.Extent,$PSCmdlet.MyInvocation.InvocationName,Information,$Null
          
    $Results += $Result
}

 
Pay particular attention to the 5 values passed to the -ArgumentList parameter of the cmdlet New-Object. To see what each of these values correspond to, we can have a look at the constructor(s) for this class :

C:\> [Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord]::new

OverloadDefinitions
-------------------
Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord new()
Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticRecord new(string message,
System.Management.Automation.Language.IScriptExtent extent, string ruleName,
Microsoft.Windows.PowerShell.ScriptAnalyzer.Generic.DiagnosticSeverity severity, string scriptName, string ruleId)

 
For the “Message” property of our [DiagnosticRecord] objects, hard-coding a relatively long message would not look nice, so here, we are reusing our carefully crafted description from the comment-based help. We don’t have to do this, but that way, we don’t reinvent the wheel.

Then, each resulting object is added to an array : $Results.
Finally, when we are done processing violations, we return that array for PSScriptAnalyzer‘s consumption :

            }
            return $Results
            #endregion
        }

 
That’s it. The module containing the full function is on this GitHub page.

Now, let’s use our custom rule with PSScriptAnalyzer against an example script :

C:\> Invoke-ScriptAnalyzer -Path .\ExampleScript.ps1 -CustomRulePath .\MBAnalyzerRules.psm1 |
 Select-Object RuleName, Line, Message | Format-Table -AutoSize -Wrap

RuleName                           Line Message
--------                           ---- -------
MBAnalyzerRules\Measure-PascalCase   15 Variable names should use a consistent capitalization style, i.e. : PascalCase.
                                        In PascalCase, only the first letter is capitalized. Or, if the variable name
                                        is made of multiple concatenated words, only the first letter of each
                                        concatenated word is capitalized.
                                        To fix a violation of this rule, please consider using PascalCase for variable
                                        names.
MBAnalyzerRules\Measure-PascalCase   28 Variable names should use a consistent capitalization style, i.e. : PascalCase.
                                        In PascalCase, only the first letter is capitalized. Or, if the variable name
                                        is made of multiple concatenated words, only the first letter of each
                                        concatenated word is capitalized.
                                        To fix a violation of this rule, please consider using PascalCase for variable
                                        names.
MBAnalyzerRules\Measure-PascalCase   86 Variable names should use a consistent capitalization style, i.e. : PascalCase.
                                        In PascalCase, only the first letter is capitalized. Or, if the variable name
                                        is made of multiple concatenated words, only the first letter of each
                                        concatenated word is capitalized.
                                        To fix a violation of this rule, please consider using PascalCase for variable
                                        names.
MBAnalyzerRules\Measure-PascalCase   88 Variable names should use a consistent capitalization style, i.e. : PascalCase.
                                        In PascalCase, only the first letter is capitalized. Or, if the variable name
                                        is made of multiple concatenated words, only the first letter of each
                                        concatenated word is capitalized.
                                        To fix a violation of this rule, please consider using PascalCase for variable
                                        names.

 
That’s cool, but we probably want to see the actual variable names which are not following our desired capitalization style. We can obtain this information like so :

VariableNames
 
We can see that in the case of this script (pun intended), the case of variable names is all over the place, and we can easily go and fix it.

Adding ConfigurationData dynamically from a DSC configuration

When writing a DSC configuration, separating the environmental data from the DSC configuration is a best practice : it allows to reuse the same configuration logic for different environments, for example the Dev, QA and Production environments . This generally means that the environment data is stored in separate .psd1 files. This is explained in this documentation page.

However, these configuration data files are relatively static, so if the environment changes frequently these files might end up containing outdated information. A solution is to keep the static environment data in the configuration data files and then adding the more dynamic data on the fly from the DSC configuration itself.

A good example of this use case is a web application, where the configuration is identical for all web servers but these servers are treated not as pets but as cattle : we create and kill them on a daily basis. Because they are cattle, we don’t call them by their name, in fact we don’t even know their name. So the configuration data file doesn’t contain any node names :

@{
    # Node specific data
    AllNodes = @(

       # All the Web Servers have following information 
       @{
            NodeName           = '*'
            WebsiteName        = 'ClickFire'
            SourcePath         = '\\DevBox\SiteContents\'
            DestinationPath    = 'C:\inetpub\wwwroot\ClickFire_Content'
            DefaultWebSitePath = 'C:\inetpub\wwwroot\ClickFire_Content'
       }
    );
    NonNodeData = ''
}

 
By the way, the web application used for illustration purposes is an internal HR app, codenamed “Project ClickFire”.

Let’s assume the above configuration data is all the information we need to configure our nodes. That’s great, but we still need some node names, otherwise there will be no MOF file generated when we run the configuration. So we’ll need the query some kind of database to get the names of the web servers for this application, Active Directory for example. This is easy to do, especially if these servers are all in the same OU and/or there is a naming convention for them :

C:\> $DynamicNodeNames = Get-ADComputer -SearchBase "OU=Project ClickFire,OU=Servers,DC=Mat,DC=lab" -Filter {Name -Like "Web*"} |
Select-Object -ExpandProperty Name

C:\> $DynamicNodeNames

Web083
Web084
Web086
  

 
Now that we have the node names, we need to add a hashtable for each node into the “AllNodes” section of our configuration data. To do that, we first need to import the data from the configuration data file and we store it into a variable for further manipulation. There is a new cmdlet introduced in PowerShell 5.0 which makes this very simple : Import-PowerShellDataFile :

C:\> $EnvironmentData = Import-PowerShellDataFile -Path "C:\Lab\EnvironmentData\Project_ClickFire.psd1"
C:\> $EnvironmentData

Name                           Value
----                           -----
AllNodes                       {System.Collections.Hashtable}
NonNodeData


C:\> $EnvironmentData.AllNodes

Name                           Value
----                           -----
DefaultWebSitePath             C:\inetpub\wwwroot\ClickFire_Content
NodeName                       *
WebsiteName                    ClickFire
DestinationPath                C:\inetpub\wwwroot\ClickFire_Content
SourcePath                     \\DevBox\SiteContents\
  

 
Now, we have our configuration available to us as a PowerShell object (a hashtable) and the “AllNodes” section inside of it is also a hashtable. More accurately, the “AllNodes” section is an array of Hashtables because each node entry within “AllNodes” is a hashtable :

C:\> $EnvironmentData.AllNodes.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array


C:\> $EnvironmentData.AllNodes | Get-Member | Select-Object TypeName -Unique

TypeName
--------
System.Collections.Hashtable
  

 
So now, what we need to do is to inject a new node entry for each node returned by our Active Directory query into the “AllNodes” section :

C:\> Foreach ( $DynamicNodeName in $DynamicNodeNames ) {
     $EnvironmentData.AllNodes += @{NodeName = $DynamicNodeName; Role = "WebServer"}
 }
  

 
For each node name, we add a new hashtable into “AllNodes”. These hashtables are pretty simple in this case, this is just to give our nodes a name and a role (in case we need to differentiate with other server types, like database servers for example).

The result of this updated configuration data is equivalent to :

@{
    # Node specific data
    AllNodes = @(

       # All the Web Servers have following information 
       @{
            NodeName           = '*'
            WebsiteName        = 'ClickFire'
            SourcePath         = '\\DevBox\SiteContents\'
            DestinationPath    = 'C:\inetpub\wwwroot\ClickFire_Content'
            DefaultWebSitePath = 'C:\inetpub\wwwroot\ClickFire_Content'
       }
       @{
            NodeName           = 'Web083'
            Role               = 'WebServer'
       }
       @{
            NodeName           = 'Web084'
            Role               = 'WebServer'
       }
       @{
            NodeName           = 'Web086'
            Role               = 'WebServer'
       }
    );
    NonNodeData = ''
}

 
So that’s it for the node data, but what if we need to add non-node data ?
It is very similar to the node data because the “NonNodeData” section of the configuration data is also a hashtable.

Let’s say we want to add a piece of XML data that may be used for the web.config file of our web servers to the “NonNodeData” section of the configuration data. We could do that in the configuration data file, right :

@{
    # Node specific data
    AllNodes = @(

       # All the Web Servers have following information 
       @{
            NodeName           = '*'
            WebsiteName        = 'ClickFire'
            SourcePath         = '\\DevBox\SiteContents\'
            DestinationPath    = 'C:\inetpub\wwwroot\ClickFire_Content'
            DefaultWebSitePath = 'C:\inetpub\wwwroot\ClickFire_Content'
       }
    );
    NonNodeData =
    @{
        DynamicConfig = [Xml](Get-Content -Path C:\Lab\SiteContents\web.config)
    }
}

Nope :

SafeGetValueErrorNew
 
This is because to safely import data from a file, the cmdlet Import-PowerShellDataFile kinda works in RestrictedLanguage mode. This means that executing cmdlets, or functions, or any type of command is not allowed in a data file. Even the XML type and a bunch of other things are not allowed in this mode. For more information : about_Language_Modes.

It does make sense : data files should contain data, not code.

OK, so we’ll do that from the DSC configuration script, then :

C:\> $DynamicConfig = [Xml](Get-Content -Path "\\DevBox\SiteContents\web.config")
C:\> $DynamicConfig

xml                            configuration
---                            -------------
version="1.0" encoding="UTF-8" configuration


C:\> $EnvironmentData.NonNodeData = @{DynamicConfig = $DynamicConfig}
C:\>
C:\> $EnvironmentData.NonNodeData.DynamicConfig.configuration


configSections      : configSections
managementOdata     : managementOdata
appSettings         : appSettings
system.web          : system.web
system.serviceModel : system.serviceModel
system.webServer    : system.webServer
runtime             : runtime
  

 
With this technique, we can put whatever we want in “NonNodeData”, even XML data, as long as it is wrapped in a hashtable. The last command shows that we can easily access this dynamic config data because it is stored as a tidy [Xml] PowerShell object.

Please note that the Active Directory query, the import of the configuration data and the manipulation of this data are all done in the same script as the DSC configuration but outside of the DSC configuration itself. That way, this modified configuration data can be passed to the DSC configuration as the value of its -ConfigurationData parameter.

Putting it all together, here is what the whole DSC configuration script looks like :

configuration Project_ClickFire
{
    Import-DscResource -Module PSDesiredStateConfiguration
    Import-DscResource -Module xWebAdministration
    
    Node $AllNodes.Where{$_.Role -eq "WebServer"}.NodeName
    {
        WindowsFeature IIS
        {
            Ensure          = "Present"
            Name            = "Web-Server"
        }
        File SiteContent
        {
            Ensure          = "Present"
            SourcePath      = $Node.SourcePath
            DestinationPath = $Node.DestinationPath
            Recurse         = $True
            Type            = "Directory"
            DependsOn       = "[WindowsFeature]IIS"
        }        
        xWebsite Project_ClickFire_WebSite
        {
            Ensure          = "Present"
            Name            = $Node.WebsiteName
            State           = "Started"
            PhysicalPath    = $Node.DestinationPath
            DependsOn       = "[File]SiteContent"
        }
    }
}

# Adding dynamic Node data
$EnvironmentData = Import-PowerShellDataFile -Path "$PSScriptRoot\..\EnvironmentData\Project_ClickFire.psd1"
$DynamicNodeNames = (Get-ADComputer -SearchBase "OU=Project ClickFire,OU=Servers,DC=Mat,DC=lab" -Filter {Name -Like "Web*"}).Name

Foreach ( $DynamicNodeName in $DynamicNodeNames ) {
    $EnvironmentData.AllNodes += @{NodeName = $DynamicNodeName; Role = "WebServer"}
}

# Adding dynamic non-Node data
$DynamicConfig = [Xml](Get-Content -Path "\\DevBox\SiteContents\web.config")
$EnvironmentData.NonNodeData = @{DynamicConfig = $DynamicConfig}

Project_ClickFire -ConfigurationData $EnvironmentData -OutputPath "C:\Lab\DSCConfigs\Project_ClickFire"
  

 
Running this script indeed generates a MOF file for each of our nodes, containing the same settings :

C:\> & C:\Lab\DSCConfigs\Project_ClickFire_Config.ps1

    Directory: C:\Lab\DSCConfigs\Project_ClickFire


Mode                LastWriteTime         Length Name                                       
----                -------------         ------ ----                                       
-a----         6/6/2016   1:37 PM           3986 Web083.mof                                 
-a----         6/6/2016   1:37 PM           3986 Web084.mof                                 
-a----         6/6/2016   1:37 PM           3986 Web086.mof        
  

 
Hopefully, this helps treating web servers really as cattle and give its full meaning to the expression “server farm“.

Integrating PSScriptAnalyzer in an Appveyor Continuous Integration pipeline

Many of us who are writing PowerShell code are using the free (and awesome) Appveyor service for Continuous Integration (especially for personal projects). And most of us use this to run Pester tests. Automated testing is great, it allows to set a certain standard of code quality without slowing down code delivery. But, this is just checking that the code behaves as we intended to.

What about code consistency, style, readability and following best practices ?

This is where a static code analysis tool like PSScriptAnalyzer comes in. Even though PSScriptAnalyzer is a perfect fit in a PowerShell “build” process, searching the web for “integrating PSScriptAnalyzer and Appveyor” doesn’t yield very helpful results. So here is the solution I came up with :

version: 1.0.{build}

os: WMF 5

# Skip on updates to the readme
skip_commits:
  message: /readme*/
  
install:
  - ps: Install-PackageProvider -Name NuGet -Force
  - ps: Install-Module PsScriptAnalyzer -Force
  
build: false

test_script:
  - ps: |
      Add-AppveyorTest -Name "PsScriptAnalyzer" -Outcome Running
      $Results = Invoke-ScriptAnalyzer -Path $pwd -Recurse -Severity Error -ErrorAction SilentlyContinue
      If ($Results) {
        $ResultString = $Results | Out-String
        Write-Warning $ResultString
        Add-AppveyorMessage -Message "PSScriptAnalyzer output contained one or more result(s) with 'Error' severity.`
        Check the 'Tests' tab of this build for more details." -Category Error
        Update-AppveyorTest -Name "PsScriptAnalyzer" -Outcome Failed -ErrorMessage $ResultString
        
        # Failing the build
        Throw "Build failed"
      }
      Else {
        Update-AppveyorTest -Name "PsScriptAnalyzer" -Outcome Passed
      }

This the content of my appveyor.yml file, which is the file from which Appveyor gets the build configuration.

Line 3 : This indicates from which VM template the build agent will be deployed. As its name indicates, this allows to have a build agent running in a VM with PowerShell version 5. If you believe only what you can see, add $PSVersionTable in the appveyor.yml and check the result in the build console. PowerShell 5 means we can easily add PowerShell scripts, modules and DSC resources to our build agent from the PowerShell Gallery using PackageManagement.

Line 10-11 : This is exactly what we do here. But first, because the PowerShell Gallery relies on NuGet, we need to install the NuGet provider. Then, we can install any PowerShell module we want from the PowerShell Gallery, PsScriptAnalyzer in this case. We didn’t specify the repository because the PowerShell Gallery is the default one.

Line 13 : This refers specifically to MSBuild and we don’t need or want MSBuild for a PowerShell project.

Line 15-End : This is where all the PSScriptAnalyzer stuff goes. So from an Appveyor point of view, this will be a test. Even though static code analysis is not testing, it kinda makes sense : we are assessing the code against a set of rules which represent a certain standard and we want a “Pass” or a “Fail” depending on whether the code meets the standard or not.

Line 16 : In YAML, the pipe character “|” allows values to span multiple lines. This is very convenient for code blocks, like here. That way, we don’t need to add “- ps:” at the beginning of each line.

Line 17 : Appveyor doesn’t have a direct integration with PSScriptAnalyzer like it has for some testing frameworks (NUnit, MSTest, etc…) but it’s OK. The Appveyor worker (the actual build agent) provides a REST API and even a few PowerShell cmdlets leveraging this API. One of these cmdlets is Add-AppveyorTest. Using this cmdlet, we are adding a new test, giving it a name and telling the build agent that the test is in the “Running” state.

Line 18 : We run PSScriptAnalyzer against all the files in the current directory, recursively. We specify the “Error” severity to output only the violations of level “Error“, because we don’t want a violation of severity “Information” or even “Warning” to make the test fail. We store the result in a variable for later use.

Line 20 : If there are any “errors” from PSScriptAnalyzer perspective, we want to display them as a message in the build console and in the error message of the “test”. That’s why we need to convert the output object(s) from PSScriptAnalyzer to a string.

Line 21 : Writing the violation(s) to the build console. We could use Write-Host or Write-Output as well but as we’ll see in a later screenshot, the warning stream makes it stand out more visibly.

Line 22 : This Appveyor-specific cmdlet adds a message to the build’s “Messages” tab. Specifying “Error” for the category just displays the message with a touch of red on its left :

Appveyor Message fail
 
Line 24 : Update-AppveyorTest is another cmdlet leveraging the Appveyor build worker API. Here, we are using it to update the status of our existing test and add an error message to it. This message is PSScriptAnalyzer output converted to a string, so we can check the test message to see exactly what the problem is :

Appveyor Test fail
 

Line 27 : We need to use “Throw” to explicitly fail the build. Otherwise, the build is considered as succeeded, even if the “test” fails.

Line 30 : If PSScriptAnalyzer didn’t output anything, meaning if there were no violation of the “Error” severity in any file scanned by PSScriptAnalyzer, we considered that our project passes the test. Again, we use Update-AppveyorTest but this time, we tell it that the outcome of the “test” is a pass.

Now, let’s see how this looks like when we run a build :

Appveyor Build success
 
Not much output, because all is well. Also, the test is green :

Appveyor Test Success
 
Do you like watching “Fail” videos on Youtube ? If yes, you are probably dying to see my build fail, right ? So, here we go :

Appveyor Build fail
 
Wow, the yellow background of the warning stream is not elegant but it sure stands out !

Also, if you want to see the “Passing” Appveyor badge on the GitHub repository, head over THERE.

This is it.
PSScriptAnalyzer is an important tool that any PowerShell scripter should use. Appveyor is awesome, so combining both of these tools is pretty powerful.

Documentation as Code : Exporting the contents of DSC MOF files to Excel

One of the greatest benefits of PowerShell DSC (and other Configuration Management tools/platforms) is the declarative syntax (as opposed to imperative scripting). Sure, a DSC configuration can contain some logic, using loops and conditional statements, but we don’t need to care about handling errors or checking if something is already present. All this (and the large majority of the logic) is handled within the resource, so we just need to describe the end result, the “Desired State”.

So all the settings and information that a configuration is made of are stored in a very simple (and pretty much human-readable) syntax, like :

Node $AllNodes.NodeName
    {
        cWindowsErrorReporting Disabled
        {
            State = "Disabled"
        }
    }

 
This allows us to use this “code” (for lack of a better word) as documentation in a way that wouldn’t be possible or practical with imperative code. For this purpose, we could use DSC configurations, or DSC configuration data files if all the configuration data is stored separately. But the best files for that would probably be the MOF files for 2 reasons :

  • Even if some settings are in different files, we can be sure that all the settings for a given node is in a single MOF file (the exception being partial configurations)
  • Even if the DSC configuration contains complex logic, there is no need to understand or parse this logic to get the end result. All this has been done for us when the MOF file has been generated

Now, imagine you have all your MOF files stored in a directory structure like this :

PS C:\> tree C:\DSCConfigs /F
Folder PATH listing for volume OS
C:\DSCCONFIGS
├───Customer A
│   ├───Dev
│   │       Server1.mof
│   │       Server2.mof
│   │
│   ├───Prod
│   │       Server1.mof
│   │       Server2.mof
│   │
│   └───QA
│           Server1.mof
│           Server2.mof
│
├───Customer B
│   ├───Dev
│   │       Server1.mof
│   │       Server2.mof
│   │
│   ├───Prod
│   │       Server1.mof
│   │       Server2.mof
│   │
│   └───QA
│           Server1.mof
│           Server2.mof
│
└───Customer C
    ├───Dev
    │       Server1.mof
    │       Server2.mof
    │
    ├───Prod
    │       Server1.mof
    │       Server2.mof
    │
    └───QA
            Server1.mof
            Server2.mof

You most likely have much more than 2 servers per environment, so there can easily be a large number a MOF files.
Then, imagine your boss tells you : “I need all the configuration settings, for all customers, all environments and all servers in an Excel spreadsheet to sort and group the data easily and to find out the differences between Dev and QA, and between QA and Prod”.

If you are like me, you may not quite understand bosses’ uncanny obsession with Excel but this definitely sounds like something useful and an interesting challenge. So, let’s do it.

We’ll divide this in 3 broad steps :

  • Converting the contents of MOF files to PowerShell objects
  • Exporting the resulting PowerShell objects to a CSV file
  • Processing the data using PowerShell and/or Excel

Converting the contents of MOF files to PowerShell objects

This is by far the most tricky part.
Fortunately, I wrote a function, called ConvertFrom-DscMof, which does exactly that. We won’t go into much details about how it works, but you can have a look at the code here.

Basically, it parses one or more MOF files and it outputs an object for each resource instance contained in the MOF file(s). All the properties of a given resource instance become properties of the corresponding object, plus a few properties related to the MOF file.

Here is an example with a very simple MOF file :

ConvertFrom-DscMofExample
 
And here is an example with all the properties of a single resource instance :

ConvertFrom-DscMofSingle
 

Exporting the resulting PowerShell objects to a CSV file

As we have the ability to get DSC configuration information in the form of PowerShell objects, it is now very easy to export all this information as CSV. But there’s a catch : different resources have different parameters, for example the Registry resource has the ValueName and ValueData parameters and the xTimeZone resource has a TimeZone parameter.

So the resulting resource instances objects will have ValueName and ValueData properties if they are an instance of the Registry resource and a TimeZone property if they are an instance of the xTimeZone resource. Even for a given resource, some parameters are optional and they will end up in the properties of the resulting PowerShell object only if they were explicitly specified in the configuration.

The problem is that Export-Csv doesn’t handle intelligently objects with different properties, it will just create the columns from the properties of the first object in the collection and apply that to all objects, even for objects which have different properties.

But, we can rely on the “ResourceID” property of each resource instance because it uniquely identify the resource instance. Also, it contains the name we gave to the resource block in the DSC configuration, which should be a nice meaningful name, right ?
Yeah, this is where “Documentation as code” meets “self-documenting code” : they are both important and very much complementary. To get an idea of what the values of ResourceID look like, refer back to the first screenshot.

Below, we can see how to export only the properties we need, and only the properties that we know will be present for all resource instances :


Get-ChildItem C:\MOFs\ -File -Filter "*.mof" -Recurse |
ConvertFrom-DscMof |
Select-Object -Property "MOF file Path","MOF Generation Date","Target Node","Resource ID","DSC Configuration Info","DSC Resource Module" |
Export-Csv -Path 'C:\DSCConfig Data\AllDSCConfigs.csv' -NoTypeInformation

 

Processing the data using PowerShell and/or Excel

The resulting CSV file can be readily opened and processed by Excel (or equivalent applications) :

CSVFileInExcel
 
Now, we have all the power of Excel at our fingertips, we can sort, filter, group all this data however we want.

Now, here is a very typical scenario : the Dev guys have tested their new build and it worked smoothly in their environment. However, the QA guys say that the same build is failing miserably in their environment. The first question which should come to mind is : “What is the difference between the Dev and QA environments ?

If all the configuration of these environments is done with PowerShell DSC, the ConvertFrom-DscMof function can be a great help to answer that very question :

C:\> $CustomerCDev = Get-ChildItem -File -Filter '*.mof' -Recurse 'C:\MOFs\Customer C\Dev\' |
ConvertFrom-DscMof
C:\> $CustomerCQA = Get-ChildItem -File -Filter '*.mof' -Recurse 'C:\MOFs\Customer C\QA\' |
ConvertFrom-DscMof
C:\> Compare-Object -ReferenceObject $CustomerCDev -DifferenceObject $CustomerCQA -Property 'Target Node','Resource ID'

Target Node Resource ID                    SideIndicator
----------- -----------                    -------------
Server1     [xRemoteFile]RabbitMQInstaller <=
Server1     [Package]RabbitMQ              <=

 
Oops, we forgot to install RabbitMQ on Server1 ! No wonder it’s not working in QA.
But now, there is hope. We, forgetful and naturally flawed human beings, can rely on this documentation automation to tell us how things really are.

So, as we have seen, Infrastructure-as-code (PowerShell DSC in this case) can be a nice stepping-stone for an infrastructure documentation.
What is the number 1 problem for any infrastructure/configuration documentation ?
Keeping it up-to-date. This can help generate dynamically the documentation, which means this documentation can be kept up-to-date pretty easily without any human intervention.