VMWare.Templates.psm1

using module .\Classes\VMWare.Auth.psm1
using module .\VMWare.Authentication.psm1

<#
    .SYNOPSIS
        Builds the folder path (relative to the datacenter's "vm" root) by walking up the Parent chain.
#>

function Get-FolderPath {
    param($Folder)

    $names = @()
    $current = $Folder
    while ($null -ne $current -and $current.GetType().Name -eq 'FolderImpl') {
        # Stop at the hidden "vm" root folder of the datacenter; it is not part of the user-facing path.
        if ($current.Name -eq 'vm' -and $current.Parent.GetType().Name -eq 'DatacenterImpl') {
            break
        }
        $names = , $current.Name + $names
        $current = $current.Parent
    }
    return ($names -join '/')
}

<#
    .SYNOPSIS
        Validates the folder's ancestry level by level against the expected path segments.
        e.g. for 'parent1/parent2/child' it confirms Child's parent is Parent2, and Parent2's parent is Parent1.
#>

function Test-FolderAncestry {
    [CmdletBinding()]
    [OutputType([bool])]
    param($Folder, [string[]]$ExpectedSegments)

    Write-Verbose "Validating candidate '$($Folder.Name)' (Id: $($Folder.Id)) against path '$($ExpectedSegments -join '/')'."
    $current = $Folder
    # Walk from the leaf up to the root, comparing each expected segment in reverse order.
    for ($i = $ExpectedSegments.Length - 1; $i -ge 0; $i--) {
        $expected = $ExpectedSegments[$i]
        if ($null -eq $current -or $current.GetType().Name -ne 'FolderImpl') {
            Write-Verbose " Level $i : ran out of parent folders while expecting '$expected'. FAIL"
            return $false
        }
        Write-Verbose " Level $i : expecting '$expected', at folder '$($current.Name)'."
        if ($current.Name -ne $expected) {
            Write-Verbose " Level $i : mismatch - expected '$expected' but found '$($current.Name)'. FAIL"
            return $false
        }
        Write-Verbose " Level $i : matched '$expected'. Moving up to parent '$($current.Parent.Name)'."
        $current = $current.Parent
    }
    Write-Verbose "Candidate '$($Folder.Name)' matched the full ancestry. PASS"
    return $true
}

<#
    .SYNOPSIS
        Resolves a full VM folder path (e.g. 'parent1/parent2/child') to the single folder whose
        ancestry matches the path, disambiguating between folders that share the same leaf name.
#>

function Resolve-VMFolder {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [String]$Path
    )

    $splitFolder = $Path -split '/'
    $leafName = $splitFolder[-1]

    # All folders matching the leaf name (there may be several across different locations).
    $candidates = @(Get-Folder -Name $leafName -Type 'VM')

    # Keep the first one whose full ancestry matches (leaf -> ... -> root), then stop looking.
    $matched = $null
    foreach ($candidate in $candidates) {
        if (Test-FolderAncestry -Folder $candidate -ExpectedSegments $splitFolder) {
            $matched = $candidate
            break
        }
    }

    if (-not $matched) {
        Write-Warning "No VM folder found matching path '$Path'. Candidates found: $($candidates.Count)"
        $candidates | ForEach-Object { Write-Warning " - $(Get-FolderPath -Folder $_)" }
        throw "Unable to resolve VM folder path '$Path'."
    }

    return $matched
}

<#
    .SYNOPSIS
        Creates a VM based on a template. Will prompt the user for all settings needed.
#>

function New-VMFromTemplate {
    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory = $false)]
        [String]$ServerName = '',
        [Parameter(Mandatory = $false)]
        [String]$ServerNameRegex = '^[A-Z]{2}-[A-Z]{2}-[A-Z]{2}-[A-Z]-[A-Z]{2}[0-9]{2}$',
        [Parameter(Mandatory = $false)]
        [String]$OSCustomizationSpec,
        [Parameter(Mandatory = $false)]
        [String]$Template,
        [Parameter(Mandatory = $false)]
        [String]$Cluster,
        [Parameter(Mandatory = $false)]
        [String]$VirtualNetwork,
        [Parameter(Mandatory = $false)]
        [String]$DataStore,
        [Parameter(Mandatory = $false)]
        [Int]$CPUCores,
        [Parameter(Mandatory = $false)]
        [Int]$MemoryGB,
        [Parameter(Mandatory = $false)]
        [String]$Folder
    )

    $errorPref = $ErrorActionPreference
    $ErrorActionPreference = 'Stop' # Stop on all errors

    # Connect to VSphere
    [VMWareAuth]::GetInstance() | Out-Null

    # Get settings to use - or use defaults if valid
    $_Spec = Select-FromObjectList(Get-OSCustomizationSpec) -DefaultValue $OSCustomizationSpec
    $_Template = Select-FromObjectList(Get-Template) -DefaultValue $Template
    $_Cluster = Select-FromObjectList(Get-Cluster) -DefaultValue $Cluster
    $_Network = Select-FromObjectList(Get-VirtualNetwork) -DefaultValue $VirtualNetwork
    $_DataStore = Select-FromObjectList(Get-Datastore) -DefaultValue $DataStore
    $_Cores = Read-ValidInput -Prompt 'Enter Number of CPU Cores' -Regex '^2|4|6|8|10|12|16|20|24|32$' -DefaultValue $CPUCores
    $_MemoryGB = Read-ValidInput -Prompt 'Enter GB of Memory' -Regex '^([1-4][0-9])|[2-9]$' -DefaultValue $MemoryGB
    $_Folder = if([string]::IsNullOrEmpty($Folder)) { $null } else { Resolve-VMFolder -Path $Folder }

    # Get and confirm name
    $_ServerName = Read-ValidInput -Prompt 'Enter Server Name' -Regex $ServerNameRegex -DefaultValue $ServerName

    Write-Output "Validating $_ServerName is available..."
    $dnsResult = Resolve-DnsName $_ServerName
    if ($null -ne $dnsResult) {
        throw 'There appears to already be a DNS record for that machine name. Confirm it is available to use.'
    }

    # Confirm Settings
    Write-Output @{
        Spec       = $_Spec.Name
        Template   = $_Template.Name
        Cluster    = $_Cluster.Name
        Network    = $_Network.Name
        DataStore  = $_DataStore.Name
        ServerName = $_ServerName
        CPUCores   = $_Cores
        MemoryGB   = $_MemoryGB
        Folder     = if ($null -ne $_Folder) { $_Folder } else { 'Default' }
    }

    if ($pscmdlet.ShouldProcess($_ServerName, 'create')){
        # Create VM
        $_LocationParam = if ($null -ne $_Folder) { @{ Location = $_Folder } } else { @{} }
        $VM = New-VM -Name $_ServerName -Template $_Template -OSCustomizationSpec $_Spec -ResourcePool $_Cluster -Datastore $_DataStore.Name @_LocationParam -Confirm:$false

        # Set Network
        $Adapters = Get-NetworkAdapter $VM
        $Adapters | Set-NetworkAdapter -NetworkName $_Network -Confirm:$false | Out-Null

        # Set CPU / Memory
        Set-VM -VM $VM -NumCpu $_Cores -CoresPerSocket $($_Cores / 2) -MemoryGB $_MemoryGB -Confirm:$false | Out-Null

        # Start VM
        Start-VM $VM -Confirm:$false
    }

    $ErrorActionPreference = $errorPref # Set back to previous value
}