Image-Factory.ps1

<#PSScriptInfo
 
.VERSION 2.9
 
.GUID 251ae35c-cc4e-417c-970c-848b221477fa
 
.AUTHOR Mike Galvin twitter.com/mikegalvin_
 
.COMPANYNAME Mike Galvin
 
.COPYRIGHT (C) Mike Galvin. All rights reserved.
 
.TAGS Microsoft Deployment Toolkit MDT Hyper-V Windows OSD
 
.LICENSEURI
 
.PROJECTURI https://gal.vin/2017/08/26/image-factory
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES Microsoft Deployment Toolkit PowerShell Modules Hyper-V Management PowerShell Modules
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
#>


<#
    .SYNOPSIS
    Automates the creation of WIM files for Windows deployment.
 
    .DESCRIPTION
    Automates the creation of WIM files for Windows deployment.
 
    This script will:
     
    Create disposable Hyper-V virtual machines to generate WIM files from Microsoft Deployment Toolkit task sequences.
 
    The process is as follows:
 
    Create a Hyper-V Virtual Machine.
    Boot it from the MDT LiteTouch boot media.
    Run the specified Task Sequence.
    Capture the .wim file to MDT.
    Destroy the Virtual Machine and VHD used.
    Move on to the next specified task sequence.
    Do the previous steps for all configured task sequences.
    Import the .wim files into the deployment share of MDT.
    Remove the captured .wim files from the capture folder.
    Optionally create a log file and email it to an address of your choice.
 
    Please note: to send a log file using ssl and an SMTP password you must generate an encrypted
    password file. The password file is unique to both the user and machine.
     
    The command is as follows:
 
    $creds = Get-Credential
    $creds.Password | ConvertFrom-SecureString | Set-Content c:\foo\ps-script-pwd.txt
     
    .PARAMETER Build
    The local or UNC path to the build share of MDT. Both this and the deploy switch can point to the same location.
 
    .PARAMETER Deploy
    The local or UNC path to the deploy share of MDT. Both this and the build switch can point to the same location.
 
    .PARAMETER TS
    The comma-separated list of task sequence ID's to build.
 
    .PARAMETER VH
    The name of the computer running Hyper-V. Can be local or remote.
 
    .PARAMETER VHD
    The path relative to the Hyper-V server of where to store the VHD file for the VM(s).
 
    .PARAMETER Boot
    The path relative to the Hyper-V server of where the ISO file to boot from is stored.
 
    .PARAMETER VNic
    The name of the virtual switch that the VM should use to communicate with the network.
 
    .PARAMETER Compat
    Set if the Hyper-V server is WS2012 R2 and the script is running on Windows 10 or Windows Server 2016.
    This loads the older version of the Hyper-V module so it is able to manage WS2012 R2 Hyper-V VMs.
 
    .PARAMETER Remote
    Set if the Hyper-V server is a remote device.
    Do not include this switch if the script is running on the same device as Hyper-V.
     
    .PARAMETER L
    The path to output the log file to.
    The file name will be Image-Factory-YYYY-MM-dd-HH-mm-ss.log
 
    .PARAMETER Subject
    The email subject that the email should have. Encapulate with single or double quotes.
 
    .PARAMETER SendTo
    The e-mail address the log should be sent to.
 
    .PARAMETER From
    The from address the log should be sent from.
 
    .PARAMETER Smtp
    The DNS name or IP address of the SMTP server.
 
    .PARAMETER User
    The user account to connect to the SMTP server.
 
    .PARAMETER Pwd
    The password for the user account.
 
    .PARAMETER UseSsl
    Connect to the SMTP server using SSL.
 
    .EXAMPLE
    Image-Factory.ps1 -Build \\mdt01\BuildShare$ -Deploy \\mdt01\DeploymentShare$ -VH hyperv01 -VHD C:\Hyper-V\VHD
    -Boot C:\iso\LiteTouchPE_x64.iso -VNic vSwitch-Ext -Remote -TS W10-1803,WS16-S -L C:\scripts\logs
    -Subject 'Server: Image Factory' -SendTo me@contoso.com -From hyperv@contoso.com -Smtp smtp.outlook.com -User user -Pwd C:\foo\pwd.txt -UseSsl
 
    This string will build a WIM file from each of the task sequences; W10-1803 & WS16-S. They will be imported to the deployment share on MDT01.
    The Hyper-V server used will be HYPERV01, the VHD for the VMs generated will be stored in C:\Hyper-V\VHD on the server HYPERV01.
    The boot iso file will be C:\iso\LiteTouchPE_x64.iso, located on the Hyper-V server. The Virtual Switch used by the VM will be called vSwitch-Ext.
    The log file will be output to C:\scripts\logs and it will be e-mailed with a custom subject line, using an SSL conection.
