cMDTBuildLab.psm1

enum Ensure
{
    Present
    Absent
}

[DscResource()]
class cMDTBuildApplication
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Name

    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Mandatory)]
    [string]$Enabled

    [DscProperty(Mandatory)]
    [string]$CommandLine

    [DscProperty(Mandatory)]
    [string]$ApplicationSourcePath

    [DscProperty(Mandatory)]
    [string]$PSDriveName

    [DscProperty(Mandatory)]
    [string]$PSDrivePath

    [void] Set()
    {
        if ($this.ensure -eq [Ensure]::Present) {
            $present = Invoke-TestPath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath
            if ( !$present ) {
                $this.ImportApplication()
            }
        }
        else {
            Invoke-RemovePath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Verbose
        }
    }

    [bool] Test()
    {
        $present = Invoke-TestPath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath

        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildApplication] Get()
    {
        return $this
    }

    [void] ImportApplication()
    {
        Import-MDTModule
        New-PSDrive -Name $this.PSDriveName -PSProvider "MDTProvider" -Root $this.PSDrivePath -Verbose:$false
        Import-MDTApplication -Path $this.Path -Enable $this.Enabled -Name $this.Name -ShortName $this.Name `
                              -CommandLine $this.CommandLine -WorkingDirectory ".\Applications\$($this.Name)" `
                              -ApplicationSourcePath $this.ApplicationSourcePath -DestinationFolder $this.Name -Verbose
    }
}

[DscResource()]
class cMDTBuildBootstrapIni
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Path

    [DscProperty()]
    [string]$Content

    [void] Set()
    {
        if ($this.Ensure -eq [Ensure]::Present) {
            $this.SetContent()
        }
        else {
            $this.SetDefaultContent()
        }
    }

    [bool] Test()
    {
        $present = $this.TestFileContent()

        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildBootstrapIni] Get()
    {
        return $this
    }

    [bool] TestFileContent()
    {
        $present = $false
        $existingConfiguration = Get-Content -Path $this.Path -Raw #-Encoding UTF8

        if ($existingConfiguration -eq $this.Content.Replace("`n","`r`n")) {
            $present = $true
        }
        return $present
    }

    [void] SetContent()
    {
        Set-Content -Path $this.Path -Value $this.Content.Replace("`n","`r`n") -NoNewline -Force #-Encoding UTF8
    }

    [void] SetDefaultContent()
    {
        $defaultContent = @"
[Settings]
Priority=Default
 
[Default]
 
"@

        Set-Content -Path $this.Path -Value $defaultContent -NoNewline -Force #-Encoding UTF8
    }
}

[DscResource()]
class cMDTBuildCustomize
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Name

    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Mandatory)]
    [string]$SourcePath

    [DscProperty(Mandatory)]
    [string]$TargetPath

    [DscProperty()]
    [string[]]$TestFiles

    [void] Set()
    {
        $filename = "$($this.SourcePath)\$($this.name)"
        $extractfolder = "$($this.path)\$($this.TargetPath)"

        if ($this.ensure -eq [Ensure]::Present) {
            if ($filename -like '*.zip') {
                Invoke-ExpandArchive -Source $filename -Target $extractfolder -Verbose
            }
            else {
                Copy-Item -Path $filename $extractfolder -Verbose
            }
        }
    }

    [bool] Test()
    {
        $present = $true
        if ($this.Name -like '*.zip') {
            foreach ($file in $this.TestFiles) {
                if ( !(Test-Path -Path "$($this.path)\$($this.TargetPath)\$($file)") ) {
                    $present = $false
                    break
                }
            }
        }
        else {
            if ( !(Test-Path -Path "$($this.path)\$($this.TargetPath)\$($this.name)") ) {
                $present = $false
            }
        }

        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildCustomize] Get()
    {
        return $this
    }
}

[DscResource()]
class cMDTBuildCustomSettingsIni
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Path

    [DscProperty()]
    [string]$Content

    [void] Set()
    {
        if ($this.Ensure -eq [Ensure]::Present) {
            $this.SetContent()
        }
        else {
            $this.SetDefaultContent()
        }
    }

    [bool] Test()
    {
        $present = $this.TestFileContent()
        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildCustomSettingsIni] Get()
    {
        return $this
    }

    [bool] TestFileContent()
    {
        $present = $false
        $existingConfiguration = Get-Content -Path $this.Path -Raw #-Encoding UTF8
        if ($existingConfiguration -eq $this.Content.Replace("`n","`r`n")) {
            $present = $true
        }
        return $present
    }

    [void] SetContent()
    {
        Set-Content -Path $this.Path -Value $this.Content.Replace("`n","`r`n") -NoNewline -Force #-Encoding UTF8
    }

    [void] SetDefaultContent()
    {
        $defaultContent = @"
[Settings]
Priority=Default
Properties=MyCustomProperty
 
[Default]
OSInstall=Y
SkipCapture=YES
SkipAdminPassword=NO
SkipProductKey=YES
 
"@

        Set-Content -Path $this.Path -Value $defaultContent -NoNewline -Force #-Encoding UTF8
    }
}

