Private/Set-DiskPartition.ps1
function Set-DiskPartition { <# .Synopsis Sets the content of a Disk using a source WIM or ISO .DESCRIPTION This command will copy the content of the SourcePath ISO or WIM and populate the partitions found on the disk. You must supply the path to a valid WIM/ISO. You should also include the index number for the Windows Edition to install. If the recovery partitions are present the source WIM will be copied to the recovery partition. Optionally, you can also specify an XML file to be inserted into the OS partition as unattend.xml, any Drivers, WindowsUpdate (MSU) or Optional Features you want installed. And any additional files to add. CAUTION: This command will replace the content partitions. .EXAMPLE PS C:\> Set-VHDPartition -DiskNumber 0 -SourcePath D:\wim\Win2012R2-Install.wim -Index 1 .EXAMPLE PS C:\> Set-VHDPartition -DiskNumber 0 -SourcePath D:\wim\Win2012R2-Install.wim -Index 1 -Confirm:$false -force -Verbose #> [CmdletBinding(SupportsShouldProcess = $true, PositionalBinding = $true, ConfirmImpact = 'Medium')] Param ( # Disk number, disk must exist [Parameter(Position = 0, Mandatory, HelpMessage = 'Disk Number based on Get-Disk')] [ValidateNotNullorEmpty()] [ValidateScript( { if (Get-Disk -Number $_) { $true } else { Throw "Disk number $_ does not exist." } })] [int]$DiskNumber, # Path to WIM or ISO used to populate VHDX [parameter(Position = 1, Mandatory = $true, HelpMessage = 'Enter the path to the WIM/ISO file')] [ValidateScript( { Test-Path -Path (Get-FullFilePath -Path $_ ) })] [string]$SourcePath, # Index of image inside of WIM (Default 1) [int]$Index = 1, # Path to file to copy inside of VHD(X) as C:\unattent.xml [ValidateScript( { if ($_) { Test-Path -Path $_ } else { $true } })] [string]$Unattend, # Native Boot does not have the boot code on the disk. Only usefull for VHD(X). [switch]$NativeBoot, # Add payload for all removed features [switch]$AddPayloadForRemovedFeature, # Feature to turn on (in DISM format) [ValidateNotNullOrEmpty()] [string[]]$Feature, # Feature to remove (in DISM format) [ValidateNotNullOrEmpty()] [string[]]$RemoveFeature, # Feature Source path. If not provided, all ISO and WIM images in $sourcePath searched [ValidateNotNullOrEmpty()] [ValidateScript( { (Test-Path -Path $(Resolve-Path $_) -or ($_ -eq 'NONE') ) })] [string]$FeatureSource, # Feature Source index. If the source is a .wim provide an index Default =1 [int]$FeatureSourceIndex = 1, # Path to drivers to inject [ValidateNotNullOrEmpty()] [ValidateScript( { foreach ($Path in $_) { Test-Path -Path $(Resolve-Path $Path) } })] [string[]]$Driver, # Path of packages to install via DSIM [ValidateNotNullOrEmpty()] [ValidateScript( { foreach ($Path in $_) { Test-Path -Path $(Resolve-Path $Path) } })] [string[]]$Package, # Files/Folders to copy to root of Winodws Drive (to place files in directories mimic the direcotry structure off of C:\) [ValidateNotNullOrEmpty()] [ValidateScript( { foreach ($Path in $_) { Test-Path -Path $(Resolve-Path $Path) } })] [string[]]$filesToInject, # Bypass the warning and about lost data [switch]$Force ) Process { $SourcePath = $SourcePath | Get-FullFilePath if ($pscmdlet.ShouldProcess("[$($MyInvocation.MyCommand)] : Overwrite partitions inside [$Path] with content of [$SourcePath]", "Overwrite partitions inside [$Path] with contentce of [$SourcePath]? ", 'Overwrite WARNING!')) { if ($Force -Or $pscmdlet.ShouldContinue('Are you sure? Any existin data will be lost!', 'Warning')) { $ParametersToPass = @{ } foreach ($key in ('Whatif', 'Verbose', 'Debug')) { if ($PSBoundParameters.ContainsKey($key)) { $ParametersToPass[$key] = $PSBoundParameters[$key] } } #region ISO detection # If we're using an ISO, mount it and get the path to the WIM file. if (([IO.FileInfo]$SourcePath).Extension -ilike '.ISO') { $isoPath = (Resolve-Path $SourcePath).Path Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Opening ISO [$(Split-Path -Path $isoPath -Leaf)]" $openIso = Mount-DiskImage -ImagePath $isoPath -StorageType ISO -PassThru # Workarround for new drive letters in script modules $null = Get-PSDrive # Refresh the DiskImage object so we can get the real information about it. I assume this is a bug. $openIso = Get-DiskImage -ImagePath $isoPath $driveLetter = ($openIso | Get-Volume).DriveLetter $SourcePath = "$($driveLetter):\sources\install.wim" # Check to see if there's a WIM file. Write-Verbose -Message "[$($MyInvocation.MyCommand)] : Looking for $($SourcePath)" if (!(Test-Path $SourcePath)) { throw 'The specified ISO does not appear to be valid Windows installation media.' } } #endregion ISO detection try { #! Workarround for new drive letters in script modules $null = Get-PSDrive #region Assign Drive Letters (disable explorer popup and reset afterwords) $DisableAutoPlayOldValue = (Get-ItemProperty -path hkcu:\Software\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers -name DisableAutoplay).DisableAutoplay Set-ItemProperty -Path hkcu:\Software\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers -Name DisableAutoplay -Value 1 foreach ($partition in (Get-Partition -DiskNumber $DiskNumber | Where-Object -Property DriveLetter -EQ 0x00 | Where-Object -Property Type -NE -Value Reserved)) { $partition | Add-PartitionAccessPath -AssignDriveLetter -ErrorAction Stop } #! Workarround for new drive letters in script modules $null = Get-PSDrive Set-ItemProperty -Path hkcu:\Software\Microsoft\Windows\CurrentVersion\Explorer\AutoplayHandlers -Name DisableAutoplay -Value $DisableAutoPlayOldValue Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Partition Table" Write-Verbose -Message (Get-Partition -DiskNumber $DiskNumber | Select-Object -Property PartitionNumber, DriveLetter, Size, Type | Out-String) #endregion #region get partitions $RecoveryToolsPartition = Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -EQ -Value Recovery | Select-Object -First 1 $WindowsPartition = Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -EQ -Value Basic | Select-Object -First 1 $SystemPartition = Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -EQ -Value System | Select-Object -First 1 $DiskLayout = 'UEFI' if (-not ($WindowsPartition -and $SystemPartition)) { $WindowsPartition = Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -EQ -Value IFS | Sort-Object -Descending -Property Size | Select-Object -First 1 $SystemPartition = Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -EQ -Value IFS | Sort-Object -Property Size | Select-Object -First 1 $DiskLayout = 'BIOS' } if (Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -Like -Value 'FAT32*' ) { $WindowsPartition = Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -EQ -Value IFS | Sort-Object -Descending -Property Size | Select-Object -First 1 $SystemPartition = Get-Partition -DiskNumber $DiskNumber | Where-Object -Property Type -Like -Value 'FAT32*' | Sort-Object -Property Size | Select-Object -First 1 $DiskLayout = 'WindowsToGo' } #endregion get partitions #region Windows partition if ($WindowsPartition) { $WinDrive = Join-Path -Path "$($WindowsPartition.DriveLetter):" -ChildPath '\' $windir = Join-Path -Path $WinDrive -ChildPath Windows Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Windows Partition [$($WindowsPartition.partitionNumber)] : Applying image from [$SourcePath] to [$WinDrive] using Index [$Index]" $null = Expand-WindowsImage -ImagePath $SourcePath -Index $Index -ApplyPath $WinDrive -ErrorAction Stop #region Modify the OS with Drivers, Active Features and Packages if ($Driver) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Adding Windows Drivers to the Image" $Driver | ForEach-Object -Process { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Driver path: [$PSItem]" $Null = Add-WindowsDriver -Path $WinDrive -Recurse -Driver $PSItem } } if ($filesToInject) { # TODO: varify consistancy for folder path IE: dest exists or not. foreach ($filePath in $filesToInject) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Windows Partition [$($WindowsPartition.partitionNumber)] : Adding files from $filePath" $recurse = $false if (Test-Path $filePath -PathType Container) { $recurse = $true } Copy-Item -Path $filePath -Destination $WinDrive -Recurse:$recurse } } if ($Unattend) { try { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Windows Partition [$($WindowsPartition.partitionNumber)] : Adding Unattend.xml ($Unattend)" Copy-Item $Unattend -Destination "$WinDrive\unattend.xml" } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error Adding Unattend.xml " throw $_.Exception.Message } } if ($AddPayloadForRemovedFeature) { $Feature = $Feature + (Get-WindowsOptionalFeature -Path $WinDrive | Where-Object -Property state -EQ -Value 'DisabledWithPayloadRemoved' ).FeatureName } If ($Feature) { try { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Colecting posible source paths" $FeatureSourcePath = @() $MountFolderList = @() # ISO if ($driveLetter) { $FeatureSourcePath += Join-Path -Path "$($driveLetter):" -ChildPath 'sources\sxs' } $notWinPE = $true if ((Resolve-Path -Path $env:temp).drive.name -eq 'X') { $notWinPE = $false Write-Warning "WinPE does not support Mounting WIM, Feature sources must be present in the image OR -FeatureSource must be a Folder" } if (($FeatureSource) -and ($FeatureSource -ne 'NONE')) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Source Path provided [$FeatureSource]" if (($FeatureSource | Resolve-Path | Get-Item ).PSIsContainer -eq $true ) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Source Path [$FeatureSource] in folder" $FeatureSourcePath += $FeatureSource } elseif ((($FeatureSource | Resolve-Path | Get-Item ).extension -like '.wim') -and $notWinPE) { #$FeatureSourcePath += Convert-Path $FeatureSource $MountFolder = [System.IO.Directory]::CreateDirectory((Join-Path -Path $env:temp -ChildPath ([System.IO.Path]::GetRandomFileName().split('.')[0]))) $MountFolderList += $MountFolder.FullName Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Mounting Source [$FeatureSource] Index [$FeatureSourceIndex]" $null = Mount-WindowsImage -ImagePath $FeatureSource -Index $FeatureSourceIndex -Path $MountFolder.FullName -ReadOnly $FeatureSourcePath += Join-Path -Path $MountFolder.FullName -ChildPath 'Windows\WinSxS' } else { if ($FeatureSource -ne 'NONE') { Write-Warning -Message "$FeatureSource is not a .wim or folder" } } } elseif ($notWinPE) { #NO $FeatureSource $images = Get-WindowsImage -ImagePath $SourcePath foreach ($image in $images) { $MountFolder = [System.IO.Directory]::CreateDirectory((Join-Path -Path $env:temp -ChildPath ([System.IO.Path]::GetRandomFileName().split('.')[0]))) $MountFolderList += $MountFolder.FullName Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) : Mounting Source [$SourcePath] [$($image.ImageIndex)] [$($image.ImageName)] to [$($MountFolder.FullName)] " $null = Mount-WindowsImage -ImagePath $SourcePath -Index $image.ImageIndex -Path $MountFolder.FullName -ReadOnly $FeatureSourcePath += Join-Path -Path $MountFolder.FullName -ChildPath 'Windows\WinSxS' } } #end if FeatureSource if ($FeatureSourcePath.count -gt 0) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) [$Feature] to the Image [$WinDrive] : Search Source Path [$FeatureSourcePath]" $null = Enable-WindowsOptionalFeature -Path $WinDrive -All -FeatureName $Feature -Source $FeatureSourcePath } else { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Installing Windows Feature(s) [$Feature] to the Image [$WinDrive] : No Source Path" $null = Enable-WindowsOptionalFeature -Path $WinDrive -All -FeatureName $Feature } } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error Installing Windows Feature " throw $_.Exception.Message } finally { foreach ($MountFolder in $MountFolderList) { $null = Dismount-WindowsImage -Path $MountFolder -Discard Remove-Item $MountFolder } } } if ($Package) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Adding Windows Packages to the Image" $Package | ForEach-Object -Process { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Package path: [$PSItem]" $Null = Add-WindowsPackage -Path $WinDrive -PackagePath $PSItem } } if ($RemoveFeature) { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Removing Windows Features from the Image [$WinDrive]" try { $null = Disable-WindowsOptionalFeature -Path $WinDrive -FeatureName $RemoveFeature @ParametersToPass } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error Removeing Windows Feature [$RemoveFeature] " throw $_.Exception.Message } } #endregion } else { throw 'Unable to find OS partition' } #endregion #region System partition if ($SystemPartition -and (-not ($NativeBoot))) { $systemDrive = "$($SystemPartition.driveletter):" $bcdBootArgs = @( "$windir", # Path to the \Windows on the Disk "/s $systemDrive", # Specifies the volume letter of the drive to create the \BOOT folder on. '/v' # Enabled verbose logging. ) Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Disk Layout [$DiskLayout]" switch ($DiskLayout) { 'UEFI' { $bcdBootArgs += '/f UEFI' # Specifies the firmware type of the target system partition } 'BIOS' { $bcdBootArgs += '/f BIOS' # Specifies the firmware type of the target system partition } 'WindowsToGo' { # Create entries for both UEFI and BIOS if possible if (Test-Path -Path "$($windowsDrive)\Windows\boot\EFI\bootmgfw.efi") { $bcdBootArgs += '/f ALL' } } } Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] System Partition [$($SystemPartition.partitionNumber)] : Running [$windir\System32\bcdboot.exe] -> $bcdBootArgs" RunExecutable -Executable "$windir\System32\bcdboot.exe" -Arguments $bcdBootArgs @ParametersToPass } #endregion #region Recovery Tools if ($RecoveryToolsPartition) { $recoverfolder = Join-Path -Path "$($RecoveryToolsPartition.DriveLetter):" -ChildPath 'Recovery' $WindowsRe = Join-Path -Path "$recoverfolder" -ChildPath 'WindowsRe' Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Recovery Tools Partition [$($RecoveryToolsPartition.partitionNumber)] : Creating Recovery\WindowsRE folder [$WindowsRe]" $null = mkdir -Path "$($RecoveryToolsPartition.driveletter):\Recovery\WindowsRE" -ErrorAction SilentlyContinue #the winre.wim file is hidden Get-ChildItem -Path "$windir\System32\recovery\winre.wim" -Hidden | Copy-Item -Destination $WindowsRe.FullName #? Windows 10 and server have this defaulted to enabled and to the same location. #Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] Recovery Tools Partition [$($RecoveryToolsPartition.partitionNumber)] : Register Reovery Image " #$null = Start-Process -NoNewWindow -Wait -FilePath "$windir\System32\reagentc.exe" -ArgumentList "/setreimage /path $WindowsRe /target $windir" } #endregion } catch { Write-Error -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Error setting partition content " throw $_.Exception.Message } finally { Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Removing Drive letters" Get-Partition -DiskNumber $DiskNumber | Where-Object -FilterScript { $_.driveletter } | Where-Object -Property Type -NE -Value 'Basic' | Where-Object -Property Type -NE -Value 'IFS' | ForEach-Object -Process { $dl = "$($_.DriveLetter):" $_ | Remove-PartitionAccessPath -AccessPath $dl } #dismount if ($isoPath -and (Get-DiskImage $isoPath).Attached) { $null = Dismount-DiskImage -ImagePath $isoPath } Write-Verbose -Message "[$($MyInvocation.MyCommand)] [$DiskNumber] : Finished" } } else { Write-Warning -Message 'Process aborted by user' } } } } |