Help With Structure of Powershell Project

<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”>Hi everyone.</p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”></p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”>We need some help / advice from some experts, so I thought where better than the PowerShell.org forums!</p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”></p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”>A friend of mine has started a community based project that is written in Powershell that I am a core contributor for. The high level summary of the project is to automate the creation of As Built Reports for the various technologies we deal with on a day to day basis as consultants, primarily in the infrastructure space.</p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”></p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”>We both are not strong in powershell, and are learning a lot as we go. This is actually one of the drivers for us both pursuing the project.</p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”></p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”>The project was originally written simply as a powershell script, but we have been working to break it out in to several PowerShell modules. We want to re-release the project shortly, and we have a few key goals that we’d like the project to stand by. One of those is to get it published on the PowerShell Gallery which of course requires the project to be written as a module (or several modules … I’ll explain shortly). Some other goals are that it needs to be easy for people to use and we want it to be easy for people to contribute to.</p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”></p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”>The project is probably easiest explained as being:</p>
<p lang=“en-AU” style=“margin: 0in; font-family: Calibri; font-size: 11.0pt;”></p>

  • A core "wrapper" module / set of cmdlets that are re-used no matter what technology the report is being generated for. This also handles the structure of the saved artefact output by the cmdlet
  • Separate modules per report type / technology, and it is here where the core code is written to extract information from a particular device / product / technology. The data is then fed back in to the core wrapper script (above) where it is saved as word / text / html / xml
<p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">We've gone down the path of having separate git repositories for the core functionality, and then having one repository per report type. This easily allows contributors to work on reports for a particular technology in a fully separate module.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">For example, you can see the repositories we have at the following GitHub org - https://github.com/AsBuiltReport</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">AsbuiltReport is the core module and the main function exported here, New-AsBuiltReport, is really the only command most consumers of this project will care about.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">All of the other repositories are specifically for a particular technology or product, such as VMware vSphere, Cisco UCS Manager, so on and so forth. Each of the "Report modules" are listed as a required module in the core AsBuiltReport manifest, so when we do get around to getting this project on the gallery, the user will just need to run "Install-Module AsBuiltReport" but it will also pull down the other report modules as they are required modules.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">Hopefully you're still with me and even more so I really hope your heads aren't buried in your hands yet!</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">Now, one of our aims with this structure it to try and keep the report module / repo as separate as we can from the core / base module/repo. We want this to be as simple as we can for people to contribute, so we don't want them to have to update too much code in too many repositories just to get some simple changes.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">Now, to get to the point of specifically where we need some guidance or advice …. Each report has some configuration that is required, and we currently store that config in a JSON file. A user running the report will want to be able to modify the data in this JSON file to influence the type of data that is collected and output. To date, we have stored that JSON file in the root folder of the report, but we don't want to be telling users to have to open JSON files inside of module folders to change configurations … yuck.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">So we came up with two ideas, and we don't know if either of these are good, bad, ugly or if there are simply better ways to achieve what we are trying to achieve.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p>
  1. Keep the JSON file in the root of each report, but have a function that copies the JSON configuration to a folder the user running the cmdlet specifies. Therefore the JSON file inside of the module is simply acting as a template, if you like. The user would then edit the copy of the original JSON file, which would be stored in whatever folder they specified
  2. Write a powershell function in each report module that will create the JSON file in a folder the user running the cmdlet specifies.
<p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">For either of the above, we would plan to have a function in the base "AsBuiltReport" module that generates the configuration files. Its more so just trying to understand if one of these ways are better than the other, or if they are both crap and there is something better we could be doing, keeping in mind we are NOT PS experts and we don't have half the skills of most people on these forums.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">We accept the fact that the project will continue to change and evolve over time, but we both feel that this is a key sticking point that we currently are facing and we are not certain of the best path forward without having to refactor the project in the not too distant future, and risk confusing users.</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">One last note, is that we want a way to be able to "version control" these JSON files. For example, if a user installed the modules and created a configuration in 2 months' time, awesome. 6 months from now, we might want to add a new feature or function which requires an update to the JSON file. We want a way to be able to alert users of this change and that they should generate a new JSON configuration, but we wouldn't want to go and overwrite their JSON without them approving it first. Does anyone have some guidance on how this might be achieved?</p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;"></p> <p lang="en-AU" style="margin: 0in; font-family: Calibri; font-size: 11.0pt;">Thanks so much in advance for taking the time to read through and for any guidance or suggestions you may have.</p>