[DscResource()]
class cMDTBuildDirectory
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Key)]
    [string]$Name

    [DscProperty()]
    [string]$PSDriveName

    [DscProperty()]
    [string]$PSDrivePath

    [void] Set()
    {
        if ($this.ensure -eq [Ensure]::Present) {
            $this.CreateDirectory()
        }
        else
        {
            if (($this.PSDrivePath) -and ($this.PSDriveName)) {
                Invoke-RemovePath -Path "$($this.path)\$($this.Name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Verbose
            }
            else {
                Invoke-RemovePath -Path "$($this.path)\$($this.Name)" -Verbose
            }
        }
    }

    [bool] Test()
    {
        if (($this.PSDrivePath) -and ($this.PSDriveName)) {
            $present = Invoke-TestPath -Path "$($this.path)\$($this.Name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Verbose
        }
        else {
            $present = Invoke-TestPath -Path "$($this.path)\$($this.Name)" -Verbose
        }

        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildDirectory] Get()
    {
        return $this
    }

    [void] CreateDirectory()
    {
        if (($this.PSDrivePath) -and ($this.PSDriveName)) {
            Import-MDTModule
            New-PSDrive -Name $this.PSDriveName -PSProvider "MDTProvider" -Root $this.PSDrivePath -Verbose:$false | `
                    New-Item -ItemType Directory -Path "$($this.path)\$($this.Name)" -Verbose
        }
        else {
            New-Item -ItemType Directory -Path "$($this.path)\$($this.Name)" -Verbose
        }
    }
}

[DscResource()]
class cMDTBuildOperatingSystem
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Name

    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Key)]
    [string]$SourcePath

    [DscProperty()]
    [bool]$CustomImage = $false

    [DscProperty(Mandatory)]
    [string]$PSDriveName

    [DscProperty(Mandatory)]
    [string]$PSDrivePath

    [void] Set()
    {
        if ($this.ensure -eq [Ensure]::Present)
        {
            if ( !$this.Test() ) {
                $this.ImportOperatingSystem()
            }
        }
        else
        {
            Invoke-RemovePath -Path "$($this.PSDrivePath)\Operating Systems\$($this.Name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Verbose
            If ( $this.Test() ) { Write-Error "Cannot remove '$($this.PSDrivePath)\Operating Systems\$($this.Name)'" }
        }
    }

    [bool] Test()
    {
        $present = $false
        if ( !$this.CustomImage ) {
            $present = Invoke-TestPath -Path "$($this.PSDrivePath)\Operating Systems\$($this.Name)\sources\install.wim"
        }
        else {
            $image = $this.GetCustomImage()
            $present = Invoke-TestPath -Path "$($this.PSDrivePath)\Operating Systems\$($this.Name)\$image"
        }
        return $present
    }

    [cMDTBuildOperatingSystem] Get()
    {
        return $this
    }

    [void] ImportOperatingSystem()
    {
        Import-MDTModule
        New-PSDrive -Name $this.PSDriveName -PSProvider "MDTProvider" -Root $this.PSDrivePath -Verbose:$false

        Try {
            $ErrorActionPreference = "Stop"
            if ( !$this.CustomImage ) {
                Import-MDTOperatingSystem -Path $this.Path -SourcePath $this.SourcePath -DestinationFolder $this.Name -Verbose
            }
            else {
                $image = $this.GetCustomImage()
                if ($image -like "*.wim") {
                    Import-MDTOperatingSystem -path $this.Path -SourceFile "$($this.SourcePath)\$image" -DestinationFolder $this.Name -Verbose
                }
            }
            $ErrorActionPreference = "Continue"
        }
        Catch [Microsoft.Management.Infrastructure.CimException] {
            If ($_.FullyQualifiedErrorId -notlike "*ItemAlreadyExists*") {
                throw $_
            }
        }
        Finally {
        }
    }

    [string] GetCustomImage()
    {
        $file = Get-Item -Path "$($this.SourcePath)\*.wim" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
        if ($file) {
            return $file.Name
        }
        else {
            return ""
        }
    }
}

[DscResource()]
class cMDTBuildPackage
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Name

    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Mandatory)]
    [string]$PackageSourcePath

    [DscProperty(Mandatory)]
    [string]$PSDriveName

    [DscProperty(Mandatory)]
    [string]$PSDrivePath

    [void] Set()
    {
        if ($this.ensure -eq [Ensure]::Present) {
            $present = Invoke-TestPath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath
            if ( !$present ) {
                $this.ImportPackage()
            }
        }
        else {
            Invoke-RemovePath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Verbose
        }
    }

    [bool] Test()
    {
        $present = Invoke-TestPath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath

        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildPackage] Get()
    {
        return $this
    }

    [void] ImportPackage()
    {
        # The Import-MDTPackage command crashes WMI when run from inside DSC. Using workflow is a work around.
        workflow Import-Pkg {
            [CmdletBinding()]
            param(
                [string]$PSDriveName,
                [string]$PSDrivePath,
                [string]$Path,
                [string]$PackageSource
            )
            InlineScript {
                Import-MDTModule
                New-PSDrive -Name $Using:PSDriveName -PSProvider "MDTProvider" -Root $Using:PSDrivePath -Verbose:$false
                Import-MDTPackage -Path $Using:Path -SourcePath $Using:PackageSource -Verbose
            }
        }
        Import-Pkg -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Path $this.Path -PackageSource $this.PackageSourcePath
    }
}

[DscResource()]
class cMDTBuildPersistentDrive
{
    [DscProperty()]
    [Ensure] $Ensure = "Present"

    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Key)]
    [string]$Name

    [DscProperty(Mandatory)]
    [string]$Description

    [DscProperty(Mandatory)]
    [string]$NetworkPath

    [void] Set()
    {
        if ($this.ensure -eq [Ensure]::Present) {
            $this.CreateDirectory()
        }
        else {
            $this.RemoveDirectory()
        }
    }

    [bool] Test()
    {
        $present = $this.TestDirectoryPath()
        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildPersistentDrive] Get()
    {
        return $this
    }

    [bool] TestDirectoryPath()
    {
        $present = $false
        Import-MDTModule
        if (Test-Path -Path $this.Path -PathType Container -ErrorAction Ignore) {
            $mdtShares = (GET-MDTPersistentDrive -ErrorAction SilentlyContinue)
            If ($mdtShares) {
                ForEach ($share in $mdtShares) {
                    If ($share.Name -eq $this.Name) {
                        $present = $true
                    }
                }
            }
        }
        return $present
    }

    [void] CreateDirectory()
    {
        Import-MDTModule
        New-PSDrive -Name $this.Name -PSProvider "MDTProvider" -Root $this.Path -Description $this.Description -NetworkPath $this.NetworkPath -Verbose:$false | `
        Add-MDTPersistentDrive -Verbose
    }

    [void] RemoveDirectory()
    {
        Import-MDTModule
        Write-Verbose -Message "Removing MDTPersistentDrive $($this.Name)"
        New-PSDrive -Name $this.Name -PSProvider "MDTProvider" -Root $this.Path -Description $this.Description -NetworkPath $this.NetworkPath -Verbose:$false | `
        Remove-MDTPersistentDrive -Verbose
    }
}

[DscResource()]
class cMDTBuildPreReqs
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$DownloadPath

    [void] Set()
    {
        Write-Verbose "Starting Set MDT PreReqs..."
        [hashtable]$DownloadFiles = Get-ConfigurationData -ConfigurationData "$($PSScriptRoot)\cMDTBuildLabPrereqs.psd1"

        if ($this.ensure -eq [Ensure]::Present) {
            $present = $this.TestDownloadPath()
            if ($present) {
                Write-Verbose " Download folder present!"
            }
            else {
                New-Item -Path $this.DownloadPath -ItemType Directory -Force
            }

            # Set all files:
            ForEach ($file in $downloadFiles.Prereqs) {
                $folder = "$($this.DownloadPath)\$($file.Folder)"
                if (Test-Path -Path "$folder\$($file.File)") {
                    Write-Verbose " $($file.Name) already present!"
                }
                else {
                    Write-Verbose " Creating $($file.Name) folder..."
                    New-Item -Path $folder -ItemType Directory -Force
                    if ($file.URI -like "*/*") {
                        $this.WebClientDownload($file.URI, "$folder\$($file.File)")
                    }
                    else {
                        $this.CopyFromSource("$($PSScriptRoot)\$($file.URI)", "$folder\$($file.File)")
                    }

                    # Workaround for external source scripts from GitHub: change EOL
                    if ($file.Name -eq "CleanupBeforeSysprep" -or $file.Name -eq "RemoveDefaultApps") {
                        $script = Get-Content -Path "$folder\$($file.File)"
                        if ($script -notlike '*`r`n*') {
                            $script.Replace('`n','`r`n')
                            Set-Content -Path "$folder\$($file.File)" -Value $script
                        }
                    }
                    # Download ADK Installers
                    if ($file.Name -eq "ADK" -or $file.Name -eq "WinPE") {
                        Write-Verbose " Download $($file.Name) installers..."
                        Start-Process -FilePath "$folder\$($file.File)" -ArgumentList "/layout $Folder /norestart /quiet /ceip off" -Wait
                    }
                    # Unpack MDT hotfix
                    if ($file.Name -eq "KB4564442") {
                        Expand-Archive -Path "$folder\$($file.File)" -DestinationPath $folder
                    }
                }
            }
        }
        else {
            $this.RemoveDirectory("")
        }
        Write-Verbose "MDT PreReqs set completed!"
    }

    [bool] Test()
    {
        Write-Verbose "Testing MDT PreReqs..."
        $present = $this.TestDownloadPath()
        [hashtable]$DownloadFiles = Get-ConfigurationData -ConfigurationData "$($PSScriptRoot)\cMDTBuildLabPrereqs.psd1"

        if ($this.ensure -eq [Ensure]::Present) {
            Write-Verbose " Testing for download path.."
            if($present) {
                Write-Verbose " Download path found!"
            }
            Else {
                Write-Verbose " Download path not found!"
                return $false
            }

            ForEach ($File in $downloadFiles.Prereqs) {
                 Write-Verbose " Testing for $($File.Name)..."
                 $present = (Test-Path -Path "$($this.DownloadPath)\$($File.Folder)\$($File.File)")
                 Write-Verbose " $present"
                 if(!$Present) {return $false}
            }
        }
        else {
            if ($Present) {$present = $false}
            else {$present = $true}
        }

        Write-Verbose "Test completed!"
        return $present
    }

    [cMDTBuildPreReqs] Get()
    {
        return $this
    }

    [bool] TestDownloadPath()
    {
        $present = $false
        if (Test-Path -Path $this.DownloadPath -ErrorAction Ignore) {
            $present = $true
        }
        return $present
    }

    [void] WebClientDownload($Source,$Target)
    {
        $WebClient = New-Object System.Net.WebClient
        Write-Verbose " Downloading file $($Source)"
        Write-Verbose " Downloading to $($Target)"
        $WebClient.DownloadFile($Source, $Target)
    }

    [void] CopyFromSource($Source,$Target)
    {
        Write-Verbose " Copying $($Target)"
        Copy-Item -Path $Source -Destination $Target
    }

    [void] RemoveDirectory($referencefile = "")
    {
        Remove-Item -Path $this.DownloadPath -Force -Verbose
    }
}