#>


## Set up command line switches and what variables they map to.
[CmdletBinding()]
Param(
    [parameter(Mandatory=$True)]
    [alias("Build")]
    $MdtBuildPath,
    [parameter(Mandatory=$True)]
    [alias("Deploy")]
    $MdtDeployPath,
    [parameter(Mandatory=$True)]
    [alias("TS")]
    $TsId,
    [parameter(Mandatory=$True)]
    [alias("VH")]
    $VmHost,
    [parameter(Mandatory=$True)]
    [alias("VHD")]
    $VhdPath,
    [parameter(Mandatory=$True)]
    [alias("Boot")]
    $BootMedia,
    [parameter(Mandatory=$True)]
    [alias("VNic")]
    $VmNic,
    [alias("L")]
    $LogPath,
    [alias("Subject")]
    $MailSubject,
    [alias("SendTo")]
    $MailTo,
    [alias("From")]
    $MailFrom,
    [alias("Smtp")]
    $SmtpServer,
    [alias("User")]
    $SmtpUser,
    [alias("Pwd")]
    $SmtpPwd,
    [switch]$UseSsl,
    [switch]$Compat,
    [switch]$Remote)

## If logging is configured, start the log file.
If ($LogPath)
{
    $LogFile = ("Image-Factory-{0:yyyy-MM-dd-HH-mm-ss}.log" -f (Get-Date))
    $Log = "$LogPath\$LogFile"

    ## If the log file already exists, clear it.
    $LogT = Test-Path -Path $Log

    If ($LogT)
    {
        Clear-Content -Path $Log
    }

    Add-Content -Path $Log -Value "****************************************"
    Add-Content -Path $Log -Value "$(Get-Date -Format G) Log started"
    Add-Content -Path $Log -Value ""
}

## If compat is configured, load the older Hyper-V PS module.
If ($Compat) 
{
    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Importing Hyper-V 1.1 PowerShell Module"
    }
    
    Write-Host "$(Get-Date -Format G) Importing Hyper-V 1.1 PowerShell Module"
    Import-Module $env:windir\System32\WindowsPowerShell\v1.0\Modules\Hyper-V\1.1\Hyper-V.psd1
}

## Import MDT PS module.
If ($LogPath)
{
    Add-Content -Path $Log -Value "$(Get-Date -Format G) Importing MDT PowerShell Module"
}

Write-Host "$(Get-Date -Format G) Importing MDT PowerShell Module"
Import-Module "$env:programfiles\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1"

