├── DDA └── survey-dda.ps1 ├── Daily-Status-Report ├── DailyStatusReport.ps1 └── readme.md ├── EvacuateDisk.ps1 ├── Expand-VMConfig.ps1 ├── GuestNameCheck.ps1 ├── Image-Factory ├── .gitignore ├── Factory.ps1 ├── FactoryVariables.ps1 ├── images.csv ├── images.md └── readme.md ├── LICENSE ├── Misc └── BingCmdlets.ps1 ├── README.md ├── Snippets ├── CopyTextFileIntoVM.ps1 ├── PSCreds.ps1 ├── Readme.md ├── WaitForPSDirect.ps1 ├── WaitForVMDHCP.ps1 └── addNicWithIP.ps1 ├── Ubuntu-VM-Build ├── BaseUbuntuBuild.ps1 └── readme.md └── vmconnect.ps1 /DDA/survey-dda.ps1: -------------------------------------------------------------------------------- 1 | # 2 | # These variables are device properties. For people who are very 3 | # curious about this, you can download the Windows Driver Kit headers and 4 | # look for pciprop.h. All of these are contained in that file. 5 | # 6 | $devpkey_PciDevice_DeviceType = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 1" 7 | $devpkey_PciDevice_RequiresReservedMemoryRegion = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 34" 8 | $devpkey_PciDevice_AcsCompatibleUpHierarchy = "{3AB22E31-8264-4b4e-9AF5-A8D2D8E33E62} 31" 9 | 10 | $devprop_PciDevice_DeviceType_PciConventional = 0 11 | $devprop_PciDevice_DeviceType_PciX = 1 12 | $devprop_PciDevice_DeviceType_PciExpressEndpoint = 2 13 | $devprop_PciDevice_DeviceType_PciExpressLegacyEndpoint = 3 14 | $devprop_PciDevice_DeviceType_PciExpressRootComplexIntegratedEndpoint= 4 15 | $devprop_PciDevice_DeviceType_PciExpressTreatedAsPci = 5 16 | $devprop_PciDevice_BridgeType_PciConventional = 6 17 | $devprop_PciDevice_BridgeType_PciX = 7 18 | $devprop_PciDevice_BridgeType_PciExpressRootPort = 8 19 | $devprop_PciDevice_BridgeType_PciExpressUpstreamSwitchPort = 9 20 | $devprop_PciDevice_BridgeType_PciExpressDownstreamSwitchPort = 10 21 | $devprop_PciDevice_BridgeType_PciExpressToPciXBridge = 11 22 | $devprop_PciDevice_BridgeType_PciXToExpressBridge = 12 23 | $devprop_PciDevice_BridgeType_PciExpressTreatedAsPci = 13 24 | $devprop_PciDevice_BridgeType_PciExpressEventCollector = 14 25 | 26 | $devprop_PciDevice_AcsCompatibleUpHierarchy_NotSupported = 0 27 | $devprop_PciDevice_AcsCompatibleUpHierarchy_SingleFunctionSupported = 1 28 | $devprop_PciDevice_AcsCompatibleUpHierarchy_NoP2PSupported = 2 29 | $devprop_PciDevice_AcsCompatibleUpHierarchy_Supported = 3 30 | 31 | 32 | write-host "Generating a list of PCI Express endpoint devices" 33 | $pnpdevs = Get-PnpDevice -PresentOnly 34 | $pcidevs = $pnpdevs | Where-Object {$_.InstanceId -like "PCI*"} 35 | 36 | foreach ($pcidev in $pcidevs) { 37 | Write-Host "" 38 | Write-Host "" 39 | Write-Host -ForegroundColor White -BackgroundColor Black $pcidev.FriendlyName 40 | 41 | $rmrr = ($pcidev | Get-PnpDeviceProperty $devpkey_PciDevice_RequiresReservedMemoryRegion).Data 42 | if ($rmrr -ne 0) { 43 | write-host -ForegroundColor Red -BackgroundColor Black "BIOS requires that this device remain attached to BIOS-owned memory. Not assignable." 44 | continue 45 | } 46 | 47 | $acsUp = ($pcidev | Get-PnpDeviceProperty $devpkey_PciDevice_AcsCompatibleUpHierarchy).Data 48 | if ($acsUp -eq $devprop_PciDevice_AcsCompatibleUpHierarchy_NotSupported) { 49 | write-host -ForegroundColor Red -BackgroundColor Black "Traffic from this device may be redirected to other devices in the system. Not assignable." 50 | continue 51 | } 52 | 53 | $devtype = ($pcidev | Get-PnpDeviceProperty $devpkey_PciDevice_DeviceType).Data 54 | if ($devtype -eq $devprop_PciDevice_DeviceType_PciExpressEndpoint) { 55 | Write-Host "Express Endpoint -- more secure." 56 | } else { 57 | if ($devtype -eq $devprop_PciDevice_DeviceType_PciExpressRootComplexIntegratedEndpoint) { 58 | Write-Host "Embedded Endpoint -- less secure." 59 | } else { 60 | if ($devtype -eq $devprop_PciDevice_DeviceType_PciExpressTreatedAsPci) { 61 | Write-Host -ForegroundColor Red -BackgroundColor Black "BIOS kept control of PCI Express for this device. Not assignable." 62 | } else { 63 | Write-Host -ForegroundColor Red -BackgroundColor Black "Old-style PCI device, switch port, etc. Not assignable." 64 | } 65 | continue 66 | } 67 | } 68 | 69 | $locationpath = ($pcidev | get-pnpdeviceproperty DEVPKEY_Device_LocationPaths).data[0] 70 | 71 | # 72 | # Now do a check for the interrupts that the device uses. Line-based interrupts 73 | # aren't assignable. 74 | # 75 | $doubleslashDevId = "*" + $pcidev.PNPDeviceID.Replace("\","\\") + "*" 76 | $irqAssignments = gwmi -query "select * from Win32_PnPAllocatedResource" | Where-Object {$_.__RELPATH -like "*Win32_IRQResource*"} | Where-Object {$_.Dependent -like $doubleslashDevId} 77 | 78 | #$irqAssignments | Format-Table -Property __RELPATH 79 | 80 | if ($irqAssignments.length -eq 0) { 81 | Write-Host -ForegroundColor Green -BackgroundColor Black " And it has no interrupts at all -- assignment can work." 82 | } else { 83 | # 84 | # Find the message-signaled interrupts. They are reported with a really big number in 85 | # decimal, one which always happens to start with "42949...". 86 | # 87 | $msiAssignments = $irqAssignments | Where-Object {$_.Antecedent -like "*IRQNumber=42949*"} 88 | 89 | #$msiAssignments | Format-Table -Property __RELPATH 90 | 91 | if ($msiAssignments.length -eq 0) { 92 | Write-Host -ForegroundColor Red -BackgroundColor Black "All of the interrupts are line-based, no assignment can work." 93 | continue 94 | } else { 95 | Write-Host -ForegroundColor Green -BackgroundColor Black " And its interrupts are message-based, assignment can work." 96 | } 97 | } 98 | 99 | # 100 | # Print out the location path, as that's the way to refer to this device that won't 101 | # change even if you add or remove devices from the machine or change the way that 102 | # the BIOS is configured. 103 | # 104 | $locationpath 105 | } 106 | 107 | # 108 | # Now look at the host as a whole. Asking whether the host supports SR-IOV 109 | # is mostly equivalent to asking whether it supports Discrete Device 110 | # Assignment. 111 | # 112 | if ((Get-VMHost).IovSupport -eq $false) { 113 | Write-Host "" 114 | write-host "Unfortunately, this machine doesn't support using them in a VM." 115 | Write-Host "" 116 | (Get-VMHost).IovSupportReasons 117 | } -------------------------------------------------------------------------------- /Daily-Status-Report/DailyStatusReport.ps1: -------------------------------------------------------------------------------- 1 | # Variables 2 | $filedate = get-date 3 | $computer = gc env:computername 4 | $metricsData = get-vm | measure-vm 5 | $tableColor = "WhiteSmoke" 6 | $errorColor = "Red" 7 | $warningColor = "Yellow" 8 | $FromEmail = "email@email.org" 9 | $ToEmail = "email@email.org" 10 | 11 | # Establish Connection to SMTP server 12 | $smtpServer = "smtp.yourISP.com" 13 | $smtpCreds = new-object Net.NetworkCredential("yourUsername", "YourPassword") 14 | $smtp = new-object Net.Mail.SmtpClient($smtpServer) 15 | $smtp.UseDefaultCredentials = $false 16 | $smtp.Credentials = $smtpCreds 17 | 18 | # HTML Style Definition 19 | $message = "" 20 | $message = "" 21 | $message = "" 30 | 31 | # Title 32 | $message = $message + "

Data for Hyper-V Server '$($computer)' : $($filedate)

" 33 | 34 | # EventLog 35 | $message = $message + "" 36 | $message = $message + "Parent EventLog

" 37 | $message = $message + "Errors:
" + ((Get-EventLog system -after (get-date).AddHours(-24) -entryType Error) | ` 38 | Select-Object @{Expression={$_.InstanceID};Label="ID"}, ` 39 | @{Expression={$_.Source};Label="Source"}, ` 40 | @{Expression={$_.Message};Label="Message"} ` 41 | | ConvertTo-HTML -Fragment) ` 42 | + "
" 43 | 44 | $message = $message + "" 45 | $message = $message + "Warnings:
" + ((Get-EventLog system -after (get-date).AddHours(-24) -entryType Warning) | ` 46 | Select-Object @{Expression={$_.InstanceID};Label="ID"}, ` 47 | @{Expression={$_.Source};Label="Source"}, ` 48 | @{Expression={$_.Message};Label="Message"} ` 49 | | ConvertTo-HTML -Fragment) ` 50 | + "
" 51 | 52 | # Hyper-V EventLog 53 | $message = $message + "" 54 | $message = $message + "Hyper-V EventLog

" 55 | $message = $message + "Errors:
" + ((Get-WinEvent -FilterHashTable @{LogName ="Microsoft-Windows-Hyper-V*"; StartTime = (Get-Date).AddDays(-1); Level = 2}) | ` 56 | Select-Object @{Expression={$_.InstanceID};Label="ID"}, ` 57 | @{Expression={$_.Source};Label="Source"}, ` 58 | @{Expression={$_.Message};Label="Message"} ` 59 | | ConvertTo-HTML -Fragment) ` 60 | + "
" 61 | 62 | $message = $message + "" 63 | $message = $message + "Warnings:
" + ((Get-WinEvent -FilterHashTable @{LogName ="Microsoft-Windows-Hyper-V*"; StartTime = (Get-Date).AddDays(-1); Level = 3}) | ` 64 | Select-Object @{Expression={$_.InstanceID};Label="ID"}, ` 65 | @{Expression={$_.Source};Label="Source"}, ` 66 | @{Expression={$_.Message};Label="Message"} ` 67 | | ConvertTo-HTML -Fragment) ` 68 | + "
" 69 | 70 | # VM Health 71 | $message = $message + "" 72 | $message = $message + "Virtual Machine Health

" 73 | $message = $message + "Virtual Machine Health:
" + ((Get-VM | ` 74 | Select-Object @{Expression={$_.Name};Label="Name"}, ` 75 | @{Expression={$_.State};Label="State"}, ` 76 | @{Expression={$_.Status};Label="Operational Status"}, ` 77 | @{Expression={$_.UpTime};Label="Up Time"} ` 78 | | ConvertTo-HTML -Fragment) ` 79 | | %{if($_.Contains("Operating normally")){$_.Replace("", "")}else{$_}} ` 80 | | %{if($_.Contains("RunningOperating normally")){$_.Replace("", "")}else{$_}}) ` 81 | + "
" 82 | # VM Replication Health 83 | $message = $message + "" 84 | $message = $message + "Virtual Machine Replication Health

" 85 | $message = $message + "Virtual Machine Replication Health:
" + ((Get-VM | ` 86 | Select-Object @{Expression={$_.Name};Label="Name"}, ` 87 | @{Expression={$_.ReplicationState};Label="State"}, ` 88 | @{Expression={$_.ReplicationHealth};Label="Health"}, ` 89 | @{Expression={$_.ReplicationMode};Label="Mode"} ` 90 | | ConvertTo-HTML -Fragment) ` 91 | | %{if($_.Contains("ReplicatingNormal")){$_.Replace("", "")}else{$_}}) ` 92 | + "
" 93 | 94 | # Storage Health 95 | $message = $message + "" 96 | $message = $message + "Storage Health

