Skip to content

Instantly share code, notes, and snippets.

@altrive
Last active September 27, 2022 16:07
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save altrive/5162926 to your computer and use it in GitHub Desktop.
Save altrive/5162926 to your computer and use it in GitHub Desktop.
PowerShell script to create OS Base VHD for Windows 8/Windows Server2012

Summary

PowerShell script to create OS Base VHD for Windows 8/Windows Server2012 Hyper-V.

Note

Applying WIM image and offline patch operation take long times. Please check VirusScan engine don't waste power of CPU or DiskIO.

It is recommend to exclude policy DISM exectable(DISM.exe/DismHost.exe), and work directory. In case of WindowsDefender, it's about 2x faster if exclude policy setted.

Usage

Create OS VHD from template

#Requires Version 3
#Requires Modules PShould

param(
    [int]$index
)

Import-Module PShould
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$params=[ordered]@{
   VhdPath      = ""
   OsImageName  = ""
   MediaPath    = ""
   VhdConfig    =  [ordered]@{
       VhdSize  = 32GB           #Default: 127GB (Windows Server2012 Minimum:32GB)
       VhdType  = "Dynamic"      #Default: Dynamic
       VhdBlockSize = 2MB        #Default: 2 MB - Max 256MB (Fixed VHD always 0MB? parameter ignored)
       PartitionStyle = "MBR"    #Hyper-V Only MBR Support?
       AllocationUnitSize = 64KB  #Default: 4KB (Max 64KB)
       CreateReservedPartition = $true
   }
   DismConfig = [ordered]@{
       Culture ="ja-JP"
       PackagePath="" 
       UnAttendXmlPath = ""
       Source = ""
       ProductKey=""
       FeatureName=""
   }
   OptimizeVHD = $false
   ReadOnly = $true
}

$osList = [ordered]@{
    1 = "Windows 8 Enterprise Evaluate"
    2 = "Windows Server 2012 Standard"
    3 = "Windows Server 2012 Standard ServerCore(MinGUI)"
}

if($index -eq 0)
{
    Write-Host "Select OS index from following list..."
    Write-Host "-----------------------------------------------"
    foreach($os in $osList.GetEnumerator())
    {
        Write-Host ("`t{0}: {1}" -f $os.Name,$os.Value)
    }
    Write-Host "-----------------------------------------------"
    $index = [int](Read-Host -Prompt "OS Index")
}

$osName = $osList.Item([object]$index)

switch($index)
{
    1 {
        Write-Host "Selected OS: $osName"  #Windows 8 Enterprise Evaluate
        $params.OsImageName ="Windows 8 Enterprise Evaluation"
        $params.VhdPath     = Join-Path (Get-VMHost).VirtualHardDiskPath  "Windows8_Enterprise.vhdx"
        $params.MediaPath   = "D:\Shared\Images\9200.16384.WIN8_RTM.120725-1247_X64FRE_ENTERPRISE_EVAL_JA-JP-HRM_CENA_X64FREE_JA-JP_DV5.ISO"
        $params.DismConfig.PackagePath     = "\\172.16.0.1\Shared\Images\Windows8\WindowsUpdate"
        $params.DismConfig.UnAttendXmlPath = "\\172.16.0.1\Shared\Images\UnattendXml\Unattend_Win8.xml"
    }
    2{ 
        Write-Host "Selected OS: $osName" # Windows Server 2012 Standard
        $params.OsImageName ="Windows Server 2012 SERVERSTANDARD"
        $params.VhdPath     =  Join-Path (Get-VMHost).VirtualHardDiskPath  "WinSvr2012_Std.vhdx"
        $params.MediaPath   = "D:\Shared\Images\9200.16384.WIN8_RTM.120725-1247_X64FRE_SERVER_EVAL_JA-JP-HRM_SSS_X64FREE_JA-JP_DV5.iso"
        $params.DismConfig.PackagePath     = "\\172.16.0.1\Shared\Images\WindowsServer2012\WindowsUpdate"
        $params.DismConfig.UnAttendXmlPath = "\\172.16.0.1\Shared\Images\UnattendXml\Unattend_WinSvr2012.xml"
    }
    3{
        Write-Host "Selected OS: $osName" #Windows Server 2012 Standard ServerCore(MinGUI)
        $params.OsImageName ="Windows Server 2012 SERVERSTANDARDCORE"
        $params.VhdPath     = Join-Path (Get-VMHost).VirtualHardDiskPath  "WinSvr2012_Std_MinGUI.vhdx"
        $params.MediaPath   = "D:\Shared\Images\9200.16384.WIN8_RTM.120725-1247_X64FRE_SERVER_EVAL_JA-JP-HRM_SSS_X64FREE_JA-JP_DV5.iso"
        $params.DismConfig.PackagePath     = "\\172.16.0.1\Shared\Images\WindowsServer2012\WindowsUpdate"
        $params.DismConfig.UnAttendXmlPath = "\\172.16.0.1\Shared\Images\UnattendXml\Unattend_WinSvr2012.xml"
        $params.DismConfig.Source          = "\\172.16.0.1\Shared\Images\WindowsServer2012\WinSxs"
        $params.DismConfig.FeatureName     = "Server-Gui-Mgmt"
    }
    default{
        throw "Not Supported OS Selected!"
    }
}