## For each of the Task Sequence ID's configured, run the build process.
ForEach ($Id in $TsId)
{
    ## Test to see if the build environment is dirty, if it is exit the script.
    $EnvDirtyTest = Test-Path -Path $MdtBuildPath\Control\CustomSettings-backup.ini
    If ($EnvDirtyTest)
    {
        Write-Host "$(Get-Date -Format G) CustomSettings-backup.ini already exists."
        Write-Host "$(Get-Date -Format G) The build environment is dirty."
        Write-Host "$(Get-Date -Format G) Did the script finish successfully last time it was run?"

        If ($LogPath)
        {
            Add-Content -Path $Log -Value "$(Get-Date -Format G) CustomSettings-backup.ini already exists."
            Add-Content -Path $Log -Value "The build environment is dirty."
            Add-Content -Path $Log -Value "Did the script finish successfully last time it was run?"
        }

        Exit
    }
    
    If ($LogPath)
    {
        Add-Content -Path $Log -Value ""
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Starting process for $Id"
        Add-Content -Path $Log -Value ""
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Backing up current MDT CustomSettings.ini"
    }
    
    Write-Host ""
    Write-Host "$(Get-Date -Format G) Starting process for $Id"
    Write-Host ""
    Write-Host "$(Get-Date -Format G) Backing up current MDT CustomSettings.ini"

    ## Backup the exisiting CustomSettings.ini.
    Copy-Item $MdtBuildPath\Control\CustomSettings.ini $MdtBuildPath\Control\CustomSettings-backup.ini
    Start-Sleep -s 5

    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Setting up MDT CustomSettings.ini for Task Sequence ID: $Id"
    }

    Write-Host "$(Get-Date -Format G) Setting MDT CustomSettings.ini for Task Sequence ID: $Id"

    ## Setup MDT CustomSettings.ini for auto deploy.
    Add-Content $MdtBuildPath\Control\CustomSettings.ini ""
    Add-Content $MdtBuildPath\Control\CustomSettings.ini ""
    Add-Content $MdtBuildPath\Control\CustomSettings.ini "TaskSequenceID=$Id"
    Add-Content $MdtBuildPath\Control\CustomSettings.ini "SkipTaskSequence=YES"
    Add-Content $MdtBuildPath\Control\CustomSettings.ini "SkipComputerName=YES"

    ## Set the VM name in Hyper-V.
    $VmName = ("build-{0:yyyy-MM-dd-HH-mm-ss}" -f (Get-Date))

    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Creating VM: $VmName on $VmHost"
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Adding VHD: $VhdPath\$VmName.vhdx"
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Adding Virtual NIC: $VmNic"
    }

    Write-Host "$(Get-Date -Format G) Creating VM: $VmName on $VmHost"
    Write-Host "$(Get-Date -Format G) Adding VHD: $VhdPath\$VmName.vhdx"
    Write-Host "$(Get-Date -Format G) Adding Virtual NIC: $VmNic"

    ## Create the VM.
    New-VM -name $VmName -MemoryStartupBytes 4096MB -BootDevice CD -Generation 1 -NewVHDPath $VhdPath\$VmName.vhdx -NewVHDSizeBytes 130048MB -SwitchName $VmNic -ComputerName $VmHost

    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Configuring VM Processor Count"
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Configuring VM Static Memory"
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Configuring VM to boot from $BootMedia"
    }

    Write-Host "$(Get-Date -Format G) Configuring VM Processor Count"
    Write-Host "$(Get-Date -Format G) Configuring VM Static Memory"
    Write-Host "$(Get-Date -Format G) Configuring VM to boot from $BootMedia"

    ## Configure the VM.
    Set-VM $VmName -ProcessorCount 2 -StaticMemory -AutomaticCheckpointsEnabled $false -ComputerName $VmHost
    Set-VMDvdDrive -VMName $VmName -ControllerNumber 1 -ControllerLocation 0 -Path $BootMedia -ComputerName $VmHost

    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Starting $VmName on $VmHost with $Id"
    }

    Write-Host "$(Get-Date -Format G) Starting $VmName on $VmHost with $Id"

    ## Start the VM.
    Start-VM $VmName -ComputerName $VmHost

    ## Wait for VM to shutdown.
    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Waiting for $VmName to build $Id"
    }

    Write-Host "$(Get-Date -Format G) Waiting for $VmName to build $Id"

    While ((Get-VM -Name $VmName -ComputerName $VmHost).state -ne 'Off') {Start-Sleep -s 10}

    ## Remove VMs VHD if Remote option is set or not.
    If ($Remote)
    {
        $VmBye = Get-VM -Name $VmName -ComputerName $VmHost
        $Disks = Get-VHD -VMId $VmBye.Id -ComputerName $VmHost

        If ($LogPath)
        {
            Add-Content -Path $Log -Value "$(Get-Date -Format G) Deleting $VmName on $VmHost"
        }

        Write-Host "$(Get-Date -Format G) Deleting $VmName on $VmHost"

        Invoke-Command {Remove-Item $using:disks.path -Force} -ComputerName $VmBye.ComputerName
        Start-Sleep -s 5
    }

    Else
    {
        $VmLocal = Get-VM -Name $VmName -ComputerName $VmHost

        If ($LogPath)
        {
            Add-Content -Path $Log -Value "$(Get-Date -Format G) Deleting $VmName on $VmHost"
        }

        Write-Host "$(Get-Date -Format G) Deleting $VmName on $VmHost"

        Remove-Item $VmLocal.HardDrives.Path -Force
        Start-Sleep -s 5
    }

    ## Remove VM.
    Remove-VM $VmName -ComputerName $VmHost -Force

    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Restoring MDT CustomSettings.ini from backup"
    }

    Write-Host "$(Get-Date -Format G) Restoring MDT CustomSettings.ini from backup"

    ## Restore the CustomSettings.ini file from the backup.
    Remove-Item $MdtBuildPath\Control\CustomSettings.ini
    Move-Item $MdtBuildPath\Control\CustomSettings-backup.ini $MdtBuildPath\Control\CustomSettings.ini
    Start-Sleep -s 5

    If ($LogPath)
    {
        Add-Content -Path $Log -Value ""
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Finished process for $Id"
        Add-Content -Path $Log -Value ""
    }

    Write-Host "$(Get-Date -Format G) Finished process for $Id"
}