" 97 | $message = $message + "Physical Disk Health:
" + ((Get-PhysicalDisk | ` 98 | Select-Object @{Expression={$_.FriendlyName};Label="Physical Disk Name"}, ` 99 | @{Expression={$_.DeviceID};Label="Device ID"}, ` 100 | @{Expression={$_.OperationalStatus};Label="Operational Status"}, ` 101 | @{Expression={$_.HealthStatus};Label="Health Status"}, ` 102 | @{Expression={"{0:N2}" -f ($_.Size / 1073741824)};Label="Size (GB)"} ` 103 | | ConvertTo-HTML -Fragment) ` 104 | | %{if($_.Contains("OKHealthy")){$_.Replace("", "")}else{$_}}) ` 105 | + "
" 106 | $message = $message + "Storage Pool Health:
" + ((Get-StoragePool | ` 107 | where-object {($_.FriendlyName -ne "Primordial")} | ` 108 | Select-Object @{Expression={$_.FriendlyName};Label="Storage Pool Name"}, ` 109 | @{Expression={$_.OperationalStatus};Label="Operational Status"}, ` 110 | @{Expression={$_.HealthStatus};Label="Health Status"} ` 111 | | ConvertTo-HTML -Fragment) ` 112 | | %{if($_.Contains("OKHealthy")){$_.Replace("", "")}else{$_}}) ` 113 | + "
" 114 | $message = $message + "Virtual Disk Health:
" + ((Get-VirtualDisk | ` 115 | Select-Object @{Expression={$_.FriendlyName};Label="Virtual Disk Name"}, ` 116 | @{Expression={$_.OperationalStatus};Label="Operational Status"}, ` 117 | @{Expression={$_.HealthStatus};Label="Health Status"} ` 118 | | ConvertTo-HTML -Fragment) ` 119 | | %{if($_.Contains("OKHealthy")){$_.Replace("", "")}else{$_}}) ` 120 | + "
" 121 | 122 | # VM Metrics 123 | $message = $message + "" 124 | $message = $message + "Virtual Machine Utilization Report

