Public/New-VesterConfig.ps1

function New-VesterConfig {
    <#
    .SYNOPSIS
    Generates a Vester config file from settings in your existing VMware environment.
 
    .DESCRIPTION
    New-VesterConfig is designed to be a quick way to get started with Vester.
 
    Vester needs one config file for each vCenter server it interacts with. To
    help speed up this one-time creation process, New-VesterConfig uses PowerCLI
    to pull current values from your environment to store in the config file.
     
    You'll be prompted with the list of Clusters/Hosts/VMs/etc. discovered, and
    asked to choose one of each type to use as a baseline; i.e. "all my other
    hosts should be configured like this one." Those values are displayed
    interactively, and you can manually edit them as desired.
 
    Optionally, advanced users can use the -Quiet parameter. This suppresses
    all host output and prompts. Instead, values are pulled from the first
    Cluster/Host/VM/etc. found alphabetically. Manual review afterward of the
    config file is strongly encouraged if using the -Quiet parameter.
 
    It outputs a single Config.json file at \Vester\Configs, which may require
    admin rights. Optionally, you can use the -OutputFolder parameter to
    specify a different folder to store the Config.json file.
 
    .EXAMPLE
    New-VesterConfig
    Ensures that you are connected to only one vCenter server.
    Based on all Vester test files found in '\Vester\Tests', the command
    discovers values from your environment and displays them, occasionally
    prompting for a selection of which cluster/host/etc. to use.
    Outputs a new Vester config file to '\Vester\Configs\Config.json',
    which may require admin rights.
 
    .EXAMPLE
    New-VesterConfig -Quiet -OutputFolder "$env:USERPROFILE\Desktop"
    -Quiet suppresses all host output and prompts, instead pulling values
    from the first cluster/host/etc. found alphabetically.
    Upon completion, Config.json will be created on your Desktop.
 
    .NOTES
    This command relies on the Pester and PowerCLI modules for testing.
 
    "Get-Help about_Vester" for more information.
 
    .LINK
    https://wahlnetwork.github.io/Vester
 
    .LINK
    https://github.com/WahlNetwork/Vester
    #>

    [CmdletBinding()]
    param (
        # Select a folder to create a new Config.json file inside
        [ValidateScript({Test-Path $_ -PathType Container})]
        [object]$OutputFolder = "$(Split-Path -Parent $PSScriptRoot)\Configs",

        # Suppress all prompts and Write-Host. Create the config file
        # with the values of the first Cluster/Host/VM/etc. found.
        [switch]$Quiet
    )

    # Must have only one vCenter connection open
    # Potential future work: loop through all vCenter connections
    If ($DefaultVIServers.Count -lt 1) {
        Write-Warning 'Please connect to vCenter before running this command.'
        throw 'A single connection with Connect-VIServer is required.'
    } ElseIf ($DefaultVIServers.Count -gt 1) {
        Write-Warning 'Vester config files are designed to be unique to each vCenter server.'
        Write-Warning 'Please connect to only one vCenter before running this command.'
        Write-Warning "Current connections: $($DefaultVIServers -join ' / ')"
        throw 'A single connection with Connect-VIServer is required.'
    }
    Write-Verbose "vCenter: $($DefaultVIServers.Name)"

    # TODO: Make this a param? Or keep hardcoded?
    Write-Verbose "Assembling Vester files within $(Split-Path -Parent $PSScriptRoot)\Tests\"
    $GetVesterTest = "$(Split-Path -Parent $PSScriptRoot)\Tests\" | Get-VesterTest -Simple
    # Appending to a list is faster than rebuilding an array
    $VesterTestSuite = New-Object 'System.Collections.Generic.List[PSCustomObject]'

    # For each *.Vester.ps1 file found,
    $GetVesterTest | ForEach-Object {
        # Do the necessary Split-Path calls once and save them for later
        $v = [PSCustomObject]@{
            Full   = $_
            Parent = Split-Path (Split-Path $_ -Parent) -Leaf
            Leaf   = Split-Path $_ -Leaf
        }
        $VesterTestSuite.Add($v)
    }

    If (-not $Quiet) {
        # Introduce and inform of $null
        Write-Host 'Vester will now start pulling values from your vCenter server, '-NoNewline
        Write-Host "$($DefaultVIServers.Name)" -ForegroundColor Yellow
        Write-Host 'After each section, you will be asked if you want to edit any values.'
    }

    $config = [ordered]@{}
    $config.vcenter = @{vc = $DefaultVIServers.Name}

#region scope
    # Set the section's config, and then display it for review
    $config.scope = [ordered]@{
        datacenter = '*'
        cluster    = '*'
        dscluster  = '*'
        host       = '*'
        vm         = '*'
        vds        = '*'
    }

    If (-not $Quiet) {
        # Explain each setting
        Write-Host "`n ### Inventory Scopes" -ForegroundColor Green
        Write-Host "This dictates the scope of your vSphere environment that will be tested by Pester."
        Write-Host "Use string values. Wildcards are accepted."
        Write-Host "datacenter = [string] vSphere datacenter name(s)"
        Write-Host "cluster = [string] vSphere cluster name(s)"
        Write-Host "dscluster = [string] vSphere datastore cluster name(s)"
        Write-Host "host = [string] ESXi host name(s)"
        Write-Host "vm = [string] Virtual machine name(s)"
        Write-Host "vds = [string] vSphere Distributed Switch (VDS) name(s)"

        # Empty Write-Host just to insert extra line breaks where desired
        Write-Host ''
        $config.scope

        If ((Read-HostColor "`nWould you like to change any of those values? Y/N [N]") -like 'y*') {
            Write-Host "`nFor all values, entering nothing will keep the default * to check all objects of that category.`n"

            [string]$ManualDatacenter = Read-HostColor 'datacenter = Filter the following command: Get-Datacenter -Name YOURINPUTHERE -Server $vCenter'
            [string]$ManualCluster    = Read-HostColor 'cluster = Filter the following command: $Datacenter | Get-Cluster -Name YOURINPUTHERE'
            [string]$ManualDSCluster  = Read-HostColor 'dscluster = Filter the following command: $Datacenter | Get-DatastoreCluster -Name YOURINPUTHERE'
            [string]$ManualHost       = Read-HostColor 'host = Filter the following command: $Cluster | Get-VMHost -Name YOURINPUTHERE'
            [string]$ManualVM         = Read-HostColor 'vm = Filter the following command: $Cluster | Get-VM -Name YOURINPUTHERE'
            [string]$ManualVDS        = Read-HostColor 'vds = Filter the following command: $Datacenter | Get-VDSwitch -Name YOURINPUTHERE'

            $config.scope.datacenter = If ($ManualDatacenter -eq '') {'*'} Else {$ManualDatacenter}
            $config.scope.cluster    = If ($ManualCluster -eq '')    {'*'} Else {$ManualCluster}
            $config.scope.dscluster  = If ($ManualDSCluster -eq '')  {'*'} Else {$ManualDSCluster}
            $config.scope.host       = If ($ManualHost -eq '')       {'*'} Else {$ManualHost}
            $config.scope.vm         = If ($ManualVM -eq '')         {'*'} Else {$ManualVM}
            $config.scope.vds        = If ($ManualVDS -eq '')        {'*'} Else {$ManualVDS}
        }
    } #if not $Quiet

    Write-Verbose "Gathering inventory objects from $($DefaultVIServers.Name)"
    $vCenter    = $DefaultVIServers.Name
    $Datacenter = Get-Datacenter -Name $config.scope.datacenter -Server $vCenter
    $Cluster    = $Datacenter | Get-Cluster -Name $config.scope.cluster
    $DSCluster  = $Datacenter | Get-DatastoreCluster -Name $config.scope.dscluster
    $VMHost     = $Cluster | Get-VMHost -Name $config.scope.host
    $VM         = $Cluster | Get-VM -Name $config.scope.vm
    # Secondary modules...PowerCLI doesn't do implicit module loading as of PCLI 6.5
    # This is all the effort I'm willing to put into working around that right now
    Try {
        $Network = $Datacenter | Get-VDSwitch -Name $config.scope.vds -ErrorAction Stop
    } Catch {
        Write-Warning 'Get-VDSwitch failed. Have you manually imported module "VMware.VimAutomation.Vds"?'
    }

    If ($Quiet) {
        $Datacenter = If ($Datacenter) {$Datacenter[0]}
        $Cluster    = If ($Cluster)    {$Cluster[0]}
        $DSCluster  = If ($DSCluster)  {$DSCluster[0]}
        $VMHost     = If ($VMHost)     {$VMHost[0]}
        $VM         = If ($VM)         {$VM[0]}
        $Network    = If ($Network)    {$Network[0]}
    } Else {
        $Datacenter = If ($Datacenter) {Select-InventoryObject $Datacenter 'Datacenter'}
        $Cluster    = If ($Cluster)    {Select-InventoryObject $Cluster 'Cluster'}
        $DSCluster  = If ($DSCluster)  {Select-InventoryObject $DSCluster 'DSCluster'}
        $VMHost     = If ($VMHost)     {Select-InventoryObject $VMHost 'Host'}
        $VM         = If ($VM)         {Select-InventoryObject $VM 'VM'}
        $Network    = If ($Network)    {Select-InventoryObject $Network 'Network'}
    }
#endregion
        
    $ScopeList = ($VesterTestSuite | Select-Object -Property Parent -Unique).Parent
    Write-Verbose "Scopes supplied by test files: $($ScopeList -join ' | ')"
    
    # Not used; called below to help with manual user overrides of values
    # That block is also commented out, awaiting potential future improvements
    # $TestHistory = New-Object 'System.Collections.Generic.List[PSCustomObject]'

    # Group tests by their scope
    ForEach ($Scope in $ScopeList) {
        Write-Verbose "Processing all tests for scope $Scope"

        # Loop through each test file applicable in the current scope
        # Couldn't resist calling each file a Vest. Sorry, everyone
        ForEach ($Vest in $VesterTestSuite | Where-Object Parent -eq $Scope) {
            Write-Verbose "Processing test file $($Vest.Leaf)"
            
            # Import all variables from the current .Vester.ps1 file
            . $Vest.Full

            $Object = switch ($Scope) {
                'vCenter'    {$vCenter}
                'Datacenter' {$Datacenter}
                'Cluster'    {$Cluster}
                'DSCluster'  {$DSCluster}
                'Host'       {$VMHost}
                'VM'         {$VM}
                'Network'    {$Network}
                # If not scoped properly, don't know what object to check
                Default      {$null}
            }

            # TODO: Should probably offload this to a private function
            $CfgLine = (Select-String -Path $Vest.Full -Pattern '\$cfg') -replace '.*\:[0-9]+\:',''
            $CfgLine -match '.*\$cfg\.([a-z]+)\.([a-z]+)$' | Out-Null

            # Run the $Actual script block, storing the result in $Result
            If ($Object -and ($Result = & $Actual) -ne $null) {
                # Call module private function Set-VesterConfigValue to add the entry
                Set-VesterConfigValue -Value ($Result -as $Type)
            } Else {
                # Inventory $Object doesn't exist, or $Actual returned nothing
                # Populate with null value; Invoke-Vester will skip this test
                Set-VesterConfigValue -Value $null
            } #if $Object and $Result

            <# ### This works, but not currently used (see commented block below)
            If ($config.$($Matches[1]).Keys -contains $($Matches[2]) -and -not $Quiet) {
                # Record test/value correlation, if user wants to manually edit
                $h = [PSCustomObject]@{
                    Full = $Vest.Full
                    Leaf = $Vest.Leaf
                    CfgValue = "$($Matches[1]).$($Matches[2])"
                }
                $TestHistory.Add($h)
            }
            #>

        } #foreach $Vest

        # If any values were populated in this scope, and the -Quiet flag is not active,
        # Display all values for this scope and ask about manual overrides
        If ($config.$Scope -and -not $Quiet) {
            # Empty Write-Host just to insert extra line breaks where desired
            Write-Host ''
            Write-Host ' # Config values for scope ' -NoNewline
            Write-Host "$Scope" -ForegroundColor Green
            $Sorted = $config.$Scope.GetEnumerator() | Sort-Object Name
            $Sorted
            $config.$Scope = [ordered]@{}
            $Sorted | Foreach-Object { $config.$Scope.Add($_.Name, $_.Value) }            

            <# ###
            # Users still need to manually edit the .json file if changes are desired
            # The code block below works, but needs much more validation on entry
            # For example, entering text into a "string[]" type does the following:
                # The apostrophe, a common string wrapper, ends up in the json file as \u0027
                # Not sure how to enter multiple string values (like muliple DNS servers)
 
            If ((Read-HostColor 'Would you like to change any of these values? Y/N [N]') -like 'y*') {
                Write-Host "`nIf there are any values you never want to test, enter " -NoNewline
                Write-Host '$null' -ForegroundColor Red -NoNewline
                Write-Host " to skip those tests.`n"
                # TODO: ^ Entering $null still good instructions?
 
                ForEach ($CfgLine in $config.$Scope.GetEnumerator() | Sort Name) {
                    $TestHistory | Where CfgValue -eq "$Scope.$($CfgLine.Name)" | ForEach-Object {
                        . $_.Full
 
                        Write-Host "$($_.Leaf) : $Title"
                        Write-Host $Description
                        Write-Host "[$Type]$($CfgLine.Name) = $($CfgLine.Value)"
 
                        If ((Read-HostColor 'Would you like to change this value? Y/N [N]') -like 'y*') {
                            $UserEnteredValue = Read-HostColor "Enter the new value of type '$Type'"
                            If ($UserEnteredValue -eq $null) {
                                $NewValue = $null
                            } Else {
                                $NewValue = $UserEnteredValue -as $Type
                            }
                            Write-Verbose "Setting $($Scope.ToLower()).$($CfgLine.Name) = $UserEnteredValue"
                            $config.$Scope.($CfgLine.Name) = $UserEnteredValue
                        } #if change single value
                    } #foreach $TestHistory
                } #foreach $CfgLine
            } #if change any value
            #>


        } #if $config.$Scope
    } #foreach $Scope

    Write-Verbose "Creating config file at $OutputFolder\Config.json"
    Try {
        $config | ConvertTo-Json | Out-File $OutputFolder\Config.json -ErrorAction Stop
        Write-Host "`nConfig file created at " -ForegroundColor Green -NoNewline
        Write-Host "$OutputFolder\Config.json"
        Write-Host 'Edit the file manually to change any displayed values.'
    } Catch {
        Write-Warning "`nFailed to create config file at $OutputFolder\Config.json"
        Write-Warning 'Have you tried running PowerShell as an administrator?'
    }
}