Wow, quite an undertaking!! The first thing is the module is a bit too modular. Maybe consider a structure like this so that everything is under a single module:

AsBuiltReport
|
– Public
—|
----- New-AsBuiltReport.ps1
----- AsBuiltReport.Cisco.UcsManager
------|
-------- Get-AsBuiltReportCiscoUcsManagerStuff.ps1
----- AsBuiltReport.VMware.vSphere
----- AsBuiltReport.VMware.NSXv
|
– Private
—|
----- Update-AsBuiltReportSchema
– AsBuiltReport.psm1
– AsBuiltReport.psd1
– ASBuiltReport.config

This also makes it so you define requirements in the PSD1 for modules and other requirements and you’re doing a single import. Configuration files can be a bit of a pain, especially if people are manually editing them. Imagine they want to set InfoLevel, you are looking for 1-5 and they put 6 or type Verbose. Normally XML has a XSD to define what is allowed in the XML, a schema format. There are projects out there for JSON, like this one:

https://stackoverflow.com/questions/36152972/validate-json-against-xml-schema-xsd

You’re going to have to have some method to validate the config file is setup like you expect. Another thing you could do is possibly set default values, for instance InfoLevel, you could do if (($config | Where{$_.Name -eq ‘AsBuiltReport.VMware.NSXv’}).Options.InfoLevel -notin (1…5)) {$infoLevel = 3}.

To update the schema,from a pseudo-code perspective, you could do something like:

AsBuiltReport.psm1

#$functionFolders = @('Public', 'Internal', 'Classes')
$functionFolders = @('Public')

ForEach ($folder in $functionFolders) {
    $folderPath = Join-Path -Path $PSScriptRoot -ChildPath $folder
    If (Test-Path -Path $folderPath) {
        Write-Verbose -Message "Importing from $folder"
        $functions = Get-ChildItem -Path $folderPath -Filter '*.ps1' 
        ForEach ($function in $functions) {
            Write-Verbose -Message "  Importing $($function.BaseName)"
            . $($function.FullName)
        }
    }    
}

$publicFunctions = (Get-ChildItem -Path "$PSScriptRoot\Public" -Filter '*.ps1').BaseName
Export-ModuleMember -Function $publicFunctions

$configPath = ($MyInvocation.MyCommand.Path).Replace('psm1','config')
if (Test-Path -Path $configPath -PathType Leaf) {
    Write-Warning ('Updating {0}' -f $configPath)
    .\Private\Update-AsBuiltReportSchema
}
else {
    Write-Warning ('[Action Required] Creating {0}. You need to do stuff to this file.' -f $configPath)
}

Then you get something like this when you import it:

PS C:\> Import-Module AsBuiltReport -Force
WARNING: [Action Required] Creating C:\Users\rasim\Documents\WindowsPowerShell\Modules\AsBuiltReport\AsBuiltReport.config. You need to do stuff to this file.

The config file could look like this to have specific configuration for all sub-components, which is a PSObject converted to JSON:

$config = @()
$config += [pscustomobject]@{
    Name = 'AsBuiltReport.Cisco.UcsManager'
    Version = '0.0.1'
    Enabled = $true
    Options = [pscustomobject]@{
        "_comment_" =  "0 = Disabled, 1 = Summary, 2 = Informative, 3 = Detailed, 4 = Adv Detailed, 5 = Comprehensive"
        InfoLevel = 2
        HealthCheck = $false
    }   
}
$config += [pscustomobject]@{
    Name = 'AsBuiltReport.VMware.vSphere'
    Version = '0.0.1'
    Enabled = $true
    Options = [pscustomobject]@{
        "_comment_" =  "0 = Disabled, 1 = Summary, 2 = Informative, 3 = Detailed, 4 = Adv Detailed, 5 = Comprehensive"
        InfoLevel = 4
        HealthCheck = $false
    }   
}
$config += [pscustomobject]@{
    Name = 'AsBuiltReport.VMware.NSXv'
    Version = '0.0.1'
    Enabled = $true
    Options = [pscustomobject]@{
        "_comment_" =  "0 = Disabled, 1 = Summary, 2 = Informative, 3 = Detailed, 4 = Adv Detailed, 5 = Comprehensive"
        InfoLevel = 3
        HealthCheck = $true
    }   
}

$config | ConvertTo-Json -Depth 10 | Set-Content -Path C:\Scripts\AsBuiltReport.config

Definitely consider building a shell module first to just figure out the import, configuration, update components first. It will save you time because you may need to have components in all of the product functions.