├── .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
160 |
161 | true
162 |
163 |
164 |
165 |
166 | %SystemRoot%\System32\netsh.exe advfirewall set allprofiles state off
167 | 1
168 | Disable Windows Firewall
169 |
170 |
171 | net user administrator XXXX
172 | 2
173 | Change local administrator password
174 |
175 |
176 |
177 | true
178 | true
179 | true
180 | true
181 | Work
182 | 2
183 | true
184 | true
185 |
186 |
187 |
188 |
189 |
190 |
191 | true
192 |
193 | Local Administrator
194 | Administrator
195 | Administrators
196 | Administrator
197 |
198 |
199 |
200 | XXXX
201 | true
202 |
203 |
204 | Administrators
205 | XXXX
206 |
207 |
208 |
209 | Automate the Boring Stuff with PowerShell
210 |
211 | false
212 | Central Standard Time
213 |
214 | 2
215 |
216 |
217 |
220 | true
221 |
222 |
225 | true
226 |
227 |
228 |
--------------------------------------------------------------------------------
/AutoUnattend/Windows Server 2012 R2.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
160 |
161 | true
162 |
163 |
164 |
165 |
166 | %SystemRoot%\System32\netsh.exe advfirewall set allprofiles state off
167 | 1
168 | Disable Windows Firewall
169 |
170 |
171 | net user administrator XXXX
172 | 2
173 | Change local administrator password
174 |
175 |
176 |
177 | true
178 | true
179 | true
180 | true
181 | Work
182 | 2
183 | true
184 | true
185 |
186 |
187 |
188 |
189 |
190 |
191 | true
192 |
193 | Local Administrator
194 | Administrator
195 | Administrators
196 | Administrator
197 |
198 |
199 |
200 | XXXX
201 | true
202 |
203 |
204 | Administrators
205 | XXXX
206 |
207 |
208 |
209 | Automate the Boring Stuff with PowerShell
210 |
211 | false
212 | Central Standard Time
213 |
214 | 2
215 |
216 |
217 |
220 | true
221 |
222 |
225 | true
226 |
227 |
228 |
--------------------------------------------------------------------------------
/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 = '^(?[0-9.]+)[^\w]*(?[^#\W]*)($|[\W]{0,}#\s+(?.*))'
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 = '^(?[0-9.]+)[^\w]*(?[^#\W]*)($|[\W]{0,}#\s+(?.*))'
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 = "^(?[0-9.]+)[^\w]*($HostName)(`$|[\W]{0,}#\s+(?.*))"
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 | }
--------------------------------------------------------------------------------