[DscResource()]
class cMDTBuildSelectionProfile
{
    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Name

    [DscProperty()]
    [string]$Comments

    [DscProperty(Mandatory)]
    [string]$IncludePath

    [DscProperty(Mandatory)]
    [string]$PSDriveName

    [DscProperty(Mandatory)]
    [string]$PSDrivePath

    [void] Set()
    {
        if ($this.ensure -eq [Ensure]::Present) {
            $this.ImportSelectionProfile()
        }
        else {
            Invoke-RemovePath -Path "$($this.PSDriveName):\Selection Profiles\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Verbose
        }
    }

    [bool] Test()
    {
        $present = Invoke-TestPath -Path "$($this.PSDriveName):\Selection Profiles\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath
        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildSelectionProfile] Get()
    {
        return $this
    }

    [void] ImportSelectionProfile()
    {
        Import-MDTModule
        New-PSDrive -Name $this.PSDriveName -PSProvider "MDTProvider" -Root $this.PSDrivePath -Verbose:$false
        New-Item -Path "$($this.PSDriveName):\Selection Profiles" -enable "True" -Name $this.Name -Comments $this.Comments -Definition "<SelectionProfile><Include path=`"$($this.IncludePath)`" /></SelectionProfile>" -ReadOnly "False" -Verbose
    }
}

[DscResource()]
class cMDTBuildTaskSequence
{

    [DscProperty()]
    [Ensure]$Ensure = "Present"

    [DscProperty(Key)]
    [string]$Name

    [DscProperty(Key)]
    [string]$Path

    [DscProperty(Mandatory)]
    [string]$OSName

    [DscProperty(Mandatory)]
    [string]$Template

    [DscProperty()]
    [string]$Comments = "Build Reference Image"

    [DscProperty(Mandatory)]
    [string]$ID

    [DscProperty()]
    [string]$Owner = "Windows User"

    [DscProperty(Mandatory)]
    [string]$OrgName

    [DscProperty(Mandatory)]
    [string]$PSDriveName

    [DscProperty(Mandatory)]
    [string]$PSDrivePath

    [void] Set()
    {
        if ($this.ensure -eq [Ensure]::Present) {
            $this.ImportTaskSequence()
        }
        else {
            Invoke-RemovePath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath -Verbose
        }
    }

    [bool] Test()
    {
        $present = Invoke-TestPath -Path "$($this.path)\$($this.name)" -PSDriveName $this.PSDriveName -PSDrivePath $this.PSDrivePath
        if ($this.Ensure -eq [Ensure]::Present) {
            return $present
        }
        else {
            return -not $present
        }
    }

    [cMDTBuildTaskSequence] Get()
    {
        return $this
    }

    [void] ImportTaskSequence()
    {
        Import-MDTModule
        New-PSDrive -Name $this.PSDriveName -PSProvider "MDTProvider" -Root $this.PSDrivePath -Verbose:$false
        Import-MDTTaskSequence -path $this.Path -Name $this.Name -Template $this.Template -Comments $this.Comments -ID $this.ID -Version "1.0" -OperatingSystemPath $this.OSName -FullName $this.Owner -OrgName $this.OrgName -HomePage "about:blank" -Verbose
        # Disable Windows autoupdate
        $UnattendXML = "$($this.PSDrivePath)\control\$($this.ID)\Unattend.xml"
        $unattend = Get-Content -Path $UnattendXML
        $unattend = $unattend.Replace('<ProtectYourPC>1','<ProtectYourPC>3')
        Set-Content -Path $UnattendXML -Value $unattend
    }
}

[DscResource()]
class cMDTBuildTaskSequenceCustomize
{
    # Task Sequence File
    [DscProperty(Key)]
    [string]$TSFile

    # Step name
    [DscProperty(Key)]
    [string]$Name

    # New step name
    [DscProperty()]
    [string]$NewName

    # Step type
    [DscProperty(Mandatory)]
    [string]$Type

    # Group for step
    [DscProperty(Mandatory)]
    [string]$GroupName

    # SubGroup for step
    [DscProperty()]
    [string]$SubGroup

    # Enable/Disable step
    [DscProperty()]
    [string]$Disable

    # Description
    [DscProperty()]
    [string]$Description

    # Add this step after that step
    [DscProperty()]
    [string]$AddAfter

    # TS variable name
    [DscProperty()]
    [string]$TSVarName

    # TS variable value
    [DscProperty()]
    [string]$TSVarValue

    # OS name for OS features
    [DscProperty()]
    [string]$OSName

    # OS features
    [DscProperty()]
    [string]$OSFeatures

    # Command line for 'Run Command line' step
    [DscProperty()]
    [string]$Command

    # Start directory for 'Run Command line' step
    [DscProperty()]
    [string]$StartIn

    # Command line for 'Run PowerShell Script' step
    [DscProperty()]
    [string]$PSCommand

    # Parameters to Pass to PS Script
    [DscProperty()]
    [string]$PSParameters

    # Selection profile for 'Apply Patches' step
    [DscProperty()]
    [string]$SelectionProfile

    [DscProperty(Mandatory)]
    [string]$PSDriveName

    [DscProperty(Mandatory)]
    [string]$PSDrivePath

    [void] Set()
    {
        $TS = $this.LoadTaskSequence()

        # Set node:
        # $group - 1st level
        # $AddGroup - Group to add
        # $Step - Step (or Group) to add
        # $AfterStep - Insert after this step (may be null)
        $group = $TS.sequence.group | Where-Object {$_.Name -eq $this.GroupName}
        if ($this.Type -eq "Group") {
            $step = $group.group | Where-Object {$_.Name -eq $this.Name}
        }
        else {
            $step = $group.step | Where-Object {$_.Name -eq $this.Name}
        }

        if ($this.SubGroup) {
            $AddGroup = $group.group | Where-Object {$_.name -eq $this.SubGroup}
            $AfterStep = $addGroup.step | Where-Object {$_.Name -eq $this.AddAfter}
        }
        else {
            $addGroup = $group
            $AfterStep = $group.step | Where-Object {$_.Name -eq $this.AddAfter}
        }

        if ($step) {
            # Change existing step or group
            if ($this.Disable -ne "") {
                $step.disable = $this.Disable
            }
            if ($this.NewName -ne "") {
                $step.Name = $this.NewName
            }
            if ($this.SelectionProfile -ne "") {
                $step.defaultVarList.variable.'#text' = $this.SelectionProfile
            }
        }
        else {
            # Create new step or group
            if ($this.Type -eq "Group") {
                $newStep = $TS.CreateElement("group")
                $newStep.SetAttribute("expand", "true")
            }
            else {
                $newStep = $TS.CreateElement("step")
            }

            # Set common attributes
            $newStep.SetAttribute("name", $this.Name)
            if ($this.Disable -ne "") {
                $newStep.SetAttribute("disable", $this.Disable)
            }
            else {
                $newStep.SetAttribute("disable", "false")
            }
            $newStep.SetAttribute("continueOnError", "false")
            if ($this.Description -ne "") {
                $newStep.SetAttribute("description", $this.Description)
            }
            else {
                $newStep.SetAttribute("description", "")
            }

            # Create new step
            switch ($this.Type) {
                "Set Task Sequence Variable" {
                    $this.SetTaskSequenceVariable($TS, $newStep)
                }
                "Install Roles and Features" {
                    $this.InstallRolesAndFeatures($TS, $newStep)
                }
                "Install Application" {
                    $this.AddApplication($TS, $newStep)
                }
                "Run Command Line" {
                    $this.RunCommandLine($TS, $newStep)
                }
                "Run PowerShell Script" {
                    $this.RunPowerShellScript($TS, $newStep)
                }
                "Restart Computer" {
                    $this.RestartComputer($TS, $newStep)
                }
            }

            # Insert new step into TS
            if ($AfterStep) {
                $AddGroup.InsertAfter($newStep, $AfterStep) | Out-Null
            }
            else {
                $AddGroup.AppendChild($newStep) | Out-Null
            }
        }

        $TS.Save($this.TSFile)
    }

    [bool] Test()
    {
        $TS = $this.LoadTaskSequence()
        $present = $false

        $group = $TS.sequence.group | Where-Object {$_.Name -eq $this.GroupName}
        if ($this.Type -eq "Group") {
            $step = $group.group | Where-Object {$_.Name -eq $this.Name}
        }
        else {
            $step = $group.step | Where-Object {$_.Name -eq $this.Name}
        }

        if (!$this.AddAfter) {
            if ($step) {
                if ($this.Disable -eq "true") {
                    $present = ($step.disable -eq $this.Disable)
                }
                if (!$present) {
                    if ($this.SelectionProfile -ne "") {
                        $present = ($step.defaultVarList.variable.'#text' -eq $this.SelectionProfile)
                    }
                    if ($step.Name -eq "Set Product Key") {
                        $present = ($step.defaultVarList.variable[1].'#text' -eq $this.TSVarValue)
                    }
                    if ($step.Name -like "Windows Update (*-Application Installation)") {
                        $present = ($step.disable -eq $this.disable)
                    }
                }
            }
            else {
                if ($this.NewName -ne "") {
                    # For rename "Custom Tasks" group only
                    $present = ( ($group.group | Where-Object {$_.Name -eq $this.NewName}) )
                }
                elseif ($this.SubGroup) {
                    $addGroup = $group.group | Where-Object {$_.name -eq $this.SubGroup}
                    $present = ( ($addGroup.step | Where-Object {$_.Name -eq $this.Name}) )
                }
            }
        }
        else {
            if ($this.Type -eq "Group") {
                $present = ( ($group.group | Where-Object {$_.Name -eq $this.Name}) )
            }
            else {
                $AddGroup = $group
                if ($this.SubGroup) {
                    $AddGroup = $group.group | Where-Object {$_.name -eq $this.SubGroup}
                }
                $present = ( ($addGroup.step | Where-Object {$_.Name -eq $this.Name}) )
            }
        }

        return $present
    }

    [cMDTBuildTaskSequenceCustomize] Get()
    {
        return $this
    }

    [xml] LoadTaskSequence()
    {
        $tsPath = $this.TSFile
        $xml = [xml](Get-Content $tsPath)
        return $xml
    }

    [void] SetTaskSequenceVariable($TS, $Step)
    {
        $Step.SetAttribute("type", "SMS_TaskSequence_SetVariableAction")
        $Step.SetAttribute("successCodeList", "0 3010")

        $varList = $TS.CreateElement("defaultVarList")
        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "VariableName")
        $varName.SetAttribute("property", "VariableName")
        $varName.AppendChild($TS.CreateTextNode($this.TSVarName)) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "VariableValue")
        $varName.SetAttribute("property", "VariableValue")
        $varName.AppendChild($TS.CreateTextNode($this.TSVarValue)) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $action = $TS.CreateElement("action")
        $action.AppendChild($TS.CreateTextNode('cscript.exe "%SCRIPTROOT%\ZTISetVariable.wsf"')) | Out-Null

        $Step.AppendChild($varList) | Out-Null
        $Step.AppendChild($action) | Out-Null
    }

    [void] InstallRolesAndFeatures($TS, $Step)
    {
        $OSIndex = @{
            "Windows 7"       = 4
            "Windows 8.1"     = 10
            "Windows 2012 R2" = 11
            "Windows 10"      = 13
            "Windows 2016"    = 14
        }

        $Step.SetAttribute("successCodeList", "0 3010")
        $Step.SetAttribute("type", "BDD_InstallRoles")
        $Step.SetAttribute("runIn", "WinPEandFullOS")

        $varList = $TS.CreateElement("defaultVarList")
        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "OSRoleIndex")
        $varName.SetAttribute("property", "OSRoleIndex")
        $varName.AppendChild($TS.CreateTextNode($OSIndex.$($this.OSName))) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "OSRoles")
        $varName.SetAttribute("property", "OSRoles")
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "OSRoleServices")
        $varName.SetAttribute("property", "OSRoleServices")
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "OSFeatures")
        $varName.SetAttribute("property", "OSFeatures")
        $varName.AppendChild($TS.CreateTextNode($this.OSFeatures)) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $action = $TS.CreateElement("action")
        $action.AppendChild($TS.CreateTextNode('cscript.exe "%SCRIPTROOT%\ZTIOSRole.wsf"')) | Out-Null

        $Step.AppendChild($varList) | Out-Null
        $Step.AppendChild($action) | Out-Null
    }

    [void] AddApplication($TS, $Step)
    {
        $Step.SetAttribute("successCodeList", "0 3010")
        $Step.SetAttribute("type", "BDD_InstallApplication")
        $Step.SetAttribute("runIn", "WinPEandFullOS")

        $varList = $TS.CreateElement("defaultVarList")
        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "ApplicationGUID")
        $varName.SetAttribute("property", "ApplicationGUID")

        # Get Application GUID
        Import-MDTModule
        New-PSDrive -Name $this.PSDriveName -PSProvider "MDTProvider" -Root $this.PSDrivePath -Verbose:$false | Out-Null
        $App = Get-ChildItem -Path "$($this.PSDriveName):\Applications" -Recurse | Where-Object { $_.Name -eq  $this.Name }

        $varName.AppendChild($TS.CreateTextNode($($App.guid))) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "ApplicationSuccessCodes")
        $varName.SetAttribute("property", "ApplicationSuccessCodes")
        $varName.AppendChild($TS.CreateTextNode("0 3010")) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $action = $TS.CreateElement("action")
        $action.AppendChild($TS.CreateTextNode('cscript.exe "%SCRIPTROOT%\ZTIApplications.wsf"')) | Out-Null

        $Step.AppendChild($varList) | Out-Null
        $Step.AppendChild($action) | Out-Null
    }

    [void] RunCommandLine($TS, $Step)
    {
        $Step.SetAttribute("startIn", $this.StartIn)
        $Step.SetAttribute("successCodeList", "0 3010")
        $Step.SetAttribute("type", "SMS_TaskSequence_RunCommandLineAction")
        $Step.SetAttribute("runIn", "WinPEandFullOS")

        $varList = $TS.CreateElement("defaultVarList")
        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "PackageID")
        $varName.SetAttribute("property", "PackageID")
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "RunAsUser")
        $varName.SetAttribute("property", "RunAsUser")
        $varName.AppendChild($TS.CreateTextNode("false")) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "SMSTSRunCommandLineUserName")
        $varName.SetAttribute("property", "SMSTSRunCommandLineUserName")
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "SMSTSRunCommandLineUserPassword")
        $varName.SetAttribute("property", "SMSTSRunCommandLineUserPassword")
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "LoadProfile")
        $varName.SetAttribute("property", "LoadProfile")
        $varName.AppendChild($TS.CreateTextNode("false")) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $action = $TS.CreateElement("action")
        $action.AppendChild($TS.CreateTextNode($this.Command)) | Out-Null

        $Step.AppendChild($varList) | Out-Null
        $Step.AppendChild($action) | Out-Null
    }

    [void] RunPowerShellScript($TS, $Step)
    {
        $Step.SetAttribute("successCodeList", "0 3010")
        $Step.SetAttribute("type", "BDD_RunPowerShellAction")

        $varList = $TS.CreateElement("defaultVarList")
        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "ScriptName")
        $varName.SetAttribute("property", "ScriptName")
        $varName.AppendChild($TS.CreateTextNode($this.PSCommand)) | Out-Null

        $varList.AppendChild($varName) | Out-Null
        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "Parameters")
        $varName.SetAttribute("property", "Parameters")
        $varName.AppendChild($TS.CreateTextNode($this.PSParameters)) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "PackageID")
        $varName.SetAttribute("property", "PackageID")
        $varList.AppendChild($varName) | Out-Null

        $action = $TS.CreateElement("action")
        $action.AppendChild($TS.CreateTextNode('cscript.exe "%SCRIPTROOT%\ZTIPowerShell.wsf"')) | Out-Null

        $Step.AppendChild($varList) | Out-Null
        $Step.AppendChild($action) | Out-Null
    }

    [void] RestartComputer($TS, $Step)
    {
        $Step.SetAttribute("successCodeList", "0 3010")
        $Step.SetAttribute("type", "SMS_TaskSequence_RebootAction")
        $Step.SetAttribute("runIn", "WinPEandFullOS")

        $varList = $TS.CreateElement("defaultVarList")
        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "Message")
        $varName.SetAttribute("property", "Message")
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "MessageTimeout")
        $varName.SetAttribute("property", "MessageTimeout")
        $varName.AppendChild($TS.CreateTextNode("60")) | Out-Null
        $varList.AppendChild($varName) | Out-Null

        $varName = $TS.CreateElement("variable")
        $varName.SetAttribute("name", "Target")
        $varName.SetAttribute("property", "Target")
        $varList.AppendChild($varName) | Out-Null

        $action = $TS.CreateElement("action")
        $action.AppendChild($TS.CreateTextNode("smsboot.exe /target:WinPE")) | Out-Null

        $Step.AppendChild($varList) | Out-Null
        $Step.AppendChild($action) | Out-Null
    }
}

[DscResource()]
class cMDTBuildUpdateBootImage
{
    [DscProperty(Key)]
    [string]$Version

    [DscProperty(Key)]
    [string]$PSDeploymentShare

    [DscProperty(Mandatory)]
    [string]$PSDrivePath

    [DscProperty()]
    [string]$ExtraDirectory

    [DscProperty()]
    [string]$BackgroundFile

    [DscProperty()]
    [string]$LiteTouchWIMDescription

    [DscProperty()]
    [bool]$CreateISOx64 = $false

    [void] Set()
    {
        $this.UpdateBootImage()
    }

    [bool] Test()
    {
        Return ($this.VerifyVersion())
    }

    [cMDTBuildUpdateBootImage] Get()
    {
        return $this
    }

    [bool] VerifyVersion()
    {
        [bool]$match = $false

        if ((Get-Content -Path "$($this.PSDrivePath)\Boot\CurrentBootImage.version" -ErrorAction Ignore) -eq $this.Version) {
            $match = $true
        }
        return $match
    }

    [void] UpdateBootImage()
    {
        Import-MDTModule
        New-PSDrive -Name $this.PSDeploymentShare -PSProvider "MDTProvider" -Root $this.PSDrivePath -Verbose:$false

        If ([string]::IsNullOrEmpty($($this.ExtraDirectory))) {
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.ExtraDirectory -Value ""
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.ExtraDirectory -Value ""
        }
        ElseIf (Invoke-TestPath -Path "$($this.PSDrivePath)\$($this.ExtraDirectory)") {
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.ExtraDirectory -Value "$($this.PSDrivePath)\$($this.ExtraDirectory)"
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.ExtraDirectory -Value "$($this.PSDrivePath)\$($this.ExtraDirectory)"
        }

        If ([string]::IsNullOrEmpty($($this.BackgroundFile))) {
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.BackgroundFile -Value ""
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.BackgroundFile -Value ""
        }
        ElseIf (Invoke-TestPath -Path "$($this.PSDrivePath)\$($this.BackgroundFile)") {
             Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.BackgroundFile -Value "$($this.PSDrivePath)\$($this.BackgroundFile)"
             Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.BackgroundFile -Value "$($this.PSDrivePath)\$($this.BackgroundFile)"
        }
        Else {
             Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.BackgroundFile -Value $this.BackgroundFile
             Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.BackgroundFile -Value $this.BackgroundFile
        }

        If ($this.LiteTouchWIMDescription) {
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.LiteTouchWIMDescription -Value "$($this.LiteTouchWIMDescription) x64 $($this.Version)"
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.LiteTouchWIMDescription -Value "$($this.LiteTouchWIMDescription) x86 $($this.Version)"
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.LiteTouchISOName -Value "$($this.LiteTouchWIMDescription)_x64.iso".Replace(' ','_')
            Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.LiteTouchISOName -Value "$($this.LiteTouchWIMDescription)_x86.iso".Replace(' ','_')
        }

        Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.SelectionProfile -Value "Nothing"
        Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.SelectionProfile -Value "Nothing"

        # for offline remove Windows default apps
        # Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.FeaturePacks -Value "winpe-dismcmdlets,winpe-mdac,winpe-netfx,winpe-powershell,winpe-storagewmi"

        Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x64.GenerateLiteTouchISO -Value $this.CreateISOx64
        Set-ItemProperty "$($this.PSDeploymentShare):" -Name Boot.x86.GenerateLiteTouchISO -Value $true

        #The Update-MDTDeploymentShare command crashes WMI when run from inside DSC. This section is a work around.
        workflow Update-DeploymentShare {
            param (
                [string]$PSDeploymentShare,
                [string]$PSDrivePath
            )
            InlineScript {
                Import-MDTModule
                New-PSDrive -Name $Using:PSDeploymentShare -PSProvider "MDTProvider" -Root $Using:PSDrivePath -Verbose:$false
                Update-MDTDeploymentShare -Path "$($Using:PSDeploymentShare):" -Force:$true -Compress:$true
            }
        }
        Update-DeploymentShare $this.PSDeploymentShare $this.PSDrivePath
        Set-Content -Path "$($this.PSDrivePath)\Boot\CurrentBootImage.version" -Value "$($this.Version)"
    }
}

Function Get-ConfigurationData
{
    [CmdletBinding()]
    [OutputType([hashtable])]
    Param (
        [Parameter(Mandatory)]
        [Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformation()]
        [hashtable] $ConfigurationData
    )
    return $ConfigurationData
}

Function Import-MDTModule
{
    If ( -Not (Get-Module MicrosoftDeploymentToolkit) ) {
        Import-Module "$env:ProgramFiles\Microsoft Deployment Toolkit\Bin\MicrosoftDeploymentToolkit.psd1" -ErrorAction Stop -Global -Verbose:$False
    }
}

Function Invoke-ExpandArchive
{
    [cmdletbinding(SupportsShouldProcess=$True,ConfirmImpact="Low")]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory=$True)]
        [ValidateNotNullorEmpty()]
        [string]$Source,
        [Parameter(Mandatory=$True)]
        [ValidateNotNullorEmpty()]
        [string]$Target
    )

    [bool]$Verbosity
    If ($PSBoundParameters.Verbose) { $Verbosity = $True }
    Else { $Verbosity = $False }

    Write-Verbose "Expanding archive $($Source) to $($Target)"
    Expand-Archive $Source -DestinationPath $Target -Force -Verbose:$Verbosity
}

Function Invoke-RemovePath
{
    [cmdletbinding(SupportsShouldProcess=$True,ConfirmImpact="Low")]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory=$True)]
        [ValidateNotNullorEmpty()]
        [string]$Path,
        [Parameter()]
        [string]$PSDriveName,
        [Parameter()]
        [string]$PSDrivePath
    )

    [bool]$Verbosity
    If ($PSBoundParameters.Verbose) { $Verbosity = $True }
    Else { $Verbosity = $False }

    if (($PSDrivePath) -and ($PSDriveName)) {
        Import-MDTModule
        New-PSDrive -Name $PSDriveName -PSProvider "MDTProvider" -Root $PSDrivePath -Verbose:$False | `
        Remove-Item -Path "$($Path)" -Force -Verbose:$Verbosity
    }
    else {
        Remove-Item -Path "$($Path)" -Force -Verbose:$Verbosity
    }
}

Function Invoke-TestPath
{
    [cmdletbinding(SupportsShouldProcess=$True,ConfirmImpact="Low")]
    [OutputType([bool])]
    param(
        [Parameter(Mandatory=$True)]
        [ValidateNotNullorEmpty()]
        [string]$Path,
        [Parameter()]
        [string]$PSDriveName,
        [Parameter()]
        [string]$PSDrivePath
    )

    [bool]$present = $false

    if (($PSDrivePath) -and ($PSDriveName)) {
        Import-MDTModule
        if (New-PSDrive -Name $PSDriveName -PSProvider "MDTProvider" -Root $PSDrivePath -Verbose:$false | `
            Test-Path -Path "$($Path)" -ErrorAction Ignore) {
            $present = $true
        }
    }
    else {
        if (Test-Path -Path "$($Path)" -ErrorAction Ignore) {
            $present = $true
        }
    }
    return $present
}