├── .gitignore ├── SQLServer.ini ├── PowerLab.psd1 ├── PowerLabConfiguration.psd1 ├── AutoUnattend ├── Windows Server 2016.xml └── Windows Server 2012 R2.xml ├── Install-PowerLab.ps1 ├── PowerLab.Tests.ps1 └── PowerLab.psm1 /.gitignore: -------------------------------------------------------------------------------- 1 | .dropbox 2 | MyLabConfiguration.psd1 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /SQLServer.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adbertram/PowerLab/HEAD/SQLServer.ini -------------------------------------------------------------------------------- /PowerLab.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'PowerLab.psm1' 3 | ModuleVersion = '1.0.0' 4 | GUID = '3aad272a-fb09-41a2-8208-f3eaa1c3e7a5' 5 | Author = 'Adam Bertram' 6 | CompanyName = 'Adam the Automator, LLC' 7 | PowerShellVersion = '5.0' 8 | RequiredModules = @(@{ModuleName='Hyper-V'; ModuleVersion='1.1' }) 9 | FunctionsToExport = 'New-PowerLab', 'Remove-PowerLab', 'New-ActiveDirectoryForest', 'New-PowerLabSqlServer', 'New-WebServer', 'Get-PowerLabVm', 'Get-PowerLabVhd' 10 | FileList = 'PowerLabConfiguration.psd1', 'Convert-WindowsImage.ps1', 'SQLServer.ini', 'Install-PowerLab.ps1', 'AutoUnattend' 11 | PrivateData = @{ 12 | PSData = @{ 13 | Tags = 'PowerLab' 14 | ProjectUri = 'https://github.com/adbertram/PowerLab' 15 | } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /PowerLabConfiguration.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ProjectName = 'PowerLab' 3 | 4 | ## This will be the folder on the Hyper-V host that will be the base for all files needed 5 | ProjectRootFolder = 'C:\PowerLab' 6 | 7 | ## This is the path on the Hyper-V hosts where you have the ISOs for each OS to install on the VMs is located 8 | IsoFolderPath = 'C:\PowerLab\ISOs' 9 | 10 | ## The unattended XML file template that's used to create an answer file for all new OS installs 11 | UnattendXmlPath = '.\AutoUnattend' 12 | 13 | ## Each ISO file needs to be mapped to a particular label. Ensure every ISO is defined with a label. 14 | ISOs = @( ## Define each 15 | @{ 16 | FileName = 'en_windows_server_2016_x64_dvd_9718492.iso' 17 | Type = 'OS' 18 | Name = 'Windows Server 2016' 19 | ProductKey = '' 20 | } 21 | @{ 22 | FileName = 'en_sql_server_2016_standard_x64_dvd_8701871.iso' 23 | Type = 'Software' 24 | Name = 'SQL Server 2016' 25 | ProductKey = '' 26 | } 27 | @{ 28 | FileName = 'en_windows_server_2012_r2_with_update_x64_dvd_4065220.iso' 29 | Type = 'OS' 30 | Name = 'Windows Server 2012 R2' 31 | ProductKey = '' 32 | } 33 | ) 34 | 35 | ## Define the name and IP address of the Hyper-V host here 36 | HostServer = @{ 37 | Name = 'HYPERVSRV' 38 | IPAddress = '192.168.0.250' 39 | } 40 | 41 | ## This will be the default configuration for all Hyper-V components built by this Lab module 42 | DefaultVirtualMachineConfiguration = @{ 43 | VirtualSwitch = @{ 44 | Name = 'PowerLab' 45 | Type = 'External' ## This is in order for our client to communicate with the VMs. If this is external, we'll ignore this one and use the existing one (if exists) 46 | } 47 | VHDConfig = @{ 48 | Size = '40GB' 49 | Type = 'VHDX' 50 | Sizing = 'Dynamic' 51 | Path = 'C:\PowerLab\VHDs' 52 | PartitionStyle = 'GPT' 53 | } 54 | VMConfig = @{ 55 | StartupMemory = '2GB' 56 | ProcessorCount = 1 57 | Path = 'C:\PowerLab\VMs' 58 | Generation = 2 59 | OSEdition = 'ServerStandardCore' 60 | } 61 | } 62 | 63 | DefaultOperatingSystemConfiguration = @{ 64 | Users = @( 65 | @{ 66 | Name = 'PowerLabUser' 67 | Password = 'P@$$w0rd12' 68 | } 69 | @{ 70 | Name = 'Administrator' 71 | Password = 'P@$$w0rd12' 72 | } 73 | ) 74 | 75 | Network = @{ 76 | IpNetwork = '192.168.0.0' ## Ensure this network does not conflict with any existing 77 | DnsServer = '192.168.0.100' ## This will also be the IP of the domain controller (if deployed) 78 | } 79 | } 80 | 81 | DefaultServerConfiguration = @{ 82 | Web = @{ 83 | ApplicationPoolName = 'AutomateBoringStuff' 84 | WebSiteName = 'AutomateBoringStuff' 85 | } 86 | SQL = @{ 87 | SystemAdministratorAccount = @{ 88 | Name = 'PowerLabUser' 89 | } 90 | ServiceAccount = @{ 91 | Name = 'PowerLabUser' 92 | Password = 'P@$$w0rd12' 93 | } 94 | } 95 | } 96 | 97 | ## Define as many VM types as you want here. Calling New-PowerLab will use this to find all of the VMs you'd like to provision 98 | ## when building a new lab. When deploying more than one of a particular type of VM, the name here will be the base 99 | ## name ie. SQLSRV, SQLSRV2, SQLSRV3, etc. 100 | VirtualMachines = @( 101 | @{ 102 | BaseName = 'SQLSRV' 103 | Type = 'SQL' 104 | OS = 'Windows Server 2016' 105 | Edition = 'ServerStandardCore' 106 | } 107 | @{ 108 | BaseName = 'WEBSRV' 109 | Type = 'Web' 110 | OS = 'Windows Server 2016' 111 | Edition = 'ServerStandardCore' 112 | } 113 | @{ 114 | BaseName = 'LABDC' 115 | Type = 'Domain Controller' 116 | OS = 'Windows Server 2016' 117 | Edition = 'ServerStandardCore' 118 | } 119 | ) 120 | 121 | ## Any Lab AD-specific configuration values go here. 122 | ActiveDirectoryConfiguration = @{ 123 | DomainName = 'powerlab.local' 124 | DomainMode = 'Win2012R2' 125 | ForestMode = 'Win2012R2' 126 | SafeModeAdministratorPassword = 'P@$$w0rd12' 127 | } 128 | } -------------------------------------------------------------------------------- /AutoUnattend/Windows Server 2016.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | en-US 10 | 11 | 0409:00000409 12 | en-US 13 | en-US 14 | en-US 15 | en-US 16 | 17 | 20 | 21 | 22 | 23 | 24 | 1 25 | Primary 26 | 100 27 | 28 | 29 | true 30 | 2 31 | Primary 32 | 33 | 34 | 35 | 36 | true 37 | NTFS 38 | 39 | 1 40 | 1 41 | 0x27 42 | 43 | 44 | true 45 | NTFS 46 | 47 | C 48 | 2 49 | 2 50 | 51 | 52 | 0 53 | true 54 | 55 | 56 | 57 | 58 | 59 | 0 60 | 2 61 | 62 | false 63 | 64 | 65 | 66 | true 67 | 68 | Automate the Boring Stuff with PowerShell 69 | 70 | false 71 | 72 | 73 | 74 | 77 | false 78 | 79 | 80 | 81 | 84 | 1 85 | 86 | 87 | 88 | 91 | 0409:00000409 92 | en-US 93 | en-US 94 | en-US 95 | en-US 96 | 97 | 100 | true 101 | 102 | 105 | 0 106 | 107 | 110 | XXXXXXXX 111 | 112 | 113 | 116 | 117 | 118 | 119 | false 120 | 20 121 | false 122 | 123 | 124 | false 125 | 30 126 | true 127 | 128 | Ethernet 129 | 130 | XXXXXX 131 | 132 | 133 | 134 | 135 | 138 | 139 | 140 | Ethernet 141 | powerlab.local 142 | 143 | XXXXXXX 144 | 145 | true 146 | false 147 | 148 | 149 | powerlab.local 150 | 151 | 152 | 153 | 156 | 157 | 158 | 159 | true</PlainText> 160 | </Password> 161 | <Enabled>true</Enabled> 162 | <Username></Username> 163 | </AutoLogon> 164 | <FirstLogonCommands> 165 | <SynchronousCommand wcm:action="add"> 166 | <CommandLine>%SystemRoot%\System32\netsh.exe advfirewall set allprofiles state off</CommandLine> 167 | <Order>1</Order> 168 | <Description>Disable Windows Firewall</Description> 169 | </SynchronousCommand> 170 | <SynchronousCommand wcm:action="add"> 171 | <CommandLine>net user administrator XXXX</CommandLine> 172 | <Order>2</Order> 173 | <Description>Change local administrator password</Description> 174 | </SynchronousCommand> 175 | </FirstLogonCommands> 176 | <OOBE> 177 | <HideEULAPage>true</HideEULAPage> 178 | <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> 179 | <HideOnlineAccountScreens>true</HideOnlineAccountScreens> 180 | <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> 181 | <NetworkLocation>Work</NetworkLocation> 182 | <ProtectYourPC>2</ProtectYourPC> 183 | <SkipUserOOBE>true</SkipUserOOBE> 184 | <SkipMachineOOBE>true</SkipMachineOOBE> 185 | </OOBE> 186 | <UserAccounts> 187 | <LocalAccounts> 188 | <LocalAccount wcm:action="add"> 189 | <Password> 190 | <Value></Value> 191 | <PlainText>true</PlainText> 192 | </Password> 193 | <Description>Local Administrator</Description> 194 | <DisplayName>Administrator</DisplayName> 195 | <Group>Administrators</Group> 196 | <Name>Administrator</Name> 197 | </LocalAccount> 198 | <LocalAccount wcm:action="add"> 199 | <Password> 200 | <Value>XXXX</Value> 201 | <PlainText>true</PlainText> 202 | </Password> 203 | <DisplayName></DisplayName> 204 | <Group>Administrators</Group> 205 | <Name>XXXX</Name> 206 | </LocalAccount> 207 | </LocalAccounts> 208 | </UserAccounts> 209 | <RegisteredOrganization>Automate the Boring Stuff with PowerShell</RegisteredOrganization> 210 | <RegisteredOwner></RegisteredOwner> 211 | <DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet> 212 | <TimeZone>Central Standard Time</TimeZone> 213 | <VisualEffects> 214 | <SystemDefaultBackgroundColor>2</SystemDefaultBackgroundColor> 215 | </VisualEffects> 216 | </component> 217 | <component name="Microsoft-Windows-ehome-reg-inf" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" 218 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 219 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 220 | <RestartEnabled>true</RestartEnabled> 221 | </component> 222 | <component name="Microsoft-Windows-ehome-reg-inf" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" 223 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 224 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 225 | <RestartEnabled>true</RestartEnabled> 226 | </component> 227 | </settings> 228 | </unattend> -------------------------------------------------------------------------------- /AutoUnattend/Windows Server 2012 R2.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <unattend 3 | xmlns="urn:schemas-microsoft-com:unattend"> 4 | <settings pass="windowsPE"> 5 | <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 6 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 7 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 8 | <SetupUILanguage> 9 | <UILanguage>en-US</UILanguage> 10 | </SetupUILanguage> 11 | <InputLocale>0409:00000409</InputLocale> 12 | <SystemLocale>en-US</SystemLocale> 13 | <UILanguage>en-US</UILanguage> 14 | <UILanguageFallback>en-US</UILanguageFallback> 15 | <UserLocale>en-US</UserLocale> 16 | </component> 17 | <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 18 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 19 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 20 | <DiskConfiguration> 21 | <Disk wcm:action="add"> 22 | <CreatePartitions> 23 | <CreatePartition wcm:action="add"> 24 | <Order>1</Order> 25 | <Type>Primary</Type> 26 | <Size>100</Size> 27 | </CreatePartition> 28 | <CreatePartition wcm:action="add"> 29 | <Extend>true</Extend> 30 | <Order>2</Order> 31 | <Type>Primary</Type> 32 | </CreatePartition> 33 | </CreatePartitions> 34 | <ModifyPartitions> 35 | <ModifyPartition wcm:action="add"> 36 | <Active>true</Active> 37 | <Format>NTFS</Format> 38 | <Label>System Reserved</Label> 39 | <Order>1</Order> 40 | <PartitionID>1</PartitionID> 41 | <TypeID>0x27</TypeID> 42 | </ModifyPartition> 43 | <ModifyPartition wcm:action="add"> 44 | <Active>true</Active> 45 | <Format>NTFS</Format> 46 | <Label>OS</Label> 47 | <Letter>C</Letter> 48 | <Order>2</Order> 49 | <PartitionID>2</PartitionID> 50 | </ModifyPartition> 51 | </ModifyPartitions> 52 | <DiskID>0</DiskID> 53 | <WillWipeDisk>true</WillWipeDisk> 54 | </Disk> 55 | </DiskConfiguration> 56 | <ImageInstall> 57 | <OSImage> 58 | <InstallTo> 59 | <DiskID>0</DiskID> 60 | <PartitionID>2</PartitionID> 61 | </InstallTo> 62 | <InstallToAvailablePartition>false</InstallToAvailablePartition> 63 | </OSImage> 64 | </ImageInstall> 65 | <UserData> 66 | <AcceptEula>true</AcceptEula> 67 | <FullName></FullName> 68 | <Organization>Automate the Boring Stuff with PowerShell</Organization> 69 | </UserData> 70 | <EnableFirewall>false</EnableFirewall> 71 | </component> 72 | </settings> 73 | <settings pass="offlineServicing"> 74 | <component name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 75 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 76 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 77 | <EnableLUA>false</EnableLUA> 78 | </component> 79 | </settings> 80 | <settings pass="generalize"> 81 | <component name="Microsoft-Windows-Security-SPP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 82 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 83 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 84 | <SkipRearm>1</SkipRearm> 85 | </component> 86 | </settings> 87 | <settings pass="specialize"> 88 | <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 89 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 90 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 91 | <InputLocale>0409:00000409</InputLocale> 92 | <SystemLocale>en-US</SystemLocale> 93 | <UILanguage>en-US</UILanguage> 94 | <UILanguageFallback>en-US</UILanguageFallback> 95 | <UserLocale>en-US</UserLocale> 96 | </component> 97 | <component name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 98 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 99 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 100 | <SkipAutoActivation>true</SkipAutoActivation> 101 | </component> 102 | <component name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 103 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 104 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 105 | <CEIPEnabled>0</CEIPEnabled> 106 | </component> 107 | <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 108 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 109 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 110 | <ComputerName>XXXXXXXX</ComputerName> 111 | <ProductKey></ProductKey> 112 | </component> 113 | <component name="Microsoft-Windows-TCPIP" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 114 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 115 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 116 | <Interfaces> 117 | <Interface wcm:action="add"> 118 | <Ipv4Settings> 119 | <DhcpEnabled>false</DhcpEnabled> 120 | <Metric>20</Metric> 121 | <RouterDiscoveryEnabled>false</RouterDiscoveryEnabled> 122 | </Ipv4Settings> 123 | <Ipv6Settings> 124 | <DhcpEnabled>false</DhcpEnabled> 125 | <Metric>30</Metric> 126 | <RouterDiscoveryEnabled>true</RouterDiscoveryEnabled> 127 | </Ipv6Settings> 128 | <Identifier>Ethernet</Identifier> 129 | <UnicastIpAddresses> 130 | <IpAddress wcm:action="add" wcm:keyValue="1">XXXXXX</IpAddress> 131 | </UnicastIpAddresses> 132 | </Interface> 133 | </Interfaces> 134 | </component> 135 | <component name="Microsoft-Windows-DNS-Client" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 136 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 137 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 138 | <Interfaces> 139 | <Interface wcm:action="add"> 140 | <Identifier>Ethernet</Identifier> 141 | <DNSDomain>powerlab.local</DNSDomain> 142 | <DNSServerSearchOrder> 143 | <IpAddress wcm:action="add" wcm:keyValue="1">XXXXXXX</IpAddress> 144 | </DNSServerSearchOrder> 145 | <EnableAdapterDomainNameRegistration>true</EnableAdapterDomainNameRegistration> 146 | <DisableDynamicUpdate>false</DisableDynamicUpdate> 147 | </Interface> 148 | </Interfaces> 149 | <DNSDomain>powerlab.local</DNSDomain> 150 | </component> 151 | </settings> 152 | <settings pass="oobeSystem"> 153 | <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" 154 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 155 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 156 | <AutoLogon> 157 | <Password> 158 | <Value></Value> 159 | <PlainText>true</PlainText> 160 | </Password> 161 | <Enabled>true</Enabled> 162 | <Username></Username> 163 | </AutoLogon> 164 | <FirstLogonCommands> 165 | <SynchronousCommand wcm:action="add"> 166 | <CommandLine>%SystemRoot%\System32\netsh.exe advfirewall set allprofiles state off</CommandLine> 167 | <Order>1</Order> 168 | <Description>Disable Windows Firewall</Description> 169 | </SynchronousCommand> 170 | <SynchronousCommand wcm:action="add"> 171 | <CommandLine>net user administrator XXXX</CommandLine> 172 | <Order>2</Order> 173 | <Description>Change local administrator password</Description> 174 | </SynchronousCommand> 175 | </FirstLogonCommands> 176 | <OOBE> 177 | <HideEULAPage>true</HideEULAPage> 178 | <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen> 179 | <HideOnlineAccountScreens>true</HideOnlineAccountScreens> 180 | <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> 181 | <NetworkLocation>Work</NetworkLocation> 182 | <ProtectYourPC>2</ProtectYourPC> 183 | <SkipUserOOBE>true</SkipUserOOBE> 184 | <SkipMachineOOBE>true</SkipMachineOOBE> 185 | </OOBE> 186 | <UserAccounts> 187 | <LocalAccounts> 188 | <LocalAccount wcm:action="add"> 189 | <Password> 190 | <Value></Value> 191 | <PlainText>true</PlainText> 192 | </Password> 193 | <Description>Local Administrator</Description> 194 | <DisplayName>Administrator</DisplayName> 195 | <Group>Administrators</Group> 196 | <Name>Administrator</Name> 197 | </LocalAccount> 198 | <LocalAccount wcm:action="add"> 199 | <Password> 200 | <Value>XXXX</Value> 201 | <PlainText>true</PlainText> 202 | </Password> 203 | <DisplayName></DisplayName> 204 | <Group>Administrators</Group> 205 | <Name>XXXX</Name> 206 | </LocalAccount> 207 | </LocalAccounts> 208 | </UserAccounts> 209 | <RegisteredOrganization>Automate the Boring Stuff with PowerShell</RegisteredOrganization> 210 | <RegisteredOwner></RegisteredOwner> 211 | <DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet> 212 | <TimeZone>Central Standard Time</TimeZone> 213 | <VisualEffects> 214 | <SystemDefaultBackgroundColor>2</SystemDefaultBackgroundColor> 215 | </VisualEffects> 216 | </component> 217 | <component name="Microsoft-Windows-ehome-reg-inf" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" 218 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 219 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 220 | <RestartEnabled>true</RestartEnabled> 221 | </component> 222 | <component name="Microsoft-Windows-ehome-reg-inf" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="NonSxS" 223 | xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" 224 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 225 | <RestartEnabled>true</RestartEnabled> 226 | </component> 227 | </settings> 228 | </unattend> -------------------------------------------------------------------------------- /Install-PowerLab.ps1: -------------------------------------------------------------------------------- 1 | #requires -Version 5 -RunAsAdministrator 2 | 3 | #region Functions 4 | function Add-PlHostEntry { 5 | [CmdletBinding()] 6 | param 7 | ( 8 | 9 | [Parameter(Mandatory)] 10 | [ValidateNotNullOrEmpty()] 11 | [ValidatePattern('^[^\.]+$')] 12 | [string]$HostName, 13 | 14 | [Parameter(Mandatory)] 15 | [ValidateNotNullOrEmpty()] 16 | [ipaddress]$IpAddress, 17 | 18 | [Parameter()] 19 | [ValidateNotNullOrEmpty()] 20 | [string]$Comment, 21 | 22 | [Parameter()] 23 | [ValidateNotNullOrEmpty()] 24 | [string]$ComputerName = $env:COMPUTERNAME, 25 | 26 | [Parameter()] 27 | [ValidateNotNullOrEmpty()] 28 | [pscredential]$Credential, 29 | 30 | [Parameter()] 31 | [ValidateNotNullOrEmpty()] 32 | [string]$HostFilePath = "$env:SystemRoot\System32\drivers\etc\hosts" 33 | 34 | 35 | ) 36 | begin { 37 | $ErrorActionPreference = 'Stop' 38 | } 39 | process { 40 | try { 41 | $IpAddress = $IpAddress.IPAddressToString 42 | 43 | $getParams = @{ } 44 | if ($ComputerName -ne $env:COMPUTERNAME) { 45 | $getParams.ComputerName = $ComputerName 46 | $getParams.Credential = $Credential 47 | } 48 | 49 | $existingHostEntries = Get-PlHostEntry @getParams 50 | 51 | if ($result = $existingHostEntries | where HostName -EQ $HostName) { 52 | throw "The hostname [$($HostName)] already exists in the host file with IP [$($result.IpAddress)]" 53 | } elseif ($result = $existingHostEntries | where IPAddress -EQ $IpAddress) { 54 | Write-Warning "The IP address [$($result.IPAddress)] already exists in the host file for the hostname [$($HostName)]. You should probabloy remove the old one hostname reference." 55 | } 56 | $vals = @( 57 | $IpAddress 58 | $HostName 59 | ) 60 | if ($PSBoundParameters.ContainsKey('Comment')) { 61 | $vals += "# $Comment" 62 | } 63 | 64 | $sb = { 65 | param($HostFilePath, $vals) 66 | 67 | ## If the hosts file doesn't end with a blank line, make it so 68 | if ((Get-Content -Path $HostFilePath -Raw) -notmatch '\n$') { 69 | Add-Content -Path $HostFilePath -Value '' 70 | } 71 | Add-Content -Path $HostFilePath -Value ($vals -join "`t") 72 | } 73 | 74 | if ($ComputerName -eq (hostname)) { 75 | & $sb $HostFilePath $vals 76 | } else { 77 | $icmParams = @{ 78 | 'ComputerName' = $ComputerName 79 | 'ScriptBlock' = $sb 80 | 'ArgumentList' = $HostFilePath, $vals 81 | } 82 | if ($PSBoundParameters.ContainsKey('Credential')) { 83 | $icmParams.Credential = $Credential 84 | } 85 | [pscustomobject](Invoke-Command @icmParams) 86 | } 87 | 88 | 89 | } catch { 90 | Write-Error $_.Exception.Message 91 | } 92 | } 93 | } 94 | 95 | function Get-PlHostEntry { 96 | [CmdletBinding()] 97 | [OutputType([System.Management.Automation.PSCustomObject])] 98 | param 99 | ( 100 | [Parameter()] 101 | [ValidateNotNullOrEmpty()] 102 | [string]$ComputerName = $env:COMPUTERNAME, 103 | 104 | [Parameter()] 105 | [ValidateNotNullOrEmpty()] 106 | [pscredential]$Credential, 107 | 108 | [Parameter()] 109 | [ValidateNotNullOrEmpty()] 110 | [string]$HostFilePath = "$env:SystemRoot\System32\drivers\etc\hosts" 111 | 112 | ) 113 | begin { 114 | $ErrorActionPreference = 'Stop' 115 | } 116 | process { 117 | try { 118 | $sb = { 119 | param($HostFilePath) 120 | $regex = '^(?<ipAddress>[0-9.]+)[^\w]*(?<hostname>[^#\W]*)($|[\W]{0,}#\s+(?<comment>.*))' 121 | $matches = $null 122 | Get-Content -Path $HostFilePath | foreach { 123 | $null = $_ -match $regex 124 | if ($matches) { 125 | @{ 126 | 'IPAddress' = $matches.ipAddress 127 | 'HostName' = $matches.hostname 128 | } 129 | } 130 | $matches = $null 131 | } 132 | } 133 | 134 | if ($ComputerName -eq (hostname)) { 135 | & $sb $HostFilePath 136 | } else { 137 | $icmParams = @{ 138 | 'ComputerName' = $ComputerName 139 | 'ScriptBlock' = $sb 140 | 'ArgumentList' = $HostFilePath 141 | } 142 | if ($PSBoundParameters.ContainsKey('Credential')) { 143 | $icmParams.Credential = $Credential 144 | } 145 | [pscustomobject](Invoke-Command @icmParams) 146 | } 147 | } catch { 148 | Write-Error $_.Exception.Message 149 | } 150 | } 151 | } 152 | 153 | function Test-PsRemoting { 154 | param ( 155 | [Parameter(Mandatory = $true)] 156 | $computername, 157 | 158 | [Parameter(Mandatory)] 159 | [ValidateNotNullOrEmpty()] 160 | [pscredential]$Credential 161 | 162 | ) 163 | 164 | try { 165 | $errorActionPreference = "Stop" 166 | $result = Invoke-Command -ComputerName $computername { 1 } -Credential $Credential 167 | } catch { 168 | return $false 169 | } 170 | 171 | ## I�ve never seen this happen, but if you want to be 172 | ## thorough�. 173 | if ($result -ne 1) { 174 | Write-Verbose "Remoting to $computerName returned an unexpected result." 175 | return $false 176 | } 177 | $true 178 | } 179 | 180 | function Add-TrustedHostComputer { 181 | [CmdletBinding()] 182 | param 183 | ( 184 | [Parameter()] 185 | [ValidateNotNullOrEmpty()] 186 | [string[]]$ComputerName 187 | 188 | ) 189 | try { 190 | foreach ($c in $ComputerName) { 191 | Write-Verbose -Message "Adding [$($c)] to client WSMAN trusted hosts" 192 | $TrustedHosts = (Get-Item -Path WSMan:\localhost\Client\TrustedHosts).Value 193 | if (-not $TrustedHosts) { 194 | Set-Item -Path wsman:\localhost\Client\TrustedHosts -Value $c -Force 195 | } elseif (($TrustedHosts -split ',') -notcontains $c) { 196 | $TrustedHosts = ($TrustedHosts -split ',') + $c 197 | Set-Item -Path wsman:\localhost\Client\TrustedHosts -Value ($TrustedHosts -join ',') -Force 198 | } 199 | } 200 | } catch { 201 | Write-Error $_.Exception.Message 202 | } 203 | } 204 | #endregion 205 | 206 | try { 207 | $HostServerConfig = @{ 208 | Name = Read-Host -Prompt 'Name of your HYPERV host' 209 | IPAddress = Read-Host -Prompt 'IP address of your HYPERV host' 210 | Credential = Get-Credential -Message 'Local username/password to connect to your Hyper-V host' 211 | } 212 | 213 | if (-not (Get-PlHostEntry | where HostName -eq $hostServerConfig.Name)) { 214 | Write-Host -Object 'Adding local hosts entry for Hyper-V host...' 215 | Add-PlHostEntry -HostName $hostServerConfig.Name -IpAddress $hostServerConfig.IPAddress 216 | } 217 | 218 | Write-Host -Object "Setting local NIC to private.." 219 | Set-NetConnectionProfile -InterfaceAlias Ethernet -NetworkCategory Private 220 | 221 | Write-Host -Object 'Enabling PS remoting on local computer...' 222 | $null = Enable-PSRemoting -Force -SkipNetworkProfileCheck 223 | 224 | Write-Host -Object 'Adding server to trusted computers...' 225 | Add-TrustedHostComputer -ComputerName $hostServerConfig.Name 226 | 227 | $plParams = @{ 228 | 'ComputerName' = $HostServerConfig.Name 229 | 'Credential' = $HostServerConfig.Credential 230 | 'HostName' = $env:COMPUTERNAME 231 | 'IPAddress' = (Get-NetIPAddress -AddressFamily IPv4 | where { $_.PrefixOrigin -ne 'WellKnown' }).IPAddress 232 | } 233 | if (-not (Get-PlHostEntry -ComputerName $plParams.ComputerName -Credential $HostServerConfig.Credential | where HostName -eq $plParams.HostName)) { 234 | Write-Host -Object 'Adding hosts entry for local computer on Hyper-V host...' 235 | Add-PlHostEntry @plParams 236 | } 237 | 238 | if (-not (Test-PsRemoting -computername $hostServerConfig.Name -Credential $hostServerConfig.Credential)) { 239 | $wmiParams = @{ 240 | 'ComputerName' = $hostServerConfig.Name 241 | 'Credential' = $hostServerConfig.Credential 242 | 'Class' = 'Win32_Process' 243 | 'Name' = 'Create' 244 | 'Args' = 'c:\windows\system32\winrm.cmd quickconfig -quiet' 245 | } 246 | Write-Host -Object "PS remoting is not enabled. Enabling PS remoting on [$($hostServerConfig.Name)]" 247 | $process = Invoke-WmiMethod @wmiParams 248 | if ($process.ReturnValue -ne 0) { 249 | throw 'Enabling WinRM on host server failed' 250 | } else { 251 | Write-Host -Object 'Successfully enabled WinRM on host server' 252 | } 253 | } else { 254 | Write-Host -Object "PS remoting is already enabled on [$($hostServerConfig.Name)]" 255 | } 256 | 257 | Write-Host -Object 'Setting firewall rules on Hyper-V host...' 258 | $sb = { 259 | Enable-NetFirewallRule -DisplayGroup 'Windows Remote Management' 260 | Enable-NetFirewallRule -DisplayGroup 'Remote Event Log Management' 261 | Enable-NetFirewallRule -DisplayGroup 'Remote Volume Management' 262 | Enable-NetFirewallRule -DisplayGroup 'Windows Management Instrumentation (WMI)' 263 | Set-Service VDS -StartupType Automatic 264 | } 265 | Invoke-Command -ComputerName $hostServerConfig.Name -Credential $hostServerConfig.Credential -ScriptBlock $sb 266 | 267 | $sb = { 268 | $group = [ADSI]"WinNT://./Distributed COM Users" 269 | $members = @($group.Invoke("Members")) | foreach { 270 | $_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null) 271 | } 272 | if ($members -notcontains 'ANONYMOUS LOGON') { 273 | $group = [ADSI]"WinNT://./Distributed COM Users,group" 274 | $group.add("WinNT://./NT AUTHORITY/ANONYMOUS LOGON") 275 | } 276 | } 277 | Write-Host -Object 'Adding the ANONYMOUS LOGON user to the local machine and host server Distributed COM Users group for Hyper-V manager' 278 | Invoke-Command -ComputerName $hostServerConfig.Name -Credential $hostServerConfig.Credential -ScriptBlock $sb 279 | & $sb 280 | 281 | Write-Host -Object "Enabling remote COM access on Hyper-V host" 282 | Invoke-Command -ComputerName $hostServerConfig.Name -Credential $hostServerConfig.Credential -ScriptBlock { Set-ItemProperty -Path HKLM:\SOFTWARE\Microsoft\COM3 -Name RemoteAccessEnabled -Value 1 } 283 | 284 | Write-Host -Object 'Enabling applicable firewall rules on local machine...' 285 | Enable-NetFirewallRule -DisplayGroup 'Remote Volume Management' 286 | 287 | Write-Host -Object 'Adding saved credential on local computer for Hyper-V host...' 288 | if ((cmdkey /list:($HostServerConfig.Name)) -match '\* NONE \*') { 289 | $null = cmdkey /add:($HostServerConfig.Name) /user:($HostServerConfig.Credential.UserName) /pass:($HostServerConfig.Credential.GetNetworkCredential().Password) 290 | } 291 | 292 | if ($hyperVFeature = Get-WindowsOptionalFeature -FeatureName 'Microsoft-Hyper-V-Tools-All' -Online) { 293 | if ($hyperVFeature.State -ne 'Enabled') { 294 | Write-Host 'Enabling the Microsoft-Hyper-V-Tools-All features...' 295 | $hyperVFeature | Enable-WindowsOptionalFeature -Online -All 296 | } 297 | } else { 298 | throw 'Hyper-V Management PowerShell feature was not found. Are you on Windows 10?' 299 | } 300 | 301 | Write-Host -Object 'Lab setup is now complete.' -ForegroundColor Green 302 | } catch { 303 | Write-Warning -Message $_.Exception.Message 304 | } -------------------------------------------------------------------------------- /PowerLab.Tests.ps1: -------------------------------------------------------------------------------- 1 | $configFilePath = "$($PSScriptRoot | Split-Path -Parent)\PowerLabConfiguration.psd1" 2 | $script:LabConfiguration = Import-PowerShellDataFile -Path $configFilePath 3 | 4 | $vmConfig = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VMConfig 5 | $vms = $script:LabConfiguration.VirtualMachines 6 | $vhdConfig = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig 7 | $osConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration 8 | 9 | describe 'General VM configurations' { 10 | 11 | $icmParams = @{ 12 | ComputerName = $script:LabConfiguration.HostServer.Name 13 | } 14 | $labVMs = Invoke-Command @icmParams -ScriptBlock { 15 | Get-Vm | where { $_.Name -match ($using:vms.BaseName -join '|')} | foreach { 16 | $vmVhd = $_ | Get-VMHardDiskDrive | Get-Vhd 17 | [pscustomobject]@{ 18 | Name = $_.VmName 19 | VHDSize = ($vmVhd.Size / 1GB) 20 | VHDFormat = [string]$vmVhd.VhdFormat 21 | VHDType = [string]$vmVhd.VhdType 22 | VHDPath = $vmVhd.Path 23 | VMState = [string]$_.State 24 | VMPath = $_.Path 25 | VMMemory = ($_.MemoryStartup / 1GB) 26 | VMProcessorCount = [string]$_.ProcessorCount 27 | } 28 | } 29 | } 30 | 31 | foreach ($vm in $labVMs) { 32 | 33 | $icmParams = @{ 34 | ComputerName = $vm.Name 35 | } 36 | 37 | it "the [$($vm.Name)] VM should have a $($vhdConfig.Size) [$($vhdConfig.Type)] drive attached" { 38 | $vm.VHDSize | should be ($vhdConfig.Size -replace 'GB') 39 | $vm.VHDFormat | should be $vhdConfig.Type 40 | } 41 | 42 | it "the [$($vm.Name)] VM's VHD should have a $($vhdConfig.Sizing) sized VHDX" { 43 | $vm.VHDType | should be $vhdConfig.Sizing 44 | } 45 | 46 | it "the [$($vm.Name)] VM's VHD should be located at $($vhdConfig.Path)" { 47 | $vm.VHDPath | should be (Join-Path -Path $vhdConfig.Path -ChildPath "$($vm.Name).$($vm.VHDFormat)") 48 | } 49 | 50 | it "the [$($vm.Name)] VM should be running" { 51 | $vm.VMState | should be 'Running' 52 | } 53 | 54 | it "the [$($vm.Name)] VM should have a memory of $($vmConfig.StartupMemory)" { 55 | $vm.VMMemory | should be ($vmConfig.StartupMemory -replace 'GB') 56 | } 57 | 58 | it "the [$($vm.Name)] VM should have a processor count of $($vmConfig.ProcessorCount)" { 59 | $vm.VMProcessorCount | should be $vmConfig.ProcessorCount 60 | } 61 | 62 | it "the [$($vm.Name)] VM should be located at $($vmConfig.Path)" { 63 | $vm.VMPath | should be (Join-Path -Path $vmConfig.Path -ChildPath $vm.Name) 64 | } 65 | 66 | it "the [$($vm.Name)] VM should have a local user $($osConfig.Users.Name) in the local admins group" { 67 | $session = New-CimSession -ComputerName $vm.Name 68 | $cimParams = @{ 69 | ClassName = 'Win32_GroupUser' 70 | Filter = "GroupComponent=`"Win32_Group.Domain='$($vm.Name)',Name='Administrators'`"" 71 | } 72 | Get-CimInstance @cimParams | Select-Object -ExpandProperty PartComponent 73 | 74 | if ($session -ne $null) { 75 | $session | Remove-CimSession; 76 | } 77 | } 78 | 79 | it "the [$($vm.Name)] VM should have an IP in the $($osConfig.Network.IpNetwork) network" { 80 | 81 | $expectedIpOctets = $osConfig.Network.IpNetwork.Split('.')[0..2] -join '.' 82 | 83 | $ipInfo = Invoke-Command @icmParams -ScriptBlock { Get-NetIPAddress -AddressFamily IPv4 -PrefixLength 24 } 84 | $actualIpOctets = $ipInfo.IPAddress.Split('.')[0..2] -join '.' 85 | 86 | $actualIpOctets | should be $expectedIpOctets 87 | } 88 | 89 | it "the [$($vm.Name)] VM should have the DNS server of $($osConfig.Network.DnsServer)" { 90 | 91 | $dnsAddresses = Invoke-Command @icmParams -ScriptBlock { @(Get-DnsClientServerAddress -AddressFamily IPv4).where({$_.ServerAddresses}).ServerAddresses } 92 | if ($osConfig.Network.DnsServer -in $dnsAddresses) { 93 | $osConfig.Network.DnsServer | should bein $dnsAddresses 94 | } else { 95 | '127.0.0.1' | should bein $dnsAddresses 96 | } 97 | 98 | } 99 | } 100 | } 101 | 102 | describe 'Web Server' { 103 | 104 | $webConfig = $script:LabConfiguration.DefaultServerConfiguration.Web 105 | $webVmConfig = $script:LabConfiguration.VirtualMachines | where {$_.Type -eq 'Web'} 106 | $webSrvName = '{0}1' -f $webVmConfig 107 | 108 | it "all web servers should have the base name of [$($webVmConfig.BaseName)]" { 109 | Test-Connection -ComputerName $webSrvName -Quiet -Count 1 | should be $true 110 | } 111 | 112 | it "all web servers should have an operating system of [$($webVmConfig.OS)]" { 113 | $os = (Get-CimInstance -ComputerName $webSrvName -ClassName win32_operatingsystem -Property caption).caption 114 | $os | should match $webVmConfig.OS 115 | } 116 | 117 | it "all web servers should be running the [$($webVmConfig.Edition)] edition of Windows" { 118 | $os = (Get-CimInstance -ComputerName $webSrvName -ClassName win32_operatingsystem -Property caption).caption 119 | $os | should match $webVmConfig.Edition 120 | } 121 | 122 | it "the IIS site name should be $($webConfig.WebsiteName)" { 123 | $sites = Invoke-Command -ComputerName $webSrvName -ScriptBlock { Import-Module -Name WebAdministration; Get-IISWebSite } 124 | $webConfig.WebsiteName | should bein $sites.Name 125 | } 126 | 127 | it "the IIS application pool on the site should be $($webConfig.ApplicationPoolName) " { 128 | $apppools = Invoke-Command -ComputerName $webSrvName -ScriptBlock { Import-Module -Name WebAdministration; Get-IISApplicationPool } 129 | $webConfig.ApplicationPoolName | should bein $apppools.Name 130 | } 131 | } 132 | 133 | describe 'SQL Server' { 134 | 135 | $sqlConfig = $script:LabConfiguration.DefaultServerConfiguration.SQL 136 | $sqlVmConfig = $script:LabConfiguration.VirtualMachines | where {$_.Type -eq 'SQL'} 137 | $sqlSrvName = '{0}1' -f $SqlVmConfig 138 | 139 | it "all SQL servers should have the base name of [$($sqlVmConfig.BaseName)]" { 140 | Test-Connection -ComputerName $sqlSrvName -Quiet -Count 1 | should be $true 141 | } 142 | 143 | it "all SQL servers should have an operating system of [$($sqlVmConfig.OS)]" { 144 | $os = (Get-CimInstance -ComputerName $sqlSrvName -ClassName win32_operatingsystem -Property caption).caption 145 | $os | should match $sqlVmConfig.OS 146 | } 147 | 148 | it "all SQL servers should be running the [$($sqlVmConfig.Edition)] edition of Windows" { 149 | $os = (Get-CimInstance -ComputerName $sqlSrvName -ClassName win32_operatingsystem -Property caption).caption 150 | $os | should match $sqlVmConfig.Edition 151 | } 152 | 153 | it "the SQL administrator account should be $($sqlConfig.SystemAdministratorAccount.Name)" { 154 | $result | should 155 | } 156 | 157 | it "the SQL agent service should be running under the $($sqlConfig.ServiceAccount.Name) account" { 158 | $service = Get-CimIntance -Class 'Win32_Service' -ComputerName $sqlSrvName -Filter "Name = 'XXXXXXX'" 159 | $service.Something | should be $sqlConfig.ServiceAccount.Name 160 | } 161 | } 162 | 163 | describe 'Active Directory Forest' { 164 | 165 | $expectedAdConfig = $script:LabConfiguration.ActiveDirectoryConfiguration 166 | $adVmConfig = $script:LabConfiguration.VirtualMachines | where {$_.Type -eq 'Domain Controller'} 167 | $dcName = '{0}1' -f $adVmConfig 168 | 169 | $forest = Invoke-Command -ComputerName $dcName -ScriptBlock { Get-AdForest } 170 | $domain = Invoke-Command -ComputerName $dcName -ScriptBlock { Get-AdDomain } 171 | 172 | it "the domain controller should have the base name of [$($adVmConfig.BaseName)]" { 173 | Test-Connection -ComputerName $dcName -Quiet -Count 1 | should be $true 174 | } 175 | 176 | it "the domain controller should have an operating system of [$($adVmConfig.OS)]" { 177 | $os = (Get-CimInstance -ComputerName $dcName -ClassName win32_operatingsystem -Property caption).caption 178 | $os | should match $adVmConfig.OS 179 | } 180 | 181 | it "the domain controller should be running the [$($adVmConfig.Edition)] edition of Windows" { 182 | $os = (Get-CimInstance -ComputerName $dcName -ClassName win32_operatingsystem -Property caption).caption 183 | $os | should match $adVmConfig.Edition 184 | } 185 | 186 | it "the domain mode should be $($expectedAdConfig.DomainMode)" { 187 | $domain.DomainMode | should be $expectedAdConfig.DomainMode 188 | } 189 | 190 | it "the forest mode should be $($expectedAdConfig.ForestMode)" { 191 | $forest.ForestMode | should be $expectedAdConfig.ForestMode 192 | } 193 | 194 | it "the domain name should be $($expectedAdConfig.DomainName)" { 195 | $domain.Name | should be $expectedAdConfig.DomainName 196 | } 197 | 198 | it "the IP address of the DC should be [$($osConfig.Network.DnsServer)]" { 199 | $ipInfo = Invoke-Command -ComputerName $dcName -ScriptBlock { Get-NetIPAddress -AddressFamily IPv4 -PrefixLength 24 } 200 | $actualIpOctets = $ipInfo.IPAddress.Split('.')[0..2] -join '.' 201 | 202 | $actualIpOctets | should be $expectedIpOctets 203 | } 204 | } 205 | 206 | describe 'Hyper-V PowerLab Infrastructure' { 207 | 208 | $uncProjectRoot = ConvertToUncPath -LocalFilePath $script:LabConfiguration.ProjectRootFolder -ComputerName $script:LabConfiguration.HostServer.Name 209 | $isoRoot = ConvertToUncPath -LocalFilePath $script:LabConfiguration.IsoFolderPath -ComputerName $script:LabConfiguration.HostServer.Name 210 | $vhdRoot = ConvertToUncPath -LocalFilePath $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Path -ComputerName $script:LabConfiguration.HostServer.Name 211 | $vmRoot = ConvertToUncPath -LocalFilePath $script:LabConfiguration.DefaultVirtualMachineConfiguration.VMConfig.Path -ComputerName $script:LabConfiguration.HostServer.Name 212 | 213 | it 'the Hyper-V host is up' { 214 | Test-Connection -ComputerName $script:LabConfiguration.HostServer.Name -Quiet -Count 1 | should be $true 215 | } 216 | 217 | it 'The path to the ProjectRooFolder exists' { 218 | Test-Path -Path $uncProjectRoot -PathType Container | should be $true 219 | } 220 | 221 | it 'the path to the IsoFolderPath exists' { 222 | Test-Path -Path $isoRoot -PathType Container | should be $true 223 | } 224 | 225 | it 'the default VHD root path exists' { 226 | Test-Path -Path $vhdRoot -PathType Container | should be $true 227 | } 228 | 229 | it 'the default VM root path exists' { 230 | Test-Path -Path $vmRoot -PathType Container | should be $tru 231 | } 232 | 233 | it 'all ISOs defined in configuration exist' { 234 | @($script:LabConfiguration.ISOs).where({ -not (Test-Path -Path "$isoRoot\$($_.FileName)" -PathType Leaf)}) | should benullOrEmpty 235 | } 236 | 237 | it 'all operating systems defined in the configuration have an unattend.xml' { 238 | $validNames = $script:LabConfiguration.ISOs.where({ $_.Type -eq 'OS'}).Name 239 | $xmlFiles = Get-ChildItem "$PSScriptRoot\AutoUnattend" -Filter '*.xml' -File 240 | $validxmlFiles = $xmlFiles | Where-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -in $validNames } 241 | @($validNames).Count | shoud be @($validXmlFiles).Count 242 | } 243 | 244 | it 'all VMs in configuration have a corresponding ISO available' { 245 | $validOses = $script:LabConfiguration.ISOs.where({ $_.Type -eq 'OS'}).Name 246 | $vmOsesDefined = $script:LabConfiguration.VirtualMachines.OS 247 | $vmOsesDefined.where({ $_ -notin $validOses}) | should benullOrEmpty 248 | } 249 | } -------------------------------------------------------------------------------- /PowerLab.psm1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | 3 | #region Configuration 4 | Set-StrictMode -Version Latest 5 | 6 | $modulePath = $PSScriptRoot 7 | $configFilePath = "$modulePath\PowerLabConfiguration.psd1" 8 | $script:LabConfiguration = Import-PowerShellDataFile -Path $configFilePath 9 | 10 | #endregion 11 | function New-PowerLab { 12 | [CmdletBinding()] 13 | param ( 14 | [Parameter()] 15 | [ValidateNotNullOrEmpty()] 16 | [switch]$WinRmCopy 17 | ) 18 | $ErrorActionPreference = 'Stop' 19 | 20 | try { 21 | ## Create the switch 22 | NewLabSwitch 23 | 24 | ## Create the domain controller 25 | New-ActiveDirectoryForest 26 | 27 | # region Create the member servers 28 | foreach ($type in $($script:LabConfiguration.VirtualMachines).where({$_.Type -ne 'Domain Controller'}).Type) { 29 | & ("New-{0}Server" -f $type) -AddToDomain 30 | } 31 | #endregion 32 | } catch { 33 | Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)" 34 | } 35 | } 36 | function Remove-PowerLab { 37 | [OutputType('void')] 38 | [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] 39 | param 40 | () 41 | 42 | $ErrorActionPreference = 'Stop' 43 | 44 | ## Remove all VMs 45 | $icmParams = @{ 46 | ComputerName = $script:LabConfiguration.HostServer.Name 47 | } 48 | $nameMatch = $script:LabConfiguration.VirtualMachines.BaseName -join '|' 49 | if ($vms = Invoke-Command @icmParams -ScriptBlock { Get-Vm | where { $_.Name -match $using:nameMatch }}) { 50 | 51 | Invoke-Command @icmParams -ScriptBlock { Stop-Vm -Name $using:vms.Name -Force; Remove-Vm -Name $using:vms.Name } 52 | } 53 | 54 | ## Remove all VHDs 55 | $vhdPath = ConvertToUncPath -LocalFilePath $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Path -ComputerName $script:LabConfiguration.HostServer.Name 56 | if ($vhds = Get-ChildItem -Path $vhdPath) { 57 | if ($PSCmdlet.ShouldProcess("VHDs: [$($vhds.Name -join ',')]", 'Remove')) { 58 | $vhds | Remove-Item 59 | } 60 | } 61 | 62 | ## Remove all trusted hosts 63 | $trustedHostString = (Get-ChildItem -Path WSMan:\localhost\Client\TrustedHosts).Value 64 | $trustedHosts = $trustedHostString -split ',' 65 | $nonLabTrustedHosts = $trustedHosts | where { $_ -notmatch $nameMatch } 66 | if ($labTrustedHosts = $trustedHosts | where { $_ -match $nameMatch }) { 67 | $nonLabString = $nonLabTrustedHosts -join ',' 68 | if ($PSCmdlet.ShouldProcess('Lab trusted hosts', 'Remove')) { 69 | Set-Item -Path WSMan:\localhost\Client\TrustedHosts $nonLabString -Force 70 | } 71 | } 72 | 73 | ## Remove all cached credentials 74 | GetCachedCredential | where {$_.Name -match $nameMatch} | foreach { 75 | if ($PSCmdlet.ShouldProcess("Lab cached credential: $($_.Name)", 'Remove')) { 76 | RemoveCachedCredential -TargetName $_.Name 77 | } 78 | } 79 | } 80 | function New-ActiveDirectoryForest { 81 | [OutputType([void])] 82 | [CmdletBinding(SupportsShouldProcess)] 83 | param 84 | () 85 | 86 | $ErrorActionPreference = 'Stop' 87 | 88 | ## Build the VM 89 | $vm = New-PowerLabVm -Type 'Domain Controller' -PassThru 90 | 91 | ## Grab config values from file 92 | $forestConfiguration = $script:LabConfiguration.ActiveDirectoryConfiguration 93 | $forestParams = @{ 94 | DomainName = $forestConfiguration.DomainName 95 | DomainMode = $forestConfiguration.DomainMode 96 | ForestMode = $forestConfiguration.ForestMode 97 | Confirm = $false 98 | SafeModeAdministratorPassword = (ConvertTo-SecureString -AsPlainText $forestConfiguration.SafeModeAdministratorPassword -Force) 99 | WarningAction = 'Ignore' 100 | } 101 | 102 | ## Build the forest 103 | InvokeVmCommand -ArgumentList $forestParams -ComputerName $vm.Name -ScriptBlock { 104 | param($forestParams) 105 | $null = Install-windowsfeature -Name AD-Domain-Services -IncludeManagementTools 106 | $null = Install-ADDSForest @forestParams 107 | } 108 | 109 | ## Replace the workgroup cred with the new domain cred 110 | RemoveCachedCredential -TargetName $vm.Name 111 | $credConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 112 | $cred = New-PSCredential -UserName "$($forestConfiguration.DomainName)\$($credConfig.name)" -Password $credConfig.Password 113 | AddCachedCredential -ComputerName $vm.Name -Credential $cred 114 | } 115 | function New-PowerLabSqlServer { 116 | [OutputType([void])] 117 | [CmdletBinding(SupportsShouldProcess)] 118 | param 119 | ( 120 | [Parameter()] 121 | [ValidateNotNullOrEmpty()] 122 | [switch]$AddToDomain 123 | ) 124 | 125 | $ErrorActionPreference = 'Stop' 126 | 127 | ## Build the VM 128 | $vmparams = @{ 129 | Type = 'SQL' 130 | PassThru = $true 131 | } 132 | $vm = New-PowerLabVm @vmParams 133 | Install-PowerLabSqlServer -ComputerName $vm.Name 134 | 135 | if ($AddToDomain.IsPresent) { 136 | $credConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 137 | $domainUserName = '{0}\{1}' -f $script:LabConfiguration.ActiveDirectoryConfiguration.DomainName, $credConfig.name 138 | $domainCred = New-PSCredential -UserName $domainUserName -Password $credConfig.Password 139 | $addParams = @{ 140 | ComputerName = $vm.Name 141 | DomainName = $script:LabConfiguration.ActiveDirectoryConfiguration.DomainName 142 | Credential = $domainCred 143 | Restart = $true 144 | Force = $true 145 | } 146 | Add-Computer @addParams 147 | } 148 | } 149 | function New-WebServer { 150 | [OutputType([void])] 151 | [CmdletBinding(SupportsShouldProcess)] 152 | param 153 | ( 154 | [Parameter()] 155 | [ValidateNotNullOrEmpty()] 156 | [switch]$AddToDomain 157 | ) 158 | 159 | $ErrorActionPreference = 'Stop' 160 | 161 | ## Build the VM 162 | $vmparams = @{ 163 | Type = 'Web' 164 | PassThru = $true 165 | } 166 | $vm = New-PowerLabVm @vmParams 167 | Install-IIS -ComputerName $vm.Name 168 | 169 | if ($AddToDomain.IsPresent) { 170 | $credConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 171 | $domainUserName = '{0}\{1}' -f $script:LabConfiguration.ActiveDirectoryConfiguration.DomainName, $credConfig.name 172 | $domainCred = New-PSCredential -UserName $domainUserName -Password $credConfig.Password 173 | $addParams = @{ 174 | ComputerName = $vm.Name 175 | DomainName = $script:LabConfiguration.ActiveDirectoryConfiguration.DomainName 176 | Credential = $domainCred 177 | Restart = $true 178 | Force = $true 179 | } 180 | Add-Computer @addParams 181 | } 182 | 183 | } 184 | function Install-IIS { 185 | [OutputType([void])] 186 | [CmdletBinding(SupportsShouldProcess)] 187 | param 188 | ( 189 | [Parameter(Mandatory)] 190 | [ValidateNotNullOrEmpty()] 191 | [string]$ComputerName 192 | ) 193 | 194 | $ErrorActionPreference = 'Stop' 195 | 196 | $null = InvokeVmCommand -ComputerName $ComputerName -ScriptBlock { Install-WindowsFeature -Name Web-Server } 197 | 198 | $webConfig = $script:LabConfiguration.DefaultServerConfiguration.Web 199 | NewIISAppPool -ComputerName $ComputerName -Name $webConfig.ApplicationPoolName 200 | NewIISWebsite -ComputerName $ComputerName -Name $webConfig.WebsiteName -ApplicationPool $webConfig.ApplicationPoolName 201 | 202 | } 203 | function NewIISAppPool { 204 | [OutputType('void')] 205 | [CmdletBinding(SupportsShouldProcess)] 206 | param 207 | ( 208 | [Parameter(Mandatory)] 209 | [ValidateNotNullOrEmpty()] 210 | [string]$ComputerName, 211 | 212 | [Parameter(Mandatory)] 213 | [ValidateNotNullOrEmpty()] 214 | [string]$Name 215 | ) 216 | 217 | $ErrorActionPreference = 'Stop' 218 | 219 | $scriptBlock = { 220 | $null = Import-Module -Name 'WebAdministration' 221 | $appPoolPath = 'IIS:\AppPools\{0}' -f $Using:Name; 222 | if (-not (Test-Path -Path $appPoolPath)) { 223 | $null = New-Item -Path $appPoolPath -Force 224 | } 225 | } 226 | 227 | InvokeVmCommand -ComputerName $ComputerName -ScriptBlock $scriptBlock 228 | } 229 | function NewIISWebsite { 230 | [CmdletBinding()] 231 | param 232 | ( 233 | [Parameter(Mandatory)] 234 | [ValidateNotNullOrEmpty()] 235 | [string]$ComputerName, 236 | 237 | [Parameter(Mandatory)] 238 | [ValidateNotNullOrEmpty()] 239 | [string]$Name, 240 | 241 | [Parameter(Mandatory)] 242 | [ValidateNotNullOrEmpty()] 243 | [string]$ApplicationPool 244 | ) 245 | 246 | $ErrorActionPreference = 'Stop' 247 | 248 | $scriptBlock = { 249 | 250 | $null = Import-Module -Name 'WebAdministration' 251 | 252 | # Check if a physical path was specified or if one should be generated from the website name. 253 | # Build the full website physical path if not specified. 254 | $websitePhysicalPath = "C:\inetpub\sites\{0}" -f $Using:Name 255 | 256 | # Build the PSProvider path for the website. 257 | $websitePath = "IIS:\Sites\{0}" -f $Using:Name 258 | if (-not (Test-Path -Path $webSitePath)) { 259 | $appPoolPath = "IIS:\AppPools\{0}" -f $Using:ApplicationPool 260 | if (-not (Test-Path -Path $appPoolPath)) { 261 | throw "IIS application pool '{0}' does not exist." -f $Using:ApplicationPool 262 | } 263 | 264 | # Check if there are any existing websites. If not, we need to specify the ID, otherwise the action 265 | # will fail. 266 | if ((Get-ChildItem -Path IIS:\Sites).Count -eq 0) { 267 | $websiteParams = @{ 268 | id = 1 269 | } 270 | } 271 | 272 | # Create the website with the specified parameters. 273 | $websiteParams += @{ 274 | Path = $websitePath 275 | bindings = @{ 276 | protocol = 'http' 277 | physicalPath = $websitePhysicalPath 278 | bindingInformation = "*:80:$using:Name" 279 | } 280 | } 281 | 282 | $null = New-Item @websiteParams 283 | } 284 | 285 | } 286 | 287 | InvokeVmCommand -ComputerName $ComputerName -ScriptBlock $scriptBlock 288 | 289 | } 290 | function Install-PowerLabSqlServer { 291 | [OutputType([void])] 292 | [CmdletBinding(SupportsShouldProcess)] 293 | param 294 | ( 295 | [Parameter(Mandatory)] 296 | [ValidateNotNullOrEmpty()] 297 | [string]$ComputerName 298 | ) 299 | $ErrorActionPreference = 'Stop' 300 | 301 | try { 302 | $credConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 303 | $cred = New-PSCredential -UserName $credConfig.name -Password $credConfig.Password 304 | 305 | ## Copy the SQL server config ini to the VM 306 | $copiedConfigFile = Copy-Item -Path "$modulePath\SqlServer.ini" -Destination "\\$ComputerName\c$" -PassThru 307 | PrepareSqlServerInstallConfigFile -Path $copiedConfigFile 308 | 309 | $sqlConfigFilePath = $copiedConfigFile.FullName.Replace("\\$ComputerName\c$\", 'C:\') 310 | 311 | $isoConfig = $script:LabConfiguration.ISOs.where({$_.Name -eq 'SQL Server 2016'}) 312 | 313 | $isoPath = Join-Path -Path $script:LabConfiguration.IsoFolderPath -ChildPath $isoConfig.FileName 314 | $uncIsoPath = ConvertToUncPath -LocalFilePath $isoPath -ComputerName $script:LabConfiguration.HostServer.Name 315 | 316 | ## Copy the ISO to the VM 317 | $localDestIsoPath = 'C:\{0}' -f $isoConfig.FileName 318 | $destIsoPath = ConvertToUncPath -ComputerName $ComputerName -LocalFilePath $localDestIsoPath 319 | if (-not (Test-Path -Path $destIsoPath -PathType Leaf)) { 320 | Write-Verbose -Message "Copying SQL Server ISO to [$($destisoPath)]..." 321 | Copy-Item -Path $uncIsoPath -Destination $destIsoPath -Force 322 | } 323 | 324 | ## Execute the installer 325 | Write-Verbose -Message 'Running SQL Server installer...' 326 | $icmParams = @{ 327 | ComputerName = $ComputerName 328 | ArgumentList = $sqlConfigFilePath, $localDestIsoPath 329 | ScriptBlock = { 330 | $image = Mount-DiskImage -ImagePath $args[1] -PassThru 331 | $installerPath = "$(($image | Get-Volume).DriveLetter):" 332 | $null = & "$installerPath\setup.exe" "/CONFIGURATIONFILE=$($args[0])" 333 | $image | Dismount-DiskImage 334 | } 335 | } 336 | InvokeVmCommand @icmParams 337 | } catch { 338 | $PSCmdlet.ThrowTerminatingError($_) 339 | } finally { 340 | Write-Verbose -Message 'Cleaning up installer remnants...' 341 | Remove-Item -Path $destIsoPath, $copiedConfigFile.FullName -Recurse -ErrorAction Ignore 342 | } 343 | } 344 | function WaitWinRM { 345 | [CmdletBinding()] 346 | param 347 | ( 348 | [Parameter(Mandatory)] 349 | [ValidateNotNullOrEmpty()] 350 | [string]$ComputerName, 351 | 352 | [Parameter()] 353 | [pscredential]$Credential, 354 | 355 | [Parameter()] 356 | [ValidateNotNullOrEmpty()] 357 | [ValidateRange(1, [Int64]::MaxValue)] 358 | [int]$Timeout = 1500 359 | ) 360 | 361 | try { 362 | $icmParams = @{ 363 | ComputerName = $ComputerName 364 | ScriptBlock = { $true } 365 | SessionOption = (New-PSSessionOption -NoMachineProfile -OpenTimeout 20000 -SkipCACheck -SkipRevocationCheck) 366 | ErrorAction = 'SilentlyContinue' 367 | ErrorVariable = 'err' 368 | } 369 | 370 | if ($PSBoundParameters.ContainsKey('Credential')) { 371 | $icmParams.Credential = $Credential 372 | } 373 | 374 | $timer = [Diagnostics.Stopwatch]::StartNew() 375 | 376 | Wait-Ping -ComputerName $ComputerName -Timeout $Timeout 377 | 378 | while (-not (Invoke-Command @icmParams)) { 379 | Write-Verbose -Message "Waiting for [$($ComputerName)] to become available to WinRM..." 380 | if ($timer.Elapsed.TotalSeconds -ge $Timeout) { 381 | throw "Timeout exceeded. Giving up on WinRM availability to [$ComputerName]" 382 | } 383 | Start-Sleep -Seconds 10 384 | } 385 | } catch { 386 | $PSCmdlet.ThrowTerminatingError($_) 387 | $false 388 | } finally { 389 | if (Test-Path -Path Variable:\Timer) { 390 | $timer.Stop() 391 | } 392 | } 393 | } 394 | function PrepareSqlServerInstallConfigFile { 395 | [OutputType('void')] 396 | [CmdletBinding()] 397 | param 398 | ( 399 | [Parameter(Mandatory)] 400 | [ValidateNotNullOrEmpty()] 401 | [string]$Path 402 | ) 403 | 404 | $ErrorActionPreference = 'Stop' 405 | 406 | $sqlConfig = $script:LabConfiguration.DefaultServerConfiguration.SQL 407 | 408 | $configContents = Get-Content -Path $Path -Raw 409 | $configContents = $configContents.Replace('SQLSVCACCOUNT=""', ('SQLSVCACCOUNT="{0}"' -f $sqlConfig.ServiceAccount.Name)) 410 | $configContents = $configContents.Replace('SQLSVCPASSWORD=""', ('SQLSVCPASSWORD="{0}"' -f $sqlConfig.ServiceAccount.Password)) 411 | $configContents = $configContents.Replace('SQLSYSADMINACCOUNTS=""', ('SQLSYSADMINACCOUNTS="{0}"' -f $sqlConfig.SystemAdministratorAccount.Name)) 412 | Set-Content -Path $Path -Value $configContents 413 | 414 | } 415 | function New-PowerLabVm { 416 | [OutputType([void])] 417 | [CmdletBinding()] 418 | param 419 | ( 420 | [Parameter(Mandatory)] 421 | [ValidateNotNullOrEmpty()] 422 | [ValidateSet('SQL', 'Web', 'Domain Controller')] 423 | [string]$Type, 424 | 425 | [Parameter()] 426 | [ValidateNotNullOrEmpty()] 427 | [switch]$PassThru 428 | ) 429 | 430 | $ErrorActionPreference = 'Stop' 431 | 432 | $name = GetNextLabVmName -Type $Type 433 | 434 | ## Create the VM 435 | $scriptBlock = { 436 | $vmParams = @{ 437 | Name = $args[0] 438 | Path = $args[1] 439 | MemoryStartupBytes = $args[2] 440 | Switch = $args[3] 441 | Generation = $args[4] 442 | } 443 | New-VM @vmParams 444 | } 445 | $argList = @( 446 | $name 447 | $script:LabConfiguration.DefaultVirtualMachineConfiguration.VMConfig.Path 448 | (Invoke-Expression -Command $script:LabConfiguration.DefaultVirtualMachineConfiguration.VMConfig.StartupMemory) 449 | (GetLabSwitch).Name 450 | $script:LabConfiguration.DefaultVirtualMachineConfiguration.VmConfig.Generation 451 | ) 452 | $vm = InvokeHyperVCommand -Scriptblock $scriptBlock -ArgumentList $argList 453 | 454 | ## Create the VHD and install Windows on the VM 455 | $os = @($script:LabConfiguration.VirtualMachines).where({$_.Type -eq $Type}).OS 456 | $addparams = @{ 457 | Vm = $vm 458 | OperatingSystem = $os 459 | VmType = $Type 460 | } 461 | AddOperatingSystem @addparams 462 | 463 | InvokeHyperVCommand -Scriptblock { Start-Vm -Name $args[0] } -ArgumentList $name 464 | 465 | Add-TrustedHostComputer -ComputerName $name 466 | 467 | WaitWinRM -ComputerName $vm.Name 468 | 469 | ## Enabling CredSSP support 470 | ## Not using InvokeVMCommand here because we have to enable CredSSP first before it'll work 471 | $credConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 472 | $localCred = New-PSCredential -UserName $credConfig.name -Password $credConfig.Password 473 | Invoke-Command -ComputerName $name -ScriptBlock { $null = Enable-WSManCredSSP -Role Server -Force } -Credential $localCred 474 | 475 | if ($PassThru.IsPresent) { 476 | $vm 477 | } 478 | 479 | } 480 | function New-PSCredential { 481 | [CmdletBinding()] 482 | [OutputType([System.Management.Automation.PSCredential])] 483 | param 484 | ( 485 | [Parameter(Mandatory)] 486 | [ValidateNotNullOrEmpty()] 487 | [string]$UserName, 488 | 489 | [Parameter(Mandatory)] 490 | [ValidateNotNullOrEmpty()] 491 | [string]$Password 492 | ) 493 | 494 | $ErrorActionPreference = 'Stop' 495 | 496 | #region Build arguments 497 | $arguments = @($UserName) 498 | $arguments += ConvertTo-SecureString -String $Password -AsPlainText -Force 499 | #endregion Build arguments 500 | 501 | # Create a new credential object with the specified parameters. 502 | New-Object System.Management.Automation.PSCredential -ArgumentList $arguments 503 | } 504 | function AddCachedCredential { 505 | [OutputType('void')] 506 | [CmdletBinding()] 507 | param 508 | ( 509 | [Parameter(Mandatory)] 510 | [ValidateNotNullOrEmpty()] 511 | [string]$ComputerName, 512 | 513 | [Parameter(Mandatory)] 514 | [ValidateNotNullOrEmpty()] 515 | [pscredential]$Credential 516 | ) 517 | 518 | $ErrorActionPreference = 'Stop' 519 | 520 | if ((cmdkey /list:$ComputerName) -match '\* NONE \*') { 521 | $null = cmdkey /add:$ComputerName /user:($Credential.UserName) /pass:($Credential.GetNetworkCredential().Password) 522 | } 523 | } 524 | function RemoveCachedCredential { 525 | [OutputType([void])] 526 | [CmdletBinding()] 527 | param 528 | ( 529 | [Parameter(Mandatory)] 530 | [ValidateNotNullOrEmpty()] 531 | [string]$TargetName, 532 | 533 | [Parameter()] 534 | [ValidateNotNullOrEmpty()] 535 | [string[]]$ComputerName 536 | ) 537 | 538 | if (-not $PSBoundParameters.ContainsKey('ComputerName')) { 539 | $null = cmdkey /delete:$TargetName 540 | } else { 541 | foreach ($c in $ComputerName) { 542 | $invParams = @{ 543 | ComputerName = $c 544 | Command = "cmdkey /delete:$TargetName" 545 | } 546 | $null = Invoke-PsExec @invParams 547 | } 548 | } 549 | 550 | } 551 | function ConvertToMatchValue { 552 | [OutputType('string')] 553 | [CmdletBinding()] 554 | param 555 | ( 556 | [Parameter(Mandatory)] 557 | [ValidateNotNullOrEmpty()] 558 | [string]$String, 559 | 560 | [Parameter(Mandatory)] 561 | [ValidateNotNullOrEmpty()] 562 | [string]$RegularExpression 563 | ) 564 | 565 | ([regex]::Match($String, $RegularExpression)).Groups[1].Value 566 | 567 | } 568 | function ConvertToCachedCredential { 569 | [OutputType('pscustomobject')] 570 | [CmdletBinding()] 571 | param 572 | ( 573 | [Parameter(Mandatory)] 574 | $CmdKeyOutput 575 | ) 576 | 577 | if (-not ($CmdKeyOutput.where({ $_ -match '\* NONE \*' }))) { 578 | if (@($CmdKeyOutput).Count -eq 1) { 579 | $CmdKeyOutput = $CmdKeyOutput -split "`n" 580 | } 581 | $nullsRemoved = $CmdKeyOutput.where({ $_ }) 582 | $i = 0 583 | foreach ($j in $nullsRemoved) { 584 | if ($j -match '^\s+Target:') { 585 | [pscustomobject]@{ 586 | Name = (ConvertToMatchValue -String $j -RegularExpression 'Target: .+:target=(.*)$').Trim() 587 | Category = (ConvertToMatchValue -String $j -RegularExpression 'Target: (.+):').Trim() 588 | Type = (ConvertToMatchValue -String $nullsRemoved[$i + 1] -RegularExpression 'Type: (.+)$').Trim() 589 | User = (ConvertToMatchValue -String $nullsRemoved[$i + 2] -RegularExpression 'User: (.+)$').Trim() 590 | Persistence = ($nullsRemoved[$i + 3]).Trim() 591 | } 592 | } 593 | $i++ 594 | } 595 | } 596 | } 597 | function GetCachedCredential { 598 | [OutputType([pscustomobject])] 599 | [CmdletBinding()] 600 | param 601 | ( 602 | [Parameter()] 603 | [ValidateNotNullOrEmpty()] 604 | [string[]]$ComputerName, 605 | 606 | [Parameter()] 607 | [ValidateNotNullOrEmpty()] 608 | [string]$TargetName 609 | ) 610 | 611 | if (-not $PSBoundParameters.ContainsKey('ComputerName') -and -not ($PSBoundParameters.ContainsKey('Name'))) { 612 | ConvertToCachedCredential -CmdKeyOutput (cmdkey /list) 613 | } elseif (-not $PSBoundParameters.ContainsKey('ComputerName') -and $PSBoundParameters.ContainsKey('Name')) { 614 | ConvertToCachedCredential -CmdKeyOutput (cmdkey /list:$TargetName) 615 | } else { 616 | foreach ($c in $ComputerName) { 617 | $cmdkeyOutput = Invoke-PsExec -ComputerName $c -Command 'cmdkey /list' 618 | if ($cred = ConvertToCachedCredential -CmdKeyOutput $cmdkeyOutput) { 619 | [pscustomobject]@{ 620 | ComputerName = $c 621 | Credentials = $cred 622 | } 623 | } 624 | } 625 | } 626 | } 627 | function TestIsIsoNameValid { 628 | [OutputType([bool])] 629 | [CmdletBinding(SupportsShouldProcess)] 630 | param 631 | ( 632 | [Parameter(Mandatory)] 633 | [ValidateNotNullOrEmpty()] 634 | [string]$Name 635 | ) 636 | 637 | if ($Name -notin $script:LabConfiguration.ISOs.Name) { 638 | throw "The ISO with label '$Name' could not be found." 639 | } else { 640 | $true 641 | } 642 | 643 | } 644 | function TestIsOsNameValid { 645 | [OutputType([bool])] 646 | [CmdletBinding(SupportsShouldProcess)] 647 | param 648 | ( 649 | [Parameter(Mandatory)] 650 | [ValidateNotNullOrEmpty()] 651 | [string]$Name 652 | ) 653 | 654 | if (($Name -notin ($script:LabConfiguration.ISOs | Where-Object { $_.Type -eq 'OS' }).Name)) { 655 | throw "The operating system name '$Name' is not valid." 656 | } else { 657 | $true 658 | } 659 | 660 | } 661 | function AddOperatingSystem { 662 | [CmdletBinding()] 663 | param 664 | ( 665 | [Parameter(Mandatory)] 666 | [ValidateNotNullOrEmpty()] 667 | [object]$Vm, 668 | 669 | [Parameter(Mandatory)] 670 | [ValidateNotNullOrEmpty()] 671 | [ValidateScript({ TestIsOsNameValid $_ })] 672 | [string]$OperatingSystem, 673 | 674 | [Parameter()] 675 | [ValidateNotNullOrEmpty()] 676 | [string]$VmType, 677 | 678 | [Parameter()] 679 | [ValidateNotNullOrEmpty()] 680 | [switch]$DomainJoined 681 | ) 682 | 683 | $ErrorActionPreference = 'Stop' 684 | 685 | try { 686 | $templateAnswerFilePath = (GetUnattendXmlFile -OperatingSystem $OperatingSystem).FullName 687 | $isoConfig = $script:LabConfiguration.ISOs.where({$_.Name -eq $OperatingSystem}) 688 | 689 | $ipAddress = NewVmIpAddress 690 | $prepParams = @{ 691 | Path = $templateAnswerFilePath 692 | VMName = $vm.Name 693 | IpAddress = $ipAddress 694 | DnsServer = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Network.DnsServer 695 | ProductKey = $isoConfig.ProductKey 696 | UserName = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }).Name 697 | UserPassword = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }).Password 698 | DomainName = $script:LabConfiguration.ActiveDirectoryConfiguration.DomainName 699 | } 700 | if ($PSBoundParameters.ContainsKey('VmType')) { 701 | $prepParams.VmType = $VmType 702 | } 703 | $answerFile = PrepareUnattendXmlFile @prepParams 704 | 705 | if (-not ($vhd = NewLabVhd -OperatingSystem $OperatingSystem -AnswerFilePath $answerFile.FullName -Name $vm.Name -PassThru)) { 706 | throw 'VHD creation failed' 707 | } 708 | 709 | $invParams = @{ 710 | Scriptblock = { 711 | $vm = Get-Vm -Name $args[0] 712 | $vm | Add-VMHardDiskDrive -Path $args[1] 713 | $bootOrder = ($vm | Get-VMFirmware).Bootorder 714 | if ($bootOrder[0].BootType -ne 'Drive') { 715 | $vm | Set-VMFirmware -FirstBootDevice $vm.HardDrives[0] 716 | } 717 | } 718 | ArgumentList = $Vm.Name, $vhd.Path 719 | } 720 | InvokeHyperVCommand @invParams 721 | 722 | ## Add the VM to the local hosts file 723 | if (-not (Get-HostsFileEntry | where {$_.HostName -eq $vm.Name})) { 724 | Add-HostsFileEntry -HostName $vm.Name -IpAddress $ipAddress -ErrorAction Ignore 725 | } 726 | 727 | ## Add the cached credential the local computer 728 | $credConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 729 | if ($DomainJoined.IsPresent) { 730 | $userName = '{0}\{1}' -f $script:LabConfiguration.ActiveDirectoryConfiguration.DomainName, $credConfig.name 731 | } else { 732 | $userName = $credConfig.name 733 | 734 | $cred = New-PSCredential -UserName $userName -Password $credConfig.Password 735 | AddCachedCredential -ComputerName $vm.Name -Credential $cred 736 | } 737 | } catch { 738 | $PSCmdlet.ThrowTerminatingError($_) 739 | } 740 | } 741 | function ConvertToVirtualDisk { 742 | [CmdletBinding()] 743 | param 744 | ( 745 | [Parameter(Mandatory)] 746 | [ValidateNotNullOrEmpty()] 747 | [ValidatePattern('\.vhdx?$')] 748 | [string]$VhdPath, 749 | 750 | [Parameter(Mandatory)] 751 | [ValidateNotNullOrEmpty()] 752 | [string]$IsoFilePath, 753 | 754 | [Parameter()] 755 | [ValidateNotNullOrEmpty()] 756 | [string]$AnswerFilePath, 757 | 758 | [Parameter()] 759 | [ValidateNotNullOrEmpty()] 760 | [ValidateSet('Dynamic', 'Fixed')] 761 | [string]$Sizing = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Sizing, 762 | 763 | [Parameter()] 764 | [ValidateNotNullOrEmpty()] 765 | [string]$Edition = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.OSEdition, 766 | 767 | [Parameter()] 768 | [ValidateNotNullOrEmpty()] 769 | [ValidateRange(512MB, 64TB)] 770 | [Uint64]$SizeBytes = (Invoke-Expression $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Size), 771 | 772 | [Parameter()] 773 | [ValidateNotNullOrEmpty()] 774 | [ValidateSet('VHD', 'VHDX')] 775 | [string]$VhdFormat = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Type, 776 | 777 | [Parameter()] 778 | [ValidateNotNullOrEmpty()] 779 | [string]$VHDPartitionStyle = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.PartitionStyle 780 | 781 | ) 782 | 783 | $ErrorActionPreference = 'Stop' 784 | 785 | $projectRootUnc = ConvertToUncPath -LocalFilePath $script:LabConfiguration.ProjectRootFolder -ComputerName $script:LabConfiguration.HostServer.Name 786 | Copy-Item -Path "$PSScriptRoot\Convert-WindowsImage.ps1" -Destination $projectRootUnc -Force 787 | 788 | ## Copy the answer file to the Hyper-V host 789 | $answerFileName = $AnswerFilePath | Split-Path -Leaf 790 | Copy-Item -Path $AnswerFilePath -Destination $projectRootUnc -Force 791 | $localTempAnswerFilePath = Join-Path -Path ($projectrootunc -replace '.*(\w)\$', '$1:') -ChildPath $answerFileName 792 | 793 | $sb = { 794 | . $args[0] 795 | $convertParams = @{ 796 | SourcePath = $args[1] 797 | SizeBytes = $args[2] 798 | Edition = $args[3] 799 | VHDFormat = $args[4] 800 | VHDPath = $args[5] 801 | VHDType = $args[6] 802 | VHDPartitionStyle = $args[7] 803 | } 804 | if ($args[8]) { 805 | $convertParams.UnattendPath = $args[8] 806 | } 807 | Convert-WindowsImage @convertParams 808 | Get-Vhd -Path $args[5] 809 | } 810 | 811 | $icmParams = @{ 812 | ScriptBlock = $sb 813 | ArgumentList = (Join-Path -Path $script:LabConfiguration.ProjectRootFolder -ChildPath 'Convert-WindowsImage.ps1'), $IsoFilePath, $SizeBytes, $Edition, $VhdFormat, $VhdPath, $Sizing, $VHDPartitionStyle, $localTempAnswerFilePath 814 | } 815 | InvokeHyperVCommand @icmParams 816 | } 817 | function NewLabVhd { 818 | [CmdletBinding(DefaultParameterSetName = 'None')] 819 | param 820 | ( 821 | 822 | [Parameter(Mandatory, ParameterSetName = 'OSInstall')] 823 | [ValidateNotNullOrEmpty()] 824 | [string]$Name, 825 | 826 | [Parameter()] 827 | [ValidateNotNullOrEmpty()] 828 | [ValidateRange(512MB, 1TB)] 829 | [int64]$Size = (Invoke-Expression $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Size), 830 | 831 | [Parameter()] 832 | [ValidateNotNullOrEmpty()] 833 | [ValidateSet('Dynamic', 'Fixed')] 834 | [string]$Sizing = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Sizing, 835 | 836 | [Parameter(Mandatory, ParameterSetName = 'OSInstall')] 837 | [ValidateNotNullOrEmpty()] 838 | [ValidateScript({ TestIsOsNameValid $_ })] 839 | [string]$OperatingSystem, 840 | 841 | [Parameter(Mandatory, ParameterSetName = 'OSInstall')] 842 | [ValidateNotNullOrEmpty()] 843 | [string]$AnswerFilePath, 844 | 845 | [Parameter()] 846 | [ValidateNotNullOrEmpty()] 847 | [switch]$PassThru 848 | ) 849 | begin { 850 | $ErrorActionPreference = 'Stop' 851 | } 852 | process { 853 | try { 854 | $params = @{ 855 | 'SizeBytes' = $Size 856 | } 857 | $vhdPath = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Path 858 | if ($PSBoundParameters.ContainsKey('OperatingSystem')) { 859 | $isoFileName = $script:LabConfiguration.ISOs.where({ $_.Name -eq $OperatingSystem }).FileName 860 | 861 | $cvtParams = $params + @{ 862 | IsoFilePath = Join-Path -Path $script:LabConfiguration.IsoFolderPath -ChildPath $isoFileName 863 | VhdPath = '{0}.vhdx' -f (Join-Path -Path $vhdPath -ChildPath $Name) 864 | VhdFormat = 'VHDX' 865 | Sizing = $Sizing 866 | AnswerFilePath = $AnswerFilePath 867 | } 868 | 869 | $vhd = ConvertToVirtualDisk @cvtParams 870 | } else { 871 | $params.ComputerName = $script:LabConfiguration.HostServer.Name 872 | $params.Path = "$vhdPath\$Name.vhdx" 873 | if ($Sizing -eq 'Dynamic') { 874 | $params.Dynamic = $true 875 | } elseif ($Sizing -eq 'Fixed') { 876 | $params.Fixed = $true 877 | } 878 | 879 | $invParams = @{ 880 | ScriptBlock = { $params = $args[0]; New-VHD @params } 881 | ArgumentList = $params 882 | } 883 | $vhd = InvokeHyperVCommand @invParams 884 | } 885 | if ($PassThru.IsPresent) { 886 | $vhd 887 | } 888 | } catch { 889 | Write-Error $_.Exception.Message 890 | } 891 | } 892 | } 893 | function NewVmIpAddress { 894 | [OutputType('string')] 895 | [CmdletBinding()] 896 | param 897 | () 898 | 899 | $ipNet = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Network.IpNetwork 900 | $ipBase = $ipNet -replace ".$($ipNet.Split('.')[-1])$" 901 | $randomLastOctet = Get-Random -Minimum 10 -Maximum 254 902 | $ipBase, $randomLastOctet -join '.' 903 | 904 | } 905 | function Get-PowerLabVhd { 906 | [CmdletBinding()] 907 | param 908 | ( 909 | [Parameter()] 910 | [ValidateNotNullOrEmpty()] 911 | [string]$Name 912 | 913 | ) 914 | try { 915 | $defaultVhdPath = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VHDConfig.Path 916 | 917 | $icmParams = @{ 918 | ScriptBlock = { Get-ChildItem -Path $args[0] -File | foreach { Get-VHD -Path $_.FullName } } 919 | ArgumentList = $defaultVhdPath 920 | } 921 | InvokeHyperVCommand @icmParams 922 | } catch { 923 | $PSCmdlet.ThrowTerminatingError($_) 924 | } 925 | } 926 | function Get-PowerLabVm { 927 | [CmdletBinding(DefaultParameterSetName = 'Name')] 928 | param 929 | ( 930 | [Parameter(ParameterSetName = 'Name')] 931 | [ValidateNotNullOrEmpty()] 932 | [string]$Name, 933 | 934 | [Parameter(ParameterSetName = 'Type')] 935 | [ValidateNotNullOrEmpty()] 936 | [string]$Type 937 | 938 | ) 939 | $ErrorActionPreference = 'Stop' 940 | 941 | $nameMatch = $script:LabConfiguration.VirtualMachines.BaseName -join '|' 942 | if ($PSBoundParameters.ContainsKey('Name')) { 943 | $nameMatch = $Name 944 | } elseif ($PSBoundParameters.ContainsKey('Type')) { 945 | $nameMatch = $Type 946 | } 947 | 948 | try { 949 | $icmParams = @{ 950 | ScriptBlock = { $name = $args[0]; @(Get-VM).where({ $_.Name -match $name }) } 951 | ArgumentList = $nameMatch 952 | } 953 | InvokeHyperVCommand @icmParams 954 | } catch { 955 | if ($_.Exception.Message -notmatch 'Hyper-V was unable to find a virtual machine with name') { 956 | $PSCmdlet.ThrowTerminatingError($_) 957 | } 958 | } 959 | } 960 | function InvokeVmCommand { 961 | [CmdletBinding()] 962 | param 963 | ( 964 | [Parameter(Mandatory)] 965 | [ValidateNotNullOrEmpty()] 966 | [string]$ComputerName, 967 | 968 | [Parameter(Mandatory)] 969 | [ValidateNotNullOrEmpty()] 970 | [scriptblock]$ScriptBlock, 971 | 972 | [Parameter()] 973 | [ValidateNotNullOrEmpty()] 974 | [object[]]$ArgumentList 975 | ) 976 | 977 | $ErrorActionPreference = 'Stop' 978 | 979 | $credConfig = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 980 | $cred = New-PSCredential -UserName $credConfig.name -Password $credConfig.Password 981 | $icmParams = @{ 982 | ComputerName = $ComputerName 983 | ScriptBlock = $ScriptBlock 984 | Credential = $cred 985 | Authentication = 'CredSSP' 986 | } 987 | if ($PSBoundParameters.ContainsKey('ArgumentList')) { 988 | $icmParams.ArgumentList = $ArgumentList 989 | } 990 | Invoke-Command @icmParams 991 | 992 | 993 | } 994 | function InvokeHyperVCommand { 995 | [CmdletBinding(SupportsShouldProcess)] 996 | param 997 | ( 998 | [Parameter(Mandatory)] 999 | [ValidateNotNullOrEmpty()] 1000 | [scriptblock]$Scriptblock, 1001 | 1002 | [Parameter()] 1003 | [ValidateNotNullOrEmpty()] 1004 | [object[]]$ArgumentList 1005 | ) 1006 | 1007 | $ErrorActionPreference = 'Stop' 1008 | 1009 | $icmParams = @{ 1010 | ScriptBlock = $Scriptblock 1011 | ArgumentList = $ArgumentList 1012 | } 1013 | 1014 | if (-not (Get-Variable 'hypervSession' -Scope Script -ErrorAction Ignore)) { 1015 | $script:hypervSession = New-PSSession -ComputerName $script:LabConfiguration.HostServer.Name 1016 | } 1017 | $icmParams.Session = $script:hypervSession 1018 | 1019 | Invoke-Command @icmParams 1020 | 1021 | } 1022 | function GetLabSwitch { 1023 | [OutputType('Microsoft.HyperV.PowerShell.VMSwitch')] 1024 | [CmdletBinding()] 1025 | param 1026 | () 1027 | 1028 | $ErrorActionPreference = 'Stop' 1029 | 1030 | $switchConfig = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VirtualSwitch 1031 | 1032 | $scriptBlock = { 1033 | if ($args[1] -eq 'External') { 1034 | Get-VmSwitch -SwitchType 'External' 1035 | } else { 1036 | Get-VmSwitch -Name $args[0] -SwitchType $args[1] 1037 | } 1038 | } 1039 | InvokeHyperVCommand -Scriptblock $scriptBlock -ArgumentList $switchConfig.Name, $switchConfig.Type 1040 | } 1041 | function NewLabSwitch { 1042 | [CmdletBinding()] 1043 | param 1044 | ( 1045 | [Parameter()] 1046 | [ValidateNotNullOrEmpty()] 1047 | [string]$Name = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VirtualSwitch.Name, 1048 | 1049 | [Parameter()] 1050 | [ValidateNotNullOrEmpty()] 1051 | [ValidateSet('Internal', 'External')] 1052 | [string]$Type = $script:LabConfiguration.DefaultVirtualMachineConfiguration.VirtualSwitch.Type 1053 | 1054 | ) 1055 | begin { 1056 | $ErrorActionPreference = 'Stop' 1057 | } 1058 | process { 1059 | try { 1060 | $scriptBlock = { 1061 | if ($args[1] -eq 'External') { 1062 | if ($externalSwitch = Get-VmSwitch -SwitchType 'External') { 1063 | $switchName = $externalSwitch.Name 1064 | } else { 1065 | $switchName = $args[0] 1066 | $netAdapterName = (Get-NetAdapter -Physical| where { $_.Status -eq 'Up' }).Name 1067 | $null = New-VMSwitch -Name $args[0] -NetAdapterName $netAdapterName 1068 | } 1069 | } else { 1070 | $switchName = $args[0] 1071 | if (-not (Get-VmSwitch -Name $args[0] -ErrorAction Ignore)) { 1072 | $null = New-VMSwitch -Name $args[0] -SwitchType $args[1] 1073 | } 1074 | } 1075 | } 1076 | InvokeHyperVCommand -Scriptblock $scriptBlock -ArgumentList $Name, $Type 1077 | } catch { 1078 | Write-Error $_.Exception.Message 1079 | } 1080 | } 1081 | } 1082 | function ConvertToUncPath { 1083 | <# 1084 | .SYNOPSIS 1085 | A simple function to convert a local file path and a computer name to a network UNC path. 1086 | 1087 | .PARAMETER LocalFilePath 1088 | A file path ie. C:\Windows\somefile.txt 1089 | 1090 | .PARAMETER Computername 1091 | One or more computers in which the file path exists on 1092 | #> 1093 | [CmdletBinding()] 1094 | param ( 1095 | [Parameter(Mandatory)] 1096 | [string]$LocalFilePath, 1097 | 1098 | [Parameter(Mandatory)] 1099 | [string[]]$ComputerName 1100 | ) 1101 | process { 1102 | try { 1103 | foreach ($Computer in $ComputerName) { 1104 | $RemoteFilePathDrive = ($LocalFilePath | Split-Path -Qualifier).TrimEnd(':') 1105 | "\\$Computer\$RemoteFilePathDrive`$$($LocalFilePath | Split-Path -NoQualifier)" 1106 | } 1107 | } catch { 1108 | Write-Error $_.Exception.Message 1109 | } 1110 | } 1111 | } 1112 | function GetNextLabVmName { 1113 | [OutputType('string')] 1114 | [CmdletBinding(SupportsShouldProcess)] 1115 | param 1116 | ( 1117 | [Parameter(Mandatory)] 1118 | [ValidateNotNullOrEmpty()] 1119 | [string]$Type 1120 | ) 1121 | 1122 | if (-not ($types = @($script:LabConfiguration.VirtualMachines).where({$_.Type -eq $Type}))) { 1123 | throw "Unrecognize VM type: [$($Type)]" 1124 | } 1125 | 1126 | if (-not ($highNumberVm = Get-PowerLabVm -Type $Type | Select -ExpandProperty Name | Sort-Object -Descending | Select-Object -First 1)) { 1127 | $nextNum = 1 1128 | } else { 1129 | [int]$highNum = [regex]::matches($highNumberVm, '(\d+)$').Groups[1].Value 1130 | $nextNum = $highNum + 1 1131 | } 1132 | 1133 | $baseName = $types.BaseName 1134 | 1135 | '{0}{1}' -f $baseName, $nextNum 1136 | } 1137 | function Add-TrustedHostComputer { 1138 | [CmdletBinding()] 1139 | param 1140 | ( 1141 | [Parameter()] 1142 | [ValidateNotNullOrEmpty()] 1143 | [string[]]$ComputerName 1144 | 1145 | ) 1146 | try { 1147 | foreach ($c in $ComputerName) { 1148 | Write-Verbose -Message "Adding [$($c)] to client WSMAN trusted hosts" 1149 | $TrustedHosts = (Get-Item -Path WSMan:\localhost\Client\TrustedHosts).Value 1150 | if (-not $TrustedHosts) { 1151 | Set-Item -Path wsman:\localhost\Client\TrustedHosts -Value $c -Force 1152 | } elseif (($TrustedHosts -split ',') -notcontains $c) { 1153 | $TrustedHosts = ($TrustedHosts -split ',') + $c 1154 | Set-Item -Path wsman:\localhost\Client\TrustedHosts -Value ($TrustedHosts -join ',') -Force 1155 | } 1156 | } 1157 | } catch { 1158 | Write-Error $_.Exception.Message 1159 | } 1160 | } 1161 | function GetUnattendXmlFile { 1162 | [OutputType('System.IO.FileInfo')] 1163 | [CmdletBinding()] 1164 | param 1165 | ( 1166 | [Parameter(Mandatory)] 1167 | [ValidateNotNullOrEmpty()] 1168 | [ValidateScript({ TestIsOsNameValid $_ })] 1169 | [string]$OperatingSystem 1170 | ) 1171 | 1172 | $ErrorActionPreference = 'Stop' 1173 | 1174 | Get-ChildItem -Path "$PSScriptRoot\AutoUnattend" -Filter "$OperatingSystem.xml" 1175 | 1176 | } 1177 | function PrepareUnattendXmlFile { 1178 | [OutputType('System.IO.FileInfo')] 1179 | [CmdletBinding(SupportsShouldProcess)] 1180 | param 1181 | ( 1182 | [Parameter()] 1183 | [ValidateNotNullOrEmpty()] 1184 | [string]$Path, 1185 | 1186 | [Parameter()] 1187 | [ValidateNotNullOrEmpty()] 1188 | [string]$VMName, 1189 | 1190 | [Parameter()] 1191 | [ValidateNotNullOrEmpty()] 1192 | [string]$IpAddress, 1193 | 1194 | [Parameter()] 1195 | [ValidateNotNullOrEmpty()] 1196 | [string]$DnsServer, 1197 | 1198 | [Parameter()] 1199 | [ValidateNotNullOrEmpty()] 1200 | [string]$DomainName, 1201 | 1202 | [Parameter()] 1203 | [ValidateNotNullOrEmpty()] 1204 | [string]$ProductKey, 1205 | 1206 | [Parameter()] 1207 | [ValidateNotNullOrEmpty()] 1208 | [string]$UserName, 1209 | 1210 | [Parameter()] 1211 | [ValidateNotNullOrEmpty()] 1212 | [string]$UserPassword, 1213 | 1214 | [Parameter()] 1215 | [ValidateNotNullOrEmpty()] 1216 | [string]$VmType 1217 | ) 1218 | 1219 | $ErrorActionPreference = 'Stop' 1220 | 1221 | ## Make a copy of the unattend XML 1222 | $tempUnattend = Copy-Item -Path $Path -Destination $env:TEMP -PassThru -Force 1223 | 1224 | ## Prep the XML object 1225 | $unattendText = Get-Content -Path $tempUnattend.FullName -Raw 1226 | $xUnattend = ([xml]$unattendText) 1227 | $ns = New-Object System.Xml.XmlNamespaceManager($xunattend.NameTable) 1228 | $ns.AddNamespace('ns', $xUnattend.DocumentElement.NamespaceURI) 1229 | 1230 | if ($VmType -eq 'Domain Controller') { 1231 | $dnsIp = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Network.DnsServer 1232 | $xUnattend.SelectSingleNode('//ns:Interface/ns:UnicastIpAddresses/ns:IpAddress', $ns).InnerText = "$dnsIp/24" 1233 | $xUnattend.SelectSingleNode('//ns:DNSServerSearchOrder/ns:IpAddress', $ns).InnerText = $dnsIp 1234 | } else { 1235 | # Insert the NIC configuration 1236 | $xUnattend.SelectSingleNode('//ns:Interface/ns:UnicastIpAddresses/ns:IpAddress', $ns).InnerText = "$IpAddress/24" 1237 | $xUnattend.SelectSingleNode('//ns:DNSServerSearchOrder/ns:IpAddress', $ns).InnerText = $DnsServer 1238 | } 1239 | 1240 | ## Insert the correct product key 1241 | $xUnattend.SelectSingleNode('//ns:ProductKey', $ns).InnerText = $ProductKey 1242 | 1243 | # ## Insert the user names and password 1244 | $localuser = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -ne 'Administrator' }) 1245 | $xUnattend.SelectSingleNode('//ns:LocalAccounts/ns:LocalAccount/ns:Password/ns:Value[text()="XXXX"]', $ns).InnerXml = $localuser.Password 1246 | $xUnattend.SelectSingleNode('//ns:LocalAccounts/ns:LocalAccount/ns:Name[text()="XXXX"]', $ns).InnerXml = $localuser.Name 1247 | 1248 | $userxPaths = '//ns:FullName', '//ns:Username' 1249 | $userxPaths | foreach { 1250 | $xUnattend.SelectSingleNode($_, $ns).InnerXml = $UserName 1251 | } 1252 | 1253 | ## Change the local admin password 1254 | $localadmin = $script:LabConfiguration.DefaultOperatingSystemConfiguration.Users.where({ $_.Name -eq 'Administrator' }) 1255 | $xUnattend.SelectSingleNode('//ns:LocalAccounts/ns:LocalAccount/ns:Name[text()="Administrator"]', $ns).InnerText = $localadmin.Password 1256 | 1257 | $netUserCmd = $xUnattend.SelectSingleNode('//ns:FirstLogonCommands/ns:SynchronousCommand/ns:CommandLine[text()="net user administrator XXXX"]', $ns) 1258 | $netUserCmd.InnerText = $netUserCmd.InnerText.Replace('XXXX', $localadmin.Password) 1259 | 1260 | ## Set the lab user autologon 1261 | $xUnattend.SelectSingleNode('//ns:AutoLogon/ns:Password/ns:Value', $ns).InnerText = $UserPassword 1262 | 1263 | ## Insert the host name 1264 | $xUnattend.SelectSingleNode('//ns:ComputerName', $ns).InnerText = $VMName 1265 | 1266 | ## Set the domain names 1267 | $xUnattend.SelectSingleNode('//ns:DNSDomain', $ns) | foreach { $_.InnerText = $DomainName } 1268 | 1269 | ## Save the config back to the XML file 1270 | $xUnattend.Save($tempUnattend.FullName) 1271 | 1272 | $tempUnattend 1273 | } 1274 | function Add-HostsFileEntry { 1275 | [CmdletBinding()] 1276 | param 1277 | ( 1278 | 1279 | [Parameter(Mandatory)] 1280 | [ValidateNotNullOrEmpty()] 1281 | [ValidatePattern('^[^\.]+$')] 1282 | [string]$HostName, 1283 | 1284 | [Parameter(Mandatory)] 1285 | [ValidateNotNullOrEmpty()] 1286 | [ipaddress]$IpAddress, 1287 | 1288 | [Parameter()] 1289 | [ValidateNotNullOrEmpty()] 1290 | [string]$Comment, 1291 | 1292 | [Parameter()] 1293 | [ValidateNotNullOrEmpty()] 1294 | [string]$ComputerName = $env:COMPUTERNAME, 1295 | 1296 | [Parameter()] 1297 | [ValidateNotNullOrEmpty()] 1298 | [pscredential]$Credential, 1299 | 1300 | [Parameter()] 1301 | [ValidateNotNullOrEmpty()] 1302 | [string]$HostFilePath = "$env:SystemRoot\System32\drivers\etc\hosts" 1303 | 1304 | 1305 | ) 1306 | begin { 1307 | $ErrorActionPreference = 'Stop' 1308 | } 1309 | process { 1310 | try { 1311 | $IpAddress = $IpAddress.IPAddressToString 1312 | 1313 | $getParams = @{ } 1314 | if ($ComputerName -ne $env:COMPUTERNAME) { 1315 | $getParams.ComputerName = $ComputerName 1316 | $getParams.Credential = $Credential 1317 | } 1318 | 1319 | $existingHostEntries = Get-HostsFileEntry @getParams 1320 | 1321 | if ($result = $existingHostEntries | where HostName -EQ $HostName) { 1322 | throw "The hostname [$($HostName)] already exists in the host file with IP [$($result.IpAddress)]" 1323 | } elseif ($result = $existingHostEntries | where IPAddress -EQ $IpAddress) { 1324 | Write-Warning "The IP address [$($result.IPAddress)] already exists in the host file for the hostname [$($HostName)]. You should probabloy remove the old one hostname reference." 1325 | } 1326 | $vals = @( 1327 | $IpAddress 1328 | $HostName 1329 | ) 1330 | if ($PSBoundParameters.ContainsKey('Comment')) { 1331 | $vals += "# $Comment" 1332 | } 1333 | 1334 | $sb = { 1335 | param($HostFilePath, $vals) 1336 | 1337 | ## If the hosts file doesn't end with a blank line, make it so 1338 | if ((Get-Content -Path $HostFilePath -Raw) -notmatch '\n$') { 1339 | Add-Content -Path $HostFilePath -Value '' 1340 | } 1341 | Add-Content -Path $HostFilePath -Value ($vals -join "`t") 1342 | } 1343 | 1344 | if ($ComputerName -eq (hostname)) { 1345 | & $sb $HostFilePath $vals 1346 | } else { 1347 | $icmParams = @{ 1348 | 'ComputerName' = $ComputerName 1349 | 'ScriptBlock' = $sb 1350 | 'ArgumentList' = $HostFilePath, $vals 1351 | } 1352 | if ($PSBoundParameters.ContainsKey('Credential')) { 1353 | $icmParams.Credential = $Credential 1354 | } 1355 | [pscustomobject](Invoke-Command @icmParams) 1356 | } 1357 | 1358 | 1359 | } catch { 1360 | Write-Error $_.Exception.Message 1361 | } 1362 | } 1363 | } 1364 | function Get-HostsFileEntry { 1365 | [CmdletBinding()] 1366 | [OutputType([System.Management.Automation.PSCustomObject])] 1367 | param 1368 | ( 1369 | [Parameter()] 1370 | [ValidateNotNullOrEmpty()] 1371 | [string]$ComputerName = $env:COMPUTERNAME, 1372 | 1373 | [Parameter()] 1374 | [ValidateNotNullOrEmpty()] 1375 | [pscredential]$Credential, 1376 | 1377 | [Parameter()] 1378 | [ValidateNotNullOrEmpty()] 1379 | [string]$HostFilePath = "$env:SystemRoot\System32\drivers\etc\hosts" 1380 | 1381 | ) 1382 | begin { 1383 | $ErrorActionPreference = 'Stop' 1384 | } 1385 | process { 1386 | try { 1387 | $sb = { 1388 | param($HostFilePath) 1389 | $regex = '^(?<ipAddress>[0-9.]+)[^\w]*(?<hostname>[^#\W]*)($|[\W]{0,}#\s+(?<comment>.*))' 1390 | $matches = $null 1391 | Get-Content -Path $HostFilePath | foreach { 1392 | $null = $_ -match $regex 1393 | if ($matches) { 1394 | $output = @{ 1395 | 'IPAddress' = $matches.ipAddress 1396 | 'HostName' = $matches.hostname 1397 | } 1398 | if ('comment' -in $matches.PSObject.Properties.Name) { 1399 | $output.Comment = $matches.comment 1400 | } 1401 | $output 1402 | } 1403 | $matches = $null 1404 | } 1405 | } 1406 | 1407 | if ($ComputerName -eq (hostname)) { 1408 | & $sb $HostFilePath 1409 | } else { 1410 | $icmParams = @{ 1411 | 'ComputerName' = $ComputerName 1412 | 'ScriptBlock' = $sb 1413 | 'ArgumentList' = $HostFilePath 1414 | } 1415 | if ($PSBoundParameters.ContainsKey('Credential')) { 1416 | $icmParams.Credential = $Credential 1417 | } 1418 | [pscustomobject](Invoke-Command @icmParams) 1419 | } 1420 | } catch { 1421 | Write-Error $_.Exception.Message 1422 | } 1423 | } 1424 | } 1425 | function Remove-HostsFileEntry { 1426 | [CmdletBinding()] 1427 | param 1428 | ( 1429 | [Parameter(Mandatory)] 1430 | [ValidateNotNullOrEmpty()] 1431 | [ValidatePattern('^[^\.]+$')] 1432 | [string]$HostName, 1433 | 1434 | [Parameter()] 1435 | [ValidateNotNullOrEmpty()] 1436 | [string]$HostFilePath = "$env:SystemRoot\System32\drivers\etc\hosts" 1437 | ) 1438 | begin { 1439 | $ErrorActionPreference = 'Stop' 1440 | } 1441 | process { 1442 | try { 1443 | if (Get-HostsFileEntry | where HostName -EQ $HostName) { 1444 | $regex = "^(?<ipAddress>[0-9.]+)[^\w]*($HostName)(`$|[\W]{0,}#\s+(?<comment>.*))" 1445 | $toremove = (Get-Content -Path $HostFilePath | select-string -Pattern $regex).Line 1446 | ## Safer to create a temp file 1447 | $tempFile = [System.IO.Path]::GetTempFileName() 1448 | (Get-Content -Path $HostFilePath | where { $_ -ne $toremove }) | Add-Content -Path $tempFile 1449 | if (Test-Path -Path $tempFile -PathType Leaf) { 1450 | Remove-Item -Path $HostFilePath 1451 | Move-Item -Path $tempFile -Destination $HostFilePath 1452 | } 1453 | } else { 1454 | Write-Warning -Message "No hostname found for [$($HostName)]" 1455 | } 1456 | } catch { 1457 | Write-Error $_.Exception.Message 1458 | } 1459 | } 1460 | } 1461 | function Set-HostsFileEntry { 1462 | [CmdletBinding()] 1463 | param 1464 | ( 1465 | 1466 | ) 1467 | begin { 1468 | $ErrorActionPreference = 'Stop' 1469 | } 1470 | process { 1471 | try { 1472 | 1473 | } catch { 1474 | Write-Error $_.Exception.Message 1475 | } 1476 | } 1477 | } 1478 | function Wait-Ping { 1479 | [CmdletBinding()] 1480 | param 1481 | ( 1482 | [Parameter(Mandatory)] 1483 | [ValidateNotNullOrEmpty()] 1484 | [string]$ComputerName, 1485 | 1486 | [Parameter()] 1487 | [ValidateNotNullOrEmpty()] 1488 | [switch]$Offline, 1489 | 1490 | [Parameter()] 1491 | [ValidateNotNullOrEmpty()] 1492 | [ValidateRange(1, [Int64]::MaxValue)] 1493 | [int]$Timeout = 1500 1494 | ) 1495 | 1496 | $ErrorActionPreference = 'Stop' 1497 | try { 1498 | $timer = [Diagnostics.Stopwatch]::StartNew() 1499 | if ($Offline.IsPresent) { 1500 | while ((ping $ComputerName -n 2) -match 'Lost = 0') { 1501 | Write-Verbose -Message "Waiting for [$($ComputerName)] to go offline..." 1502 | if ($timer.Elapsed.TotalSeconds -ge $Timeout) { 1503 | throw "Timeout exceeded. Giving up on [$ComputerName] going offline"; 1504 | } 1505 | Start-Sleep -Seconds 10; 1506 | } 1507 | } else { 1508 | ## Using good ol' fashioned ping.exe because it just uses ICMP. Test-Connection uses CIM and NetworkInformation.Ping sometimes hangs 1509 | while (-not ((ping $ComputerName -n 2) -match 'Lost = 0')) { 1510 | Write-Verbose -Message "Waiting for [$($ComputerName)] to become pingable..." 1511 | if ($timer.Elapsed.TotalSeconds -ge $Timeout) { 1512 | throw "Timeout exceeded. Giving up on ping availability to [$ComputerName]"; 1513 | } 1514 | Start-Sleep -Seconds 10; 1515 | } 1516 | } 1517 | } catch { 1518 | $PSCmdlet.ThrowTerminatingError($_) 1519 | } finally { 1520 | if (Test-Path -Path Variable:\Timer) { 1521 | $timer.Stop(); 1522 | } 1523 | } 1524 | } --------------------------------------------------------------------------------