Skip to content

Commit 0d885af

Browse files
authored
v1.0.3 release
* Fixes #55 Start-RemotelyJobPorcessing logic - Fixes use of alias within the code - Fixes indentation * Fixes #48 naming convention for tests file and the NUnit XMl result file - based on the nodename the above files are named. * Adds unit test for Start-RemotelyJobProcessing function logic - Fixes Write-VerboseLog to handle Pester mocks - Fixes Start-RemotelyJobProcessing to stop when ProcessRemotely* function fail in pipeline * Fixes PSRemotely.json of the empty element in ModulesRequired * Adds Enter-PSRemotely function and Integration tests for it * Adds integration tests for issue #48 - naminv convention for tests & results file * Refactors config data tests * Adds another fix for #48 - the tests file dumped should also use nodename * Add -PassThru switch to Enter-PSRemotely * Fixes typo in the Debug.Integration.Tests * Fixes another set of typos in Debug.Integration.Tests * Adds check for invalid filename chars #56 * Adds check for invalid filename chars to Invoke-PSRemotely & bootstrap.ps1 #56 * Fixes integration tests for #48 & #56 * Removes the empty element in the modulesrequired @ PSRemotely.json * Fixes the naming logic for the Nunit XML file * Fixes typo in the Debug.Integration.Tests - replaces the localhost with Node1 in nodename * Adds unit and integration tests for issue #27 - issue with parsing describe blocks * Refactor the integration tests for issue #27 * Adds code logic for issue #27 * Adds quick refactor to the integration test for issue #27 * Fixes typo * Swaps $Env:BHPSModulePath with $ENV:BHModulePath * Fixes Invoke-Pester call in the remote Invoke-PSRemotely function * Swaps BHPSModulePath with BHModulePath in psremotely.psdeploy file * Adds logo #58 * Initial draft of the PITCHME.md #57 * Removing PITCHME.MD * Adds back PITCHME.md * Adds pitchme badge to README * Fixes typo in doc & adds content to PITCHME * Fixes for presentation at PSBUG * Updates doc TroubleShoot.md - now references new Enter-PSRemotely function * Fixes typos in Integration tests * Fixes for the integration tests failing - Not enough storage for IPv6Address.Integration.Tests - Typos in the ConfigData.Integration.Tests * Adds enable-rdp to Appveyor * Block RDP in appveyor.yml * Block Appveyor VM on build finish * Fixes possible race condition issue with IPv6 integration tests
1 parent 6d15547 commit 0d885af

36 files changed

+920
-150
lines changed

PITCHME.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
## Operations Validation
2+
3+
- <span style="font-size:0.9em; color:gray">Validating your Infrastructure as Code.</span>
4+
- <span style="font-size:0.9em; color:gray">Tests if the infrastructure components are functional.</span>
5+
6+
+++
7+
8+
<span style="font-size:1.0em; color:gray">Fits into the DevOps ecosystem.</span> |
9+
<span style="font-size:1.0em; color:gray">Is my infrastructure functioning as expected?</span>
10+
11+
---
12+
## PowerShell Scripts ?
13+
14+
- <span style="font-size:0.9em; color:gray">Can be used.</span>
15+
- <span style="font-size:0.9em; color:gray">Maintenance nightmare.</span>
16+
17+
+++
18+
19+
<span style="font-size:1.0em; color:red">Tip - Avoid writing scripts for validating your infrastructure.</span>
20+
21+
---
22+
23+
## Pester and PoshSpec
24+
25+
Pester is a Unit testing framework.
26+
Only the code logic is tested.
27+
28+
```powershell
29+
Function Get-ServerInfo {
30+
Get-CIMInstance -Class Win32_ComputerSystem |
31+
Select-Object -Property *
32+
}
33+
34+
Describe 'Get-ServerInfo Unit tests' {
35+
# Arrange
36+
Mock -Command Get-CimInstance -FilterParameter {$Class -eq 'Win32_ComputerSystem' }
37+
# Act
38+
Get-ServerInfo
39+
# Assert
40+
It "Should query the Win32_ComputerSystem class" {
41+
Assert-MockCalled -Command Get-CimInstance -Times 1 -Exactly -Scope Describe
42+
}
43+
}
44+
```
45+
46+
+++
47+
### Pester for Ops validation
48+
49+
But Pester can be extended to validate/test Infrastructure as well.
50+
51+
```powershell
52+
Describe "TestIPConfiguration" {
53+
It "Should have a valid IP address on the Management NIC" {
54+
(Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias 'vEthernet(Management)' | Select-Object -ExpandProperty IPAddress) |
55+
Should be '10.10.10.1'
56+
}
57+
}
58+
```
59+
60+
+++
61+
62+
### PoshSpec fits in
63+
64+
PoshSpec adds yet another layer of abstraction on our infrastructure tests.
65+
The tests look concise and easy to maintain.
66+
67+
```powershell
68+
Describe "TestIPConfiguration" {
69+
Context "Validate the Management NIC " {
70+
# Custom public type added to PoshSpec for our use.
71+
IPv4Address 'vEthernet(Management)' {Should be '10.10.10.1'}
72+
}
73+
}
74+
```
75+
76+
---
77+
78+
## Challenges for Solution stack Ops validation
79+
- <span style="font-size:0.9em; color:gray">Targeting remote nodes for ops validation is still an overhead.</span>
80+
- <span style="font-size:0.9em; color:gray">Remote nodes need to bootstrapped before invoking the ops validation.</span>
81+
- <span style="font-size:0.9em; color:gray">Challenge in specifying node and solution configuration data.</span>
82+
83+
84+
---
85+
86+
## Remote Ops validation
87+
Targeting tests written to remote node(s).
88+
PSRemotely was engineered with solution stack operations validation in mind. Few of its features:-
89+
- <span style="font-size:0.6em; color:gray">Target Pester/PoshSpec based operations validation tests on the remote nodes.</span>
90+
- <span style="font-size:0.6em; color:gray">Decouples node and solution configuration data using DSC config data syntax.</span>
91+
- <span style="font-size:0.6em; color:gray">Self-contained framework, bootstraps remote node(s) for running the ops validation.</span>
92+
- <span style="font-size:0.6em; color:gray">Allows re-running failed tests.</span>
93+
- <span style="font-size:0.6em; color:gray">Easier debugging.</span>
94+
---
95+
96+
## Demo Validating a S2D cluster using PSRemotely
97+
98+
![alt](PSRemotely.png)

