├── .gitattributes ├── .gitignore ├── DeployImage.psm1 ├── DeployWindowsPE.ps1 ├── DeployWindowsToGo.ps1 ├── DeployWindowsVHDDomain.ps1 ├── README.md ├── deployimage.format.ps1xml ├── deployimage.psd1 └── foo.txtg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /DeployImage.psm1: -------------------------------------------------------------------------------- 1 | function Convert-WIMtoVHD 2 | { 3 | [CmdletBinding()] 4 | Param( 5 | # Size of the VHD file 6 | # 7 | $Size=20GB, 8 | # Obtain the default location of 9 | # Virtual HardDisks in Hyper-V 10 | # 11 | $VHDPath='.\', 12 | # Location of the WindowsImage (WIM) 13 | # File to convert to VHD 14 | # 15 | $Wimfile='C:\Windows7\Install.wim', 16 | # Netbios name of computer and VMName 17 | # 18 | $VM='Contoso-Win7', 19 | # Index of Image in WIM file 20 | # 21 | $Index=1 22 | 23 | ) 24 | 25 | If ($Vhdpath -eq '.\' -and (Get-Command Get-vmhost).count -ge 1) 26 | { 27 | $VHDPath=(Get-VMHost).VirtualHardDiskPath 28 | } 29 | 30 | # Define VHD filename 31 | # 32 | $VHD="$VhdPath\$VM.vhd" 33 | $OSDrive=(Get-NextActiveDriveLetter).DriveLetter 34 | If ((Test-Path $VHD) -ne $false -or $OSDrive -eq 'None') 35 | { 36 | Return "Filename $VHD already exists or no available drive letter" 37 | } 38 | Else 39 | { 40 | # Create a new VHD 41 | # 42 | $Result=New-VHD -Path $Vhd -SizeBytes $Size -Dynamic 43 | 44 | # Mount the VHD and identify it's Disk Object 45 | # 46 | $Result=Mount-VHD -Path $vhd 47 | $Disk=Get-Vhd -Path $Vhd | Get-Disk 48 | 49 | # Create a new Partition Structure of style 50 | # MBR, Format and Partition System and OSDrive 51 | # 52 | New-PartitionStructure -Disk $disk -MBR -BootDrive $OSDrive -OSDrive $OsDrive 53 | 54 | # Expand the Windows Image to the OSDrive 55 | # 56 | Expand-WindowsImage -imagepath "$wimfile" -index $Index -ApplyPath "$OSDrive`:\" 57 | 58 | # Send the Boot files to the Disk Structure 59 | # 60 | Send-BootCode -BootDrive $OSDrive -OSDrive $OSDrive 61 | # Dismount the Completed VHD 62 | # 63 | Dismount-VHD $VHD 64 | # Return path of VHD 65 | # 66 | Return $VHD 67 | } 68 | } 69 | 70 | Function Copy-WithProgress 71 | { 72 | [CmdletBinding()] 73 | Param 74 | ( 75 | [Parameter(Mandatory=$true, 76 | ValueFromPipelineByPropertyName=$true, 77 | Position=0)] 78 | $Source, 79 | [Parameter(Mandatory=$true, 80 | ValueFromPipelineByPropertyName=$true, 81 | Position=0)] 82 | $Destination 83 | 84 | ) 85 | $Source=$Source.tolower() 86 | 87 | $Filelist=get-childitem -path $source -Recurse 88 | $Total=$Filelist.count 89 | $Position=0 90 | foreach ($File in $Filelist) 91 | { 92 | $Filename=$File.Fullname.tolower().replace($Source,'') 93 | $DestinationFile=($Destination+$Filename).replace('\\','\') 94 | Write-Progress -Activity "Copying data from $source to $Destination" -Status "Copying Files" -PercentComplete (($Position/$total)*100) 95 | Copy-Item -path $File.FullName -Destination $DestinationFile 96 | $File.Fullname 97 | $DestinationFile 98 | $Position++ 99 | } 100 | } 101 | 102 | <# 103 | .Synopsis 104 | Copies supplied sample scripts from the DeployImage module 105 | .DESCRIPTION 106 | Copy all sample PS1 files from DeployImage to the destination directory 107 | .EXAMPLE 108 | Copies sample scripts to current directory 109 | 110 | Copy-DeployImageSample 111 | 112 | .EXAMPLE 113 | Copies sample scripts to C:\Foo 114 | 115 | Copy-DeployImageSample -Destination C:\Foo 116 | 117 | 118 | #> 119 | 120 | Function Copy-DeployImageSample 121 | { 122 | [CmdletBinding()] 123 | Param 124 | ( 125 | [Parameter(Mandatory=$false, 126 | ValueFromPipelineByPropertyName=$true, 127 | Position=0)] 128 | $Destination='.\' 129 | 130 | ) 131 | $Modulepath=Split-path -path ((get-module -Name deployimage).path) 132 | get-childitem -Path "$Modulepath\*.ps1" | copy-item -Destination $Destination 133 | } 134 | 135 | <# 136 | .Synopsis 137 | Removes a Drive Letter from an assigned partition 138 | .DESCRIPTION 139 | Removes a Drive Letter from an assigned partition 140 | .EXAMPLE 141 | Remove L: from it's assigned partition, freeing it back to available drive letters 142 | 143 | Remove-DriveLetter -DriveLetter L 144 | #> 145 | 146 | Function Remove-DriveLetter 147 | { 148 | [CmdletBinding()] 149 | Param 150 | ( 151 | [Parameter(Mandatory=$false, 152 | ValueFromPipelineByPropertyName=$true, 153 | Position=0)] 154 | [string]$DriveLetter 155 | ) 156 | 157 | Get-Volume -Drive $DriveLetter | Get-Partition | Remove-PartitionAccessPath -accesspath "$DriveLetter`:\" 158 | 159 | Do { 160 | $status=(Get-Volume -DriveLetter $DriveLetter -erroraction SilentlyContinue) 161 | } 162 | until ($Status -eq $NULL) 163 | } 164 | 165 | <# 166 | .Synopsis 167 | Builds an array of Drive Letters in use in Windows 168 | .DESCRIPTION 169 | This will return an Array of Letters sorted that are presently in use in the Windows O/S 170 | .EXAMPLE 171 | Get-ActiveDriveLetter 172 | #> 173 | 174 | Function Get-ActiveDriveLetter() 175 | { 176 | 177 | Get-Volume | Where-Object { $_.DriveLetter } | Sort-object DriveLetter | Select-Object -ExpandProperty DriveLetter 178 | 179 | } 180 | 181 | <# 182 | .Synopsis 183 | Tests if a Drive Letter is availale in Windows 184 | .DESCRIPTION 185 | This will return a $TRUE if a Drive Letter is available to use in the Windows O/S 186 | .EXAMPLE 187 | Test-ActiveDriveLetter 188 | #> 189 | 190 | Function Test-ActiveDriveLetter([String]$DriveLetter) 191 | { 192 | 193 | $DriveLetter -in (Get-ActiveDriveLetter) 194 | 195 | } 196 | 197 | <# 198 | .Synopsis 199 | Provides the Next available Drive Letter for use in Windows 200 | .DESCRIPTION 201 | This will the next letter available for use 202 | .EXAMPLE 203 | Get-NextActiveDriveLetter 204 | #> 205 | 206 | Function Get-NextActiveDriveLetter() 207 | { 208 | $Counter=67 209 | do 210 | { 211 | $DriveLetter=[char][byte]$Counter 212 | $Result=Test-ActiveDriveLetter -DriveLetter $DriveLetter 213 | $Counter++ 214 | } 215 | until ($Result -eq $False -or $Counter -eq 91) 216 | 217 | If ($Result -eq $True) 218 | { 219 | [pscustomobject]@{'DriveLetter'='None'} 220 | } 221 | else 222 | { 223 | [pscustomobject]@{'DriveLetter'=$DriveLetter} 224 | } 225 | } 226 | 227 | <# 228 | .Synopsis 229 | Identifies if the Operating System is 32bit or 64bit 230 | .DESCRIPTION 231 | If the Operating system is 64bit, it will return the string " (x86)" to append to a "Program Files" path 232 | .EXAMPLE 233 | $Folder="C:\Program Files$(Get-Architecture)" 234 | #> 235 | 236 | function Get-ArchitectureString 237 | { 238 | $Arch=(Get-CimInstance -Classname win32_operatingsystem).OSArchitecture 239 | if ($Arch -eq '64-Bit') 240 | { 241 | Return [string]' (x86)' 242 | } 243 | 244 | } 245 | 246 | <# 247 | .Synopsis 248 | Tests for the existence of the Windows 10 ADK 249 | .DESCRIPTION 250 | This Cmdlet will return a Boolean True if the Windows 10 ADK is installed. It depends upon the Get-ArchitectureString Cmdlet supplied within this module 251 | .EXAMPLE 252 | $AdkInstalled = Test-WindowsADK 253 | .EXAMPLE 254 | If (Test-WindowsADK -eq $TRUE) 255 | { 256 | Write-Output 'Windows 10 ADK is installed' 257 | } 258 | 259 | #> 260 | 261 | function Test-WindowsADK 262 | { 263 | (Test-Path -Path "C:\Program Files$(Get-ArchitectureString)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment") 264 | } 265 | 266 | function Get-AttachedDisk 267 | { 268 | [CmdletBinding()] 269 | Param 270 | ( 271 | [Parameter(Mandatory=$false, 272 | ValueFromPipelineByPropertyName=$true, 273 | Position=0)] 274 | [switch]$USB, 275 | [Parameter(Mandatory=$false, 276 | ValueFromPipelineByPropertyName=$true, 277 | Position=2)] 278 | [switch]$GUI 279 | ) 280 | 281 | If ($USB) 282 | { 283 | $DiskType='USB' 284 | } 285 | Else 286 | { 287 | $DiskType='SATA','SCSI','RAID' 288 | } 289 | 290 | 291 | if ($GUI -and ((Get-CimInstance -Classname Win32_OperatingSystem).OperatingSystemSKU -ne 1)) 292 | { 293 | Get-Disk | Where-Object { $DiskType -match $_.BusType } | Out-GridView -PassThru 294 | } 295 | Else 296 | { 297 | Get-Disk | Where-Object { $DiskType -match $_.BusType } 298 | } 299 | } 300 | 301 | <# 302 | .Synopsis 303 | Erase the Partition structure on a Disk 304 | .DESCRIPTION 305 | When provided with a Disk object from "Get-Disk" it will target and cleanly remove all partitions. It addresses some of the limitations found with the Clear-Disk Cmdlet. 306 | .EXAMPLE 307 | Erase partition structure on Disk Number 1 308 | 309 | $Disk=Get-Disk -Number 1 310 | Clear-DiskStructure -Disk $disk 311 | .EXAMPLE 312 | Erase partition structure on all USB attached disks 313 | 314 | $DiskList=Get-Disk | Where { $_.BusType -eq 'USB'} 315 | Foreach ( $Disk in $Disklist ) 316 | { 317 | Clear-DiskStructure -Disk $Disk 318 | } 319 | 320 | #> 321 | 322 | function Clear-DiskStructure 323 | { 324 | [CmdletBinding()] 325 | Param 326 | ( 327 | [Parameter(Mandatory=$true, 328 | ValueFromPipelineByPropertyName=$true, 329 | Position=0)] 330 | $Disk 331 | 332 | ) 333 | Get-Disk -Number ($Disk.number) | Get-Partition | Remove-partition -confirm:$false -erroraction SilentlyContinue 334 | Clear-Disk -Number $Disk.Number -RemoveData -RemoveOEM -confirm:$false -ErrorAction SilentlyContinue 335 | } 336 | 337 | <# 338 | .Synopsis 339 | Create a Partition structure, whether UEFI or MBR 340 | .DESCRIPTION 341 | Creates a Partition structure when provided with a target disk from the GET-Disk Cmdlet including formatting and assigning drive letters. 342 | .EXAMPLE 343 | Create a UEFI Partition structure on Disk 0, assign Drive Z: to the System Drive and Drive Y: to the OSDrive 344 | 345 | $Disk=Get-Disk -number 0 346 | New-PhysicalPartitionStructure -Disk $Disk -BootDrive Z -OSDrive Y 347 | 348 | #> 349 | 350 | function New-PartitionStructure 351 | { 352 | [CmdletBinding()] 353 | Param 354 | ( 355 | [Parameter(Mandatory=$true, 356 | ValueFromPipelineByPropertyName=$true, 357 | Position=0)] 358 | $Disk, 359 | [Parameter(Mandatory=$false, 360 | ValueFromPipelineByPropertyName=$true, 361 | Position=1)] 362 | [switch]$MBR, 363 | [Parameter(Mandatory=$false, 364 | ValueFromPipelineByPropertyName=$true, 365 | Position=2)] 366 | [switch]$USB, 367 | [Parameter(Mandatory=$false, 368 | ValueFromPipelineByPropertyName=$true, 369 | Position=3)] 370 | [string]$BootDrive, 371 | [Parameter(Mandatory=$false, 372 | ValueFromPipelineByPropertyName=$true, 373 | Position=4)] 374 | [string]$OSDrive 375 | 376 | ) 377 | 378 | Clear-DiskStructure $Disk 379 | 380 | if ($MBR) 381 | { 382 | $Result=Initialize-Disk -Number $Disk.Number -PartitionStyle MBR -ErrorAction SilentlyContinue 383 | 384 | if ($USB) 385 | { 386 | $Partition=New-Partition -DiskNumber $Disk.Number -UseMaximumSize -IsActive 387 | $Result=Format-Volume -Partition $Partition -FileSystem FAT32 -NewFileSystemLabel 'Windows' 388 | $Partition | Add-PartitionAccessPath -AccessPath "$($OSDrive):\" 389 | 390 | 391 | } 392 | else 393 | { 394 | $Partition=New-Partition -DiskNumber $Disk.Number -UseMaximumSize -IsActive 395 | $Result=Format-Volume -Partition $Partition -FileSystem NTFS -NewFileSystemLabel 'Windows' 396 | $Partition | Add-PartitionAccessPath -AccessPath "$($OSDrive):\" 397 | } 398 | 399 | } 400 | Else 401 | { 402 | $Result=Initialize-Disk -Number $Disk.Number -PartitionStyle GPT 403 | 404 | $Partition=New-Partition -DiskNumber $Disk.Number -Size 128MB ; # Create Microsoft Basic Partition 405 | $Result=Format-Volume -Partition $Partition -FileSystem Fat32 -NewFileSystemLabel 'MSR' 406 | $Result=Set-Partition -DiskNumber $Disk.Number -PartitionNumber $Partition.PartitionNumber -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' 407 | 408 | $Partition=New-Partition -DiskNumber $Disk.Number -Size 300MB ; # Create Microsoft Basic Partition and Set System as bootable 409 | $Result=Format-Volume -Partition $Partition -FileSystem Fat32 -NewFileSystemLabel 'Boot' 410 | $Partition | Add-PartitionAccessPath -AccessPath "$($Bootdrive):\" 411 | 412 | $Result=Set-Partition -DiskNumber $Disk.Number -PartitionNumber $Partition.PartitionNumber 413 | 414 | $Partition=New-Partition -DiskNumber $Disk.Number -UseMaximumSize ; # Take remaining Disk space for Operating System 415 | $Result=Format-Volume -Partition $Partition -FileSystem NTFS -NewFileSystemLabel 'Windows' 416 | $Partition | Add-PartitionAccessPath -AccessPath "$($OSDrive):\" 417 | 418 | } 419 | 420 | } 421 | 422 | 423 | <# 424 | .Synopsis 425 | Set San Policy for a Windows To Go key 426 | .DESCRIPTION 427 | Creates and injects the necessary Disk policy which protects Windows to Go from Internal Drives. Requires the drive letter of the target OSDrive. 428 | .EXAMPLE 429 | Set Windows To Go key on Drive L with the proper San Policy 430 | 431 | Send-SanPolicy -OSDrive L 432 | #> 433 | 434 | Function Send-SanPolicy 435 | { 436 | [CmdletBinding()] 437 | Param 438 | ( 439 | [Parameter(Mandatory=$true, 440 | ValueFromPipelineByPropertyName=$true, 441 | Position=0)] 442 | $OSDrive 443 | ) 444 | 445 | $SanPolicyXML=@" 446 | 447 | 448 | 449 | 458 | 4 459 | 460 | 469 | 4 470 | 471 | 472 | 473 | "@ 474 | 475 | Add-content -path "$OSDrive`:\san-policy.xml" -Value $SanpolicyXML 476 | 477 | Use-WindowsUnattend -UnattendPath "$OSDrive`:\san-policy.xml" -path "$OSdrive`:\" | Out-Null 478 | } 479 | 480 | <# 481 | .Synopsis 482 | Creates an Unattend.XML file for injection into an O/S 483 | .DESCRIPTION 484 | This Cmdlet will create an Unattend.XML file with suitable content. Depending upon the provided parameters it can inject the needed content or credentials for a Domain Join or be left to join a Workgroup Configuration. 485 | .EXAMPLE 486 | Create Unattend file for a computer named TestPC with all defaults for Password 487 | 488 | New-UnattendXMLContent -computername TestPC 489 | .EXAMPLE 490 | 491 | 492 | #> 493 | function New-UnattendXMLContent 494 | { 495 | [CmdletBinding()] 496 | Param 497 | ( 498 | [Parameter(Mandatory=$true, 499 | ValueFromPipelineByPropertyName=$true, 500 | Position=0)] 501 | [string]$Computername, 502 | [Parameter(Mandatory=$false, 503 | ValueFromPipelineByPropertyName=$true, 504 | Position=1)] 505 | [string]$Timezone='Eastern Standard Time', 506 | [Parameter(Mandatory=$false, 507 | ValueFromPipelineByPropertyName=$true, 508 | Position=2)] 509 | [string]$Owner='Nano Owner', 510 | [Parameter(Mandatory=$false, 511 | ValueFromPipelineByPropertyName=$true, 512 | Position=3)] 513 | [string]$Organization='Nano Organization', 514 | [Parameter(Mandatory=$false, 515 | ValueFromPipelineByPropertyName=$true, 516 | Position=4)] 517 | [string]$AdminPassword='P@ssw0rd', 518 | [Parameter(Mandatory=$false, 519 | ValueFromPipelineByPropertyName=$true, 520 | Position=5, 521 | ParameterSetName='DomainOnline')] 522 | [Switch]$Online, 523 | [Parameter(Mandatory=$false, 524 | ValueFromPipelineByPropertyName=$true, 525 | Position=6, 526 | ParameterSetName='DomainOnline')] 527 | [string]$DomainName, 528 | [Parameter(Mandatory=$false, 529 | ValueFromPipelineByPropertyName=$true, 530 | Position=7, 531 | ParameterSetName='DomainOnline')] 532 | [string]$DomainAccount, 533 | [Parameter(Mandatory=$false, 534 | ValueFromPipelineByPropertyName=$true, 535 | Position=8, 536 | ParameterSetName='DomainOnline')] 537 | [string]$DomainPassword, 538 | [Parameter(Mandatory=$false, 539 | ValueFromPipelineByPropertyName=$true, 540 | Position=9, 541 | ParameterSetName='DomainOnline')] 542 | [string]$DomainOU, 543 | [Parameter(Mandatory=$false, 544 | ValueFromPipelineByPropertyName=$true, 545 | Position=10)] 546 | [string]$OfflineBlob, 547 | [Parameter(Mandatory=$false, 548 | ValueFromPipelineByPropertyName=$true, 549 | Position=11)] 550 | [switch]$SkipOOBE 551 | 552 | ) 553 | 554 | $UnattendXML=@" 555 | 556 | 557 | 558 | 559 | $Computername 560 | $Organization 561 | $Owner 562 | $Timezone 563 | 564 | "@ 565 | 566 | If($JoinDomain -eq 'Online') 567 | { 568 | $UnattendXML=$UnattendXML+@" 569 | 570 | 571 | 572 | 573 | $DomainName 574 | $Domainpassword 575 | $Domainaccount 576 | 577 | $DomainName 578 | $DomainOU 579 | False 580 | 581 | 582 | "@ 583 | } 584 | 585 | if($Network) 586 | { 587 | $UnattendXML=$UnattendXML+@" 588 | 589 | 590 | 591 | false 592 | 593 | Local Area Connection 594 | 595 | $IPv4/$Mask 596 | 597 | 598 | 599 | 0 600 | 0.0.0.0/0 601 | 20 602 | $Gateway 603 | 604 | 605 | 606 | 607 | "@ 608 | } 609 | 610 | $UnattendXML=$UnattendXML+@" 611 | 612 | 613 | 614 | 615 | 616 | 617 | $Adminpassword 618 | true</PlainText> 619 | </AdministratorPassword> 620 | </UserAccounts> 621 | <AutoLogon> 622 | <Password> 623 | <Value>$Adminpassword</Value> 624 | <PlainText>true</PlainText> 625 | </Password> 626 | <Username>administrator</Username> 627 | <LogonCount>1</LogonCount> 628 | <Enabled>true</Enabled> 629 | </AutoLogon> 630 | <RegisteredOrganization>$Organization</RegisteredOrganization> 631 | <RegisteredOwner>$Owner</RegisteredOwner> 632 | "@ 633 | 634 | If ($SkipOOBE) 635 | { 636 | $UnattendXML=$UnattendXML+@" 637 | <OOBE> 638 | <HideEULAPage>true</HideEULAPage> 639 | <SkipMachineOOBE>true</SkipMachineOOBE> 640 | </OOBE> 641 | </component> 642 | "@ 643 | } 644 | 645 | $UnattendXML=$UnattendXML+@" 646 | 647 | </settings> 648 | "@ 649 | 650 | If ($OfflineBlob) 651 | { 652 | $UnattendXML=$UnattendXML+@" 653 | 654 | <settings pass="offlineServicing"> 655 | <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"> 656 | <OfflineIdentification> 657 | <Provisioning> 658 | <AccountData>$OfflineBlob</AccountData> 659 | </Provisioning> 660 | </OfflineIdentification> 661 | </component> 662 | </settings> 663 | "@ 664 | } 665 | 666 | 667 | $UnattendXML=$UnattendXML+@" 668 | 669 | <cpi:offlineImage cpi:source="" xmlns:cpi="urn:schemas-microsoft-com:cpi" /> 670 | </unattend> 671 | "@ 672 | 673 | 674 | Return $UnattendXML 675 | 676 | } 677 | 678 | Function Send-UnattendXML 679 | { 680 | [CmdletBinding()] 681 | Param 682 | ( 683 | [Parameter(Mandatory=$true, 684 | ValueFromPipelineByPropertyName=$true, 685 | Position=0)] 686 | [string]$OSDrive, 687 | [Parameter(Mandatory=$true, 688 | ValueFromPipelineByPropertyName=$true, 689 | Position=1)] 690 | [string]$UnattendData 691 | ) 692 | $Filename="$((Get-random).ToString()).xml" 693 | 694 | Remove-item -Path $Filename -force -ErrorAction SilentlyContinue 695 | New-Item -ItemType File -Path $Filename 696 | Add-Content -Path $Filename -Value $UnattendData 697 | Copy-Item -Path $filename -Destination "$OSDrive`:\Windows\System32\Sysprep\unattend.xml" 698 | Remove-item -Path $Filename -force -ErrorAction SilentlyContinue 699 | 700 | } 701 | 702 | function Send-BootCode 703 | { 704 | [CmdletBinding()] 705 | Param 706 | ( 707 | [Parameter(Mandatory=$false, 708 | ValueFromPipelineByPropertyName=$true, 709 | Position=0)] 710 | [string]$BootDrive, 711 | [Parameter(Mandatory=$true, 712 | ValueFromPipelineByPropertyName=$true, 713 | Position=1)] 714 | [string]$OSDrive, 715 | [Parameter(Mandatory=$false, 716 | ValueFromPipelineByPropertyName=$true, 717 | Position=2)] 718 | [switch]$USB 719 | 720 | ) 721 | $oldpref=$ErrorActionPreference 722 | $ErrorActionPreference='SilentlyContinue' 723 | 724 | if ($USB) 725 | { 726 | & "$($env:windir)\system32\bootsect.exe" /nt60 "$OSDrive`:" > NULL 727 | } 728 | else 729 | { 730 | & "$($env:windir)\system32\bcdboot" "$OSDrive`:\Windows" /s "$BootDrive`:" /f ALL > NULL 731 | } 732 | $ErrorActionPreference=$oldpref 733 | } 734 | 735 | 736 | function New-WindowsPEWim 737 | { 738 | [CmdletBinding()] 739 | Param 740 | ( 741 | [Parameter(Mandatory=$false, 742 | ValueFromPipelineByPropertyName=$true)] 743 | $WinPETemp='C:\TempPE', 744 | [Parameter(Mandatory=$false, 745 | ValueFromPipelineByPropertyName=$true)] 746 | $Destination='C:\PeWim' 747 | 748 | ) 749 | 750 | Begin 751 | { 752 | } 753 | 754 | Process 755 | { 756 | $Env:WinPERoot="C`:\Program Files$(Get-ArchitectureString)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment" 757 | 758 | $WinADK="$($Env:WinPERoot)\amd64" 759 | 760 | Remove-item -Path $WinPETemp -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 761 | New-Item -ItemType Directory -Path $WinPETemp -Force | Out-Null 762 | Copy-Item -Path "$WinAdk\Media" -Destination $WinPETemp -Recurse -Force | Out-Null 763 | New-Item -ItemType Directory -Path "$WinPETemp\Media\Sources" -Force | Out-Null 764 | Copy-Item -path "$WinAdk\en-us\winpe.wim" -Destination "$WinPETemp\Media\Sources\boot.wim" | Out-Null 765 | New-Item -ItemType Directory -Path "$WinPETemp\Mount" -Force | Out-Null 766 | 767 | Mount-WindowsImage -ImagePath "$WinPETemp\Media\Sources\boot.wim" -Index 1 -path "$WinPETemp\Mount" | Out-Null 768 | 769 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\WinPE-WMI.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 770 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\en-us\WinPE-WMI_en-us.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 771 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\WinPE-NetFx.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 772 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\en-us\WinPE-NetFx_en-us.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 773 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\WinPE-Scripting.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 774 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\en-us\WinPE-Scripting_en-us.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 775 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\WinPE-PowerShell.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 776 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\en-us\WinPE-PowerShell_en-us.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 777 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\WinPE-DismCmdlets.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 778 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\en-us\WinPE-DismCmdlets_en-us.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 779 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\WinPE-EnhancedStorage.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 780 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\en-us\WinPE-EnhancedStorage_en-us.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 781 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\WinPE-StorageWMI.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 782 | Add-WindowsPackage -PackagePath "$($WinAdk)\Winpe_OCS\en-us\WinPE-StorageWMI_en-us.cab" -Path "$WinPeTemp\Mount" -IgnoreCheck | Out-Null 783 | 784 | # Custom PowerShell Script to launch after Wpeinit.exe 785 | # This is hardcoded presently to automatically 786 | # Start the DeployImage module 787 | # from the WinPE media for Easy Server Deployment 788 | $PowerShellScript=@' 789 | Set-ExecutionPolicy -executionpolicy Bypass 790 | $USBDisk=(Get-Disk | Where-Object { $_.BusType -eq 'USB' -and '$_.IsActive' }) 791 | $DriveLetter=($USBDisk | Get-Partition).DriveLetter 792 | Set-Location ($DriveLetter+':\DeployImage\') 793 | Import-Module ($DriveLetter+':\DeployImage\DeployImage.Psd1') 794 | '@ 795 | 796 | # Carriage Return (Ascii13) and Linefeed (Ascii10) 797 | # the characters at the end of each line in a Here String 798 | $CRLF=[char][byte]13+[char][byte]10 799 | 800 | $PowerShellCommand=$PowerShellScript.replace($CRLF,';') 801 | 802 | $PowerShellStart='powershell.exe -executionpolicy bypass -noexit -command "'+$PowerShellCommand+'"' 803 | Add-Content -Path "$WinPEtemp\Mount\Windows\System32\Startnet.cmd" -Value $PowerShellStart 804 | 805 | Dismount-WindowsImage -path "$WinPETemp\Mount" -Save | Out-Null 806 | 807 | New-Item -Path $Destination -ItemType Directory -Force | Out-Null 808 | Copy-Item -path "$WinPETemp\Media\Sources\boot.wim" -destination "$Destination\" | Out-Null 809 | Remove-Item -Path "$Destination\Custom.wim" -erroraction SilentlyContinue | Out-Null 810 | Rename-Item -Path "$Destination\Boot.wim" -NewName 'Custom.wim' | Out-Null 811 | Remove-item -Path $WinPETemp -Recurse -Force -ErrorAction SilentlyContinue | Out-Null 812 | Return "$Destination\Custom.wim" 813 | 814 | } 815 | End 816 | { 817 | } 818 | } 819 | -------------------------------------------------------------------------------- /DeployWindowsPE.ps1: -------------------------------------------------------------------------------- 1 | $Wimfile='C:\Pewim\custom.wim' 2 | $OSDrive='L' 3 | $WinPEDrive='C' 4 | $Disk 5 | $DriverPath='C:\Dell' 6 | $WinPETemp='C:\TempPE' 7 | 8 | $Disk=Get-AttachedDisk -USB -GUI 9 | $Env:WinPERoot="$($WinPEDrive)`:\Program Files$(Get-ArchitectureString)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment" 10 | $WinADK="$($Env:WinPERoot)\amd64" 11 | 12 | Remove-item -Path $WinPETemp -Recurse -Force 13 | New-Item -ItemType Directory -Path $WinPETemp -Force 14 | Copy-Item -Path "$WinAdk\Media" -Destination $WinPETemp -Recurse -Force 15 | New-Item -ItemType Directory -Path "$WinPETemp\Media\Sources" -Force 16 | Copy-Item -path "$WinAdk\en-us\winpe.wim" -Destination "$WinPETemp\Media\Sources\boot.wim" 17 | 18 | if ($Wimfile -ne $NULL) 19 | { 20 | Copy-Item -Path $Wimfile -Destination "$WinPETemp\Media\Sources\boot.wim" 21 | } 22 | 23 | New-PartitionStructure -Disk $disk -OSDrive $OSDrive -USB -MBR 24 | $WinPEKey=$OsDrive+':' 25 | 26 | Copy-Item -Path "$WinPETemp\Media\*" -destination "$WinPeKey\" -Recurse 27 | 28 | Send-BootCode -OSDrive $OSDrive -USB 29 | 30 | Remove-DriveLetter -DriveLetter $OSDrive 31 | -------------------------------------------------------------------------------- /DeployWindowsToGo.ps1: -------------------------------------------------------------------------------- 1 | $Wimfile='C:\Sources\Install.wim' 2 | $BootDrive='Y' 3 | $OSDrive='Z' 4 | 5 | $Disk=Get-AttachedDisk -GUI 6 | $DriverPath='C:\Drivers' 7 | 8 | New-PartitionStructure -Disk $disk -BootDrive $BootDrive -OSDrive $OsDrive 9 | Expand-WindowsImage –imagepath "$wimfile" –index 1 –ApplyPath "$OSDrive`:\" 10 | 11 | $Unattendfile='C:\Foo\Unattend.xml' 12 | $Content=New-UnattendXMLContent -computername WTGKey -TimeZone 'Eastern Standard Time' 13 | Add-Content -path $Unattendfile -value $Content 14 | Copy-Item $UnattendFile -destination "$OSDrive`:\Windows\System32\Sysprep" 15 | Send-BootCode -BootDrive $BootDrive -OSDrive $OSDrive 16 | Send-SanPolicy -OSDrive $OSDrive 17 | 18 | If ($DriverPath -ne $NULL) 19 | { 20 | Add-WindowsDriver -Driver $DriverPath -Recurse -Path "$OSDrive`:\" -ErrorAction SilentlyContinue 21 | } 22 | -------------------------------------------------------------------------------- /DeployWindowsVHDDomain.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | $SystemDrive='L', 3 | # Drive Letter being assigned to Windows Drive 4 | # 5 | $OSDrive='L', 6 | # Netbios name of computer and VMName 7 | # 8 | $Computername='Contoso-Win7' 9 | ) 10 | $VHD=Convert-WimtoVhd -Wimfile C:\Windows7\install.wim -vm $Computername 11 | 12 | $Mount=Mount-vhd $VHD 13 | Add-PartitionAccessPath -DiskId 14 | 15 | # Clear out the old Unattend.xml file if it exists 16 | # 17 | Remove-Item -Path Unattend.xml -Force -ErrorAction SilentlyContinue 18 | 19 | # Create an Unattend.XML to define the Computername, owner, 20 | # Timezone and default Admin Password 21 | # 22 | $XMLContent=New-UnattendXMLContent -Computername $Computername -Timezone 'Eastern Standard Time' -Owner 'Contoso' -Organization 'Contoso' -AdminPassword 'P@ssw0rd' -Online -DomainName Contoso -DomainAccount 'Administrator' -DomainPassword 'P@ssw0rd' 23 | 24 | # Create the Unattend.xml file 25 | # 26 | New-Item -ItemType File -Name Unattend.xml -Force | Out-Null 27 | Add-content Unattend.xml -Value $XMLContent 28 | 29 | # Inject the Unattend.xml file into the VHD image 30 | # 31 | Copy .\Unattend.xml "$OSdrive`:\Windows\system32\sysprep" 32 | 33 | # Build the post Unattend.xml - Pre login script 34 | # This will define the static IP address and Perform an 35 | # Offline Domain Join from the provide domainjoin.djoin file 36 | # 37 | $SetupCompleteCMD=@" 38 | "@ 39 | 40 | # Remove the old SetupComplete.CMD if it exists 41 | # 42 | Remove-Item -Path SetupComplete.cmd -Force -ErrorAction SilentlyContinue 43 | 44 | # Create the new one 45 | # 46 | New-Item -ItemType File -Name SetupComplete.cmd -Force | Out-Null 47 | Add-content SetupComplete.cmd -Value $SetupCompleteCMD 48 | 49 | # Inject into the disk image 50 | # 51 | Copy .\SetupComplete.cmd "$OSdrive`:\Windows\setup\scripts" 52 | 53 | # Remove Drive Letter from Assigned Disk and place 54 | # back into pool. 55 | # 56 | Remove-DriveLetter -DriveLetter $OSDrive 57 | 58 | # Disconnect VHD 59 | # 60 | Dismount-VHD -Path $vhd 61 | 62 | # From this point forward you can manually create a 63 | # Virtual Machine in Hyper-V or use this VHD file for 64 | # booting on a Physical Disk 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeployImage 2 | The goal of this PowerShell Module initially is to supply a better and easier way to image the new Nano Server from Microsoft. However since the processes are for the most part identical, I am going to extend this to Windows to Go deployments as well as standard servers (whether physical or virtual) 3 | 4 | It will have to ability to create a Workgroup or Domain based Unattend.xml file to be injected directly into the destination environment. 5 | 6 | In addition (as it grows and improves) it will be able to inject features directly into the relevant operating systems. 7 | 8 | Another target of this Module is to allow easy Deployment of a Windows PE key that is PowerShell enabled. 9 | 10 | This module can be found on the PowerShell Gallery under 'DeployImage' and also on Github under https://github.com/energizedtech/DeployImage 11 | 12 | It introduces 13 new Cmdlets to try and simplify Deploying Nanoserver and Wim files to VHD, USB and Physical Disk environments. (Nano Server being the Dominant reason) 13 | 14 | Clear-DiskStructure 15 | 16 | This Cmdlets task is to Enhance the Clear-Disk Cmdlet and simplify it as I have encountered situations in which you need to remove Partitions first before Clear-Disk successfully cleans the Target Disk 17 | 18 | Copy-WithProgress 19 | 20 | This Cmdlet is still being tested and is meant as a "Copy-Item" with Progress to show SOME indication of where a large folder copy is at 21 | 22 | Get-ArchitectureString 23 | 24 | This Cmdlet returns the string " (x86)" if you are on 64bi Windows. It is meant as a way to inject into the 32 Program Files pathname 25 | 26 | Get-ActiveDriveLetter 27 | 28 | This Cmdlet pulls up an array of Drive Letters presently in use 29 | 30 | Test-ActiveDriveLetter 31 | 32 | It will provide a Boolean $True or $False if a Drive Letter is in use when provided 33 | 34 | Get-NextActiveDriveLetter 35 | 36 | This Cmdlet will identify the next available drive letter for mapping or other purposes 37 | 38 | Get-AttachedDisk 39 | 40 | Get-AttachedDisk is a Wrapper on the Get-Disk Cmdlet which targets either Internal or USB. It also provides a GUI parameter to launch Out-Gridview to select disks 41 | 42 | New-PartitionStructure 43 | 44 | This Cmdlet creates one of three stock basic Parition structures, either MBR for USB, MBR for Filesystem or GPT for UEFI and Formats targeted drives 45 | 46 | New-UnattendXMLContent 47 | 48 | This is a Parameterized Cmdlet to provide an Unattend.XML file content. Domain Join is an option and will populate the additional needed content. 49 | 50 | New-WindowsPEWim 51 | 52 | This builds a WindowsPE Wim file pre-populated with the necessary OCS for Windows PowerShell, DISM and Storage Cmdlets. It also autolaunches PowerShell with the Set-ExecutionPolicy to Bypass 53 | 54 | Remove-DriveLetter 55 | 56 | This simply Removes and verifies removal of a Drive Letter from the Drive system 57 | 58 | Send-BootCode 59 | 60 | Makes a Disk bootable 61 | 62 | Send-SanPolicy 63 | 64 | This is meant for Windows to Go. It just builds and applies a targeted disk with the SanPolicy for Windows to Go 65 | 66 | Send-UnattendXML 67 | 68 | Deploys an Unattend file to a target Operating System 69 | 70 | Test-WindowsADK 71 | 72 | Test for the Presence of the Windows 10 ADK 73 | 74 | There are also a sample script provided 75 | 76 | DeployWindowsPE.ps1 77 | 78 | Prompts for an available USB key, Prepares it as Bootable WinPE with PowerShell 79 | 80 | -------------------------------------------------------------------------------- /deployimage.format.ps1xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" ?> 2 | <Configuration> 3 | <ViewDefinitions> 4 | </ViewDefinitions> 5 | </Configuration> -------------------------------------------------------------------------------- /deployimage.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'deployimage' 3 | # 4 | # Generated by: Sean Kearney 5 | # 6 | # Generated on: 8/14/2019 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | # RootModule = '' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '2.1' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '4de6b7a8-bb7d-4209-bb48-d5327ba26f0f' 22 | 23 | # Author of this module 24 | Author = 'Sean Kearney' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Energized About Technology' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2015 Sean Kearney. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'A module to help simplify the of WindowsPE, Windows to Go and other WIM files to both physical and VHD' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | PowerShellVersion = '2.0' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | # RequiredAssemblies = @() 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | # FormatsToProcess = @() 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | NestedModules = @('DeployImage.PSM1') 70 | 71 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 72 | FunctionsToExport = ('Clear-DiskStructure,Convert-WIMtoVHD,Copy-DeployImageSample,Copy-WithProgress,Get-ActiveDriveLetter,Get-ArchitectureString,Get-AttachedDisk,Get-NextActiveDriveLetter,New-PartitionStructure,New-UnattendXMLContent,New-WindowsPEWim,Remove-DriveLetter,Send-BootCode,Send-SanPolicy,Send-UnattendXML,Test-ActiveDriveLetter,Test-WindowsADK') 73 | 74 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 75 | CmdletsToExport = '*' 76 | 77 | # Variables to export from this module 78 | VariablesToExport = '*' 79 | 80 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 81 | AliasesToExport = '*' 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | # Tags = @() 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'http://opensource.org/licenses/MS-PL' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/energizedtech/DeployImage' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | ReleaseNotes = 'Copy-DeployImageSample Cmdlet added to easily provide sample scripts. New-NanoServerWim Cmdlet supports customization of all built in roles, New-WindowsPEwim builds PowerShell 5 PE with autolaunch to PowerShell. Sample scripts to deploy NanoServer, NanoServerMedia and building WindowsPE supplied. Some help added.' 111 | 112 | # Prerelease string of this module 113 | # Prerelease = '' 114 | 115 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 116 | # RequireLicenseAcceptance = $false 117 | 118 | # External dependent modules of this module 119 | # ExternalModuleDependencies = @() 120 | 121 | } # End of PSData hashtable 122 | 123 | } # End of PrivateData hashtable 124 | 125 | # HelpInfo URI of this module 126 | # HelpInfoURI = '' 127 | 128 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 129 | # DefaultCommandPrefix = '' 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /foo.txtg: -------------------------------------------------------------------------------- 1 | hello --------------------------------------------------------------------------------