" 125 | 126 | $message = $message + "CPU utilization data:
" + ($metricsData | ` 127 | select-object @{Expression={$_.VMName};Label="Virtual Machine"}, ` 128 | @{Expression={$_.AvgCPU};Label="Average CPU Utilization (MHz)"} ` 129 | | ConvertTo-HTML -Fragment) ` 130 | +"
" 131 | $message = $message + "Memory utilization data:
" + ($metricsData | ` 132 | select-object @{Expression={$_.VMName};Label="Virtual Machine"}, ` 133 | @{Expression={$_.AvgRAM};Label="Average Memory (MB)"}, ` 134 | @{Expression={$_.MinRAM};Label="Minimum Memory (MB)"}, ` 135 | @{Expression={$_.MaxRAM};Label="Maximum Memory (MB)"} ` 136 | | ConvertTo-HTML -Fragment) ` 137 | +"
" 138 | $message = $message + "Network utilization data:
" + ($metricsData | ` 139 | select-object @{Expression={$_.VMName};Label="Virtual Machine"}, ` 140 | @{Expression={"{0:N2}" -f (($_.NetworkMeteredTrafficReport | where-object {($_.Direction -eq "Inbound")}` 141 | | measure-object TotalTraffic -sum).sum / 1024)};Label="Inbound Network Traffic (GB)"}, ` 142 | @{Expression={"{0:N2}" -f (($_.NetworkMeteredTrafficReport | where-object {($_.Direction -eq "Outbound")} ` 143 | | measure-object TotalTraffic -sum).sum / 1024)};Label="Outbound Network Traffic (GB)"} ` 144 | | ConvertTo-HTML -Fragment) ` 145 | +"
" 146 | $message = $message + "Disk utilization data:
" + ($metricsData | ` 147 | select-object @{Expression={$_.VMName};Label="Virtual Machine"}, ` 148 | @{Expression={"{0:N2}" -f ($_.TotalDisk / 1024)};Label="Disk Space Used (GB)"} ` 149 | | ConvertTo-HTML -Fragment) ` 150 | +"
" 151 | $message = $message + "Metering Duration data:
" + ($metricsData | ` 152 | select-object @{Expression={$_.VMName};Label="Virtual Machine"}, ` 153 | @{Expression={$_.MeteringDuration};Label="Metering data duration"} ` 154 | | ConvertTo-HTML -Fragment) ` 155 | +"
" 156 | 157 | # Reset metrics 158 | get-vm | Reset-VMResourceMetering 159 | get-vm | Enable-VMResourceMetering 160 | 161 | $message = $message + "" 162 | 163 | $email = new-object Net.Mail.MailMessage 164 | $email.Subject = "Hyper-V Server Report: $($filedate)" 165 | $email.From = new-object Net.Mail.MailAddress($FromEmail) 166 | $email.IsBodyHtml = $true 167 | $email.Body = $message 168 | $email.To.Add($ToEmail) 169 | 170 | # Send Email 171 | $smtp.Send($email) 172 | -------------------------------------------------------------------------------- /Daily-Status-Report/readme.md: -------------------------------------------------------------------------------- 1 | For information read: http://blogs.msdn.com/b/virtual_pc_guy/archive/2014/03/17/my-daily-hyper-v-status-email-part-1-of-5.aspx 2 | -------------------------------------------------------------------------------- /EvacuateDisk.ps1: -------------------------------------------------------------------------------- 1 | $sourcePath = "F:\" 2 | $destinationPath = "D:\" 3 | 4 | (Get-ChildItem $sourcePath -Recurse | Where-Object{$_.Attributes -ne "Directory"}).count 5 | 6 | Get-VM | ForEach-Object{ 7 | 8 | $VM = $_ 9 | 10 | # Check the configuration path 11 | $currentVMPath = $_.ConfigurationLocation 12 | 13 | if ($currentVMPath.StartsWith($sourcePath)) { 14 | $newVMPath = ($destinationPath) + ($currentVMPath.TrimStart($sourcePath)) 15 | Move-VMStorage -VM $VM -VirtualMachinePath $newVMPath} 16 | 17 | # Check the snapshot path 18 | $currentSnapshotFilePath = $_.SnapshotFileLocation 19 | 20 | if ($currentSnapshotFilePath.StartsWith($sourcePath)) { 21 | $newSnapshotFilePath = ($destinationPath) + ($currentSnapshotFilePath.TrimStart($sourcePath)) 22 | Move-VMStorage -VM $VM -SnapshotFilePath $newSnapshotFilePath} 23 | 24 | # Check the smart paging file path 25 | $currentSmartPagingFilePath = $_.SmartPagingFilePath 26 | 27 | if ($currentSmartPagingFilePath.StartsWith($sourcePath)) { 28 | $newSmartPagingFilePath = ($destinationPath) + ($currentSmartPagingFilePath.TrimStart($sourcePath)) 29 | Move-VMStorage -VM $VM -SmartPagingFilePath $newSmartPagingFilePath} 30 | 31 | # Go over each hard drive 32 | $_.HardDrives | ForEach-Object{ 33 | # Check the hard drive 34 | $currentHardDrivePath = $_.Path 35 | 36 | if ($currentHardDrivePath.StartsWith($sourcePath)) { 37 | $newHardDrivePath = ($destinationPath) + ($currentHardDrivePath.TrimStart($sourcePath)) 38 | Move-VMStorage -VM $VM -VHDs @(@{"SourceFilePath" = $currentHardDrivePath; "DestinationFilePath" = $newHardDrivePath})} 39 | } 40 | 41 | } 42 | 43 | (Get-ChildItem $sourcePath -Recurse | Where-Object{$_.Attributes -ne "Directory"}).count 44 | -------------------------------------------------------------------------------- /Expand-VMConfig.ps1: -------------------------------------------------------------------------------- 1 | Function Expand-VMConfig ($VMConfig) { 2 | $tempVM = (Compare-VM -Register -Path $VMConfig).VM 3 | 4 | write-host "VM Configuration Data" 5 | write-host "=====================" 6 | $tempVM | Select-Object * 7 | 8 | write-host "VM Network Adapters" 9 | write-host "=====================" 10 | $tempVM.NetworkAdapters 11 | 12 | write-host "VM Hard Drives" 13 | write-host "=====================" 14 | $tempVM.HardDrives 15 | 16 | write-host "VM DVD Drives" 17 | write-host "=====================" 18 | $tempVM.DVDDrives 19 | 20 | write-host "VM Floppy Drive" 21 | write-host "=====================" 22 | $tempVM.FloppyDrive 23 | 24 | write-host "VM Fibre Channel" 25 | write-host "=====================" 26 | $tempVM.FibreChannelHostBusAdapters 27 | 28 | write-host "VM COM1" 29 | write-host "=====================" 30 | $tempVM.ComPort1 31 | 32 | write-host "VM COM2" 33 | write-host "=====================" 34 | $tempVM.ComPort2} 35 | -------------------------------------------------------------------------------- /GuestNameCheck.ps1: -------------------------------------------------------------------------------- 1 | # Get the virtual machine name from the parent partition 2 | $vmName = (Get-ItemProperty –path “HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters”).VirtualMachineName 3 | # Replace any non-alphanumeric characters with an underscore 4 | $vmName = [Regex]::Replace($vmName,"\W","_") 5 | # Trim names that are longer than 15 characters 6 | $vmName = $vmName.Substring(0,[System.Math]::Min(15, $vmName.Length)) 7 | 8 | # Check the trimmed and cleaned VM name against the guest OS name 9 | # If it is different, change the guest OS name and reboot 10 | if ($env:computername -ne $vmName) {(Get-WmiObject win32_computersystem).Rename($vmName); shutdown -r -t 0} 11 | -------------------------------------------------------------------------------- /Image-Factory/.gitignore: -------------------------------------------------------------------------------- 1 | images-with-product-keys.csv 2 | FactoryVariables-personal.ps1 -------------------------------------------------------------------------------- /Image-Factory/Factory.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | [string]$variables=".\FactoryVariables.ps1", 4 | [string]$images 5 | ) 6 | 7 | # Load variables from a seperate file - this when you pull down the latest factory file you can keep your paths / product keys etc... 8 | . $variables 9 | $startTime = get-date 10 | 11 | 12 | [string] $driveLetter = "" 13 | 14 | 15 | # Helper function to make sure that needed folders are present 16 | function checkPath 17 | { 18 | param 19 | ( 20 | [string] $path 21 | ) 22 | if (!(Test-Path $path)) 23 | { 24 | $null = md $path; 25 | } 26 | } 27 | 28 | # Test that necessary paths and files exist 29 | # Don't create the workingDir automatically - if it doesn't exist, the variables probably need the be configured first. 30 | if(-not (Test-Path -Path $workingDir -PathType Container)) { 31 | Throw "Working directory $workingDir does not exist - edit FactoryVariables.ps1 to configure script" 32 | } 33 | 34 | # Create folders in workingDir if they don't exist 35 | checkPath "$($workingdir)\Share" 36 | checkPath "$($workingdir)\Bases" 37 | checkPath "$($workingdir)\ISOs" 38 | checkPath "$($workingdir)\Resources" 39 | checkPath "$($workingdir)\Resources\bits" 40 | 41 | ### Load Convert-WindowsImage - making sure it exists and is unblocked 42 | if(Test-Path -Path "$($workingDir)\resources\Convert-WindowsImage.ps1") { 43 | . "$($workingDir)\resources\Convert-WindowsImage.ps1" 44 | if(!(Get-Command Convert-WindowsImage -ErrorAction SilentlyContinue)) { 45 | Write-Host -ForegroundColor Green 'Convert-WindowsImage.ps1 could not be loaded. Unblock the script or check execution policy' 46 | Throw 'Convert-WindowsImage was not loaded' 47 | } 48 | } else { 49 | Write-Host -ForegroundColor Green "Please download Convert-WindowsImage.ps1 and place in $($workingDir)\Resources\" 50 | Write-Host -ForegroundColor Green "`nhttps://gallery.technet.microsoft.com/scriptcenter/Convert-WindowsImageps1-0fe23a8f`n" 51 | Throw 'Missing Convert-WindowsImage.ps1 script' 52 | } 53 | 54 | # Check that PSWindowsUpdate module exists 55 | if(!(Test-Path -Path "$($workingDir)\Resources\bits\PSWindowsUpdate\PSWindowsUpdate.psm1")) { 56 | Write-Host -ForegroundColor Green "Please download PSWindowsUpdate and extract to $($workingDir)\Resources\bits" 57 | Write-Host -ForegroundColor Green "`nhttps://gallery.technet.microsoft.com/scriptcenter/2d191bcd-3308-4edd-9de2-88dff796b0bc`n" 58 | Throw 'Missing PSWindowsUpdate module' 59 | } 60 | 61 | 62 | ### Sysprep unattend XML 63 | $unattendSource = [xml]@" 64 | 65 | 66 | 67 | 68 | 69 | * 70 | Key 71 | Organization 72 | Owner 73 | TZ 74 | 75 | 76 | false 77 | 78 | 79 | 0 80 | 81 | 82 | 83 | 84 | true 85 | all 86 | @FirewallAPI.dll,-28752 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | true 95 | true 96 | true 97 | Work 98 | 1 99 | 100 | 101 | 102 | password 103 | True</PlainText> 104 | </AdministratorPassword> 105 | <LocalAccounts> 106 | <LocalAccount wcm:action="add"> 107 | <Password> 108 | <Value>password</Value> 109 | <PlainText>True</PlainText> 110 | </Password> 111 | <DisplayName>Demo</DisplayName> 112 | <Group>Administrators</Group> 113 | <Name>demo</Name> 114 | </LocalAccount> 115 | </LocalAccounts> 116 | </UserAccounts> 117 | <AutoLogon> 118 | <Password> 119 | <Value>password</Value> 120 | </Password> 121 | <Enabled>true</Enabled> 122 | <LogonCount>1000</LogonCount> 123 | <Username>Administrator</Username> 124 | </AutoLogon> 125 | <LogonCommands> 126 | <AsynchronousCommand wcm:action="add"> 127 | <CommandLine>%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell -NoLogo -NonInteractive -ExecutionPolicy Unrestricted -File %SystemDrive%\Bits\Logon.ps1</CommandLine> 128 | <Order>1</Order> 129 | </AsynchronousCommand> 130 | </LogonCommands> 131 | </component> 132 | <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"> 133 | <InputLocale>en-us</InputLocale> 134 | <SystemLocale>en-us</SystemLocale> 135 | <UILanguage>en-us</UILanguage> 136 | <UILanguageFallback>en-us</UILanguageFallback> 137 | <UserLocale>en-us</UserLocale> 138 | </component> 139 | </settings> 140 | </unattend> 141 | "@ 142 | 143 | function CSVLogger { 144 | param 145 | ( 146 | [string] $vhd, 147 | [switch] $sysprepped 148 | ); 149 | 150 | $createLogFile = $false; 151 | $entryExists = $false; 152 | $logCsv = @(); 153 | $newEntry = $null; 154 | 155 | # Check if the log file exists 156 | if (-not (Test-Path $logFile)) 157 | { 158 | $createLogFile = $true; 159 | } 160 | else 161 | { 162 | $logCsv = import-csv $logFile; 163 | 164 | if (($logCsv.Image -eq $null) -or ` 165 | ($logCsv.Created -eq $null) -or ` 166 | ($logCsv.Sysprepped -eq $null) -or ` 167 | ($logCsv.Checked -eq $null)) 168 | { 169 | # Something is wrong with the log file 170 | cleanupFile $logFile; 171 | $createLogFile = $true; 172 | } 173 | } 174 | 175 | if ($createLogFile) 176 | { 177 | $logCsv = @(); 178 | } 179 | else 180 | { 181 | $logCsv = Import-Csv $logFile; 182 | } 183 | 184 | # If we find an entry for the VHD, update it 185 | foreach ($entry in $logCsv) 186 | { 187 | if ($entry.Image -eq $vhd) 188 | { 189 | $entryExists = $true; 190 | $entry.Checked = ((Get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString()); 191 | 192 | if ($sysprepped) 193 | { 194 | $entry.Sysprepped = ((Get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString()); 195 | } 196 | } 197 | } 198 | 199 | # if no entry is found, create a new one 200 | if (-not $entryExists) 201 | { 202 | $newEntry = New-Object PSObject -Property @{ 203 | Image = $vhd 204 | Created = ((Get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString()) 205 | Sysprepped = ((Get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString()) 206 | Checked = ((Get-Date).ToShortDateString() + "::" + (Get-Date).ToShortTimeString()) 207 | }; 208 | } 209 | 210 | # Write out the CSV file 211 | $logCsv | Export-CSV $logFile -NoTypeInformation; 212 | if (-not ($newEntry -eq $null)) 213 | { 214 | $newEntry | Export-CSV $logFile -NoTypeInformation -Append; 215 | } 216 | } 217 | 218 | function Logger { 219 | param 220 | ( 221 | [string]$systemName, 222 | [string]$message 223 | ); 224 | 225 | # Function for displaying formatted log messages. Also displays time in minutes since the script was started 226 | write-host (Get-Date).ToShortTimeString() -ForegroundColor Cyan -NoNewline; 227 | write-host " - [" -ForegroundColor White -NoNewline; 228 | write-host $systemName -ForegroundColor Yellow -NoNewline; 229 | write-Host "]::$($message)" -ForegroundColor White; 230 | } 231 | 232 | # Helper function for no error file cleanup 233 | function cleanupFile 234 | { 235 | param 236 | ( 237 | [string] $file 238 | ) 239 | 240 | if (Test-Path $file) 241 | { 242 | Remove-Item $file -Recurse > $null; 243 | } 244 | } 245 | 246 | 247 | 248 | function GetUnattendChunk 249 | { 250 | param 251 | ( 252 | [string] $pass, 253 | [string] $component, 254 | [xml] $unattend 255 | ); 256 | 257 | # Helper function that returns one component chunk from the Unattend XML data structure 258 | return $Unattend.unattend.settings | ? pass -eq $pass ` 259 | | select -ExpandProperty component ` 260 | | ? name -eq $component; 261 | } 262 | 263 | function makeUnattendFile 264 | { 265 | param 266 | ( 267 | [string] $key, 268 | [string] $logonCount, 269 | [string] $filePath, 270 | [bool] $desktop = $false, 271 | [bool] $is32bit = $false 272 | ); 273 | 274 | # Composes unattend file and writes it to the specified filepath 275 | 276 | # Reload template - clone is necessary as PowerShell thinks this is a "complex" object 277 | $unattend = $unattendSource.Clone(); 278 | 279 | # Customize unattend XML 280 | GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.ProductKey = $key}; 281 | GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.RegisteredOrganization = $Organization}; 282 | GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.RegisteredOwner = $Owner}; 283 | GetUnattendChunk "specialize" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.TimeZone = $Timezone}; 284 | GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.UserAccounts.AdministratorPassword.Value = $adminPassword}; 285 | GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.AutoLogon.Password.Value = $adminPassword}; 286 | GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.AutoLogon.LogonCount = $logonCount}; 287 | 288 | if ($desktop) 289 | { 290 | GetUnattendChunk "oobeSystem" "Microsoft-Windows-Shell-Setup" $unattend | %{$_.UserAccounts.LocalAccounts.LocalAccount.Password.Value = $userPassword}; 291 | # HideLocalAccountScreen setting applies only to the Windows Server editions, and only to Windows Server 2012 and above 292 | # This will remove the setting for desktop images 293 | $ns = New-Object System.Xml.XmlNamespaceManager($unattend.NameTable); 294 | $ns.AddNamespace("ns", $unattend.DocumentElement.NamespaceURI); 295 | $node = $unattend.SelectSingleNode("//ns:HideLocalAccountScreen", $ns); 296 | $node.ParentNode.RemoveChild($node) | Out-Null; 297 | } 298 | else 299 | { 300 | # Desktop needs a user other than "Administrator" to be present 301 | # This will remove the creation of the other user for server images 302 | $ns = New-Object System.Xml.XmlNamespaceManager($unattend.NameTable); 303 | $ns.AddNamespace("ns", $unattend.DocumentElement.NamespaceURI); 304 | $node = $unattend.SelectSingleNode("//ns:LocalAccounts", $ns); 305 | $node.ParentNode.RemoveChild($node) | Out-Null; 306 | 307 | if ($FriendlyName.substring(0,19) -eq "Windows Server 2008") 308 | { 309 | # HideLocalAccountScreen setting applies only to the Windows Server editions, and only to Windows Server 2012 and above 310 | # This will remove the setting for Windows Server 2008 R2 images 311 | $ns = New-Object System.Xml.XmlNamespaceManager($unattend.NameTable); 312 | $ns.AddNamespace("ns", $unattend.DocumentElement.NamespaceURI); 313 | $node = $unattend.SelectSingleNode("//ns:HideLocalAccountScreen", $ns); 314 | $node.ParentNode.RemoveChild($node) | Out-Null; 315 | } 316 | } 317 | 318 | if ($is32bit) 319 | { 320 | $unattend.InnerXml = $unattend.InnerXml.Replace('processorArchitecture="amd64"', 'processorArchitecture="x86"'); 321 | } 322 | 323 | # Write it out to disk 324 | cleanupFile $filePath; $Unattend.Save($filePath); 325 | } 326 | 327 | function createRunAndWaitVM 328 | { 329 | param 330 | ( 331 | [string] $vhd, 332 | [string] $gen 333 | ); 334 | 335 | # Function for whenever I have a VHD that is ready to run 336 | New-VM $factoryVMName -MemoryStartupBytes $VMMemory -VHDPath $vhd -Generation $Gen -SwitchName $virtualSwitchName -ErrorAction Stop| Out-Null 337 | 338 | If($UseVLAN) { 339 | Get-VMNetworkAdapter -VMName $factoryVMName | Set-VMNetworkAdapterVlan -Access -VlanId $VlanId 340 | } 341 | 342 | set-vm -Name $factoryVMName -ProcessorCount 2; 343 | Start-VM $factoryVMName; 344 | 345 | # Give the VM a moment to start before we start checking for it to stop 346 | Sleep -Seconds 10; 347 | 348 | # Wait for the VM to be stopped for a good solid 5 seconds 349 | do 350 | { 351 | $state1 = (Get-VM | ? name -eq $factoryVMName).State; 352 | Start-Sleep -Seconds 5; 353 | 354 | $state2 = (Get-VM | ? name -eq $factoryVMName).State; 355 | Start-Sleep -Seconds 5; 356 | } 357 | until (($state1 -eq "Off") -and ($state2 -eq "Off")) 358 | 359 | # Clean up the VM 360 | Remove-VM $factoryVMName -Force; 361 | } 362 | 363 | function MountVHDandRunBlock 364 | { 365 | param 366 | ( 367 | [string]$vhd, 368 | [scriptblock]$block, 369 | [switch]$ReadOnly 370 | ); 371 | 372 | # This function mounts a VHD, runs a script block and unmounts the VHD. 373 | # Drive letter of the mounted VHD is stored in $driveLetter - can be used by script blocks 374 | if($ReadOnly) { 375 | $driveLetter = (Mount-VHD $vhd -ReadOnly -Passthru | Get-Disk | Get-Partition | Where { -not $_.IsHidden } | Get-Volume | Where { $_.DriveLetter }).DriveLetter; 376 | } else { 377 | $driveLetter = (Mount-VHD $vhd -Passthru | Get-Disk | Get-Partition | Where { -not $_.IsHidden } | Get-Volume | Where { $_.DriveLetter } ).DriveLetter; 378 | } 379 | & $block; 380 | 381 | Dismount-VHD $vhd; 382 | 383 | # Wait 2 seconds for activity to clean up 384 | Start-Sleep -Seconds 2; 385 | } 386 | 387 | ### Update script block 388 | $updateCheckScriptBlock = { 389 | function Logger { 390 | param 391 | ( 392 | [string]$message 393 | ); 394 | 395 | # Function for displaying formatted log messages. Also displays time in minutes since the script was started 396 | write-host (Get-Date).ToShortTimeString() -ForegroundColor Cyan -NoNewline; 397 | write-host " - ::$($message)" -ForegroundColor White; 398 | } 399 | 400 | # Clean up unattend file if it is there 401 | if (Test-Path "$ENV:SystemDrive\Unattend.xml") 402 | { 403 | Remove-Item -Force "$ENV:SystemDrive\Unattend.xml" 404 | } 405 | 406 | # Elements of the script rely on PowerShell 3.0, so install updated PowerShell if necessary 407 | if ($PSVersionTable.PSVersion.Major -lt 3) 408 | { 409 | logger "Not running PowerShell 3.0 or above" 410 | # First check .NET Framework 4.5 full version prerequisite 411 | $DNVersion = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse | Get-ItemProperty -name Version -EA 0 | Where-Object { $_.PSChildName -match '^(?!S)\p{L}'} | Sort-Object version -Descending | Select-Object -ExpandProperty Version -First 1 412 | $DNVersions = $DNVersion.Split(".") 413 | $DNVersionMajor = $DNVersions[0] 414 | $DNVersionMinor = $DNVersions[1] 415 | $DNVersionBuild = $DNVersions[2] 416 | 417 | if (($DNVersionMajor -eq 4 -and $DNVersionMinor -ge 5) -or ($DNVersionMajor -ge 4)) 418 | { 419 | logger ".NET prerequisites met" 420 | } 421 | else 422 | { 423 | logger ".NET update required" 424 | if (!(test-path -Path "C:\Temp")) 425 | { 426 | New-Item -ItemType Directory -Force -Path "C:\Temp" > $null 427 | } 428 | logger "Downloading .NET 4.5.2" 429 | $download = New-Object Net.WebClient 430 | $url = "http://download.microsoft.com/download/E/2/1/E21644B5-2DF2-47C2-91BD-63C560427900/NDP452-KB2901907-x86-x64-AllOS-ENU.exe" 431 | $file = ("C:\Temp\NDP452-KB2901907-x86-x64-AllOS-ENU.exe") 432 | $download.Downloadfile($url,$file) 433 | if (!(Test-Path -Path "C:\Temp\NDP452-KB2901907-x86-x64-AllOS-ENU.exe")) 434 | { 435 | logger "Download failed. Please check your Internet connection" 436 | } 437 | else 438 | { 439 | logger "Installing .NET 4.5.2" 440 | $InstallDotNet = Start-Process $file -ArgumentList "/q /norestart" -Wait -PassThru 441 | logger ".NET 4.5.2 installation complete" 442 | } 443 | } 444 | 445 | logger "Downloading Windows Management Framework 4.0" 446 | if (!(test-path -Path "C:\Temp")) 447 | { 448 | New-Item -ItemType Directory -Force -Path "C:\Temp" > $null 449 | } 450 | $download = New-Object Net.WebClient 451 | $url = "http://download.microsoft.com/download/3/D/6/3D61D262-8549-4769-A660-230B67E15B25/Windows6.1-KB2819745-x64-MultiPkg.msu" 452 | $file = ("C:\Temp\Windows6.1-KB2819745-x64-MultiPkg.msu") 453 | $commandline = "C:\Temp\Windows6.1-KB2819745-x64-MultiPkg.msu /quiet /norestart" 454 | $download.Downloadfile($url,$file) 455 | if (!(Test-Path -Path "C:\Temp\Windows6.1-KB2819745-x64-MultiPkg.msu")) 456 | { 457 | logger "Download failed. Please check your Internet connection" 458 | } 459 | else 460 | { 461 | logger "Installing Windows Management Framework 4.0" 462 | $InstallWMF = Start-Process -FilePath 'wusa.exe' -ArgumentList "$commandline" -Wait -PassThru 463 | logger "Windows Management Framework 4.0 installation complete" 464 | } 465 | Invoke-Expression 'shutdown -r -t 0' 466 | } 467 | 468 | # Check to see if files need to be unblocked - if they do, do it and reboot 469 | if ((Get-ChildItem $env:SystemDrive\Bits | ` 470 | Get-Item -Stream "Zone.Identifier" -ErrorAction SilentlyContinue).Count -gt 0) 471 | { 472 | Get-ChildItem $env:SystemDrive\Bits | Unblock-File; 473 | Invoke-Expression 'shutdown -r -t 0' 474 | } 475 | 476 | # To get here - the files are unblocked 477 | Import-Module $env:SystemDrive\Bits\PSWindowsUpdate\PSWindowsUpdate; 478 | 479 | # Set static IP address - do not change values here, change them in FactoryVariables.ps1 480 | $UseStaticIP = STATICIPBOOLPLACEHOLDER 481 | if($UseStaticIP) { 482 | $IP = 'IPADDRESSPLACEHOLDER' 483 | $MaskBits = 'SUBNETMASKPLACEHOLDER' 484 | $Gateway = 'GATEWAYPLACEHOLDER' 485 | $DNS = 'DNSPLACEHOLDER' 486 | $IPType = 'IPTYPEPLACEHOLDER' 487 | 488 | $adapter = Get-NetAdapter | Where-Object {$_.Status -eq 'up'} 489 | # Remove any existing IP, gateway from our ipv4 adapter 490 | If (($adapter | Get-NetIPConfiguration).IPv4Address.IPAddress) { 491 | $adapter | Remove-NetIPAddress -AddressFamily $IPType -Confirm:$false 492 | } 493 | If (($adapter | Get-NetIPConfiguration).Ipv4DefaultGateway) { 494 | $adapter | Remove-NetRoute -AddressFamily $IPType -Confirm:$false 495 | } 496 | # Configure the IP address and default gateway 497 | $adapter | New-NetIPAddress -AddressFamily $IPType ` 498 | -IPAddress $IP ` 499 | -PrefixLength $MaskBits ` 500 | -DefaultGateway $Gateway 501 | # Configure the DNS client server IP addresses 502 | $adapter | Set-DnsClientServerAddress -ServerAddresses $DNS 503 | } 504 | 505 | 506 | 507 | # Need to add check for Internet connectivity due to Windows 7 driver load timing fail 508 | logger "Checking for Internet connection" 509 | do 510 | { 511 | Start-Sleep -Seconds 5; 512 | logger "Checking for Internet connection" 513 | } until (Test-Connection -computername www.microsoft.com) 514 | 515 | # Run pre-update script if it exists 516 | if (Test-Path "$env:SystemDrive\Bits\PreUpdateScript.ps1") { 517 | & "$env:SystemDrive\Bits\PreUpdateScript.ps1" 518 | } 519 | 520 | # Check if any updates are needed - leave a marker if there are 521 | logger "Checking for updates" 522 | if ((Get-WUList).Count -gt 0) 523 | { 524 | if (-not (Test-Path $env:SystemDrive\Bits\changesMade.txt)) 525 | { 526 | New-Item $env:SystemDrive\Bits\changesMade.txt -type file > $null; 527 | } 528 | } 529 | 530 | # Apply all the updates 531 | logger "Applying the updates" 532 | Get-WUInstall -AcceptAll -IgnoreReboot -IgnoreUserInput -NotCategory "Language packs"; 533 | 534 | # Run post-update script if it exists 535 | if (Test-Path "$env:SystemDrive\Bits\PostUpdateScript.ps1") { 536 | & "$env:SystemDrive\Bits\PostUpdateScript.ps1" 537 | } 538 | 539 | # Remove static IP address 540 | if($UseStaticIP) { 541 | $adapter = Get-NetAdapter | ? {$_.Status -eq "up"} 542 | $interface = $adapter | Get-NetIPInterface -AddressFamily $IPType 543 | 544 | If ($interface.Dhcp -eq "Disabled") { 545 | If (($interface | Get-NetIPConfiguration).Ipv4DefaultGateway) { 546 | $interface | Remove-NetRoute -Confirm:$false 547 | } 548 | $interface | Set-NetIPInterface -DHCP Enabled 549 | $interface | Set-DnsClientServerAddress -ResetServerAddresses 550 | } 551 | } 552 | 553 | # Reboot if needed - otherwise shutdown because we are done 554 | if (Get-WURebootStatus -Silent) 555 | { 556 | Invoke-Expression 'shutdown -r -t 0'; 557 | } 558 | else 559 | { 560 | invoke-expression 'shutdown -s -t 0'; 561 | } 562 | }; 563 | 564 | function Set-UpdateCheckPlaceHolders { 565 | $block = $updateCheckScriptBlock | Out-String -Width 4096 566 | 567 | if($UseStaticIP) { 568 | $block = $block.Replace('$UseStaticIP = STATICIPBOOLPLACEHOLDER', '$UseStaticIP = $true') 569 | $block = $block.Replace('IPADDRESSPLACEHOLDER', $IP) 570 | $block = $block.Replace('SUBNETMASKPLACEHOLDER', $MaskBits) 571 | $block = $block.Replace('GATEWAYPLACEHOLDER', $Gateway) 572 | $block = $block.Replace('DNSPLACEHOLDER', $DNS) 573 | $block = $block.Replace('IPTYPEPLACEHOLDER', $IPType) 574 | } else { 575 | $block = $block.Replace('$UseStaticIP = STATICIPBOOLPLACEHOLDER', '$UseStaticIP = $false') 576 | } 577 | return $block 578 | } 579 | 580 | ### Sysprep script block 581 | $sysprepScriptBlock = { 582 | # Run pre-sysprep script if it exists 583 | if (Test-Path "$env:SystemDrive\Bits\PreSysprepScript.ps1") { 584 | & "$env:SystemDrive\Bits\PreSysprepScript.ps1" 585 | } 586 | 587 | # Remove Unattend entries from the autorun key if they exist 588 | foreach ($regvalue in (Get-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run).Property) 589 | { 590 | if ($regvalue -like "Unattend*") 591 | { 592 | # could be multiple unattend* entries 593 | foreach ($unattendvalue in $regvalue) 594 | { 595 | Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run -name $unattendvalue 596 | } 597 | } 598 | } 599 | 600 | $unattendedXmlPath = "$ENV:SystemDrive\Bits\Unattend.xml"; 601 | & "$ENV:SystemRoot\System32\Sysprep\Sysprep.exe" `/generalize `/oobe `/shutdown `/unattend:"$unattendedXmlPath"; 602 | }; 603 | 604 | ### Post Sysprep script block 605 | $postSysprepScriptBlock = { 606 | # Remove Unattend entries from the autorun key if they exist 607 | foreach ($regvalue in (Get-Item -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run).Property) 608 | { 609 | if ($regvalue -like "Unattend*") 610 | { 611 | # could be multiple unattend* entries 612 | foreach ($unattendvalue in $regvalue) 613 | { 614 | Remove-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run -name $unattendvalue 615 | } 616 | } 617 | } 618 | 619 | # Run post-sysprep script if it exists 620 | if (Test-Path "$env:SystemDrive\Bits\PostSysprepScript.ps1") { 621 | & "$env:SystemDrive\Bits\PostSysprepScript.ps1" 622 | } 623 | 624 | # Clean up unattend file if it is there 625 | if (Test-Path "$ENV:SystemDrive\Unattend.xml") 626 | { 627 | Remove-Item -Force "$ENV:SystemDrive\Unattend.xml"; 628 | } 629 | 630 | # Clean up bits 631 | if(Test-Path "$ENV:SystemDrive\Bits") 632 | { 633 | Remove-Item -Force -Recurse "$ENV:SystemDrive\Bits"; 634 | } 635 | 636 | # Clean up temp 637 | if(Test-Path "$ENV:SystemDrive\Temp") 638 | { 639 | Remove-Item -Force -Recurse "$ENV:SystemDrive\Temp"; 640 | } 641 | 642 | # Remove Demo user 643 | $computer = $env:computername 644 | $user = "Demo" 645 | if ([ADSI]::Exists("WinNT://$computer/$user")) 646 | { 647 | [ADSI]$server = "WinNT://$computer" 648 | $server.delete("user",$user) 649 | } 650 | 651 | # Put any code you want to run Post sysprep here 652 | Invoke-Expression 'shutdown -r -t 0'; 653 | }; 654 | 655 | # This is the main function of this script 656 | function Start-ImageFactory 657 | { 658 | <# 659 | .SYNOPSIS 660 | Creates or updates a windows image with the latest windows updates. 661 | 662 | .DESCRIPTION 663 | This function creates fully updated VHD images for deployment as virtual machines. 664 | 665 | The first time it is called for a particular windows type, it creates a fresh install in a Hyper-V virtual machine, 666 | updates it, and saves a sysprepped image. The unsysprepped VHD is kept so future runs only need to install new updates. 667 | 668 | Much of the configuration is done in FactoryVariables.ps1, which must be edited to suit your environment. 669 | 670 | .PARAMETER FriendlyName 671 | Used as the file name for the image. 672 | 673 | .PARAMETER ISOFile 674 | The ISO or WIM file to use as the base of the windows image. 675 | 676 | .PARAMETER SKUEdition 677 | The name of index number of the edition to use from the ISO/WIM file. 678 | eg. "Professional" or "ServerDataCenter" 679 | 680 | .PARAMETER ProductKey 681 | The product key to use for the unattended installation. 682 | 683 | .PARAMETER Desktop 684 | Set to $true for desktop windows versions. Creates a regular user account, which is required by the desktop unattended installation. 685 | 686 | .PARAMETER Is32Bit 687 | Set to $true for 32 bit images to create the unattend file correctly. 688 | 689 | .PARAMETER Generation2 690 | Create a generation 2 virtual machine. Default is generation 1 691 | 692 | .PARAMETER GenericSysprep 693 | Run the final sysprep without specifying a /unattend file. This is required by some deployment tools like SCVMM which will run their own sysprep when deploying. 694 | 695 | .NOTES 696 | This script requires two additional downloads: 697 | PSWindowsUpdate from https://gallery.technet.microsoft.com/scriptcenter/2d191bcd-3308-4edd-9de2-88dff796b0bc 698 | 699 | Convert-WindowsImage.ps1 from https://gallery.technet.microsoft.com/scriptcenter/Convert-WindowsImageps1-0fe23a8f 700 | 701 | .LINK 702 | https://github.com/BenjaminArmstrong/Hyper-V-PowerShell 703 | 704 | .EXAMPLE 705 | Start-ImageFactory -FriendlyName "Windows Server 2012 R2 DataCenter with GUI" -ISOFile c:\path\to\isos\server2012r2.iso -ProductKey "W3GGN-FT8W3-Y4M27-J84CP-Q3VJ9" -SKUEdition "ServerDataCenter" 706 | 707 | .EXAMPLE 708 | Start-ImageFactory -FriendlyName "Windows Server 2012 R2 DataCenter Core - Gen 2" -ISOFile c:\path\to\isos\server2012r2.iso -ProductKey "W3GGN-FT8W3-Y4M27-J84CP-Q3VJ9" -SKUEdition "ServerDataCenterCore" -Generation2 709 | 710 | .EXAMPLE 711 | Start-ImageFactory -FriendlyName "Windows 8.1 Professional - 32 bit" -ISOFile c:\path\to\isos\windows81.iso -ProductKey "GCRJD-8NW9H-F2CDX-CCM8D-9D6T9" -SKUEdition "Professional" -desktop $true -is32bit $true 712 | 713 | #> 714 | 715 | 716 | param 717 | ( 718 | [Parameter(Mandatory=$true)][string]$FriendlyName, 719 | [string]$ISOFile, 720 | [string]$ProductKey, 721 | [string]$SKUEdition, 722 | [bool]$desktop = $false, 723 | [bool]$is32bit = $false, 724 | [switch]$Generation2, 725 | [bool] $GenericSysprep = $false 726 | ); 727 | 728 | logger $FriendlyName "Starting a new cycle!" 729 | 730 | # Setup a bunch of variables 731 | $sysprepNeeded = $true; 732 | 733 | $VHDFormat = "vhdx"; 734 | if ($LegacyVHD) 735 | { 736 | $VHDFormat = "vhd"; 737 | } 738 | 739 | $baseVHD = "$($workingDir)\bases\$($FriendlyName)-base.$($VHDFormat)"; 740 | $updateVHD = "$($workingDir)\$($FriendlyName)-update.$($VHDFormat)"; 741 | $sysprepVHD = "$($workingDir)\$($FriendlyName)-sysprep.$($VHDFormat)"; 742 | $finalVHD = "$($workingDir)\share\$($FriendlyName).$($VHDFormat)"; 743 | 744 | $VHDPartitionStyle = "MBR"; 745 | $Gen = 1; 746 | if ($Generation2) 747 | { 748 | $VHDPartitionStyle = "GPT"; 749 | $Gen = 2; 750 | } 751 | 752 | # Verify product key 753 | if(-not ($ProductKey -imatch '^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{5}$')) { 754 | logger $FriendlyName 'Invalid product key, skipping this product' 755 | return 756 | } 757 | 758 | logger $FriendlyName "Checking for existing Factory VM"; 759 | 760 | # Check if there is already a factory VM - and kill it if there is 761 | if ((Get-VM | ? Name -eq $factoryVMName).Count -gt 0) 762 | { 763 | Stop-VM $factoryVMName -TurnOff -Confirm:$false -Passthru | Remove-VM -Force; 764 | } 765 | 766 | # Check for a base VHD 767 | if (-not (test-path $baseVHD)) 768 | { 769 | if (-not (Test-Path $ISOFile)) { 770 | logger $FriendlyName 'ISO/WIM file missing, skipping this product.' 771 | return 772 | } 773 | 774 | # No base VHD - we need to create one 775 | logger $FriendlyName "No base VHD!"; 776 | 777 | # Make unattend file 778 | logger $FriendlyName "Creating unattend file for base VHD"; 779 | 780 | # Logon count is just "large number" 781 | makeUnattendFile -key $ProductKey -logonCount "1000" -filePath "$($workingDir)\unattend.xml" -desktop $desktop -is32bit $is32bit; 782 | 783 | # Time to create the base VHD 784 | logger $FriendlyName "Create base VHD using Convert-WindowsImage.ps1"; 785 | $ConvertCommand = "Convert-WindowsImage"; 786 | $ConvertCommand = $ConvertCommand + " -SourcePath `"$ISOFile`" -VHDPath `"$baseVHD`""; 787 | $ConvertCommand = $ConvertCommand + " -SizeBytes 80GB -VHDFormat $VHDFormat -UnattendPath `"$($workingDir)\unattend.xml`""; 788 | $ConvertCommand = $ConvertCommand + " -Edition $SKUEdition -VHDPartitionStyle $VHDPartitionStyle"; 789 | 790 | Invoke-Expression "& $ConvertCommand"; 791 | 792 | # Clean up unattend file - we don't need it any more 793 | logger $FriendlyName "Remove unattend file now that that is done"; 794 | cleanupFile "$($workingDir)\unattend.xml"; 795 | 796 | logger $FriendlyName "Mount VHD and copy bits in, also set startup file"; 797 | MountVHDandRunBlock $baseVHD { 798 | cleanupFile -file "$($driveLetter):\Convert-WindowsImageInfo.txt"; 799 | 800 | # Copy bits to VHD 801 | Copy-Item "$($ResourceDirectory)\bits" -Destination ($driveLetter + ":\") -Recurse; 802 | 803 | # Create first logon script 804 | Set-UpdateCheckPlaceHolders | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096; 805 | 806 | # Disable Windows 10 (todo: check if Windows 10 - but wont break stuff on non-Win 10 right?) 807 | & reg load HKLM\NewOS "$($driveLetter):\windows\system32\config\SOFTWARE" 808 | & reg add HKLM\NewOS\Policies\Microsoft\WindowsStore /v AutoDownload /t REG_DWORD /d 2 /f 809 | & reg unload HKLM\NewOS 810 | } 811 | 812 | logger $FriendlyName "Create virtual machine, start it and wait for it to stop..."; 813 | createRunAndWaitVM $baseVHD $Gen; 814 | 815 | # Remove Page file 816 | logger $FriendlyName "Removing the page file"; 817 | MountVHDandRunBlock $baseVHD { 818 | attrib -s -h "$($driveLetter):\pagefile.sys"; 819 | cleanupFile "$($driveLetter):\pagefile.sys"; 820 | } 821 | 822 | # Compact the base file 823 | logger $FriendlyName "Compacting the base file"; 824 | Optimize-VHD -Path $baseVHD -Mode Full; 825 | } 826 | else 827 | { 828 | # The base VHD existed - time to check if it needs an update 829 | logger $FriendlyName "Base VHD exists - need to check for updates"; 830 | 831 | # create new diff to check for updates 832 | logger $FriendlyName "Create new differencing disk to check for updates"; 833 | cleanupFile $updateVHD; 834 | New-VHD -Path $updateVHD -ParentPath $baseVHD | Out-Null; 835 | 836 | logger $FriendlyName "Copy login file for update check, also make sure flag file is cleared" 837 | MountVHDandRunBlock $updateVHD { 838 | # Refresh the Bits folder 839 | cleanupFile "$($driveLetter):\Bits" 840 | Copy-Item "$($ResourceDirectory)\bits" -Destination ($driveLetter + ":\") -Recurse; 841 | # Create the update check logon script 842 | Set-UpdateCheckPlaceHolders | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096; 843 | } 844 | 845 | logger $FriendlyName "Create virtual machine, start it and wait for it to stop..."; 846 | createRunAndWaitVM $updateVHD $Gen; 847 | 848 | # Mount the VHD 849 | logger $FriendlyName "Mount the differencing disk"; 850 | $driveLetter = (Mount-VHD $updateVHD -Passthru | Get-Disk | Get-Partition | Get-Volume).DriveLetter; 851 | 852 | # Check to see if changes were made 853 | logger $FriendlyName "Check to see if there were any updates"; 854 | if (Test-Path "$($driveLetter):\Bits\changesMade.txt") 855 | { 856 | cleanupFile "$($driveLetter):\Bits\changesMade.txt"; 857 | logger $FriendlyName "Updates were found"; 858 | } 859 | else 860 | { 861 | logger $FriendlyName "No updates were found"; 862 | $sysprepNeeded = $false; 863 | } 864 | 865 | # Dismount 866 | logger $FriendlyName "Dismount the differencing disk"; 867 | Dismount-VHD $updateVHD; 868 | 869 | # Wait 2 seconds for activity to clean up 870 | Start-Sleep -Seconds 2; 871 | 872 | # If changes were made - merge them in. If not, throw it away 873 | if ($sysprepNeeded) 874 | { 875 | logger $FriendlyName "Merge the differencing disk"; 876 | Merge-VHD -Path $updateVHD -DestinationPath $baseVHD; 877 | } 878 | else 879 | { 880 | logger $FriendlyName "Delete the differencing disk"; 881 | CSVLogger $finalVHD; 882 | cleanupFile $updateVHD; 883 | } 884 | } 885 | 886 | # Final Check - if the final VHD is missing - we need to sysprep and make it 887 | if (-not (Test-Path $finalVHD)) 888 | { 889 | $sysprepNeeded = $true; 890 | } 891 | 892 | if ($sysprepNeeded) 893 | { 894 | # create new diff to sysprep 895 | logger $FriendlyName "Need to run Sysprep"; 896 | logger $FriendlyName "Creating differencing disk"; 897 | cleanupFile $sysprepVHD; new-vhd -Path $sysprepVHD -ParentPath $baseVHD | Out-Null; 898 | 899 | logger $FriendlyName "Mount the differencing disk and copy in files"; 900 | MountVHDandRunBlock $sysprepVHD { 901 | $sysprepScriptBlockString = $sysprepScriptBlock | Out-String; 902 | 903 | if($GenericSysprep) 904 | { 905 | $sysprepScriptBlockString = $sysprepScriptBlockString.Replace(' `/unattend:"$unattendedXmlPath"', ""); 906 | } 907 | else 908 | { 909 | # Make unattend file 910 | if (-not (Test-Path "$($driveLetter):\Bits")) { 911 | New-Item "$($driveLetter):\Bits" -ItemType Directory | Out-Null 912 | } 913 | 914 | makeUnattendFile -key $ProductKey -logonCount "1" -filePath "$($driveLetter):\Bits\unattend.xml" -desktop $desktop -is32bit $is32bit; 915 | } 916 | 917 | # Make the logon script 918 | cleanupFile "$($driveLetter):\Bits\Logon.ps1"; 919 | $sysprepScriptBlockString | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096; 920 | } 921 | 922 | logger $FriendlyName "Create virtual machine, start it and wait for it to stop..."; 923 | createRunAndWaitVM $sysprepVHD $Gen; 924 | 925 | logger $FriendlyName "Mount the differencing disk and cleanup files"; 926 | MountVHDandRunBlock $sysprepVHD { 927 | cleanupFile "$($driveLetter):\Bits\unattend.xml"; 928 | cleanupFile "$($driveLetter):\Bits\Logon.ps1"; 929 | 930 | if(-not $GenericSysprep) 931 | { 932 | # Make the logon script 933 | $postSysprepScriptBlock | Out-String | Out-File -FilePath "$($driveLetter):\Bits\Logon.ps1" -Width 4096; 934 | } 935 | else 936 | { 937 | # Cleanup \Bits as the postSysprepScriptBlock is not run anymore 938 | cleanupFile "$($driveLetter):\Bits"; 939 | } 940 | } 941 | 942 | # Remove Page file 943 | logger $FriendlyName "Removing the page file"; 944 | MountVHDandRunBlock $sysprepVHD { 945 | 946 | attrib -s -h "$($driveLetter):\pagefile.sys"; 947 | cleanupFile "$($driveLetter):\pagefile.sys"; 948 | } 949 | 950 | # Produce the final disk 951 | cleanupFile $finalVHD; 952 | logger $FriendlyName "Convert differencing disk into pristine base image"; 953 | Convert-VHD -Path $sysprepVHD -DestinationPath $finalVHD -VHDType Dynamic; 954 | if($CleanWinSXS) { 955 | logger $FriendlyName "Cleaning windows component store. Be patient, this may take awhile." 956 | MountVHDandRunBlock $finalVHD { 957 | # Clean up the WinSXS store, and remove any superceded components. Updates will no longer be able to be uninstalled, 958 | # but saves a considerable amount of disk space. 959 | dism.exe /image:$($driveLetter):\ /Cleanup-Image /StartComponentCleanup /ResetBase 960 | } 961 | } 962 | logger $FriendlyName "Optimizing VHD file" 963 | # Mounting the VHD read only allows it to be compacted better. 964 | # Running Optimize-VHD twice seems to be necessary - don't know why, but it works. 965 | MountVHDandRunBlock -ReadOnly $finalVHD { 966 | Optimize-VHD $finalVHD -Mode Full 967 | Optimize-VHD $finalVHD -Mode Full 968 | } 969 | logger $FriendlyName "Delete differencing disk"; 970 | CSVLogger $finalVHD -sysprepped; 971 | cleanupFile $sysprepVHD; 972 | } 973 | } 974 | 975 | function Process-ImagesFile { 976 | if (-not (Test-Path -Path $images -PathType Leaf)) { 977 | Write-Host -ForegroundColor Green "CSV Image file not found: $images" 978 | Throw 'Missing images csv file' 979 | } 980 | 981 | <# 982 | .SYNOPSIS 983 | Creates a friendly name for the target VM. 984 | 985 | .DESCRIPTION 986 | This function returns a string friendly name for the VM based on the target parameters. 987 | If the target specified a name explicitly, that name is returned. Otherwise, the name will be generated 988 | using this pattern: 989 | 990 | <product> <edition> [Core | with GUI] [- Gen 2] 991 | <product> <edition> <servicing branch> [- 32 bit] [- Gen 2] 992 | <product> <edition> <servicing branch> [- Gen 2] 993 | 994 | .PARAMETER target 995 | Specifies the target configuration. 996 | 997 | .PARAMETER gui 998 | Specifies if the VM OS will be GUI OS. If not GUI, it will be a Core OS. 999 | 1000 | .PARAMETER generation 1001 | Specifies the Hyper-V VM generation. 1 or 2. 1002 | #> 1003 | function Create-FriendlyName ($target, [bool]$gui, [int]$generation) { 1004 | if ($target.Name -ne "") { 1005 | # use the user supplied friendly name 1006 | return $target.Name 1007 | } else { 1008 | # Build FriendlyName compatible with existing system 1009 | # 1010 | # <product> <edition> [Core | with GUI] [- Gen 2] 1011 | # <product> <edition> <servicing branch> [- 32 bit] [- Gen 2] 1012 | # <product> <edition> <servicing branch> [- Gen 2] 1013 | $friendlyName = $target.Product + " " + $target.Edition 1014 | 1015 | switch ($target.Edition) 1016 | { 1017 | "DataCenter" { 1018 | if ($gui) { 1019 | $friendlyName += " with GUI"; 1020 | } else { 1021 | $friendlyName += " Core"; 1022 | } 1023 | } 1024 | 1025 | "Standard" { 1026 | if ($gui) { 1027 | $friendlyName += " with GUI"; 1028 | } else { 1029 | $friendlyName += " Core"; 1030 | } 1031 | } 1032 | 1033 | "Ultimate" { 1034 | } 1035 | 1036 | "Enterprise" { 1037 | # only Enterprise has servicing branches 1038 | if ($target.SB -ne "") { 1039 | $friendlyName += " " + $target.SB 1040 | } 1041 | } 1042 | 1043 | "Professional" { 1044 | } 1045 | } 1046 | 1047 | if ($target.Arch -eq "x86") { 1048 | $friendlyName += " - 32 bit"; 1049 | } 1050 | 1051 | if ($generation -eq 2) { 1052 | $friendlyName += " - Gen 2" 1053 | } 1054 | 1055 | return $friendlyName 1056 | } 1057 | } 1058 | 1059 | function Create-Arguments ($target, [bool]$gui, [int]$generation) { 1060 | $arguments = @{} 1061 | 1062 | $arguments.Add("ISOFile", $target.Image) 1063 | $arguments.Add("ProductKey", $target.ProductKey) 1064 | 1065 | # 1066 | $friendlyName = Create-FriendlyName -target $target -gui $gui -generation $generation 1067 | $arguments.Add("FriendlyName", $friendlyName) 1068 | 1069 | switch ($target.Edition) 1070 | { 1071 | "DataCenter" { 1072 | if ($gui) { 1073 | $arguments.Add("SKUEdition", "Server" + $target.Edition) 1074 | } else { 1075 | $arguments.Add("SKUEdition", "Server"+ $target.Edition+"Core") 1076 | } 1077 | } 1078 | 1079 | "Standard" { 1080 | if ($gui) { 1081 | $arguments.Add("SKUEdition", "Server" + $target.Edition) 1082 | } else { 1083 | $arguments.Add("SKUEdition", "Server"+ $target.Edition+"Core") 1084 | } 1085 | } 1086 | 1087 | "Ultimate" { 1088 | $arguments.Add("SKUEdition", $target.Edition) 1089 | $arguments.Add("desktop", $true) 1090 | } 1091 | 1092 | "Enterprise" { 1093 | $arguments.Add("SKUEdition", $target.Edition) 1094 | $arguments.Add("desktop", $true) 1095 | } 1096 | 1097 | "Professional" { 1098 | $arguments.Add("SKUEdition", $target.Edition) 1099 | $arguments.Add("desktop", $true) 1100 | } 1101 | } 1102 | 1103 | if ($target.Arch -eq "x86") { 1104 | } 1105 | 1106 | if ($generation -eq 2) { 1107 | $arguments.Add("Generation2", $true) 1108 | } 1109 | 1110 | 1111 | if ($target.Arch -eq "x86") { 1112 | $arguments.Add("Is32Bit", $true) 1113 | } 1114 | 1115 | return $arguments; 1116 | } 1117 | 1118 | <# 1119 | .SYNOPSIS 1120 | Creates zero or more arguments to be passed to Start-ImageFactory 1121 | 1122 | .DESCRIPTION 1123 | This function returns an array of arguments that can be passed to Start-ImageFactory. 1124 | The number of return values depends if the configuration specified a GUI, Core, Generation 1 or Generation 2 VM. 1125 | 1126 | .PARAMETER target 1127 | Specifies the target configuration. 1128 | #> 1129 | function Create-StartImageFactoryArguments ($target) { 1130 | $arguments = @() 1131 | 1132 | if ($target.GUI -eq "TRUE") { 1133 | 1134 | if ($target.Gen1 -eq "TRUE") { 1135 | $arguments += Create-Arguments -target $target -gui $true -generation 1 1136 | } 1137 | 1138 | if ($target.Gen2 -eq "TRUE") { 1139 | $arguments += Create-Arguments -target $target -gui $true -generation 2 1140 | } 1141 | } 1142 | 1143 | if ($target.Core -eq "TRUE") { 1144 | 1145 | if ($target.Gen1 -eq "TRUE") { 1146 | $arguments += Create-Arguments -target $target -gui $false -generation 1 1147 | } 1148 | 1149 | if ($target.Gen2 -eq "TRUE") { 1150 | $arguments += Create-Arguments -target $target -gui $false -generation 2 1151 | } 1152 | } 1153 | 1154 | return $arguments; 1155 | } 1156 | 1157 | <# 1158 | .SYNOPSIS 1159 | Gets the full path to image in the specified path. 1160 | 1161 | .DESCRIPTION 1162 | This function returns the full path or $null if the specified file was not found. 1163 | 1164 | .PARAMETER image 1165 | Specifies the image file name to find. 1166 | 1167 | .PARAMETER path 1168 | Specifies a path to search. 1169 | #> 1170 | function Get-Image($image, $path) { 1171 | if (Test-Path $path -PathType Container) { 1172 | $item = Get-ChildItem -Path $path -Filter $image -Recurse 1173 | if ($item -ne $null) { 1174 | return $item.FullName 1175 | } 1176 | } 1177 | 1178 | return $null 1179 | } 1180 | 1181 | $targets = Import-Csv -Path $images 1182 | ForEach ($target In $targets) { 1183 | 1184 | if ($target.Skip -eq "TRUE" -or $target.Image -eq "") { 1185 | #Write-Host -ForegroundColor Green "Skipping $($target)" 1186 | continue; 1187 | } 1188 | 1189 | # search in the working ISOs directory if exists 1190 | if (-not (Test-Path -Path $target.Image -PathType Leaf)) { 1191 | $workingIsoDir = [io.path]::Combine($workingDir,"ISOs") 1192 | $image = Get-Image -image $target.Image -path $workingIsoDir 1193 | if ($image -ne $null) { 1194 | $target.Image = $image 1195 | } 1196 | } 1197 | 1198 | # search in the iso search directory if specified 1199 | if (-not (Test-Path -Path $target.Image -PathType Leaf)) { 1200 | $image = Get-Image -image $target.Image -path $isoSearchDir 1201 | if ($image -ne $null) { 1202 | $target.Image = $image 1203 | } 1204 | } 1205 | 1206 | if (-not (Test-Path -Path $target.Image -PathType Leaf)) { 1207 | $iso = $target.Image 1208 | $gui = $target.Core -ne "TRUE" 1209 | $friendlyName = Create-FriendlyName -target $target -gui $gui -generation $generation 1210 | Write-Host -ForegroundColor Green "ISO/WIM not found [$friendlyName]: $iso" 1211 | continue; 1212 | } 1213 | 1214 | # one target could create Gen 1/Gen 2/Core/Gui variations 1215 | $arguments = Create-StartImageFactoryArguments -target $target 1216 | ForEach ($arg In $arguments) { 1217 | Start-ImageFactory @arg 1218 | } 1219 | } 1220 | } 1221 | 1222 | if($startfactory) { 1223 | if ($images -ne $null) { 1224 | Process-ImagesFile 1225 | } else { 1226 | # process original way 1227 | Start-ImageFactory -FriendlyName "Windows Server 2016 DataCenter Core - Gen 2" -ISOFile $2016Image -ProductKey $Windows2016Key -SKUEdition "ServerDataCenterCore" -Generation2; 1228 | Start-ImageFactory -FriendlyName "Windows Server 2016 DataCenter with GUI - Gen 2" -ISOFile $2016Image -ProductKey $Windows2016Key -SKUEdition "ServerDataCenter" -Generation2; 1229 | Start-ImageFactory -FriendlyName "Windows Server 2012 R2 DataCenter with GUI" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenter"; 1230 | Start-ImageFactory -FriendlyName "Windows Server 2012 R2 DataCenter Core" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenterCore"; 1231 | Start-ImageFactory -FriendlyName "Windows Server 2012 R2 DataCenter with GUI - Gen 2" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenter" -Generation2; 1232 | Start-ImageFactory -FriendlyName "Windows Server 2012 R2 DataCenter Core - Gen 2" -ISOFile $2012R2Image -ProductKey $Windows2012R2Key -SKUEdition "ServerDataCenterCore" -Generation2; 1233 | Start-ImageFactory -FriendlyName "Windows Server 2012 DataCenter with GUI" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenter"; 1234 | Start-ImageFactory -FriendlyName "Windows Server 2012 DataCenter Core" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenterCore"; 1235 | Start-ImageFactory -FriendlyName "Windows Server 2012 DataCenter with GUI - Gen 2" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenter" -Generation2; 1236 | Start-ImageFactory -FriendlyName "Windows Server 2012 DataCenter Core - Gen 2" -ISOFile $2012Image -ProductKey $Windows2012Key -SKUEdition "ServerDataCenterCore" -Generation2; 1237 | Start-ImageFactory -FriendlyName "Windows Server 2008 R2 DataCenter with GUI" -ISOFile $2008R2Image -ProductKey $Windows2008R2Key -SKUEdition "ServerDataCenter"; 1238 | Start-ImageFactory -FriendlyName "Windows Server 2008 R2 DataCenter Core" -ISOFile $2008R2Image -ProductKey $Windows2008R2Key -SKUEdition "ServerDataCenterCore"; 1239 | Start-ImageFactory -FriendlyName "Windows 8.1 Professional" -ISOFile $81x64Image -ProductKey $Windows81Key -SKUEdition "Professional" -desktop $true; 1240 | Start-ImageFactory -FriendlyName "Windows 8.1 Professional - Gen 2" -ISOFile $81x64Image -ProductKey $Windows81Key -SKUEdition "Professional" -Generation2 -desktop $true; 1241 | Start-ImageFactory -FriendlyName "Windows 8.1 Professional - 32 bit" -ISOFile $81x86Image -ProductKey $Windows81Key -SKUEdition "Professional" -desktop $true -is32bit $true; 1242 | Start-ImageFactory -FriendlyName "Windows 8 Professional" -ISOFile $8x64Image -ProductKey $Windows8Key -SKUEdition "Professional" -desktop $true; 1243 | Start-ImageFactory -FriendlyName "Windows 8 Professional - Gen 2" -ISOFile $8x64Image -ProductKey $Windows8Key -SKUEdition "Professional" -Generation2 -desktop $true; 1244 | Start-ImageFactory -FriendlyName "Windows 8 Professional - 32 bit" -ISOFile $8x86Image -ProductKey $Windows8Key -SKUEdition "Professional" -desktop $true -is32bit $true; 1245 | Start-ImageFactory -FriendlyName "Windows 7 Enterprise" -ISOFile $7x64Image -ProductKey $Windows7Key -SKUEdition "Enterprise" -desktop $true; 1246 | Start-ImageFactory -FriendlyName "Windows 7 Enterprise - 32 bit" -ISOFile $7x86Image -ProductKey $Windows7Key -SKUEdition "Enterprise" -desktop $true -is32bit $true; 1247 | Start-ImageFactory -FriendlyName "Windows 10 Professional" -ISOFile $10x64Image -ProductKey $Windows10Key -SKUEdition "Professional" -desktop $true; 1248 | Start-ImageFactory -FriendlyName "Windows 10 Professional - Gen 2" -ISOFile $10x64Image -ProductKey $Windows10Key -SKUEdition "Professional" -Generation2 -desktop $true; 1249 | Start-ImageFactory -FriendlyName "Windows 10 Professional - 32 bit" -ISOFile $10x86Image -ProductKey $Windows10Key -SKUEdition "Professional" -desktop $true -is32bit $true; 1250 | } 1251 | } else { 1252 | If($myinvocation.InvocationName -eq '.') { 1253 | Write-Host 'Start-ImageFactory is ready to use' 1254 | } else { 1255 | Write-Host 'Image Factory is set to be dot sourced. If you want to run this script directly, changed $startfactory to $true in FactoryVariables.ps1' 1256 | } 1257 | } 1258 | -------------------------------------------------------------------------------- /Image-Factory/FactoryVariables.ps1: -------------------------------------------------------------------------------- 1 | # Seperate file for variables - this when you pull down the latest factory file you can keep your paths / product keys etc... 2 | $workingDir = "C:\FactoryTest" 3 | $logFile = "$($workingDir)\Share\Details.csv" 4 | $ResourceDirectory = "$($workingDir)\Resources" 5 | $Organization = "The Power Elite" 6 | $Owner = "Ben Armstrong" 7 | $Timezone = "Pacific Standard Time" 8 | $adminPassword = "P@ssw0rd" 9 | $userPassword = "P@ssw0rd" 10 | 11 | # optional. string or string[] 12 | # if using image file, this dir will be recursively search for images only if image not found in $workingDir\ISOs 13 | #$isoSearchDir = "C:\msdn" 14 | 15 | # VM Settings 16 | # Name of the VM - make sure this does not conflict with any existing virtual machine, because it gets deleted automatically! 17 | $factoryVMName = "Factory VM" 18 | 19 | # Amount of memory to give the VM - more memory usually makes updates install faster 20 | $VMMemory = 2048mb 21 | 22 | # Virtual switch to connect to - needs internet access to download updates 23 | $virtualSwitchName = "Virtual Switch" 24 | 25 | # Set to $true and specify VLAN id to connect the VM to a specific VLAN 26 | $UseVLAN = $false 27 | $VlanId = 1 28 | 29 | # Enable to use a static IP address for the VM. Default is to use DHCP 30 | $UseStaticIP = $false 31 | $IP = '10.10.10.10' 32 | $MaskBits = 24 33 | $Gateway = '10.10.10.1' 34 | $Dns = '10.10.10.2' 35 | $IPType = 'IPv4' 36 | 37 | 38 | # Keys 39 | $Windows10Key = "" 40 | $Windows7Key = "" 41 | $Windows2008R2Key = "" 42 | 43 | $Windows81Key = "" 44 | $Windows2012R2Key = "" 45 | 46 | $Windows8Key = "" 47 | $Windows2012Key = "" 48 | 49 | $Windows2016Key = "" 50 | 51 | # ISOs / WIMs 52 | $2016Image = "$($workingDir)\ISOs\en_windows_server_2016_technical_preview_4_x64_dvd_7258292.iso" 53 | $2008R2Image = "$($workingDir)\ISOs\en_windows_server_2008_r2_standard_enterprise_datacenter_and_web_with_sp1_x64_dvd_617601.wim" 54 | $2012Image = "$($workingDir)\ISOs\en_windows_server_2012_x64_dvd_915478.wim" 55 | $2012R2Image = "$($workingDir)\ISOs\en_windows_server_2012_r2_x64_dvd_2707946.wim" 56 | $7x86Image = "$($workingDir)\ISOs\en_windows_7_enterprise_with_sp1_x86_dvd_u_677710.wim" 57 | $7x64Image = "$($workingDir)\ISOs\en_windows_7_enterprise_with_sp1_x64_dvd_u_677651.wim" 58 | $8x86Image = "$($workingDir)\ISOs\en_windows_8_x86_dvd_915417.wim" 59 | $8x64Image = "$($workingDir)\ISOs\en_windows_8_x64_dvd_915440.wim" 60 | $81x86Image = "$($workingDir)\ISOs\en_windows_8_1_x86_dvd_2707392.wim" 61 | $81x64Image = "$($workingDir)\ISOs\en_windows_8_1_x64_dvd_2707217.wim" 62 | $10x86Image = "$($workingDir)\ISOs\en_windows_10_multiple_editions_x86_dvd_6848465.wim" 63 | $10x64Image = "$($workingDir)\ISOs\en_windows_10_multiple_editions_x64_dvd_6846432.wim" 64 | 65 | # Misc Settings 66 | # If $startfactory is false, just import the functions to use in other scripts. If true, start image generation. 67 | $StartFactory = $true 68 | 69 | # If enabled, will use dism.exe to clean the WinSXS folder and remove old updates. This can give significant disk savings, but takes longer (20-30 minutes per image). Updates will not be able to be uninstalled if this is enabled. 70 | $CleanWinSXS = $false 71 | 72 | # If enabled, will use the legacy VHD format rather than VHDX to support Hyper-V before Windows Server 2012 73 | $LegacyVHD = $false 74 | -------------------------------------------------------------------------------- /Image-Factory/images.csv: -------------------------------------------------------------------------------- 1 | Name,Product,Edition,SB,Arch,GUI,Core,Gen1,Gen2,ProductKey,Image,SHA1,Version,Sort,Notes 2 | ,Windows Server 2016,DataCenter,,x64,TRUE,FALSE,FALSE,TRUE,,en_windows_server_2016_x64_dvd_9327751.iso,7E33DF150CB4D1FE3503E23433A1867DDD79A6EE,10,1, 3 | ,Windows Server 2016,Standard,,x64,TRUE,FALSE,FALSE,TRUE,,en_windows_server_2016_x64_dvd_9327751.iso,7E33DF150CB4D1FE3503E23433A1867DDD79A6EE,10,1, 4 | ,Windows Server 2012 R2,DataCenter,,x64,TRUE,FALSE,FALSE,TRUE,,en_windows_server_2012_r2_with_update_x64_dvd_6052708.iso,865494E969704BE1C4496D8614314361D025775E,6.3,1, 5 | ,Windows Server 2012 R2,Standard,,x64,TRUE,FALSE,FALSE,TRUE,,en_windows_server_2012_r2_with_update_x64_dvd_6052708.iso,865494E969704BE1C4496D8614314361D025775E,6.3,1, 6 | ,Windows Server 2012,DataCenter,,x64,TRUE,FALSE,FALSE,TRUE,,en_windows_server_2012_x64_dvd_915478.iso,D09E752B1EE480BC7E93DFA7D5C3A9B8AAC477BA,6.2,1, 7 | ,Windows Server 2012,Standard,,x64,TRUE,FALSE,FALSE,TRUE,,en_windows_server_2012_x64_dvd_915478.iso,D09E752B1EE480BC7E93DFA7D5C3A9B8AAC477BA,6.2,1, 8 | ,Windows Server 2008 R2,DataCenter,,x64,TRUE,FALSE,FALSE,FALSE,,en_windows_server_2008_r2_with_sp1_x64_dvd_617601.iso,D3FD7BF85EE1D5BDD72DE5B2C69A7B470733CD0A,6.1,1, 9 | ,Windows Server 2008 R2,Standard,,x64,TRUE,FALSE,FALSE,FALSE,,en_windows_server_2008_r2_with_sp1_x64_dvd_617601.iso,D3FD7BF85EE1D5BDD72DE5B2C69A7B470733CD0A,6.1,1, 10 | ,Windows Server 2008 R2,Enterprise,,x64,TRUE,FALSE,FALSE,FALSE,,en_windows_server_2008_r2_with_sp1_x64_dvd_617601.iso,D3FD7BF85EE1D5BDD72DE5B2C69A7B470733CD0A,6.1,1, 11 | ,Windows 10,Enterprise,,x86,TRUE,,FALSE,FALSE,,en_windows_10_enterprise_x86_dvd_6851156.iso,E7138032986BFCFA0E5F1A8E41E2E9FD1EC94268,10,2, 12 | ,Windows 10,Enterprise,,x64,TRUE,,FALSE,TRUE,,en_windows_10_enterprise_x64_dvd_6851151.iso,30AD1CDF5D0670F12788005131E24862F6AB8AAB,10,2, 13 | ,Windows 10,Enterprise,2015 LTSB,x64,TRUE,,FALSE,TRUE,,en_windows_10_enterprise_2015_ltsb_x64_dvd_6848446.iso,264D48C902E6B586A1ED965062F7DA4D4DA99B35,10,2, 14 | ,Windows 10,Enterprise,2015 LTSB,x86,TRUE,,FALSE,FALSE,,en_windows_10_enterprise_2015_ltsb_x86_dvd_6848454.iso,ABE400AD86C604197A3B163E31AFCC1B7C53325E,10,2, 15 | ,Windows 10,Enterprise,2016 LTSB,x64,TRUE,,FALSE,TRUE,,en_windows_10_enterprise_2016_ltsb_x64_dvd_9059483.iso,031ED6ACDC47B8F582C781B039F501D83997A1CF,10,2, 16 | ,Windows 10,Enterprise,2016 LTSB,x86,TRUE,,FALSE,FALSE,,en_windows_10_enterprise_2016_ltsb_x86_dvd_9060010.iso,45E72D02FF17125C699558719EB946D8E140C9CC,10,2, 17 | ,Windows 10,Enterprise,1607,x64,TRUE,,FALSE,TRUE,,en_windows_10_enterprise_version_1607_updated_jul_2016_x64_dvd_9054264.iso,F9FFEA3A40BF39CCDE105BB064E153343560D73E,10,2, 18 | ,Windows 10,Enterprise,1607,x86,TRUE,,FALSE,FALSE,,en_windows_10_enterprise_version_1607_updated_jul_2016_x86_dvd_9060097.iso,89E415FEC5F4ED23D96CB543D3CEA838F57A10A0,10,2, 19 | ,Windows 10,Professional,1607,x64,TRUE,,FALSE,TRUE,,en_windows_10_multiple_editions_version_1607_updated_jul_2016_x64_dvd_9058187.iso,99FD8082A609997AE97A514DCA22BECF20420891,10,2, 20 | ,Windows 10,Professional,1607,x86,TRUE,,FALSE,FALSE,,en_windows_10_multiple_editions_version_1607_updated_jul_2016_x86_dvd_9053863.iso,5A4D2C953D884578A6ADB448BEE5753B80EC1CFA,10,2, 21 | ,Windows 10,Professional,,x64,TRUE,,FALSE,TRUE,,en_windows_10_multiple_editions_version_1511_updated_apr_2016_x64_dvd_8705583.iso,1B247B5B348E78C9BC3AFD3C1CBE10CEE3D1B9D5,10,2, 22 | ,Windows 10,Professional,,x86,TRUE,,FALSE,FALSE,,en_windows_10_multiple_editions_version_1511_updated_apr_2016_x86_dvd_8735185.iso,2DDE3A5BFA60B7F10CD38DBD544CF00A075F8908,10,2, 23 | ,Windows 8.1,Professional,with Update,x64,TRUE,,FALSE,FALSE,,,,6.3,2, 24 | ,Windows 8.1,Professional,with Update,x86,TRUE,,FALSE,FALSE,,,,6.3,2, 25 | ,Windows 8.1,Professional,,x64,TRUE,,FALSE,FALSE,,,,6.3,2, 26 | ,Windows 8.1,Professional,,x86,TRUE,,FALSE,FALSE,,,,6.3,2, 27 | ,Windows 8.1,Enterprise,,x64,TRUE,,FALSE,FALSE,,en_windows_8_1_enterprise_x64_dvd_2971902.iso,AE792B2EF982DAC7391224B624EAB8D6340D78AB,6.3,2, 28 | ,Windows 8.1,Enterprise,,x86,TRUE,,FALSE,FALSE,,en_windows_8_1_enterprise_x86_dvd_2972289.iso,5EFB981C94E1223C85F3BB504D04E0642A85C1D4,6.3,2, 29 | ,Windows 8,Enterprise,,x64,TRUE,,FALSE,FALSE,,en_windows_8_enterprise_x64_dvd_917522.iso,4EADFE83E736621234C63E8465986F0AF6AA3C82,6.2,2, 30 | ,Windows 8,Professional,,x64,TRUE,,FALSE,FALSE,,,,6.2,2, 31 | ,Windows 7,Professional,,x64,TRUE,,FALSE,FALSE,,en_windows_7_professional_with_sp1_x64_dvd_u_676939.iso,0BCFC54019EA175B1EE51F6D2B207A3D14DD2B58,6.1,2, 32 | ,Windows 7,Ultimate,,x64,TRUE,,FALSE,FALSE,,en_windows_7_ultimate_with_sp1_x64_dvd_u_677332.iso,36AE90DEFBAD9D9539E649B193AE573B77A71C83,6.1,2, 33 | ,Windows 7,Ultimate,,x86,TRUE,,FALSE,FALSE,,en_windows_7_ultimate_with_sp1_x86_dvd_u_677460.iso,65FCE0F445D9BF7E78E43F17E441E08C63722657,6.1,2, 34 | ,Windows 7,Professional,,x64,TRUE,,FALSE,FALSE,,en_windows_7_professional_with_sp1_x64_dvd_u_676939.iso,36AE90DEFBAD9D9539E649B193AE573B77A71C83,6.1,2, 35 | ,Windows 7,Professional,,x86,TRUE,,FALSE,FALSE,,en_windows_7_professional_with_sp1_x86_dvd_u_677056.iso,D89937DF3A9BC2EC1A1486195FD308CD3DADE928,6.1,2, 36 | -------------------------------------------------------------------------------- /Image-Factory/images.md: -------------------------------------------------------------------------------- 1 | # Images CSV File 2 | 3 | The images CSV file is used to manage the various Windows products, editions and options for running Image Factory. Each row defines a Windows product, edition and processor architecture. 4 | 5 | ## File Format 6 | 7 | 8 | ### Columns 9 | 10 | | Column | Description | 11 | | ------------- | -------------------- | 12 | | **Name** | Optional. Used as the file name for the image. If not specified, the Product, Edition, SB, Arch, GUI/Core and Gen1/Gen2 will be used to generate a name. 13 | | **Product** | The name of the Windows product without the edition, for example 'Windows 10'. 14 | | **Edition** | The edition such as DataCenter, Standard, Enterprise, Professional, etc 15 | | **SB** | Servicing Branch or release (such as *1511* or *1607* for Windows 10, LTSB) 16 | | **Arch** | x64 or x86. Used to set Is32Bit parameter. Needed to create the unattend file correctly. 17 | | **GUI** | TRUE/FALSE to install GUI version. For non-server OS, this must be TRUE. 18 | | **Core** | TRUE/FALSE to install Core version. Only applies to server products that support Core deployments. 19 | | **Gen1** | Create a Generation 1 virtual machine. 20 | | **Gen2** | Create a Generation 2 virtual machine. 21 | | **ProductKey** | The product key to use for the unattended installation. 22 | | **Image** | The ISO or WIM file to use as the base of the windows image. 23 | | **SHA1** | The SHA1 hash of Image. Not used by Image Factory. Could be used in future to validate ISO before installing. 24 | | **Version** | [Version of Windows](https://en.wikipedia.org/wiki/Windows_NT). Used to sort file. Not used by Image Factory. Only machines with version of 6.2 or higher can use secure boot. 25 | | **Sort** | User field. Helper column used to help sorting when using Excel to edit. 26 | | **Notes** | User field. User specific notes. Not used by Image Factory. 27 | -------------------------------------------------------------------------------- /Image-Factory/readme.md: -------------------------------------------------------------------------------- 1 | # Hyper-V Image Factory # 2 | 3 | This is a PowerShell script that creates and maintains a set of Windows virtual hard disks for me that are always up to date. 4 | 5 | # What is this all about? # 6 | 7 | For more information - read here: http://blogs.msdn.com/b/virtual_pc_guy/archive/2015/06/16/script-image-factory-for-hyper-v.aspx 8 | 9 | 10 | # Change Log # 11 | 9/3/16 - 12 | * Changes from Phil Bolduc 13 | * optionally load target images from a CSV file using a command line argument (-images) 14 | * optionally load variables from different files using a command line argument (-variables) 15 | 16 | 10/30/15 - 17 | * Changes from Garry Martin 18 | * Add support for Windows 7 and Windows Server 2008 R2 by installing WMF and .NET Framework 19 | * Add LegacyVHD parameter to allow legacy VHD format files to be created instead of VHDX files 20 | * Change behaviour for removal of autorun registry key 21 | * Clean up Temp directory and Demo user if created 22 | 23 | 8/21/15 - 24 | * Changes from Grant Emsley 25 | * Support configuration of static IP address on the factory VM 26 | * Verify product keys have been set 27 | 28 | 8/17/15 - 29 | * Changes from Grant Emsley 30 | * Optimize final VHD file 31 | * Optionally clean the windows component store to save disk space 32 | 33 | 8/16/15 - 34 | 35 | * Changes from Grant Emsley 36 | * Support for configuring Factory VM RAM and VLAN 37 | * Better error handling on missing folders / dependancies 38 | * Better handling of missing ISO / WIM files 39 | 40 | 8/1/15 - 41 | 42 | * Tested with Windows 10 (yay!) 43 | * Moved variables into separate file to make it easier to accept changes - and to stop people from giving me their product keys 44 | * Added code to check for and create directories that are missing 45 | 46 | 7/29/15 - 47 | 48 | * Accepted changes from Christoph Petersen that: 49 | * Cleaned up files inside the virtual machine at the end 50 | * Added support for an external sysprep process 51 | * Made the code a lot prettier to look at (thanks Christoph!) 52 | 53 | # To do # 54 | 55 | This is the list of things currently in my "to do" list. Feel free to tackle any of them yourself and request a pull. 56 | 57 | * MD5 summing for virtual hard drives in the share folder 58 | * Make variables for the build and share directories - so they do not have to be under the working directory 59 | * Update this readme to have more information from the blog post - and vice versa 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Benjamin Armstrong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Misc/BingCmdlets.ps1: -------------------------------------------------------------------------------- 1 | Add-Type -Assembly System.Web 2 | $WebClient = New-Object system.Net.WebClient 3 | 4 | Function Get-BingCount([string] $Term) { 5 | 6 | # Add plus and quotes, encodes for URL 7 | $Term = '+"' + $Term + '"' 8 | $Term = [System.Web.HttpUtility]::UrlEncode($Term) 9 | 10 | # Load the page as a string 11 | $URL = "http://www.bing.com/search?q=" + $Term 12 | $Page = $WebClient.DownloadString($URL) 13 | 14 | # searches for the string before the number of hits on the page 15 | $String1 = '<span class="sb_count">' 16 | $Index1 = $Page.IndexOf($String1) 17 | 18 | # if found the right string, finds the exact end of the number 19 | If ($Index1 -ne -1) { 20 | $Index1 += $String1.Length 21 | $Index2 = $Page.IndexOf(" ", $Index1) 22 | $result = $Page.Substring($Index1, $Index2 - $index1) 23 | } else { $result = "0" } 24 | 25 | # Return the count 26 | return $result 27 | } 28 | 29 | $CmdletList = Get-Command -Module Hyper-V | Select Name 30 | $CmdletCount = $CmdletList.Count -1 31 | $hashTable = $null 32 | $hashTable = @{} 33 | 34 | 0..$CmdletCount | % { 35 | 36 | # Tracks progress 37 | Write-Progress -Activity "Checking cmdlet popularity" -PercentComplete ($_ * 100 / $CmdletCount) 38 | 39 | # Check the popularity with Bing 40 | $cmdlet = $CmdletList[$_].Name 41 | $count = [int] (Get-BingCount $cmdlet) 42 | 43 | # Put data in a hashtable 44 | $hashTable.add($cmdlet, $count) 45 | } 46 | 47 | $hashTable.GetEnumerator() | sort value -Descending | select @{N="Cmdlet";E={$_.Name}}, @{N="Popularity";E={$_.Value}} 48 | 49 | Write-Progress -Activity "Checking cmdlet popularity" -Completed 50 | 51 | # Releases resources used by the web client 52 | $WebClient.Dispose() 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyper-V-PowerShell 2 | Scripts that primarily use the Hyper-V PowerShell interfaces driving virtual machines 3 | -------------------------------------------------------------------------------- /Snippets/CopyTextFileIntoVM.ps1: -------------------------------------------------------------------------------- 1 | # Pass in script file - array trick from http://powershell.com/cs/forums/t/4169.aspx 2 | function copyTextFileIntoVM([string]$VMName, $cred, [string]$sourceFilePath, [string]$destinationFilePath){ 3 | $content = Get-Content $sourceFilePath 4 | icm -VMName $VMName -Credential $cred {param($Script, $file) 5 | $script | set-content $file} -ArgumentList (,$content), $destinationFilePath} -------------------------------------------------------------------------------- /Snippets/PSCreds.ps1: -------------------------------------------------------------------------------- 1 | $localCred = new-object -typename System.Management.Automation.PSCredential -argumentlist "Administrator", (ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force) -------------------------------------------------------------------------------- /Snippets/Readme.md: -------------------------------------------------------------------------------- 1 | # Snippets # 2 | 3 | Random snippets of useful PowerShell -------------------------------------------------------------------------------- /Snippets/WaitForPSDirect.ps1: -------------------------------------------------------------------------------- 1 | function waitForPSDirect([string]$VMName, $cred){ 2 | Write-Output "[$($VMName)]:: Waiting for PowerShell Direct (using $($cred.username))" 3 | while ((icm -VMName $VMName -Credential $cred {"Test"} -ea SilentlyContinue) -ne "Test") {Sleep -Seconds 1}} -------------------------------------------------------------------------------- /Snippets/WaitForVMDHCP.ps1: -------------------------------------------------------------------------------- 1 | # Wait for DHCP 2 | while ((Get-NetIPAddress | ? AddressFamily -eq IPv4 | ? IPAddress -ne 127.0.0.1).SuffixOrigin -ne "Dhcp") {sleep -Milliseconds 10} -------------------------------------------------------------------------------- /Snippets/addNicWithIP.ps1: -------------------------------------------------------------------------------- 1 | function addNicWithIP([string]$VMName,$cred, [string]$Switch, [string]$IPaddress, [string]$subnetPrefixLength){ 2 | $newNetAdapter = Add-VMNetworkAdapter -VMName $VMName -SwitchName $Switch -Passthru 3 | Write-Output "[$($VMName)]:: Wait for IP address and create virtual switch" 4 | waitForPSDirect $VMName $cred 5 | icm -VMName $VMName -Credential $cred { 6 | param($IPAddress, $subnetPrefixLength, $MacAddress) 7 | # Wait for the NIC to appear 8 | do {sleep 1} until (@(get-netadapter | ? PermanentAddress -eq $MacAddress).Count -eq 1) 9 | # Setup IP Address 10 | New-NetIPAddress -IPAddress $IPAddress ` 11 | -InterfaceAlias (get-netadapter | ? PermanentAddress -eq $MacAddress).Name ` 12 | -PrefixLength $subnetPrefixLength 13 | } -ArgumentList $IPAddress, $subnetPrefixLength, $newNetAdapter.MacAddress 14 | } -------------------------------------------------------------------------------- /Ubuntu-VM-Build/BaseUbuntuBuild.ps1: -------------------------------------------------------------------------------- 1 | $tempPath = [System.IO.Path]::GetTempPath() + [System.Guid]::NewGuid().ToString() 2 | 3 | # ADK Download - https://docs.microsoft.com/en-us/windows-hardware/get-started/adk-install 4 | # You only need to install the deployment tools 5 | $oscdimgPath = "${Env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\oscdimg.exe" 6 | 7 | # Download qemu-img from here: http://www.cloudbase.it/qemu-img-windows/ 8 | $qemuImgPath = "${Env:UserProfile}\Downloads\qemu-img.exe" 9 | 10 | # Update this to the release of Ubuntu that you want 11 | $ubuntuPath = "http://cloud-images.ubuntu.com/trusty/current/trusty-server-cloudimg-amd64" 12 | 13 | $GuestOSName = "Hyper-V-VM" 14 | $GuestOSID = "iid-123456" 15 | $GuestAdminPassword = "P@ssw0rd" 16 | 17 | $VMName = "Ubuntu Test" 18 | $virtualSwitchName = "Virtual Switch" 19 | 20 | $vmPath = "${Env:Public}\Documents\Hyper-V\$VMName" 21 | $imageCachePath = $tempPath 22 | $vhdx = "$($vmPath)\test.vhdx" 23 | $metaDataIso = "$($vmPath)\metadata.iso" 24 | 25 | # Get the timestamp of the latest build on the Ubuntu cloud-images site 26 | $stamp = (Invoke-WebRequest "$($ubuntuPath).manifest").BaseResponse.LastModified.ToFileTimeUtc() 27 | 28 | $metadata = @" 29 | instance-id: $($GuestOSID) 30 | local-hostname: $($GuestOSName) 31 | "@ 32 | 33 | $userdata = @" 34 | #cloud-config 35 | password: $($GuestAdminPassword) 36 | runcmd: 37 | - [ useradd, -m, -p, "", ben ] 38 | - [ chage, -d, 0, ben ] 39 | "@ 40 | 41 | # Check Paths 42 | if (!(test-path $vmPath)) {mkdir $vmPath} 43 | if (!(test-path $imageCachePath)) {mkdir $imageCachePath} 44 | 45 | # Helper function for no error file cleanup 46 | Function cleanupFile ([string]$file) {if (test-path $file) {Remove-Item $file}} 47 | 48 | # Delete the VM if it is around 49 | If ((Get-VM | ? name -eq $VMName).Count -gt 0) 50 | {stop-vm $VMName -TurnOff -Confirm:$false -Passthru | Remove-VM -Force} 51 | 52 | cleanupFile $vhdx 53 | cleanupFile $metaDataIso 54 | 55 | # Make temp location 56 | md -Path $tempPath 57 | md -Path "$($tempPath)\Bits" 58 | 59 | if (!(test-path "$($imageCachePath)\ubuntu-$($stamp).img")) { 60 | # If we do not have a matching image - delete the old ones and download the new one 61 | Remove-Item "$($imageCachePath)\ubuntu-*.img" 62 | Invoke-WebRequest "$($ubuntuPath)-disk1.img" -UseBasicParsing -OutFile "$($imageCachePath)\ubuntu-$($stamp).img" 63 | } 64 | 65 | # Output meta and user data to files 66 | sc "$($tempPath)\Bits\meta-data" ([byte[]][char[]] "$metadata") -Encoding Byte 67 | sc "$($tempPath)\Bits\user-data" ([byte[]][char[]] "$userdata") -Encoding Byte 68 | 69 | # Convert cloud image to VHDX 70 | & $qemuImgPath convert -f qcow2 "$($imageCachePath)\ubuntu-$($stamp).img" -O vhdx -o subformat=dynamic $vhdx 71 | Resize-VHD -Path $vhdx -SizeBytes 50GB 72 | 73 | # Create meta data ISO image 74 | & $oscdimgPath "$($tempPath)\Bits" $metaDataIso -j2 -lcidata 75 | 76 | # Clean up temp directory 77 | rd -Path $tempPath -Recurse -Force 78 | 79 | # Create new virtual machine and start it 80 | new-vm $VMName -MemoryStartupBytes 2048mb -VHDPath $vhdx -Generation 1 ` 81 | -SwitchName $virtualSwitchName -Path $vmPath | Out-Null 82 | set-vm -Name $VMName -ProcessorCount 2 83 | Set-VMDvdDrive -VMName $VMName -Path $metaDataIso 84 | Start-VM $VMName 85 | 86 | # Open up VMConnect 87 | Invoke-Expression "vmconnect.exe localhost `"$VMName`"" 88 | -------------------------------------------------------------------------------- /Ubuntu-VM-Build/readme.md: -------------------------------------------------------------------------------- 1 | Scripts for creating Ubuntu VMs on Hyper-V 2 | -------------------------------------------------------------------------------- /vmconnect.ps1: -------------------------------------------------------------------------------- 1 | function Start-VMConnect 2 | { 3 | 4 | [CmdletBinding()] 5 | param 6 | ( 7 | # CMDLet takes a single parameter of VMNames. 8 | # Can be single or plural, can come from the pipeline. 9 | 10 | [Parameter(Mandatory=$True,ValueFromPipeline=$True)]$VMnames 11 | ) 12 | 13 | begin 14 | { 15 | # Path to the binary for FreeRDP 16 | 17 | $FreeRDPPath = "C:\FreeRDP\wfreerdp.exe" 18 | } 19 | 20 | process 21 | { 22 | foreach ($VMname in $VMnames) 23 | { 24 | if ($pscmdlet.ShouldProcess($VMname)) 25 | { 26 | # Get the ID of the virtual machine 27 | # If a VM object has been provided - grab the ID directly 28 | # Otherwise, try and get the VM object and get the ID from there 29 | 30 | if ($VMname.GetType().name -eq "VirtualMachine") 31 | {$VMID = $VMname.ID} 32 | else 33 | {$VMID = (get-vm $VMname | Select-Object -first 1).ID} 34 | 35 | # Start FreeRDP 36 | start-process $FreeRDPPath -ArgumentList "/v:localhost:2179 /vmconnect:$($VMID)" 37 | } 38 | } 39 | } 40 | 41 | end {} 42 | } 43 | --------------------------------------------------------------------------------