PITCHME.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
theme : black
2+
highlight : monokai

PSRemotely.png

30.6 KB
Loading

PSRemotely/private/ASTFunctions.ps1

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,82 @@ Function Get-TestNameAndTestBlock {
1717
$ast = Get-ASTFromInput -Content $Content
1818
$commandAST = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.CommandAst]}, $true)
1919
$output = @()
20+
# fetch all the Describe blocks using AST
2021
$describeASTs = $commandAST | Where-Object -FilterScript {$PSItem.GetCommandName() -eq 'Describe'}
2122
if ($describeASTs) {
23+
# iterate over each Describe block, this means that PSRemotely allows usage of multiple Describe
24+
# block within a Node block.
2225
foreach ($describeAST in $describeASTs) {
23-
$TestNameElement = $describeAST.CommandElements | Select-Object -First 2 | Where-Object -FilterScript {$PSitem.Value -ne 'Describe'}
2426

25-
Switch -Exact ($TestNameElement.StringConstantType ) {
26-
27-
'DoubleQuoted' {
28-
# if the test name is a double quoted string
29-
$output += @{
30-
#Add the test name as key and testBlock string as value
31-
$($ExecutionContext.InvokeCommand.ExpandString($TestNameElement.Value)) = $($describeAST.Extent.Text)
32-
}
33-
break
34-
}
35-
'SingleQuoted' {
36-
# if the test name is a single quoted string
37-
$output += @{
38-
$($TestNameElement.Value) = $($describeAST.Extent.Text)
39-
}
40-
break
41-
}
42-
default {
43-
throw 'TestName passed to Describe block should be a string'
44-
}
27+
$ScriptBlock = [scriptblock]::create("$($describeAST.Extent.Text)")
28+
#$TestNameElement = $describeAST.CommandElements | Select-Object -First 2 | Where-Object -FilterScript {$PSitem.Value -ne 'Describe'}
29+
$ParametersPassedToDescribe = Get-ParametersPassedToDSLKeyword -FunctionInfo $(Get-Command -Module Pester -Name Describe) -ScriptBlock $ScriptBlock
30+
# since there is a limitation while generating parameters from the proxy command
31+
# we need to explicitly check that the name and fixture are not empty here
32+
if (-not $ParametersPassedToDescribe['name'] -or (-not $ParametersPassedToDescribe['Fixture'])) {
33+
throw 'Name or Fixture missing in the Describe block'
34+
}
35+
$output += @{
36+
$ParametersPassedToDescribe['Name'] = $($describeAST.Extent.Text)
4537
}
38+
4639
} # end foreach block
4740
Write-Output -InputObject $output
4841
}
4942
else {
5043
throw 'Describe block not found in the Test Body.'
5144
}
52-
45+
}
46+
47+
Function Get-ParametersPassedToDSLKeyword {
48+
<#
49+
This function is a magic function. Specify it a module's DSL keyword function metadata along
50+
with the actual usage of the DSL and it will return the PSBoundParameters being passed to the
51+
original DSL keyword.
52+
53+
This was written in order to determine the test names of the Describe block wrapped inside
54+
PSRemotely DSL. Since these test names are later used while dropping individual .Tests.ps1
55+
files on the PSRemotely node(s).
56+
#>
57+
[cmdletbinding()]
58+
param(
59+
# Supply the script info object, output of Get-Command -Name <DeploymentScript>.ps1
60+
[Parameter(Mandatory)]
61+
[System.Management.Automation.FunctionInfo]$FunctionInfo,
62+
63+
[Parameter(Mandatory)]
64+
[ScriptBlock]$ScriptBlock
65+
)
66+
67+
TRY {
68+
$Metadata = [System.Management.Automation.CommandMetadata]::New($FunctionInfo)
69+
$CmdletBinding = [System.Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($Metadata)
70+
$Parameters = [System.Management.Automation.ProxyCommand]::GetParamBlock($Metadata)
71+
72+
# bad formatting due to usage of here-string
73+
$FunctionBody = @"
74+
$CmdletBinding
75+
param(
76+
$Parameters
77+
)
78+
`$returnHashtable = `$PSBoundParameters
79+
`$returnHashtable
80+
"@
81+
$DummyFunction = [scriptblock]::Create($FunctionBody)
82+
$Null = New-Item -Path Function:\ -Name pSRemotelyDescribeOverride -Value $DummyFunction -Force
83+
# create a temporary override for the Pester's Describe keyword
84+
$null = New-Alias -Name Describe -Value pSRemotelyDescribeOverride -Force
85+
86+
# Now invoke the scriptblock
87+
$ParametersHash = & $ScriptBlock
88+
Write-Output -InputObject $ParametersHash
89+
}
90+
CATCH {
91+
Write-Warning -Message "$($PSItem.Exception)"
92+
$PSCmdlet.ThrowTerminatingError($PSItem)
93+
}
94+
FINALLY {
95+
# Clean up the alias
96+
Remove-Item -Path Alias:\Describe -Force -ErrorAction SilentlyContinue
97+
}
5398
}

PSRemotely/private/Helper.ps1

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,44 @@ Function ProcessRemotelyJob {
66
)
77
$NodeName = $InputObject.Key
88
$Job = $InputObject.Value
9-
10-
foreach($childJob in $Job.ChildJobs)
11-
{
12-
if($childJob.Output.Count -eq 0){
13-
[object] $outputStream = New-Object psobject
14-
}
15-
else {
16-
[object] $outputStream = $childJob.Output | % { $_ }
17-
}
18-
19-
$errorStream = CopyStreams $childJob.Error
20-
$verboseStream = CopyStreams $childJob.Verbose
21-
$debugStream = CopyStreams $childJob.Debug
22-
$warningStream = CopyStreams $childJob.Warning
23-
$progressStream = CopyStreams $childJob.Progress
24-
25-
$allStreams = @{
26-
Error = $errorStream
27-
Verbose = $verboseStream
28-
DebugOutput = $debugStream
29-
Warning = $warningStream
30-
ProgressOutput = $progressStream
31-
}
32-
33-
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType NoteProperty -Name __Streams -Value $allStreams
34-
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetError -Value { return $this.__Streams.Error }
35-
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetVerbose -Value { return $this.__Streams.Verbose }
36-
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetDebugOutput -Value { return $this.__Streams.DebugOutput }
37-
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetProgressOutput -Value { return $this.__Streams.ProgressOutput }
38-
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetWarning -Value { return $this.__Streams.Warning }
39-
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType NoteProperty -Name RemotelyTarget -Value $NodeName
40-
41-
if($childJob.State -eq 'Failed'){
42-
$childJob | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable jobError
43-
$outputStream.__Streams.Error = $jobError
44-
}
45-
46-
Write-Output -InputObject $outputStream
47-
}
9+
foreach($childJob in $Job.ChildJobs){
10+
if($childJob.Output.Count -eq 0){
11+
[object] $outputStream = New-Object psobject
12+
}
13+
else {
14+
15+
[object] $outputStream = $childJob.Output | Foreach-Object -Process { $_ }
16+
}
17+
18+
$errorStream = CopyStreams $childJob.Error
19+
$verboseStream = CopyStreams $childJob.Verbose
20+
$debugStream = CopyStreams $childJob.Debug
21+
$warningStream = CopyStreams $childJob.Warning
22+
$progressStream = CopyStreams $childJob.Progress
23+
24+
$allStreams = @{
25+
Error = $errorStream
26+
Verbose = $verboseStream
27+
DebugOutput = $debugStream
28+
Warning = $warningStream
29+
ProgressOutput = $progressStream
30+
}
31+
#Write-Host -Object "$($outputStream.RemotelyTarget) NodeName -> $NodeName" -ForegroundColor red
32+
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType NoteProperty -Name __Streams -Value $allStreams
33+
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetError -Value { return $this.__Streams.Error }
34+
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetVerbose -Value { return $this.__Streams.Verbose }
35+
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetDebugOutput -Value { return $this.__Streams.DebugOutput }
36+
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetProgressOutput -Value { return $this.__Streams.ProgressOutput }
37+
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType ScriptMethod -Name GetWarning -Value { return $this.__Streams.Warning }
38+
$outputStream = Add-Member -InputObject $outputStream -PassThru -MemberType NoteProperty -Name RemotelyTarget -Value $NodeName
39+
40+
if($childJob.State -eq 'Failed'){
41+
$childJob | Receive-Job -ErrorAction SilentlyContinue -ErrorVariable jobError
42+
$outputStream.__Streams.Error = $jobError
43+
}
44+
45+
Write-Output -InputObject $outputStream
46+
}
4847
}
4948

5049
Function ProcessRemotelyOutputToJSON {
@@ -86,13 +85,13 @@ Function GetFormattedTestResult {
8685
$outputHashArray = @()
8786
$testsGroup = $testResult |Group-Object -Property Describe
8887
foreach ($testGroup in $testsGroup) {
89-
$result = ($TestGroup.Group | select -ExpandProperty Passed ) -Notcontains $false
88+
$result = ($TestGroup.Group | Select-Object -ExpandProperty Passed ) -Notcontains $false
9089
$outputHashArray += @{
9190
Name = $testGroup.Name
9291
Result = $result
9392
TestResult = @($testGroup.Group |
94-
Where -Property Result -eq 'Failed' |
95-
Select -Property Describe, Context, Name, Result, ErrorRecord)
93+
Where-Object -Property Result -eq 'Failed' |
94+
Select-Object -Property Describe, Context, Name, Result, ErrorRecord)
9695
}
9796
}
9897
Write-Output -InputObject $outputHashArray
@@ -130,7 +129,10 @@ Function Write-VerboseLog
130129
$Message = $ErrorInfo.Exception.Message
131130
$Functionname = $ErrorInfo.InvocationInfo.InvocationName
132131
$LineNo = $ErrorInfo.InvocationInfo.ScriptLineNumber
133-
$scriptname = $(Split-Path -Path $ErrorInfo.InvocationInfo.ScriptName -Leaf)
132+
if ($ErrorInfo.InvocationInfo.ScriptName) {
133+
# this is done to correctly recieve the original error back from Pester mocks
134+
$scriptname = $(Split-Path -Path $ErrorInfo.InvocationInfo.ScriptName -Leaf)
135+
}
134136
Write-Verbose -Message "$scriptname - $Functionname - LineNo : $LineNo - $Message"
135137
#$PSCmdlet.ThrowTerminatingError($ErrorInfo)
136138
#Write-Error -ErrorRecord $ErrorInfo -ErrorAction Stop # throw back the Error record
@@ -151,10 +153,10 @@ Function Start-RemotelyJobProcessing {
151153
$AllJobsCompletedHash.Add($PSItem, $False)
152154
}
153155

154-
$CloneJobHash = $AllJobsCompletedHash.Clone() # used to iterate over the Hashtable
155-
156156
do {
157+
$CloneJobHash = $AllJobsCompletedHash.Clone() # used to iterate over the Hashtable
157158
foreach ($nodeJobStatus in $CloneJobHash.GetEnumerator()) {
159+
Write-VerboseLog -Message "Processing for Node -> $($nodeJobStatus.key) "
158160
if ($nodeJobStatus.Value) {
159161
# node job status is True, it has been processed
160162
Write-VerboseLog -Message "PSRemotely job already processed for Node -> $($nodeJobStatus.key) "
@@ -167,13 +169,11 @@ Function Start-RemotelyJobProcessing {
167169

168170
if ($enum.Value | Where-Object -Property State -In @('Completed', 'Failed')) {
169171
Write-VerboseLog -Message "PSRemotely job finished for Node -> $($nodeJobStatus.key). Processing it now."
170-
$enum | ProcessRemotelyJob | ProcessRemotelyOutputToJSON
172+
$enum | ProcessRemotelyJob -ErrorAction Stop | ProcessRemotelyOutputToJSON -ErrorAction Stop
171173
$AllJobsCompletedHash[$enum.key] = $true
172174
}
173-
174175
}
175176
}
176-
177177
} until (@($allJobsCompletedHash.Values) -notcontains $False)
178178
}
179179
CATCH {

0 commit comments

Comments
 (0)