#region parameter assertion
$params.MediaPath                  | should exist
$params.DismConfig.UnAttendXmlPath | should exist
if(![String]::IsNullOrEmpty($params.DismConfig.PackagePath)){
    $params.DismConfig.PackagePath | should exist
}
if(![String]::IsNullOrEmpty($params.DismConfig.Source)){
    $params.DismConfig.Source | should exist
}
#endregion 

$sw = [Diagnostics.Stopwatch]::StartNew()
New-OSBaseImage @params -Verbose -Debug -Force
Write-Host ("Elapsed {0} [minutes]." -f $sw.Elapsed.TotalMinutes)

Unattend.xml for Windows Server 2012

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="generalize">
        <component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SkipRearm>1</SkipRearm>
        </component>
    </settings>
    <settings pass="specialize">
		<component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SkipAutoActivation>true</SkipAutoActivation>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComputerName>*</ComputerName>
        </component>
        <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Identification>
                <JoinWorkgroup>WORKGROUP</JoinWorkgroup>
            </Identification>
        </component>
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ExtendOSPartition>
                <Extend>true</Extend>
            </ExtendOSPartition>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 			<AutoLogon>
                <Enabled>true</Enabled>
                <LogonCount>1</LogonCount>
                <Username>Administrator</Username>
				<Password>
					<Value>Password!</Value>
                    <PlainText>true</PlainText>
				</Password>
            </AutoLogon>
            <UserAccounts>
                <AdministratorPassword>
                    <Value>Password!</Value>
                    <PlainText>true</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <TimeZone>Tokyo Standard Time</TimeZone>
            <OOBE>
				<ProtectYourPC>1</ProtectYourPC>
                <HideEULAPage>true</HideEULAPage>
                <SkipUserOOBE>true</SkipUserOOBE>
            </OOBE>
        </component>
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>0411:00000411</InputLocale>
            <SystemLocale>ja-JP</SystemLocale>
            <UILanguage>ja-JP</UILanguage>
            <UserLocale>ja-JP</UserLocale>
        </component>
    </settings>
</unattend>

Unattend.xml for Windows 8

*Note: WinRM for local administrator enabled *

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="generalize">
        <component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SkipRearm>1</SkipRearm>
        </component>
    </settings>
    <settings pass="specialize">
		<component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SkipAutoActivation>true</SkipAutoActivation>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ComputerName>*</ComputerName>
        </component>
        <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Identification>
                <JoinWorkgroup>WORKGROUP</JoinWorkgroup>
            </Identification>
        </component>
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <ExtendOSPartition>
                <Extend>true</Extend>
            </ExtendOSPartition> 
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
			<AutoLogon>
                <Enabled>true</Enabled>
                <LogonCount>1</LogonCount>
                <Username>Administrator</Username>
				<Password>
					<Value>Password!</Value>
                    <PlainText>true</PlainText>
				</Password>
            </AutoLogon>
			<UserAccounts>
				<LocalAccounts>
                    <LocalAccount wcm:action="add">
                        <Group>Administrators</Group>
                        <Name>Administrator</Name>
                    </LocalAccount>
                </LocalAccounts>
				<AdministratorPassword>
                    <Value>Password!</Value>
                    <PlainText>true</PlainText>
                </AdministratorPassword>
            </UserAccounts>
            <TimeZone>Tokyo Standard Time</TimeZone>
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
				<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
				<NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>1</ProtectYourPC>
                <SkipUserOOBE>true</SkipUserOOBE>
				<HideLocalAccountScreen>true</HideLocalAccountScreen>
                <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
                <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
            </OOBE>
			<FirstLogonCommands>
				<SynchronousCommand wcm:action="add">
					<Order>1</Order>
                    <CommandLine>powershell -Command &quot;Enable-PSRemoting -Force&quot;</CommandLine>
                    <RequiresUserInput>false</RequiresUserInput>
			   </SynchronousCommand>
			</FirstLogonCommands>
        </component>
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>0411:00000411</InputLocale>
            <SystemLocale>ja-JP</SystemLocale>
            <UILanguage>ja-JP</UILanguage>
            <UserLocale>ja-JP</UserLocale>
        </component>
    </settings>