## Connect to MDT.
If ($LogPath)
{
    Add-Content -Path $Log -Value "$(Get-Date -Format G) Creating PSDrive to $MdtDeployPath"
}

Write-Host "$(Get-Date -Format G) Creating PSDrive to $MdtDeployPath"

## Create PSDrive to the MDT deploy path.
New-PSDrive -Name "DS002" -PSProvider MDTProvider -Root $MdtDeployPath

## Get the WIM files and store them in a variable.
$Wims = Get-ChildItem $MdtBuildPath\Captures\*.wim

## For each of the WIMs, import them into MDT.
ForEach ($File in $Wims)
{
    If ($LogPath)
    {
        Add-Content -Path $Log -Value "$(Get-Date -Format G) Importing WIM File: $File"
    }

    Write-Host "$(Get-Date -Format G) Importing WIM File: $File"

    Import-MDTOperatingSystem -path "DS002:\Operating Systems" -SourceFile $File -DestinationFolder $File.Name
}

If ($LogPath)
{
    Add-Content -Path $Log -Value "$(Get-Date -Format G) Removing captured WIM files"
}

Write-Host "$(Get-Date -Format G) Removing captured WIM files"

## Remove captured WIMs.
Remove-Item $MdtBuildPath\Captures\*.wim

## If log was configured stop the log.
If ($LogPath)
{
    Add-Content -Path $Log -Value ""
    Add-Content -Path $Log -Value "$(Get-Date -Format G) Log finished"
    Add-Content -Path $Log -Value "****************************************"

    ## If email was configured, set the variables for the email subject and body.
    If ($SmtpServer)
    {
        # If no subject is set, use the string below.
        If ($Null -eq $MailSubject)
        {
            $MailSubject = "Image Factory Log"
        }

        $MailBody = Get-Content -Path $Log | Out-String

        ## If an email password was configured, create a variable with the username and password.
        If ($SmtpPwd)
        {
            $SmtpPwdEncrypt = Get-Content $SmtpPwd | ConvertTo-SecureString
            $SmtpCreds = New-Object System.Management.Automation.PSCredential -ArgumentList ($SmtpUser, $SmtpPwdEncrypt)

            ## If ssl was configured, send the email with ssl.
            If ($UseSsl)
            {
                Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer -UseSsl -Credential $SmtpCreds
            }

            ## If ssl wasn't configured, send the email without ssl.
            Else
            {
                Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer -Credential $SmtpCreds
            }
        }

        ## If an email username and password were not configured, send the email without authentication.
        Else
        {
            Send-MailMessage -To $MailTo -From $MailFrom -Subject $MailSubject -Body $MailBody -SmtpServer $SmtpServer
        }
    }
}

## End