</unattend>
#Create OS BaseImage VHD
function New-OSBaseImage
{
[Cmdletbinding()]
param(
$VhdPath,
$OSImageName,
$MediaPath,
$VhdConfig,
$DismConfig,
$ReadOnly,
$OptimizeVHD,
[switch] $Force
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
#Delete VHD if -Force parameter specified
if(Test-Path $params.VhdPath)
{
if($Force.IsPresent)
{
Remove-item $params.VhdPath -Confirm -Force
}
else{
Write-Error "VHD file is already exist. set -Force option to delete existing VHD."
}
}
try{
#Mount OS Image Disk(or use existing media drive)
$mediaDriveLetter = Resolve-OSImageDisk -MediaPath $MediaPath
$wimPath = "{0}:\sources\install.wim" -f $mediaDriveLetter
#Get OSImageIndex from WIM (if os name is not specified, show OS select dialog)
$osImageIndex = Get-WindowsImageIndex -ImagePath $wimPath -OSImageName $OSImageName
#Create System VHD and initialize partitions then mount VHD
New-SystemVHD -VhdPath $VhdPath @VhdConfig | Out-Null
#Mount VHD
Mount-VHD -Path $VhdPath -NoDriveLetter:$false
#Get partition drive letters
$vhdInfo = Get-VHDInfo -VhdPath $VhdPath
#Apply OS Image
$params =@{
OSRootDir = $vhdInfo.OSRootDir
WimPath = $wimPath
OSImageIndex= $osImageIndex
Culture = $DismConfig.Culture
ProductKey = $DismConfig.ProductKey
UnattendXmlPath = $DismConfig.UnattendXmlPath
}
Apply-WindowsOSImage @params
#Write Boot Information
$params =@{
BootRootDir= $vhdInfo.BootRootDir
OSRootDir = $vhdInfo.OSRootDir
PartitionStyle = $VhdConfig.PartitionStyle
Culture= $DismConfig.Culture
}
Set-WindowsBootInformation @params
#Enable Windows Features
if(![String]::IsNullOrEmpty($DismConfig.FeatureName))
{
Enable-WindowsFeature -OSRootDir $vhdInfo.OsRootDir -DismConfig $dismConfig
}
#Apply Windows Patches
if(![String]::IsNullOrEmpty($DismConfig.PackagePath))
{
Add-WindowsPatch -OSRootDir $vhdInfo.OsRootDir -PackagePath $DismConfig.PackagePath
}
#Register cleanup script
Register-CleanupScript -OSRootDir $vhdInfo.OsRootDir
#Dismound Media and VHD
if([IO.Path]::GetExtension($MediaPath) -eq ".iso"){
Dismount-DiskImage -ImagePath $MediaPath
}
Dismount-VHD -Path $VhdPath
#Optimize VHD (no effect for Fixed disk)
if($OptimizeVHD)
{
Invoke-OptimizeVHD -VhdPath $vhdPath
}
#Set OS Base VHD as ReadOnly
if($ReadOnly)
{
Write-Verbose "Setting VHD image as ReadOnly..."
Set-ItemProperty $VhdPath -Name IsReadOnly -Value $true
}
}
finally{
Write-Verbose "Cleanup Mounted Disks..."
$mediaDiskImage = Get-DiskImage -ImagePath $MediaPath -ErrorAction Ignore
if(($mediaDiskImage -ne $null) -and $mediaDiskImage.Attached)
{
Write-Host "`tDismount media image...."
Dismount-DiskImage -ImagePath $MediaPath -ErrorAction Ignore
}
$vhd = Get-VHD -Path $VhdPath -ErrorAction Ignore
if(($vhd -ne $null) -and $vhd.Attached)
{
Write-Host "`tDismount VHD...."
Dismount-VHD -Path $VhdPath -ErrorAction Ignore
}
$vhdDiskImage = Get-DiskImage -ImagePath $VhdPath -ErrorAction Ignore
if(($vhdDiskImage -ne $null) -and $vhdDiskImage.Attached)
{
Write-Host "`tDismount Disk image...."
Dismount-DiskImage -ImagePath $VhdPath -ErrorAction Ignore
}
}
}
#Get OS ImageIndex from ISO image by name(if name is not specified show select dialog)
function Get-WindowsImageIndex
{
[Cmdletbinding()]
[OutputType([int])]
param(
[string]$ImagePath,
[string]$OSImageName
)
$indexList = Get-WindowsImage -ImagePath $wimPath -Verbose:$false
$selectedImage = $null
if(![String]::IsNullOrEmpty($OSImageName)){
#Select OS Image By Name
$selectedImage = $indexList | where ImageName -eq $OSImageName
if($selectedImage -eq $null)
{
Write-Warning "Selected OS Image Not Nound! :ImageName=$OSImageName"
}
}
if($selectedImage -eq $null)
{
#Select OS Image From List
do
{
Write-Host "Select OS Image From Dialog..."
$selectedImage = $indexList | Out-GridView -OutputMode Single -Title "Select OS Image..."
}while($selectedImage -eq $null)
Write-Verbose "Selected OS: $($selectedImage.ImageName)"
}
[int]$osImageIndex=$selectedImage.ImageIndex
return $osImageIndex
}
#Create OS System VHD and create partitions
function New-SystemVHD
{
[Cmdletbinding()]
param(
[string]$VhdType,
[string]$VhdPath,
[string]$VhdSize,
[string]$VhdBlockSize,
[string]$PartitionStyle,
[string]$AllocationUnitSize,
[switch]$CreateReservedPartition
)
#Create VHD
Write-Verbose "Creating OS VHD File..."
switch($VhdType)
{
"Fixed" { New-VHD -Path $VhdPath -SizeBytes $VhdSize -BlockSizeBytes $VhdBlockSize -Fixed | Out-Null}
"Dynamic"{ New-VHD -Path $VhdPath -SizeBytes $VhdSize -BlockSizeBytes $VhdBlockSize -Dynamic | Out-Null}
default { throw "Invalid VHD Type Specified!"}
}
#Mount VHD
Mount-DiskImage -ImagePath $VhdPath -NoDriveLetter
$vhdDiskNumber = (Get-DiskImage -ImagePath $VhdPath | Get-Disk).Number
#Initialize VHD
Write-Verbose "`tInitialize Disks..."
Initialize-Disk -Number $vhdDiskNumber -PartitionStyle $PartitionStyle
#Create Reserved Partition
if($CreateReservedPartition.IsPresent)
{
Write-Verbose "`tCreating System Reserved Partition..."
switch($PartitionStyle)
{
"MBR" { $reservedPartition = New-Partition -DiskNumber $vhdDiskNumber -Size 350MB -IsActive -AssignDriveLetter:$false}
"GPT" { $reservedPartition = New-Partition -DiskNumber $vhdDiskNumber -Size 350MB -AssignDriveLetter:$false}
default { throw "Invalid Disk Partition Style Specified!"}
}
sleep 1 #Issue
#Currently ReFS is not supported for windowsboot volume, use NTFS.
$reservedVolume = Format-Volume -Partition $reservedPartition -FileSystem NTFS -NewFileSystemLabel "System Reserved" -Confirm:$false
Get-Partition -Volume $reservedVolume | Add-PartitionAccessPath –AssignDriveLetter:$false
$reservedVolume = Get-Volume -Partition $reservedPartition
}
#Create OS Partition
Write-Verbose "`tCreating OS Partition..."
switch($PartitionStyle)
{
"MBR" { $osPartition = New-Partition -DiskNumber $vhdDiskNumber -UseMaximumSize -IsActive -AssignDriveLetter:$false}
"GPT" { $osPartition = New-Partition -DiskNumber $vhdDiskNumber -UseMaximumSize -AssignDriveLetter:$false}
default { throw "Invalid Disk Partition Style Specified!"}
}
sleep 1 #Issue
#Format Volume
#Currently ReFS is not supported for windowsboot volume, use NTFS.
$osVolume = Format-Volume -Partition $osPartition -FileSystem NTFS -AllocationUnitSize ($AllocationUnitSize) -Confirm:$false
#Get-Partition -Volume $osVolume | Add-PartitionAccessPath –AssignDriveLetter:$true
#Dismount DiskImage
Dismount-DiskImage -ImagePath $VhdPath
return Get-VHD -Path $VhdPath
}
#Return VHD Partition drive letter info
function Get-VHDInfo{
[Cmdletbinding()]
param(
[string] $VhdPath
)
#Get partition drive letters
$driveLetters = [array](Get-VHD -Path $VhdPath | Get-Disk | Get-Partition | Get-Volume | sort Size).DriveLetter #Drive Order is not assured if not sorted
if($driveLetters.Count -eq 2) #CreateReservedPartition specified
{
$bootRootDir = "{0}:\" -f $driveLetters[0]
$osRootDir = "{0}:\" -f $driveLetters[1]
}
else
{
$bootRootDir = "{0}:\" -f $driveLetters[0]
$osRootDir = "{0}:\" -f $driveLetters[0]
}
#Check PSDrive can access(in some condition it failed)
if(!(Test-Path -Path $osRootDir))
{
Write-Error "Can't access mounted VHD PSDrive. please try reset PowerShell runspace"
}
return [pscustomobject]@{BootRootDir = $bootRootDir;OSRootDir=$osRootDir}
}
function Enable-WindowsFeature{
[Cmdletbinding()]
param(
[string] $OSRootDir,
$DismConfig
)
Write-Verbose "Enabling Windows Features..."
#Need to mount to Directory
#Enable-WindowsOptionalFeature -Source property can't accept wim source "wim:E:\sources\install.wim:1" (Add-WindowsFeature accept this)
#Validate FeatureNames
$availableFeatures =Get-WindowsOptionalFeature -Path $OSRootDir -Verbose:$false
foreach($featureName in $DismConfig.FeatureName.Split(",",[StringSplitOptions]::RemoveEmptyEntries))
{
$featureName= $featureName.Trim()
$feature = $availableFeatures | where FeatureName -eq $featureName
if($feature -eq $null)
{
Write-Warning ("Feature '{0}' is not found" -f $feature.Name)
break
}
if($feature.State -eq "Enabled")
{
Write-Warning ("Feature '{0}' is already enabled" -f $feature.Name)
break
}
}
Enable-WindowsOptionalFeature -Path $vhdInfo.OSRootDir -FeatureName $DismConfig.FeatureName -Source $DismConfig.Source -All -LimitAccess -Verbose:$false | Out-Null
}
#Set Windows Boot Information to boot partition
function Set-WindowsBootInformation
{
[Cmdletbinding()]
param(
$OSRootDir,
$BootRootDir,
$PartitionStyle,
$Culture
)
#Write Boot information
Write-Verbose "Writing Boot Information..."
$windir = $OSRootDir + "Windows"
$bcdBootArgs = $null
switch($PartitionStyle)
{
"MBR" { $bcdBootArgs = "$windir /s $BootRootDir /l $Culture /f BIOS"}
"GPT" { $bcdBootArgs = "$windir /s $BootRootDir /l $Culture /f UEFI"} #BIOS/UEFI両方に対応の場合はALLを指定?
default { throw "Invalid Disk Partition Style"}
}
Invoke-BCDBoot $bcdBootArgs | Out-Null
}
#Apply WindowsImage to disk
function Apply-WindowsOSImage
{
[Cmdletbinding()]
param(
$OSRootDir,
$WimPath,
$OSImageIndex,
$Culture,
$ProductKey,
$UnattendXmlPath
)
Write-Verbose "Apply OS Images to Disk..."
#Apply OS Image
Write-Progress -Activity "CreateOSImage" -Status "Apply OS image to disk..."
Invoke-Dism "/Apply-Image /ImageFile:$wimPath /Index:$osImageIndex /ApplyDir:$OSRootDir" | Out-Null
Write-Progress -Activity "CreateOSImage" -Completed
#Set Culture
if(![String]::IsNullOrEmpty($Culture))
{
Write-Verbose "`tSetting OS Culture..."
Invoke-Dism "/Image:$OSRootDir /Set-AllIntl:$Culture" | Out-Null
Invoke-Dism "/Image:$OSRootDir /SKUIntlDefaults:$Culture" | Out-Null
#Set Keyboard Type
if($Culture -eq "ja-JP")
{
Write-Verbose "`tSetting Keyboard Type to Japanese..."
Invoke-Dism "/Image:$OSRootDir /Set-LayeredDriver:6" | Out-Null #WIMからインストールする場合、英語キーボードとして認識される。
}
}
#Set Product Key
if(![String]::IsNullOrEmpty($ProductKey))
{
Write-Verbose "`tSetting ProductKey..."
Set-WindowsProductKey -Path $OSRootDir -ProductKey $ProductKey -Verbose:$false
}
#Set Unattend.xml
if(![String]::IsNullOrEmpty($UnAttendXmlPath))
{
Write-Verbose "`tSetting Unattend.xml ..."
#Copy Unattend.xml to %SYSTEMDRIVE% (Used Low Priority)
Copy-Item -Path $UnAttendXmlPath -Destination "$($OSRootDir)Unattend.xml"
#register delete batch for Unattnd.xml(under %SYSTEMDRIVE%,Panther Cache) and batch itself
$path = "{0}Windows\Setup\Scripts\SetupComplete.cmd" -f $osRootDir
New-Item -ItemType Directory ([IO.Path]::GetDirectoryName($path)) -ErrorAction Ignore | Out-Null
Add-Content -Path $path -Encoding Ascii -Value "del %SystemDrive%\Unattend.xml 2>nul"
Add-Content -Path $path -Encoding Ascii -Value "del %WinDir%\Panther\Unattend.xml 2>nul"
#Note: Setting Unattend.xml by Dism command is valid only offlineServicing section
#Use-WindowsUnattend -Path $OSRootDir -UnattendPath $UnattendXmlPath -Verbose:$false | Out-Null
}
#Show Settings
#Invoke-Dism "/Image:$OSRootDir /Get-intl" | Write-Verbose
}
#Mount ISO and Resolve Drive Letter
function Resolve-OSImageDisk
{
[OutputType([char])]
[Cmdletbinding()]
param(
[string] $MediaPath
)
if(!(Test-Path $MediaPath))
{
Write-Error "OS Media not Found"
}
if($MediaPath.EndsWith(".iso") -and (Get-DiskImage -ImagePath $MediaPath).Attached)
{
Write-Error "Specified file is currently attached by other process"
}
#Mount OS Disk
if([IO.Path]::GetExtension($MediaPath) -eq ".iso")
{
Write-Verbose "Mounting ISO OS Image..."
Mount-DiskImage -ImagePath $MediaPath
[char]$mediaDriveLetter = (Get-DiskImage -ImagePath $MediaPath | Get-Volume).DriveLetter
}
else
{
Write-Verbose "Use Existing Drive Image..."
[char]$mediaDriveLetter = $MediaPath[0]
}
$wimPath = "{0}:\sources\install.wim" -f $mediaDriveLetter
if(![IO.File]::Exists($wimPath))
{
Write-Error ("Can't find WIM file: Path={0}" -f $wimPath)
}
return $mediaDriveLetter
}
#Invoke BCDBoot.exe with logging
function Invoke-BCDBoot
{
[Cmdletbinding()]
param(
[string] $Argument
)
Write-Verbose $("`tBCDBoot.exe invoked with following args: {0}" -f $Argument)
$results = Invoke-Expression ("BCDBoot.exe {0}" -f $Argument)
return $results
}
#Invoke Dism.exe with logging
function Invoke-Dism
{
[Cmdletbinding()]
param(
[string] $Argument
)
Write-Verbose $("`tDism.exe invoked with following args: {0}" -f $Argument)
$results = Invoke-Expression ("dism.exe {0}" -f $Argument)
return $results
}
#Apply offline patch and prepare online patch.
function Add-WindowsPatch
{
[Cmdletbinding()]
param(
[string]$OSRootDir,
[string]$PackagePath
)
Write-Verbose "Applying Windows Patches..."
#Patch list that can't apply online
$skippedPatchs = @()
foreach($patch in (Get-ChildItem $packagePath |sort LastWriteTime ))
{
#Skip .exe or .msi package
if($patch.Name.EndsWith(".exe") -or $patch.Name.EndsWith(".msi"))
{
Write-Verbose "`tSkip Exe patch: $($patch.Name)"
$skippedPatchs += $patch.FullName
continue
}
#Skip IME package
if($patch.Name.StartsWith("im") -and $patch.Name.EndsWith(".cab"))
{
Write-Verbose "`tSkip IME patch: $($patch.Name)"
$skippedPatchs += $patch.FullName
continue
}
#Skip KB2771431(Don't support offline patch)
if($patch.Name.StartsWith("windows8-rt-kb2771431"))
{
Write-Verbose "`tSkip KB2771431 patch: $($patch.Name)"
$skippedPatchs += $patch.FullName
continue
}
#Apply patch
$saved = $ErrorActionPreference
try{
$ErrorActionPreference="Continue"
Add-WindowsPackage -Path $OSRootDir -PackagePath $patch.FullName -Verbose:$false | Out-Null
}
catch{
#Ignore Error from Add-WindowsPackage (outputted warning message from Cmdlets)
}
finally{
$ErrorActionPreference = $saved
}
}
#Copy skipped patch to OS Temp Directory
if($skippedPatchs.Count -gt 0)
{
Write-Verbose "---------------------------------------------------------------------------"
Write-Verbose "`t$($skippedPatchs.Count) patches found that can't apply offline mode. It will be automatically appled when OS setup completed. Check install log at %WinDir%\Temp\WindowsUpdate\install.log"
$tempPath = Join-Path $OSRootDir "Windows\Temp\WindowsUpdate"
$tempDir = New-Item -ItemType Directory -Path $tempPath
#Copy patches
foreach($patch in $skippedPatchs)
{
Copy-Item -Path $patch -Destination $tempPath
}
#Copy functin ScriptBlock to file
$scriptPath = Join-Path $tempPath "install.ps1"
Set-Content -Path $scriptPath -Value ${function:Install-WindowsPatch} -Encoding UTF8
#Add install command to SetupComplete.cmd
$path = "{0}Windows\Setup\Scripts\SetupComplete.cmd" -f $OSRootDir
New-Item -ItemType Directory ([IO.Path]::GetDirectoryName($path)) -ErrorAction Ignore | Out-Null
Add-Content -Path $path -Encoding Ascii -Value "setlocal enabledelayedexpansion"
Add-Content -Path $path -Encoding Ascii -Value 'powershell -ExecutionPolicy Unrestricted -File "%WinDir%\Temp\WindowsUpdate\install.ps1" > "%WinDir%\Temp\WindowsUpdate\install.log" '
Add-Content -Path $path -Encoding Ascii -Value "endlocal"
}
}
#This function block is used to apply OS patch online
function Install-WindowsPatch
{
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$tempDir = Join-Path $env:WinDir "Temp\WindowsUpdate"
Set-Location $tempDir
foreach($file in (Get-ChildItem $tempDir | sort LastWriteTime ))
{
$fileName = $file.Name
try{
switch($file.Extension)
{
".cab"{
#Check IME package
if($file.Name.StartsWith("im"))
{
#Extract CAB
$shell = New-Object -ComObject "Shell.Application"
$items = $shell.Namespace($file.FullName).items()
$folder = $shell.Namespace($tempDir)
$folder.CopyHere($items,0x14) #overwrite if file exist
$extractedFileName = [IO.Path]::GetFileName($items.Item(0).Path)
$targetExe = Join-Path $tempDir $extractedFileName
#Remove CAB
Remove-Item -Path $file.FullName
#Apply patch
Write-Host "Apply patch: $extractedFileName with arguments /quiet"
Start-Process -FilePath $targetExe -ArgumentList "/quiet" -Wait
}
else{
Write-Host "Apply patch: $fileName"
Add-WindowsPackage -Online -PackagePath $file.FullName -Verbose:$false| Out-Null
}
break
}
".exe"{
if($file.Name.StartsWith("windows-kb890830")){
Write-Host "Apply patch: $fileName with arguments /quiet"
Start-Process -FilePath $file.FullName -ArgumentList "/quiet" -Wait
}
elseif($file.Name.StartsWith("delmigprov_")){
#KB2756872 delmigprov.exe failed when start with Start-Process?
Write-Host "Apply patch: $fileName with arguments /quiet"
& $file.FullName "/quiet"
sleep 5
}
else{
#TODO: other exe may not support "/quiet" option
Write-Host "Apply patch: $fileName with arguments"
Start-Process -FilePath $file.FullName -Wait
}
break
}
".msi"{
Write-Host "Apply patch: $fileName with arguments /quiet"
Start-Process -FilePath $file.FullName -ArgumentList "/quiet" -Wait
break
}
default{
#skip
}
}
}
catch
{
Write-Host "`r`n------------------------------------`r`n"
Write-Host $_.Exception
Write-Host "`r`n------------------------------------`r`n"
continue
}
Write-Host "`r`n" #Notepad can't handle LF
}
}
function Register-CleanupScript{
param(
[string] $OSRootDir
)
#register delete batch for Unattnd.xml(under %SYSTEMDRIVE%,Panther Cache) and batch itself
$path = "{0}Windows\Setup\Scripts\SetupComplete.cmd" -f $OSRootDir
if(Test-Path $path)
{
#Append script to delete batch file itself
Add-Content -Path $path -Value "del /F %0 2>nul" -Encoding Ascii
}
}
function Invoke-OptimizeVHD
{
[Cmdletbinding()]
param(
[string] $VhdPath
)
Write-Verbose "Optimizing VHD Image..."
Write-Verbose ("`tBefore Optimize:{0} GB" -f ((Get-Item $VhdPath).Length/1GB))
try
{
Mount-Vhd -Path $VhdPath -ReadOnly #Mount as readonly
Optimize-Vhd -Path $VhdPath #Default Mode VHD=Full VHDX=Quick
}
finally{
Dismount-VHD -Path $VhdPath
}
Write-Verbose ("`tAfter Optimize :{0} GB" -f ((Get-Item $VhdPath).Length/1GB))
}

TODO List

  • VHDXディスクのNative 4K対応 ⇒ 起動しない。Hyper-Vでは対応していない? またはネイティブ側のディスクが4K対応の必要ありとか?
  • NTFSクラスタサイズ 64KB対応 ⇒ 64KBクラスタ使用時は専用のブートパーティションが必要?
  • VHDXのブロックサイズ 256MB 対応 ⇒ ディスクサイズが増える副作用あり。
  • OSベースイメージのVHDのブロックサイズを最大値の256MBに設定⇒速度に変化なし。固定ディスクは設定が無視される。
  • DISMによるUnattend.xmlの適用 ⇒ DISMで適用した場合、offlineServicing 構成パスしか使用されない。
  • unattend.xml が %WINDIR%\Pantherにキャッシュされるので削除。フロッピーよりも優先度が高いのでsysprepした際に問題になる可能性あり。SetupComplete.cmdによる削除処理を追加
  • VHDブート向けの最適化 (GPTディスク/ UEFIブート対応) ⇒ 動作未検証
  • New-Partition実行時に表示されるフォーマットダイアログの抑止 ⇒ ドライブレターを後から割当てるよう修正?
  • 英語OS+LanguagePackの追加
  • パッチの自動ダウンロード ⇒https://gist.github.com/altrive/5268181
  • オフラインパッチ適用
  • ライセンス認証の動作確認。
  • 対応OSの追加(Windows 8/Windows Server 2008 R2)
  • DISMによるイメージ適用の進行状況の取得と表示。(できるか不明)
  • 固定ディスクを使用時の初期ディスク容量を最小化(12GB?)⇒差分ディスク側を拡張後、sysprepでパーティション拡張を実施する対応を追加。
  • OSベースイメージ:固定ディスクと可変ディスクの性能比較⇒速度に変化なし
  • OSベースイメージのディスクを固定と可変で作成し比較⇒速度に変化なし
  • System Reserved Partitionの必要性の確認 ⇒ NTFS クラスタサイズ64KBを使用しない場合は不要?
  • 物理DVDイメージマウントへの対応 ⇒対応
  • 各種パッチ類のネットワーク共有経由での適用対応
  • Format-Volumeでエラーが発生する場合がある。"Cannot perform the requested operation when the drive is read only" ⇒New-Partition の後に1秒sleepを挟む仮対応を追加
  • ReFS対応 (Windows 8では未サポート)
  • WhatIf/Confirm対応
  • SysInternalsのContig.exeを使用したデフラグ⇒効果薄い?
  • Get-Volume で取得したドライブレターの順序が稀に入れ替わる問題 ⇒ サイズでソートして仮対応
@gpuido
Copy link

gpuido commented May 26, 2016

Very nice, thank you for this !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment