From b5d46be3a9d01a7aebba75dd8994a427a4f07d55 Mon Sep 17 00:00:00 2001 From: Captain Firmware Date: Tue, 4 Mar 2025 17:42:50 -0500 Subject: [PATCH 1/3] Fixed VMIntegrationService error on non-English systems #24 Fixed authentication error to use MgGraph #29 Added error checking when AutopilotConfigurationFile.json is missing Added fix when multiple Autopilot profile exists Improved code formatting Improved comments Fixed errors when using custom Virtual Machines Disks and Configs path Added dynamic memory support for VM creation Improved path handling for ISO and VM locations Added support for internal virtual switch configuration Improved error handling for ISO path validation --- Intune.HV.Tools/Intune.HV.Tools.psd1 | Bin 8250 -> 7201 bytes Intune.HV.Tools/Intune.HV.Tools.psm1 | 18 +- .../Private/Get-AutopilotPolicy.ps1 | 60 +++---- .../Private/Get-ImageIndexFromWim.ps1 | 19 +- Intune.HV.Tools/Private/New-ClientDevice.ps1 | 167 +++++++++++++++--- Intune.HV.Tools/Private/New-ClientVHDX.ps1 | 53 +++--- .../Private/Publish-AutoPilotConfig.ps1 | 22 ++- Intune.HV.Tools/Private/Write-LogEntry.ps1 | 6 +- Intune.HV.Tools/Public/Add-ImageToConfig.ps1 | 85 +++++---- .../Public/Add-NetworkToConfig.ps1 | 17 +- Intune.HV.Tools/Public/Add-TenantToConfig.ps1 | 54 +++--- Intune.HV.Tools/Public/Get-HVToolsConfig.ps1 | 25 ++- Intune.HV.Tools/Public/Initialize-HVTools.ps1 | 38 ++-- Intune.HV.Tools/Public/New-ClientVM.ps1 | 138 ++++++++++----- Intune.HV.Tools/ReleaseNotes.txt | 2 - New-ClientVM2.ps1 | 94 +++++----- README.md | 111 +++++++----- Tests/codecheck.ps1 | 4 +- Tests/codecheck.test.ps1 | 27 ++- build.ps1 | 29 ++- pr-pipeline.yml | 8 +- prod-pipeline.yml | 20 +-- 22 files changed, 593 insertions(+), 404 deletions(-) delete mode 100644 Intune.HV.Tools/ReleaseNotes.txt diff --git a/Intune.HV.Tools/Intune.HV.Tools.psd1 b/Intune.HV.Tools/Intune.HV.Tools.psd1 index 7615cf7309a8169477493f7015364b2e076098fd..812635e76e718d5637c380a8c7887a15fbb5095e 100644 GIT binary patch literal 7201 zcmc&(ZBHCI68_GwurwzDD`tQpgyc?r`t2*O}E`^yMf6n|NWk_ z-R|yTAeUWfFZqznw7aTYFHb#H^Zxg>HHJR}c9R)dIacbtE(FttO{C&x(R?wL_?BC% zGs&IE*@JX*=H}9}k}EkC){VTNYpvZyknxVKpDR}@F*>^&-D+LfQDw{B^+w~JFji{C zR9#MlVR{joG!Z7c!3{2<2Y=XzMbyBJ$QXE%qDO0;j8_< z@$k*T?#^(3%qM%}*W*{a(`hpAuy%7evwt}+ELyE#|vN;)Hw z7=IPDpt#%%m9Lf<4_)$7keyfh0UjQniK6JpsTM}g=8jp1IF`67n?hQMr1!ix?npQ1 zAZR1oWaFLj4r>h^vBRQZUXo=-SYhr(K1ynih4tw~;q_M+Y+9>~g2M~x7OXP*9*)l0 zWxsaLB zR!B z6+d9d+Fc5lRQ`_^ySZSQE=wd_!PTthGeosUA)^*Hjy_&*{uQPl6=s#WS4L+7&}kSq z*&M#fTy0?1rB-5#{ru+j7CS8S*RP(nG2&U0bJ!Jw5@|!}E7+o(IiS2o0!|Q!G6JMj zz&Uc%tk4rq$$u}6RvrW@BMAm{JT2Ai2_{^}A_<^uJ(9lvJ$!0%_umrW^k9K)`brks zh0w8;914a#BdKp9S+_qB&8E_V3VW+#V)c9DEg+V4)IXL5EYOru z@tzNJG_H_ctMX;QNW^pEPqitLq<>C95b~UgCQ(yXgTOEas)87mu~H&#hmXuwB9l{z z=kRVzWwwK)=xCQN5%<+Ff=oCVKAu-fRJmZ zv?F4I_EzK>H@Rh8<;{a!6ax54!AI(w#g;z`${@-Lt(6t^USZ6a<0J9 z&=&G3b?h{bhmv+L&cYJndm zDhKro08Hgv@$3swyf>e;tacFFkbkpRuU%OIlD;SD6u|sc&Xtk(;MFYWj?*10j7;<| z73+^-Or%sLYMB>SgWM>`fl3^6&M`@!b31pGTY&`gJa8%W^{A*6j(a=&hy$YC(#;UA zXsnk8Z^TiMdT~_I|B@a6)A00XnGf>F_uT-r>d#R8Tv52%7D z_ssV#w9sG+38z0^JP;@n2ZEiiAWI&9Kq}BOFhkI%I04Xq@1$`xK}vWShPxn+EIwqP zuRk_HC}buSY+M?8`bG@c$8hYkku*ugGJHm4Nnv>q6VG~+TueB>f~NEZqtVirQS98g z%Km+OdxlxMo{T_Ww-Hb~1o{sx@=w_9rP1bjLjeC0DS`?E3Aez}rH;k>HwCtC`_|<kTG?D=Unl^cArTp_hzy4&SO-TCevE##@Bld%qHsQm znaaoTZdn&jR;ckLghppVD6k6PpT#TZCBDM=4T#8nVxveX{QXBI@nkw4g5nLeK}k)z zS%z@|1SJ*|yWkjAK$h6Cv|j@M2=-&l!-gW6nlBVDWyVTTf|4^E)-k@m?k-qng!g>c z7J41%rE=iF%pW|z`Q!->UMO@36}anvN?exNJ{ijPmsW28)C#191pA!G#qWu27(0J`|J&B zL(~B=xVnsDLjDG%rn&F^+OUGJrc5kIVuY{1ykN`e~=l412(Uz}{U5d>-w^ z;S+@zWF8C{ zbzZD7cJ|?heW*dIB`~wePv%P&!b98v8@|hVv{GZiz&2=IhYI;+ z{sr*G&EEb>FiB50m(e0+Cq^kMWM8O%MlAXXQq*HOO%j1^g@g%fxvmrY2mKV%phpT? z|4elBAG0vzVE`}%z@kj>dDx5F&VJiI?h#g96Gkd@jF__l8RQ zd=RT6#WhfPM&YGW@N-meF^Q8m4l$!dU-L((<+8a3Bi zDM4F2`b`7_JqLJcl{9rsY8<%Qq&9w(gu|GHB&rMgJA0J>F}zJJKOui`c2k+w8LFfg z$QriI?eSHd_O#&G`C$R5a-vHZE9Qb$2;JEw<|39&j~DKfjdmS2%&AGWW*V}F}s+!4zh{^T(>{-#8lU#eatr~}r#cj1fS zxKm%H{uZ9yKg0Z%X*Rr8-wrjj7{&RX_%;IY9^7NDQzQ zpK|a$TBA=v0Lhie52PjbKVgSNg|)Or8nI8vID;Qke4z5aj6m(sF|mLe1}Cr~5&`nV SM3A%|1x?{uH0#^i+J6B~gheR; literal 8250 zcmeI1Yfl?T6o%(>rT&MN`2ekofi@w%s8ZYzLZk#mgwTEk8xuG9QrmK<#}RO<$s*>rpENWX`gI~1jfTbba4Zv!CH$ zGM{L4AfCqJ6l_uix*9XgjwSCzV{JW&(p~D`)VQ&LF00gA%*rHXhmP^K)fgJ(M8?5!okCRx8}a6wSvcPN8=Yl z%&c-My8Oore$(|-l+k|ATP8M-ckm?DYs4r;`;mJgKY{-Ik0I1`--+s7{oZri`mW2G zPTaTJb6u8w*L@?qUf2J7((0z}?&|-sX7GGl-#dEV)yMoU{7yO>vmXnq}^z#ixAZ`!Ko z`Kk88C;Q^6FB(XQKI?h^+0VwJE;Xl6WC#}UTsk%V7rG9lW9T9;=;84_e|J-v>UJnc z#YOT(`^~j*JxTCw%h$Ir#oml|GqY`4t2My z5sO%t8Xdb=H?~0Hz~P750*}Naf(_#t7;H9rbg@}l^ zq#2n?VIYccB~zL;%)hB)_O)VKBj@cS9pq?>r@lT`*PKgY{QE-JGcV`ZM>OJ@Wr{Ub z5l>Wi5_2PR++F4U|6c4(qMKm+OtMqGk~^?es(otSwzn;8h#YX?5h|3U_C3C*?kmmv zMK#H`ziSE8j_&U)fo9o$svNcE^0PP=#kve1z)r~HRN_>8+)?-9Bh-?}9%@jsg4Ke- zy3SU&6UxWWX_6S&q*Kj|SsMT3mL)veuCT(c$ds)SDd{yIS z(>)TN$I{SC(O-tIZLQlC*DD0#7A%Rf5Ao?j!@yW zIA|7*=&Qs3kdr=T9gv?+kYUMaXcPM=q&xZ*xU^aWZh3dIqbJ5Yeey*5hRLArZ z=~)nzb1UH=S(urDW_-uOo4@)~r`+>$Y$JD}$Bmv>eZv>#a?-I6w26 zqw^g3m}(W;)T3Bm@dWJgbgBhAZRY$e`be&Oki>sEPp8(f>Vp~{EfLA>eELlMgucdl zz8tNmag9SHAy)!Ysp?3-Id_C^AFxCSb%7 diff --git a/Intune.HV.Tools/Intune.HV.Tools.psm1 b/Intune.HV.Tools/Intune.HV.Tools.psm1 index 2fc6859..d379806 100644 --- a/Intune.HV.Tools/Intune.HV.Tools.psm1 +++ b/Intune.HV.Tools/Intune.HV.Tools.psm1 @@ -5,10 +5,9 @@ $cfg = Get-Content "$env:USERPROFILE\.hvtoolscfgpath" -ErrorAction SilentlyConti $script:tick = [char]0x221a if ($cfg) { - $script:hvConfig = if (Get-Content -Path $cfg -raw -ErrorAction SilentlyContinue) { - Get-Content -Path $cfg -raw -ErrorAction SilentlyContinue | ConvertFrom-Json - } - else { + $script:hvConfig = if (Get-Content -Path $cfg -Raw -ErrorAction SilentlyContinue) { + Get-Content -Path $cfg -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json + } else { $script:hvConfig = $null } } @@ -17,8 +16,7 @@ if ($cfg) { foreach ($import in @($Public + $Private)) { try { . $import.FullName - } - catch { + } catch { Write-Error -Message "Failed to import function $($import.FullName): $_" } } @@ -64,7 +62,7 @@ $vLan = { } Register-ArgumentCompleter -CommandName Add-NetworkToConfig -ParameterName VSwitchName -ScriptBlock $vLan -$win10Builds = { +$winBuilds = { param ( $commandName, $parameterName, @@ -82,6 +80,6 @@ $win10Builds = { ) } } -Register-ArgumentCompleter -CommandName Add-TenantToConfig -ParameterName ImageName -ScriptBlock $win10Builds -Register-ArgumentCompleter -CommandName New-ClientVM -ParameterName OSBuild -ScriptBlock $win10Builds -#endregion \ No newline at end of file +Register-ArgumentCompleter -CommandName Add-TenantToConfig -ParameterName ImageName -ScriptBlock $winBuilds +Register-ArgumentCompleter -CommandName New-ClientVM -ParameterName OSBuild -ScriptBlock $winBuilds +#endregion diff --git a/Intune.HV.Tools/Private/Get-AutopilotPolicy.ps1 b/Intune.HV.Tools/Private/Get-AutopilotPolicy.ps1 index 8218fbc..0d6ed7b 100644 --- a/Intune.HV.Tools/Private/Get-AutopilotPolicy.ps1 +++ b/Intune.HV.Tools/Private/Get-AutopilotPolicy.ps1 @@ -1,67 +1,65 @@ -#requires -Modules @{ ModuleName="WindowsAutoPilotIntune"; ModuleVersion="4.3" } -#requires -Modules @{ ModuleName="Microsoft.Graph.Intune"; ModuleVersion="6.1907.1.0"} +#requires -Modules @{ ModuleName="WindowsAutoPilotIntune"; ModuleVersion="5.7" } +#requires -Modules @{ ModuleName="Microsoft.Graph.Identity.DirectoryManagement"; ModuleVersion="2.26.1"} function Get-AutopilotPolicy { - [cmdletbinding()] + [CmdletBinding()] param ( [parameter(Mandatory = $true)] [System.IO.FileInfo]$FileDestination ) try { - if (!(Test-Path "$FileDestination\AutopilotConfigurationFile.json" -ErrorAction SilentlyContinue)) { + if (-not(Test-Path "$FileDestination\AutopilotConfigurationFile.json" -ErrorAction SilentlyContinue)) { + Write-Host 'Autopilot Configuration file not found.' -ForegroundColor Yellow + Write-Host 'Grabbing Autopilot config...' -ForegroundColor Cyan $modules = @( - "WindowsAutoPilotIntune", - "Microsoft.Graph.Intune" + 'WindowsAutoPilotIntune', + 'Microsoft.Graph.Identity.DirectoryManagement' ) if ($PSVersionTable.PSVersion.Major -eq 7) { $modules | ForEach-Object { Import-Module $_ -UseWindowsPowerShell -ErrorAction SilentlyContinue 3>$null } - } - else { + } else { $modules | ForEach-Object { Import-Module $_ } } - #region Connect to Intune - Connect-MSGraph | Out-Null - #endregion Connect to Intune + #region Connect to Microsoft Graph + Connect-MgGraph -Scopes 'DeviceManagementServiceConfig.Read.All,Organization.Read.All' -NoWelcome -ClientTimeout 300 + #endregion Connect to Microsoft Graph #region Get policies $apPolicies = Get-AutopilotProfile - if (!($apPolicies)) { - Write-Warning "No Autopilot policies found.." - } - else { - if ($apPolicies.count -gt 1) { - Write-Host "Multiple Autopilot policies found - select the correct one.." -ForegroundColor Cyan - $apPol = $apPolicies | Select-Object displayName | Out-GridView -passthru - } - else { - Write-Host "Policy found - saving to $FileDestination.." -ForegroundColor Cyan + if (-not($apPolicies)) { + Write-Warning 'No Autopilot policies found.' + } else { + if ($apPolicies.id.count -gt 1) { + Write-Host 'Multiple Autopilot policies found - Please select the correct one.' -ForegroundColor Yellow + $selectedAp = $apPolicies | Select-Object displayName | Out-GridView -PassThru + $apPol = $apPolicies | Where-Object { $_.displayName -eq $selectedAp.displayName } + } else { + Write-Host "Policy found - saving to $FileDestination..." -ForegroundColor Cyan $apPol = $apPolicies } $apPol | ConvertTo-AutopilotConfigurationJSON | Out-File "$FileDestination\AutopilotConfigurationFile.json" -Encoding ascii -Force Write-Host "Autopilot profile selected: $($apPol.displayName)" -ForegroundColor Green + Disconnect-MgGraph | Out-Null } #endregion Get policies - } - else { + } else { Write-Host "Autopilot Configuration file found locally: $FileDestination\AutopilotConfigurationFile.json" -ForegroundColor Green } - } - catch { + } catch { $errorMsg = $_ - } - finally { + } finally { if ($PSVersionTable.PSVersion.Major -eq 7) { $modules = @( - "WindowsAutoPilotIntune", - "Microsoft.Graph.Intune" + 'WindowsAutoPilotIntune', + 'Microsoft.Graph.Identity.DirectoryManagement' ) | ForEach-Object { Remove-Module $_ -ErrorAction SilentlyContinue 3>$null } } - if ($errrorMsg) { + if ($errorMsg) { Write-Warning $errorMsg } } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Private/Get-ImageIndexFromWim.ps1 b/Intune.HV.Tools/Private/Get-ImageIndexFromWim.ps1 index 7007cab..eb925ac 100644 --- a/Intune.HV.Tools/Private/Get-ImageIndexFromWim.ps1 +++ b/Intune.HV.Tools/Private/Get-ImageIndexFromWim.ps1 @@ -1,22 +1,21 @@ function Get-ImageIndexFromWim { - [cmdletbinding()] + [CmdletBinding()] param ( [parameter(Mandatory = $true)] $wimPath ) try { - Write-Verbose "Getting windows images from $wimPath" + Write-Host "Getting windows images from $wimPath..." -ForegroundColor Cyan $images = Get-WindowsImage -ImagePath $wimPath - Write-Host "Select an Image from the below available options:" -ForegroundColor Cyan - $images | Select-Object ImageIndex, ImageName | Format-Table | Out-String | ForEach-Object { Write-Host $_ } - $rh = Read-Host "Select Image Index..($($images[0].ImageIndex)..$($images[-1].ImageIndex))" + Write-Host 'Select an Image from the below available options:' -ForegroundColor Cyan + $images | Select-Object ImageIndex, ImageName | Format-Table | Out-String | ForEach-Object { Write-Host $_ -ForegroundColor Cyan } + $rh = Read-Host "Select Image Index ($($images[0].ImageIndex) - $($images[-1].ImageIndex))" while ($rh -notin $images.ImageIndex) { - $rh = Read-Host "Select Image Index..($($images[0].ImageIndex)..$($images[-1].ImageIndex))" + $rh = Read-Host "Select Image Index ($($images[0].ImageIndex) - $($images[-1].ImageIndex))" } - Write-Host "Image $rh / $(($images | Where-Object {$_.ImageIndex -eq $rh}).ImageName) selected..`n" -ForegroundColor Green + Write-Host "Image $rh / $(($images | Where-Object {$_.ImageIndex -eq $rh}).ImageName) selected `n" -ForegroundColor Green return ($images | Where-Object { $_.ImageIndex -eq $rh }).ImageIndex - } - catch { + } catch { Write-Warning $_.Exception.Message } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Private/New-ClientDevice.ps1 b/Intune.HV.Tools/Private/New-ClientDevice.ps1 index ffa6caf..d30e2a0 100644 --- a/Intune.HV.Tools/Private/New-ClientDevice.ps1 +++ b/Intune.HV.Tools/Private/New-ClientDevice.ps1 @@ -1,54 +1,171 @@ function New-ClientDevice { [cmdletBinding(SupportsShouldProcess)] param ( - [parameter(Position = 1, Mandatory = $true)] + [parameter(Position = 0, Mandatory = $true)] [string]$VMName, - [parameter(Position = 2, Mandatory = $true)] + [parameter(Position = 1, Mandatory = $true)] [string]$ClientPath, - [parameter(Position = 3, Mandatory = $true)] + [parameter(Position = 2, Mandatory = $true)] [string]$RefVHDX, - [parameter(Position = 4, Mandatory = $true)] + [parameter(Position = 3, Mandatory = $true)] [string]$VSwitchName, - [parameter(Position = 5, Mandatory = $false)] + [parameter(Position = 4, Mandatory = $false)] [string]$VLanId, - [parameter(Position = 6, Mandatory = $true)] + [parameter(Position = 5, Mandatory = $true)] [string]$CPUCount, - [parameter(Position = 7, Mandatory = $true)] - [string]$VMMMemory, + [parameter(Position = 6, Mandatory = $true)] + [string]$VMMemory, + + [parameter(Position = 7, Mandatory = $false)] + [switch]$DynamicMemory, [parameter(Position = 8, Mandatory = $false)] [switch]$skipAutoPilot ) - Copy-Item -path $RefVHDX -Destination "$ClientPath\$VMName.vhdx" - if (!($skipAutoPilot)) { + + # Display all parameters for debugging + Write-Verbose 'New-ClientDevice Parameters:' + Write-Verbose "VMName: $VMName" + Write-Verbose "ClientPath: $ClientPath" + Write-Verbose "RefVHDX: $RefVHDX" + Write-Verbose "VSwitchName: $VSwitchName" + Write-Verbose "VLanId: $VLanId" + Write-Verbose "CPUCount: $CPUCount" + Write-Verbose "VMMemory: $VMMemory" + Write-Verbose "DynamicMemory: $DynamicMemory" + Write-Verbose "skipAutoPilot: $skipAutoPilot" + + # Check if Hyper-V is running properly + Write-Host 'Checking Hyper-V service status...' -ForegroundColor Cyan + $hvService = Get-Service -Name 'vmms' -ErrorAction SilentlyContinue + if (-not $hvService -or $hvService.Status -ne 'Running') { + Write-Error 'Hyper-V Virtual Machine Management Service is not running. Please ensure Hyper-V is properly installed and the service is running.' + return + } + + # Check Hyper-V configuration paths + try { + Write-Host 'Checking Hyper-V configuration paths...' -ForegroundColor Cyan + #$hyperVConfig = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization' -ErrorAction SilentlyContinue + + # Get default VM and VHD paths + $defaultVMPath = (Get-VMHost).VirtualMachinePath + $defaultVHDPath = (Get-VMHost).VirtualHardDiskPath + + Write-Verbose "Default VM Path: $defaultVMPath" -ForegroundColor Green + Write-Verbose "Default VHD Path: $defaultVHDPath" -ForegroundColor Green + + # Check if these paths exist + if (-not (Test-Path -Path $defaultVMPath -PathType Container)) { + Write-Warning "Default VM path does not exist: $defaultVMPath" + Write-Host 'Creating default VM path...' -ForegroundColor Cyan + New-Item -Path $defaultVMPath -ItemType Directory -Force | Out-Null + } + + if (-not (Test-Path -Path $defaultVHDPath -PathType Container)) { + Write-Warning "Default VHD path does not exist: $defaultVHDPath" + Write-Host 'Creating default VHD path...' -ForegroundColor Cyan + New-Item -Path $defaultVHDPath -ItemType Directory -Force | Out-Null + } + } catch { + Write-Warning "Could not verify Hyper-V configuration paths: $_" + } + + $VHDXPath = "$ClientPath\$VMName.vhdx" + # Copy VHDX file + Write-Host "Copying VHDX from $RefVHDX to $VHDXPath..." -ForegroundColor Cyan + Copy-Item -Path $RefVHDX -Destination $VHDXPath -Force + # Publish AutoPilot config if needed + if (-not $skipAutoPilot) { + Write-Host 'Publishing AutoPilot configuration...' -ForegroundColor Cyan Publish-AutoPilotConfig -vmName $VMName -clientPath $ClientPath } - New-VM -Name $VMName -MemoryStartupBytes $VMMMemory -VHDPath "$ClientPath\$VMName.vhdx" -Generation 2 | Out-Null - Enable-VMIntegrationService -vmName $VMName -Name "Guest Service Interface" - Set-VM -name $VMName -CheckpointType Disabled - Set-VMProcessor -VMName $VMName -Count $CPUCount - Set-VMFirmware -VMName $VMName -EnableSecureBoot On - Get-VMNetworkAdapter -vmName $VMName | Connect-VMNetworkAdapter -SwitchName $VSwitchName | Set-VMNetworkAdapter -Name $VSwitchName -DeviceNaming On + # Verify virtual switch exists + if (-not (Get-VMSwitch -Name $VSwitchName -ErrorAction SilentlyContinue)) { + Write-Error "Virtual switch '$VSwitchName' does not exist. Please create it first." + return + } + # Check if VM already exists and remove it + $existingVM = Get-VM -Name $VMName -ErrorAction SilentlyContinue + if ($existingVM) { + Write-Host "VM '$VMName' already exists. Removing it." -ForegroundColor Yellow + Remove-VM -Name $VMName -Force + } + + Write-Verbose "Using memory value: $VMMemory bytes" + # Create the VM with full path to VHDX + Write-Host "Creating VM: $VMName with VHDX: $VHDXPath..." -ForegroundColor Cyan + + # Try creating VM with explicit configuration path + $vmConfigPath = Join-Path -Path $defaultVMPath -ChildPath $VMName + if (-not (Test-Path -Path $vmConfigPath -PathType Container)) { + New-Item -Path $vmConfigPath -ItemType Directory -Force | Out-Null + } + + # Create VM with explicit paths + $vm = New-VM -Name $VMName -MemoryStartupBytes $VMMemory -VHDPath $VHDXPath -Generation 2 -Path $vmConfigPath -ErrorAction Stop + + # Configure VM settings + Write-Host 'Configuring VM settings...' -ForegroundColor Cyan + Get-VMIntegrationService -VMName $VMName | Where-Object Name -Match 'Interface' | Enable-VMIntegrationService + Set-VM -Name $VMName -CheckpointType Disabled -AutomaticStartAction Nothing -AutomaticStopAction ShutDown -ErrorAction Stop + Set-VMProcessor -VMName $VMName -Count $CPUCount -ErrorAction Stop + + # Enable Dynamic Memory if specified + If ($DynamicMemory) { + Write-Host 'Enabling dynamic memory...' -ForegroundColor Cyan + # Set dynamic memory parameters according to documented ranges: + # StartupBytes: Must be multiple of 2MB between 32MB and 65536MB (64GB) + # MinimumBytes: Must be between 32MB and StartupBytes + # MaximumBytes: Must be between StartupBytes and 1TB + # BufferPercent: Must be between 5-2000 + Set-VM -Name $VMName -DynamicMemory -ErrorAction Stop + Set-VMMemory -VMName $VMName ` + -DynamicMemoryEnabled $true ` + -StartupBytes $VMMemory ` + -MinimumBytes 512MB ` + -MaximumBytes $VMMemory ` + -Buffer 20 ` + -ErrorAction Stop + } + + # Configure firmware and networking + Set-VMFirmware -VMName $VMName -EnableSecureBoot On -ErrorAction Stop + Get-VMNetworkAdapter -VMName $VMName -ErrorAction Stop | Connect-VMNetworkAdapter -SwitchName $VSwitchName -ErrorAction Stop | Set-VMNetworkAdapter -Name $VSwitchName -DeviceNaming On -ErrorAction Stop + + # Configure VLAN if specified if ($VLanId) { - Set-VMNetworkAdapterVlan -Access -VMName $VMName -VlanId $VLanId + Write-Host "Setting VLAN ID: $VLanId..." -ForegroundColor Cyan + Set-VMNetworkAdapterVlan -VMName $VMName -Access -VlanId $VLanId -ErrorAction Stop } + + # Configure TPM $owner = Get-HgsGuardian UntrustedGuardian -ErrorAction SilentlyContinue - If (!$owner) { + If (-not $owner) { # Creating new UntrustedGuardian since it did not exist $owner = New-HgsGuardian -Name UntrustedGuardian -GenerateCertificates } $kp = New-HgsKeyProtector -Owner $owner -AllowUntrustedRoot - Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData - Enable-VMTPM -VMName $VMName - Start-VM -Name $VMName - #Set VM Info with Serial number - $vmSerial = (Get-CimInstance -Namespace root\virtualization\v2 -class Msvm_VirtualSystemSettingData | Where-Object { ($_.VirtualSystemType -eq "Microsoft:Hyper-V:System:Realized") -and ($_.elementname -eq $VMName )}).BIOSSerialNumber - Get-VM -Name $VMname | Set-VM -Notes "Serial# $vmSerial" -} \ No newline at end of file + Set-VMKeyProtector -VMName $VMName -KeyProtector $kp.RawData -ErrorAction Stop + Enable-VMTPM -VMName $VMName -ErrorAction Stop + + # Start the VM + Write-Host "Starting VM: $VMName..." -ForegroundColor Cyan + Start-VM -Name $VMName -ErrorAction Stop + + # Set VM Info with Serial number + $vmSerial = (Get-CimInstance -Namespace root\virtualization\v2 -class Msvm_VirtualSystemSettingData | + Where-Object { ($_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized') -and ($_.ElementName -eq $VMName) }).BIOSSerialNumber + if ($vmSerial) { + Get-VM -Name $VMName | Set-VM -Notes "Serial Number: $vmSerial" + } + + Write-Host "VM '$VMName' created and started successfully!" -ForegroundColor Green +} diff --git a/Intune.HV.Tools/Private/New-ClientVHDX.ps1 b/Intune.HV.Tools/Private/New-ClientVHDX.ps1 index caafa61..8f47588 100644 --- a/Intune.HV.Tools/Private/New-ClientVHDX.ps1 +++ b/Intune.HV.Tools/Private/New-ClientVHDX.ps1 @@ -1,18 +1,18 @@ #requires -Modules "Hyper-ConvertImage" function New-ClientVHDX { - [cmdletbinding(SupportsShouldProcess)] + [CmdletBinding(SupportsShouldProcess)] param ( - [Parameter(Position = 1, Mandatory = $true)] - [string]$vhdxPath, - - [Parameter(Position = 2, Mandatory = $true)] - [string]$winIso, + [Parameter(Position = 0, Mandatory = $true)] + [string]$VhdxPath, - [Parameter(Position = 3, Mandatory = $false)] - [switch]$unattend + [Parameter(Position = 1, Mandatory = $true)] + [string]$IsoPath, + [Parameter(Position = 2, Mandatory = $false)] + [switch]$Unattend ) + try { $module = Get-Module -ListAvailable -Name 'Hyper-ConvertImage' if ($module.count -lt 1) { @@ -21,36 +21,33 @@ function New-ClientVHDX { } if ($PSVersionTable.PSVersion.Major -eq 7) { Import-Module -Name (Split-Path $module.ModuleBase -Parent) -UseWindowsPowerShell -ErrorAction SilentlyContinue 3>$null - } - else { + } else { Import-Module -Name 'Hyper-ConvertImage' } $currVol = Get-Volume - Mount-DiskImage -ImagePath $winIso | Out-Null - $dl = (Get-Volume | Where-Object { $_.DriveLetter -notin $currVol.DriveLetter}).DriveLetter + Mount-DiskImage -ImagePath $IsoPath | Out-Null + $dl = (Get-Volume | Where-Object { $_.DriveLetter -notin $currVol.DriveLetter }).DriveLetter $imageIndex = Get-ImageIndexFromWim -wimPath "$dl`:\sources\install.wim" - Dismount-DiskImage -ImagePath $winIso | Out-Null + Dismount-DiskImage -ImagePath $IsoPath | Out-Null $params = @{ - SourcePath = $winIso - Edition = $imageIndex - VhdType = "Dynamic" - VhdFormat = "VHDX" - VhdPath = $vhdxPath - DiskLayout = "UEFI" - SizeBytes = 127gb + SourcePath = $IsoPath + Edition = $imageIndex + VhdType = 'Dynamic' + VhdFormat = 'VHDX' + VhdPath = $VhdxPath + DiskLayout = 'UEFI' + SizeBytes = 127gb } - if ($unattend) { - $params.UnattendPath = $unattend + if ($Unattend) { + $params.UnattendPath = $Unattend } - Write-Host "Building reference image.." -ForegroundColor Cyan -NoNewline + Write-Host 'Building reference image...' -ForegroundColor Cyan -NoNewline Convert-WindowsImage @params - } - catch { + } catch { Write-Warning $_ - } - finally { + } finally { if ($PSVersionTable.PSVersion.Major -eq 7) { Remove-Module -Name 'Hyper-ConvertImage' -Force } } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Private/Publish-AutoPilotConfig.ps1 b/Intune.HV.Tools/Private/Publish-AutoPilotConfig.ps1 index 90f1c86..e7dda5a 100644 --- a/Intune.HV.Tools/Private/Publish-AutoPilotConfig.ps1 +++ b/Intune.HV.Tools/Private/Publish-AutoPilotConfig.ps1 @@ -1,5 +1,5 @@ function Publish-AutoPilotConfig { - [cmdletBinding()] + [CmdletBinding()] param ( [parameter(Position = 1, Mandatory = $true)] [string]$VMName, @@ -8,26 +8,24 @@ function Publish-AutoPilotConfig { [string]$ClientPath ) try { - Write-Host "Mounting $VMName.vhdx.. " -ForegroundColor Cyan -NoNewline + Write-Host "Mounting $VMName.vhdx... " -ForegroundColor Cyan -NoNewline $disk = (Mount-VHD -Path "$ClientPath\$VMName.vhdx" -Passthru | Get-Disk | Get-Partition | Where-Object { $_.type -eq 'Basic' }).DriveLetter if ($disk) { Write-Host $script:tick -ForegroundColor Green - Write-Host "Publishing Autopilot config to $VMName`.vhdx.. " -ForegroundColor Cyan -NoNewline + Write-Host "Publishing Autopilot config to $VMName`.vhdx... " -ForegroundColor Cyan -NoNewline $AutopilotFolder = "$disk`:\Windows\Provisioning\Autopilot" - IF(!(Test-path -Path $AutopilotFolder -PathType Container)){ + if (-not(Test-Path -Path $AutopilotFolder -PathType Container)) { New-Item -Path $AutopilotFolder -ItemType Directory -Force } - Copy-Item -path "$ClientPath\AutopilotConfigurationFile.json" -Destination "$AutopilotFolder\AutopilotConfigurationFile.json" -Force + Copy-Item -Path "$ClientPath\AutopilotConfigurationFile.json" -Destination "$AutopilotFolder\AutopilotConfigurationFile.json" -Force Write-Host $script:tick -ForegroundColor Green - Write-Host "Config published successfully to $ClientPath\$VMName.vhdx..`n" -ForegroundColor Green + Write-Host "Config published successfully to $ClientPath\$VMName.vhdx.`n" -ForegroundColor Green } - } - catch { - throw "Error occurred during config publish.." - } - Finally{ + } catch { + throw 'Error occurred during config publish.' + } finally { Write-Host $script:tick -ForegroundColor Green Write-Host "Dismounting $VMName.vhdx " -ForegroundColor Cyan -NoNewline Dismount-VHD "$ClientPath\$VMName.vhdx" } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Private/Write-LogEntry.ps1 b/Intune.HV.Tools/Private/Write-LogEntry.ps1 index 5e3fa9c..c054625 100644 --- a/Intune.HV.Tools/Private/Write-LogEntry.ps1 +++ b/Intune.HV.Tools/Private/Write-LogEntry.ps1 @@ -10,12 +10,12 @@ function Write-LogEntry { switch ($Type) { 'Error' { $severity = 3 - $fgColour = "Red" + $fgColor = "Red" break; } 'Information' { $severity = 6 - $fgColour = "Yellow" + $fgColor = "Yellow" break; } } @@ -35,5 +35,5 @@ function Write-LogEntry { "file=`"$($scriptName.ScriptName)`">"; $logLine | Out-File -Append -Encoding utf8 -FilePath $logFile -Force - Write-Host $Message -ForegroundColor $fgColour + Write-Host $Message -ForegroundColor $fgColor } \ No newline at end of file diff --git a/Intune.HV.Tools/Public/Add-ImageToConfig.ps1 b/Intune.HV.Tools/Public/Add-ImageToConfig.ps1 index 1eaa7c8..456f688 100644 --- a/Intune.HV.Tools/Public/Add-ImageToConfig.ps1 +++ b/Intune.HV.Tools/Public/Add-ImageToConfig.ps1 @@ -1,42 +1,63 @@ function Add-ImageToConfig { - [cmdletbinding()] + [CmdletBinding()] param ( - [parameter(Position = 1, Mandatory = $true)] - $ImageName, - [parameter(Mandatory = $true, ParameterSetName = 'ISO')] - $IsoPath, - [Parameter(Mandatory = $true, ParameterSetName = 'RefVHDX')] - $ReferenceVHDX + [Parameter(Position = 0, Mandatory = $true)] + [string]$ImageName, + + [Parameter(Position = 1, Mandatory = $true)] + [ValidateScript({ + if (-not (Test-Path $_)) { + throw 'Path does not exist' + } + if (-not $_.EndsWith('.iso')) { + throw 'Path must end with .iso extension' + } + return $true + })] + [string]$IsoPath, + + [Parameter(Position = 2, Mandatory = $false)] + [ValidateScript({ + if (-not $_.EndsWith('.vhdx')) { + throw 'Path must end with .vhdx extension' + } + return $true + })] + [string]$ReferenceVHDX ) + try { - Write-Host "Adding $ImageName to config.. " -ForegroundColor Cyan -NoNewline - if(!$PSBoundParameters.ContainsKey('ReferenceVHDX')){ - $ReferenceVHDX = "$($script:hvConfig.vmPath)\wks$($ImageName)ref.vhdx" + Write-Host "Adding image '$ImageName' to config... " -ForegroundColor Cyan -NoNewline + + # Set reference VHDX path if not provided + if (-not $PSBoundParameters.ContainsKey('ReferenceVHDX')) { + $ReferenceVHDX = Join-Path -Path $script:hvConfig.vmPath -ChildPath "wks$($ImageName)ref.vhdx" } - $newTenant = [pscustomobject]@{ - imageName = $ImageName - imagePath = $IsoPath + + # Check if image already exists + if ($script:hvConfig.images.imageName -contains $ImageName) { + throw "Image '$ImageName' already exists in configuration" + } + + # Create new image configuration + $newImage = [PSCustomObject]@{ + imageName = $ImageName + imagePath = $IsoPath refImagePath = $ReferenceVHDX } - $script:hvConfig.images += $newTenant - $script:hvConfig | ConvertTo-Json -Depth 20 | Out-File -FilePath $hvConfig.hvConfigPath -Encoding ascii -Force + + # Add to config and save + $script:hvConfig.images += $newImage + $script:hvConfig | ConvertTo-Json -Depth 20 | Out-File -FilePath $script:hvConfig.hvConfigPath -Encoding ascii -Force Write-Host $script:tick -ForegroundColor Green - #region Check for ref image - if it's not there, build it - if (!(Test-Path -Path $newTenant.refImagePath -ErrorAction SilentlyContinue)) { - Write-Host "Creating reference Autopilot VHDX - this may take some time.." -ForegroundColor Yellow - New-ClientVHDX -vhdxPath $newTenant.refImagePath -winIso $newTenant.imagePath - } - #endregion - } - catch { - $errorMsg = $_ - } - finally { - if ($errorMsg) { - Write-Warning $errorMsg - } - else { - Write-Host $script:tick -ForegroundColor Green + + # Create reference image if needed + if (-not (Test-Path -Path $newImage.refImagePath)) { + #Write-LogEntry -Type Information -Message "Creating reference VHDX for $ImageName" + Write-Host 'Creating reference Autopilot VHDX - this may take some time...' -NoNewline -ForegroundColor Cyan + New-ClientVHDX -VhdxPath $newImage.refImagePath -IsoPath $newImage.imagePath } + } catch { + Write-Warning $_.Exception.Message } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Public/Add-NetworkToConfig.ps1 b/Intune.HV.Tools/Public/Add-NetworkToConfig.ps1 index 866537d..7b3c240 100644 --- a/Intune.HV.Tools/Public/Add-NetworkToConfig.ps1 +++ b/Intune.HV.Tools/Public/Add-NetworkToConfig.ps1 @@ -1,5 +1,5 @@ function Add-NetworkToConfig { - [cmdletbinding()] + [CmdletBinding()] param ( [parameter(Position = 1, Mandatory = $true)] $VSwitchName, @@ -8,22 +8,19 @@ function Add-NetworkToConfig { $VLanId ) try { - Write-Host "Adding virtual switch details to config.. " -ForegroundColor Cyan -NoNewline + Write-Host 'Adding virtual switch details to config...' -ForegroundColor Cyan -NoNewline $script:hvConfig.vSwitchName = $VSwitchName if ($VLanId) { $script:hvConfig.vLanId = $VLanId } - $script:hvConfig | ConvertTo-Json -Depth 20 | Out-File -FilePath $hvConfig.hvConfigPath -Encoding ascii -Force - } - catch { + $script:hvConfig | ConvertTo-Json -Depth 20 | Out-File -FilePath $script:hvConfig.hvConfigPath -Encoding ascii -Force + } catch { $errorMsg = $_ - } - finally { + } finally { if ($errorMsg) { Write-Warning $errorMsg - } - else { + } else { Write-Host $script:tick -ForegroundColor Green } } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Public/Add-TenantToConfig.ps1 b/Intune.HV.Tools/Public/Add-TenantToConfig.ps1 index 49ad867..f734788 100644 --- a/Intune.HV.Tools/Public/Add-TenantToConfig.ps1 +++ b/Intune.HV.Tools/Public/Add-TenantToConfig.ps1 @@ -1,34 +1,42 @@ function Add-TenantToConfig { - [cmdletbinding()] + [CmdletBinding()] param ( + [parameter(Position = 0, Mandatory = $true)] + [string]$TenantName, + [parameter(Position = 1, Mandatory = $true)] - $TenantName, + [string]$ImageName, [parameter(Position = 2, Mandatory = $true)] - $ImageName, - - [parameter(Position = 3, Mandatory = $true)] - $AdminUpn + [string]$AdminUpn ) try { - Write-Host "Adding $TenantName to config.. " -ForegroundColor Cyan -NoNewline - $newTenant = [pscustomobject]@{ - TenantName = $TenantName - ImageName = $ImageName - AdminUpn = $AdminUpn + Write-Host "Adding tenant '$TenantName' to config..." -ForegroundColor Cyan -NoNewline + + # Validate image exists in config + if ($null -eq $script:hvConfig.images -or $script:hvConfig.images.Count -eq 0) { + throw 'No images found in configuration. Add an image first using Add-ImageToConfig.' + } elseif ($script:hvConfig.images.imageName -notcontains $ImageName) { + throw "Image '$ImageName' not found in configuration. Add it first using Add-ImageToConfig." } - $script:hvConfig.tenantConfig += $newTenant - $script:hvConfig | ConvertTo-Json -Depth 20 | Out-File -FilePath $hvConfig.hvConfigPath -Encoding ascii -Force - } - catch { - $errorMsg = $_ - } - finally { - if ($errorMsg) { - Write-Warning $errorMsg + + # Check if tenant already exists + if ($script:hvConfig.tenantConfig.TenantName -contains $TenantName) { + throw "Tenant '$TenantName' already exists in configuration." } - else { - Write-Host $script:tick -ForegroundColor Green + + # Add new tenant config + $script:hvConfig.tenantConfig += [PSCustomObject]@{ + TenantName = $TenantName + ImageName = $ImageName + AdminUpn = $AdminUpn } + + # Save updated config + $script:hvConfig | ConvertTo-Json -Depth 20 | Out-File -FilePath $script:hvConfig.hvConfigPath -Encoding ascii -Force + + Write-Host $script:tick -ForegroundColor Green + } catch { + Write-Warning $_.Exception.Message } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Public/Get-HVToolsConfig.ps1 b/Intune.HV.Tools/Public/Get-HVToolsConfig.ps1 index d06dec9..b62d43d 100644 --- a/Intune.HV.Tools/Public/Get-HVToolsConfig.ps1 +++ b/Intune.HV.Tools/Public/Get-HVToolsConfig.ps1 @@ -1,18 +1,15 @@ function Get-HVToolsConfig { - [cmdletbinding()] - param ( + [CmdletBinding()] + param () - ) try { - if ($script:hvConfig) { - $script:hvConfig = (get-content -Path "$(get-content "$env:USERPROFILE\.hvtoolscfgpath" -ErrorAction SilentlyContinue)" -raw -ErrorAction SilentlyContinue | ConvertFrom-Json) - return $script:hvConfig - } - else { - throw "Couldnt find HVTools configuration file - please run Initialize-HVTools to create the configuration file." - } - } - catch { - Write-Warning $_.Exception.Message + # Check if config path exists + $configPath = Get-Content -Path "$env:USERPROFILE\.hvtoolscfgpath" -ErrorAction Stop + + # Load and return config + $script:hvConfig = Get-Content -Path $configPath -Raw | ConvertFrom-Json + return $script:hvConfig + } catch { + Write-Warning "Couldn't find HVTools configuration file. Please run Initialize-HVTools first to create the configuration file." } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Public/Initialize-HVTools.ps1 b/Intune.HV.Tools/Public/Initialize-HVTools.ps1 index 7de6534..cfef384 100644 --- a/Intune.HV.Tools/Public/Initialize-HVTools.ps1 +++ b/Intune.HV.Tools/Public/Initialize-HVTools.ps1 @@ -1,10 +1,10 @@ function Initialize-HVTools { - [cmdletbinding()] + [CmdletBinding()] param ( - [parameter(Position = 1, Mandatory = $true)] + [parameter(Position = 0, Mandatory = $true)] [System.IO.FileInfo]$Path, - [parameter(Position = 2, Mandatory = $false)] + [parameter(Position = 1, Mandatory = $false)] [switch]$Reset ) try { @@ -12,35 +12,33 @@ function Initialize-HVTools { "$Path\.hvtools", "$Path\.hvtools\tenantVMs" ) - Write-Host "Creating hvtools folder structure.." -ForegroundColor Cyan + Write-Host 'Creating hvtools folder structure...' -ForegroundColor Cyan foreach ($p in $paths) { - if (!(Test-Path $p -ErrorAction SilentlyContinue)) { - Write-Host " + Creating $p.. " -ForegroundColor Cyan -NoNewline + if (-not(Test-Path $p -ErrorAction SilentlyContinue)) { + Write-Host " + Creating $p..." -ForegroundColor Cyan -NoNewline New-Item -Path $p -ItemType Directory -Force | Out-Null Write-Host $script:tick -ForegroundColor Green } } $cfgPath = "$Path\.hvtools\hvconfig.json" - Write-Host " + Creating $cfgPath.. " -ForegroundColor Cyan -NoNewline + Write-Host " + Creating $cfgPath..." -ForegroundColor Cyan -NoNewline if ((Test-Path $cfgPath -ErrorAction SilentlyContinue) -and ($Reset -eq $false)) { - Write-Host "$script:tick (Already created - no need to run this again..)" -ForegroundColor Green - } - else { + Write-Host "$script:tick (Already created - no need to run this again.)" -ForegroundColor Green + } else { $initCfg = @{ 'hvConfigPath' = $cfgPath - 'images' = @() - "vmPath" = "$Path\.hvtools\tenantVMs" - 'vSwitchName' = $null - 'vLanId' = $null + 'images' = @() + 'vmPath' = "$Path\.hvtools\tenantVMs" + 'vSwitchName' = $null + 'vLanId' = $null 'tenantConfig' = @() } | ConvertTo-Json -Depth 20 - $initCfg | Out-File $cfgPath -Encoding ascii -Force - $cfgPath | Out-File "$env:USERPROFILE\.hvtoolscfgpath" -Encoding ascii -Force + $initCfg | Out-File -FilePath $cfgPath -Encoding ascii -Force + $cfgPath | Out-File -FilePath "$env:USERPROFILE\.hvtoolscfgpath" -Encoding ascii -Force Write-Host $script:tick -ForegroundColor Green - $script:hvConfig = (get-content -Path "$(get-content "$env:USERPROFILE\.hvtoolscfgpath" -ErrorAction SilentlyContinue)" -raw -ErrorAction SilentlyContinue | ConvertFrom-Json) + $script:hvConfig = (Get-Content -Path "$(Get-Content "$env:USERPROFILE\.hvtoolscfgpath" -ErrorAction SilentlyContinue)" -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json) } - } - catch { + } catch { Write-Warning $_ } -} \ No newline at end of file +} diff --git a/Intune.HV.Tools/Public/New-ClientVM.ps1 b/Intune.HV.Tools/Public/New-ClientVM.ps1 index 0cea44e..135a020 100644 --- a/Intune.HV.Tools/Public/New-ClientVM.ps1 +++ b/Intune.HV.Tools/Public/New-ClientVM.ps1 @@ -1,108 +1,156 @@ function New-ClientVM { [CmdletBinding(SupportsShouldProcess)] param ( - [parameter(Position = 1, Mandatory = $true)] + [parameter(Position = 0, Mandatory = $true)] [string]$TenantName, - [parameter(Position = 2, Mandatory = $false)] + [parameter(Position = 1, Mandatory = $false)] [string]$OSBuild, - [parameter(Position = 3, Mandatory = $true)] + [parameter(Position = 2, Mandatory = $true)] [ValidateRange(1, 999)] - [string]$NumberOfVMs, + [int]$NumberOfVMs, - [parameter(Position = 4, Mandatory = $true)] + [parameter(Position = 3, Mandatory = $true)] [ValidateRange(1, 999)] - [string]$CPUsPerVM, + [int]$CPUsPerVM, - [parameter(Position = 5, Mandatory = $false)] + [parameter(Position = 4, Mandatory = $false)] [ValidateRange(2gb, 20gb)] - [int64]$VMMemory, + [int64]$VMMemory = 4gb, + + [parameter(Position = 5, Mandatory = $false)] + [switch]$DynamicMemory, [parameter(Position = 6, Mandatory = $false)] [switch]$SkipAutoPilot ) try { #region Config - #pre-load HV module.. + # Pre-load Hyper-V module Get-Command -Module 'Hyper-V' | Out-Null $clientDetails = $script:hvConfig.tenantConfig | Where-Object { $_.TenantName -eq $TenantName } if ($OSBuild) { $imageDetails = $script:hvConfig.images | Where-Object { $_.imageName -eq $OSBuild } - } - else { + } else { $imageDetails = $script:hvConfig.images | Where-Object { $_.imageName -eq $clientDetails.imageName } } $clientPath = "$($script:hvConfig.vmPath)\$($TenantName)" - if($imageDetails.refimagePath -like '*wks$($ImageName)ref.vhdx'){ - if (!(Test-Path $imageDetails.imagePath -ErrorAction SilentlyContinue)) { + if ($imageDetails.refimagePath -like "*wks$($ImageName)ref.vhdx") { + if (-not (Test-Path $imageDetails.imagePath -ErrorAction SilentlyContinue)) { throw "Installation media not found at location: $($imageDetails.imagePath)" } } - if (!(Test-Path $clientPath)) { + if (-not (Test-Path -Path $clientPath)) { New-Item -ItemType Directory -Force -Path $clientPath | Out-Null } Write-Verbose "Autopilot Reference VHDX: $($imageDetails.refImagePath)" Write-Verbose "Client name: $TenantName" - Write-Verbose "Win10 ISO is located: $($imageDetails.imagePath)" + Write-Verbose "Windows ISO is located: $($imageDetails.imagePath)" Write-Verbose "Path to client VMs will be: $clientPath" Write-Verbose "Number of VMs to create: $NumberOfVMs" Write-Verbose "Admin user for $TenantName is: $($clientDetails.adminUpn)`n" #endregion #region Check for ref image - if it's not there, build it - if (!(Test-Path -Path $imageDetails.refImagePath -ErrorAction SilentlyContinue)) { - Write-Host "Creating reference Autopilot VHDX - this may take some time.." -ForegroundColor Yellow + if (-not (Test-Path -Path $imageDetails.refImagePath -ErrorAction SilentlyContinue)) { + Write-Host 'Creating reference Autopilot VHDX - this may take some time...' -NoNewline -ForegroundColor Cyan New-ClientVHDX -vhdxPath $imageDetails.refImagePath -winIso $imageDetails.imagePath - Write-Host "Reference Autopilot VHDX has been created.." -ForegroundColor Yellow + Write-Host 'Reference Autopilot VHDX has been created.' -ForegroundColor Green } #endregion #region Get Autopilot policy - if (!($SkipAutoPilot)) { - Write-Host "Grabbing Autopilot config.." -ForegroundColor Yellow + if (-not ($SkipAutoPilot)) { Get-AutopilotPolicy -FileDestination "$clientPath" + if (-not (Test-Path "$clientPath\AutopilotConfigurationFile.json" -ErrorAction SilentlyContinue)) { + throw 'Autopilot config not found.' + } } #endregion #region Build the client VMs - if (!(Test-Path -Path $clientPath -ErrorAction SilentlyContinue)) { + if (-not (Test-Path -Path $clientPath -ErrorAction SilentlyContinue)) { New-Item -Path $clientPath -ItemType Directory -Force | Out-Null } + + # Ensure all required parameters are properly set and validated + if (-not $script:hvConfig.vSwitchName) { + throw 'Virtual switch name is not configured. Please check the hvConfig settings.' + } + + if (-not $imageDetails.refImagePath) { + throw 'Reference VHDX path is not configured. Please check the image settings.' + } + + # Create base parameter hashtable with all mandatory parameters $vmParams = @{ - ClientPath = $clientPath - RefVHDX = $imageDetails.refImagePath + ClientPath = $clientPath + RefVHDX = $imageDetails.refImagePath VSwitchName = $script:hvConfig.vSwitchName - CPUCount = $CPUsPerVM - VMMMemory = $VMMemory + CPUCount = $CPUsPerVM + VMMemory = $VMMemory } + + # Add optional parameters only if they are specified if ($SkipAutoPilot) { $vmParams.skipAutoPilot = $true } + + if ($DynamicMemory) { + $vmParams.DynamicMemory = $true + } + if ($script:hvConfig.vLanId) { $vmParams.VLanId = $script:hvConfig.vLanId } - if ($numberOfVMs -eq 1) { - $max = ((Get-VM -Name "$TenantName*").name -replace "\D" | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum) + 1 - $vmParams.VMName = "$($TenantName)_$max" - Write-Host "Creating VM: $($vmParams.VMName).." -ForegroundColor Yellow + + # Display the parameters being passed to New-ClientDevice for debugging + Write-Host 'Parameters being passed to New-ClientDevice:' -NoNewline -ForegroundColor Cyan + $vmParams.GetEnumerator() | ForEach-Object { Write-Host "$($_.Key): $($_.Value)" -NoNewline -ForegroundColor Cyan } + + if ($NumberOfVMs -eq 1) { + # Calculate the VM name before adding it to the parameters + $max = 1 + $existingVMs = Get-VM -Name "$TenantName*" -ErrorAction SilentlyContinue + if ($existingVMs) { + $max = ($existingVMs.name -replace "$TenantName`_" -replace '\D' | + Where-Object { $_ -match '^\d+$' } | + Measure-Object -Maximum | + Select-Object -ExpandProperty Maximum) + 1 + } + + # Add VMName to parameters - this is a mandatory parameter for New-ClientDevice + $vmName = "$($TenantName)_$max" + $vmParams.VMName = $vmName + + Write-Host "Creating VM: $vmName..." -NoNewline -ForegroundColor Cyan New-ClientDevice @vmParams - } - else { + } else { (1..$NumberOfVMs) | ForEach-Object { - $max = ((Get-VM -Name "$TenantName*").name -replace "\D" | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum) + 1 - $vmParams.VMName = "$($TenantName)_$max" - Write-Host "Creating VM: $($vmParams.VMName).." -ForegroundColor Yellow - New-ClientDevice @vmParams + # Calculate the VM name before adding it to the parameters + $max = 1 + $existingVMs = Get-VM -Name "$TenantName*" -ErrorAction SilentlyContinue + if ($existingVMs) { + $max = ($existingVMs.name -replace "$TenantName`_" -replace '\D' | + Where-Object { $_ -match '^\d+$' } | + Measure-Object -Maximum | + Select-Object -ExpandProperty Maximum) + 1 + } + + # Add VMName to parameters - this is a mandatory parameter for New-ClientDevice + $vmName = "$($TenantName)_$max" + $vmParams.VMName = $vmName + + Write-Host "Creating VM: $vmName..." -NoNewline -ForegroundColor Cyan + New-ClientDevice @vmParams + } + } + #endregion + } catch { + $errorMsg = $_.Exception.Message + } finally { + if ($errorMsg) { + Write-Warning $errorMsg } - } - #endregion - } - catch { - $errorMsg = $_.Exception.Message - } - finally { - if ($errorMsg) { - Write-Warning $errorMsg } } -} \ No newline at end of file diff --git a/Intune.HV.Tools/ReleaseNotes.txt b/Intune.HV.Tools/ReleaseNotes.txt deleted file mode 100644 index 2551c46..0000000 --- a/Intune.HV.Tools/ReleaseNotes.txt +++ /dev/null @@ -1,2 +0,0 @@ -*** - Improving cmdlet autocomplete -*** - Added checks for empty variables and handled failed VHDX mount \ No newline at end of file diff --git a/New-ClientVM2.ps1 b/New-ClientVM2.ps1 index 2473112..1632a63 100644 --- a/New-ClientVM2.ps1 +++ b/New-ClientVM2.ps1 @@ -5,42 +5,40 @@ function New-ClientVHDX { ( [string]$vhdxPath, [Parameter(Mandatory = $false)] - [string]$unattend = "none", + [string]$unattend = 'none', [string]$WinISO ) - $convMod = get-module -ListAvailable -Name 'Convert-WindowsImage' + $convMod = Get-Module -ListAvailable -Name 'Convert-WindowsImage' if ($convMod.count -ne 1) { - Install-Module -name 'Convert-WindowsImage' -Scope AllUsers + Install-Module -Name 'Convert-WindowsImage' -Scope AllUsers + } else { + Update-Module -Name 'Convert-WindowsImage' } - else { - Update-Module -Name 'Convert-WindowsImage' - } - Import-module -name 'Convert-Windowsimage' - if ($unattend -eq "none") { + Import-Module -Name 'Convert-Windowsimage' + if ($Unattend -eq 'none') { Convert-WindowsImage -SourcePath $WinISO -Edition 3 -VhdType Dynamic -VhdFormat VHDX -VhdPath $vhdxPath -DiskLayout UEFI -SizeBytes 127gb - } - else { - Convert-WindowsImage -SourcePath $WinISO -Edition 3 -VhdType Dynamic -VhdFormat VHDX -VhdPath $vhdxPath -DiskLayout UEFI -SizeBytes 127gb -UnattendPath $unattend + } else { + Convert-WindowsImage -SourcePath $WinISO -Edition 3 -VhdType Dynamic -VhdFormat VHDX -VhdPath $vhdxPath -DiskLayout UEFI -SizeBytes 127gb -UnattendPath $unattend } } function Write-LogEntry { [cmdletBinding()] param ( - [ValidateSet("Information", "Error")] - $Type = "Information", + [ValidateSet('Information', 'Error')] + $Type = 'Information', [parameter(Mandatory = $true)] $Message ) switch ($Type) { 'Error' { $severity = 3 - $fgColor = "Red" - break; + $fgColor = 'Red' + break } 'Information' { $severity = 6 - $fgColour = "Yellow" - break; + $fgColour = 'Yellow' + break } } $dateTime = New-Object -ComObject WbemScripting.SWbemDateTime @@ -56,8 +54,8 @@ function Write-LogEntry { "context=`"$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)`" " + ` "type=`"$severity`" " + ` "thread=`"$PID`" " + ` - "file=`"$($scriptName.ScriptName)`">"; - + "file=`"$($scriptName.ScriptName)`">" + $logLine | Out-File -Append -Encoding utf8 -FilePath $logFile -Force #Write-Host $Message -ForegroundColor $fgColor } @@ -69,13 +67,13 @@ function New-ClientVM { [pscredential]$localAdmin, [string]$refApVHDX ) - If (!(Test-Path "$clientPath\$vmName")){ New-Item -ItemType Directory -Path "$clientPath\$vmName" } - copy-item -path $refApVHDX -Destination "$clientPath\$vmName\$vmName.vhdx" - $disk = (Mount-VHD -Path "$clientPath\$vmName\$vmName.vhdx" -Passthru | Get-disk | Get-Partition | Where-Object {$_.type -eq 'Basic'}).DriveLetter - copy-item -path "$clientPath\AutoPilotProfile\AutopilotConfigurationFile.json" -Destination "$disk`:\Windows\Provisioning\Autopilot\" -Recurse -Filter "AutopilotConfigurationFile.json" - dismount-vhd "$clientPath\$vmName\$vmName.vhdx" - new-vm -Name $vmName -Path $clientPath\ -MemoryStartupBytes 4Gb -VHDPath "$clientPath\$vmName\$vmName.vhdx" -Generation 2 | out-null - Enable-VMIntegrationService -vmName $vmName -Name "Guest Service Interface" + If (-not(Test-Path "$clientPath\$vmName")) { New-Item -ItemType Directory -Path "$clientPath\$vmName" } + Copy-Item -Path $refApVHDX -Destination "$clientPath\$vmName\$vmName.vhdx" + $disk = (Mount-VHD -Path "$clientPath\$vmName\$vmName.vhdx" -Passthru | Get-Disk | Get-Partition | Where-Object { $_.type -eq 'Basic' }).DriveLetter + Copy-Item -Path "$clientPath\AutoPilotProfile\AutopilotConfigurationFile.json" -Destination "$disk`:\Windows\Provisioning\Autopilot\" -Recurse -Filter 'AutopilotConfigurationFile.json' + Dismount-VHD "$clientPath\$vmName\$vmName.vhdx" + New-VM -Name $vmName -Path $clientPath\ -MemoryStartupBytes 4Gb -VHDPath "$clientPath\$vmName\$vmName.vhdx" -Generation 2 | Out-Null + Enable-VMIntegrationService -VMName $vmName -Name 'Guest Service Interface' #Disable Checkpoints #set-vm -name $vmName -CheckpointType Disabled # Enable TPM @@ -84,15 +82,15 @@ function New-ClientVM { #Disable AutoCheckpoint Set-VM $VMName -AutomaticCheckpointsEnabled $false #start-vm -Name $vmName - Get-VMNetworkAdapter -vmName $vmName | Connect-VMNetworkAdapter -SwitchName 'BSALabNAT' | Set-VMNetworkAdapter -Name 'Internet' -DeviceNaming On + Get-VMNetworkAdapter -VMName $vmName | Connect-VMNetworkAdapter -SwitchName 'BSALabNAT' | Set-VMNetworkAdapter -Name 'Internet' -DeviceNaming On } #endregion #region Config $scriptPath = $PSScriptRoot $config = Get-Content "$scriptPath\client.json" -Raw | ConvertFrom-Json -$clientDetails = $config.ENVConfig | Where-Object {$_.ClientName -eq $config.Client} +$clientDetails = $config.ENVConfig | Where-Object { $_.ClientName -eq $config.Client } $clientPath = "$($config.ClientVMPath)\$($config.Client)" -if (!(Test-Path $clientPath)) {new-item -ItemType Directory -Force -Path $clientPath | Out-Null} +if (-not(Test-Path $clientPath)) { New-Item -ItemType Directory -Force -Path $clientPath | Out-Null } $script:logfile = "$clientPath\Build.log" $refApVHDX = $config.Win10APVHDX $clientName = $clientDetails.ClientName @@ -105,48 +103,44 @@ Write-LogEntry -Type Information -Message "Win10 ISO is located: $win10iso" Write-LogEntry -Type Information -Message "Path to client VMs will be: $clientPath" Write-LogEntry -Type Information -Message "Number of VMs to create: $numOfVMs" Write-LogEntry -type Information -Message "Admin user for tenant: $clientName is: $adminUser" -if (!(test-path -path $refApVHDX -ErrorAction SilentlyContinue)) { - Write-LogEntry -Type Information -Message "Creating Workstation AutoPilot VHDX" +if (-not(Test-Path -Path $refApVHDX -ErrorAction SilentlyContinue)) { + Write-LogEntry -Type Information -Message 'Creating Workstation AutoPilot VHDX' new-ClientVHDX -vhdxpath $refApVHDX -winiso $win10iso - Write-LogEntry -Type Information -Message "Workstation AutoPilot VHDX has been created" + Write-LogEntry -Type Information -Message 'Workstation AutoPilot VHDX has been created' } #endregion #region getAPPolicy -if (!(Test-path -path "$clientPath\AutoPilotProfile\AutopilotConfigurationFile.json" -ErrorAction SilentlyContinue)) { - if ((get-module -listavailable -name WindowsAutoPilotIntune).count -ne 1) { - install-module -name WindowsAutoPilotIntune -scope allusers -Force +if (-not(Test-Path -Path "$clientPath\AutoPilotProfile\AutopilotConfigurationFile.json" -ErrorAction SilentlyContinue)) { + if ((Get-Module -ListAvailable -Name WindowsAutoPilotIntune).count -ne 1) { + Install-Module -Name WindowsAutoPilotIntune -Scope allusers -Force + } else { + Update-Module -Name WindowsAutoPilotIntune } - else { - update-module -name WindowsAutoPilotIntune - } - import-module -name WindowsAutoPilotIntune + Import-Module -Name WindowsAutoPilotIntune Connect-AutoPilotIntune -user $adminUser $appolicies = Get-AutoPilotProfile - if($appolicies.count -gt 1) - { + if ($appolicies.count -gt 1) { $appol = $appolicies | Out-GridView -PassThru - } - else { + } else { $appol = $appolicies } - If (!(Test-Path "$clientPath\AutoPilotProfile\")){ New-Item -ItemType Directory -Path "$clientPath\AutoPilotProfile\" } + If (-not(Test-Path "$clientPath\AutoPilotProfile\")) { New-Item -ItemType Directory -Path "$clientPath\AutoPilotProfile\" } $appol | ConvertTo-AutoPilotConfigurationJSON | Out-File "$clientPath\AutoPilotProfile\AutopilotConfigurationFile.json" -Encoding ascii } #endregion #region New Client VM $apOut = @() -if (!(test-path -Path $clientPath\$vmName\)) {New-Item -ItemType Directory -Force -Path $clientPath\$vmName\} +if (-not(Test-Path -Path $clientPath\$vmName\)) { New-Item -ItemType Directory -Force -Path $clientPath\$vmName\ } if ($numOfVMs -eq 1) { $vmName = "$($clientName)$numOfVMs" $AP = new-clientVM -vmName $vmName -clientpath $clientPath\$vmName\ -localAdmin $localAdmin -refAPVHDX $refApVHDX $ap | Out-File -FilePath "$clientPath\$numOfVMs.csv" $apOut += $ap -} -else { +} else { $vnum = 1 - $existingvms = (get-vm -name "$($clientname)*").name -replace "$($clientname)" + $existingvms = (Get-VM -Name "$($clientname)*").name -replace "$($clientname)" while ($vnum -ne ($numOfVMs + 1 + $existingvms.Count)) { - if (!($vnum -in $existingvms)) { + if (-not($vnum -in $existingvms)) { $vmName = "$($clientName)$vnum" $apOut += new-clientVM -vmName $vmName -clientpath $clientPath\$vmName\ -localAdmin $localAdmin -refAPVHDX $refApVHDX } @@ -155,4 +149,4 @@ else { } #endregion -#$clientPath\$vmName\$vmName.vhdx \ No newline at end of file +#$clientPath\$vmName\$vmName.vhdx diff --git a/README.md b/README.md index ef35d0c..39584f6 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,21 @@ # Intune.HV.Tools + [![Build Status](https://dev.azure.com/powers-hell/Intune.USB.Creator/_apis/build/status/tabs-not-spaces.Intune.HV.Tools%20-%20Publish%20Prod?branchName=master)](https://dev.azure.com/powers-hell/Intune.USB.Creator/_build/latest?definitionId=37&branchName=master) ![PowerShell Gallery](https://img.shields.io/powershellgallery/v/Intune.HV.Tools.svg?style=flat&logo=powershell&label=PSGallery%20Version) ![PSGallery Downloads](https://img.shields.io/powershellgallery/dt/Intune.HV.Tools.svg?style=flat&logo=powershell&label=PSGallery%20Downloads) + ## Summary -A set of tools to assist with the creation of Intune managed virtual machines in Hyper-V. +A set of tools to assist with the creation of Intune-managed virtual machines in Hyper-V. Created in collaboration with: - [AdamGrossTX](https://github.com/AdamGrossTX) - [brucesa85](https://github.com/brucesa85) - [OnPremCloudGuy](https://github.com/onpremcloudguy) +- [Jonathan Pitre](https://github.com/JonathanPitre) -## Pre-Reqs +## Prerequisites - [WindowsAutoPilotIntune](https://www.powershellgallery.com/packages/WindowsAutoPilotIntune) - [Microsoft.Graph.Intune](https://www.powershellgallery.com/packages/Microsoft.Graph.Intune/) @@ -34,13 +37,13 @@ Install-Module -Name Intune.HV.Tools -Scope CurrentUser Initialize-HVTools -Path C:\Lab ``` -If the path provided doesn't exist it will be automatically created. Please note this tool creates very large reference images - if your system drive is small, dont initialize the tools on it. +If the path provided doesn't exist it will be automatically created. Please note this tool creates very large reference images - if your system drive is small, don't initialize the tools on it. The environment is a simple folder structure containing the configuration file for the tool, reference images to be used for provisioning of VMs and tenant folders containing offline Autopilot configuration files and provisioned *.vhdx images. Folder structure example displayed below: -``` +```text 📦.hvtools ┣ 📂tenantVMs ┃ ┣ 📂MegaCorp @@ -103,7 +106,7 @@ If you name your images based on editions you can have multiple images per insta Add-TenantToConfig -TenantName 'MegaCorp' -ImageName 2004 -AdminUpn 'intune-admin@megacorp.com' ``` -You can add as many tenants to the environment as you want. The ImageName parameter auto-completes to the available images from your environment. +You can add as many tenants to the environment as you want. The `ImageName` parameter auto-completes to the available images from your environment. The ImageName provides the ability to set a default reference image per tenant, however this can be overwritten during creation. @@ -113,7 +116,7 @@ The ImageName provides the ability to set a default reference image per tenant, Add-NetworkToConfig -VSwitchName 'Default Switch' ``` -VSwitchName autocompletes to the available virtual switches already created in your Hyper-V environment. At the moment you can only have one network config in your environment. +`VSwitchName` autocompletes to the available virtual switches already created in your Hyper-V environment. At the moment you can only have one network config in your environment. ### Get HV.Tools configuration @@ -130,8 +133,8 @@ New-ClientVM -TenantName 'Powers-Hell' -OSBuild 2004 -NumberOfVMs 10 -CPUsPerVM ``` The example above will create 10 VMs using the reference image from the environment config named '2004' with 2 CPUs per VM and 8gb of ram. -TenantName autocompletes from the list of tenants in your environment. -OSBuild autocompletes from the list of images in your environment. +`TenantName` autocompletes from the list of tenants in your environment. +`OSBuild` autocompletes from the list of images in your environment. Reference images are now created in the "Add-ImageToConfig" stage, but if you've deleted the reference image or if the image can't be found, it will be created at this point. You will be asked which edition you want to use for the reference image. @@ -145,7 +148,7 @@ Once this Autopilot configuration is captured locally, you will not be required New-ClientVM -TenantName 'Powers-Hell' -OSBuild 2004 -NumberOfVMs 10 -CPUsPerVM 2 -VMMemory 8gb -SkipAutopilot ``` -Exactly the same as the previous step. Using the parameter SkipAutopilot allows you to build VMs without injecting the Autopilot configuration file into the *.VHDX. +Exactly the same as the previous step. Using the parameter `SkipAutopilot` allows you to build VMs without injecting the Autopilot configuration file into the *.VHDX. ## Caveat Emptor @@ -155,49 +158,69 @@ If you find a problem and want to contribute - please do! I love community invol ## Release Notes +## 1.0.0.320 + +- Fixed VMIntegrationService error on non-English systems [#24](https://github.com/tabs-not-spaces/Intune.HV.Tools/pull/24) +- Fixed authentication error to use MgGraph [#29](https://github.com/tabs-not-spaces/Intune.HV.Tools/issues/29) +- Added error checking when AutopilotConfigurationFile.json is missing +- Added fix when multiple Autopilot profile exists +- Improved code formatting +- Improved comments +- Fixed errors when using custom Virtual Machines Disks and Configs path +- Added dynamic memory support for VM creation +- Improved path handling for ISO and VM locations +- Added support for internal virtual switch configuration +- Improved error handling for ISO path validation + +### 1.0.0.312 + +- Added ISOPath and RefVHDX as required parameters +- Fixed documentation typo on line 37 +- Added support for custom VHDX files + ### 1.0.0.289 -- Feature: Build ref images from Add-ImageToConfig -- New Build fixes ServerOS issues -- Adds Index from wim -- General code cleanup -- Improved VM naming code -- Updated required module versions -- Updated documentation +- Added ability to build reference images from Add-ImageToConfig +- Fixed compatibility issues with Server OS +- Added Windows image index selection +- Improved code organization and readability +- Enhanced VM naming consistency +- Updated minimum required module versions +- Improved documentation clarity ### 1.0.0.281 -- Adding check to create HGS Guardian if not present -- Create folder if needed and dismount VHDX (@hkystar35) -- Added erroraction (@hkystar35) -- updated build script to grab release notes from git -- formatting release notes -- updated release notes (@hkystar35) -- squashing an encoding bug +- Added missing HGS Guardian creation check +- Added automatic folder creation and VHDX dismount (thanks [hkystar35](https://github.com/hkystar35)) +- Added error handling improvements (thanks [hkystar35](https://github.com/hkystar35)) +- Added git release notes to build script +- Improved release notes formatting +- Enhanced release notes clarity (thanks [hkystar35](https://github.com/hkystar35)) +- Fixed file encoding issue ### 1.0.0.205 -- Small bug fixes (@hkystar35) -- fixing old variable reference -- Improving cmdlet autocomplete -- updating parameter values to be standardized -- Updating cmdlet names for better use -- updating cmdlet name for easier use.. -- Fixing module dependency for pwsh7 support -- Initialize-HVTools parameter Path now required. -- updating description and release notes -- Ready for prime time.. (#3) -- added serial number to notes (@brucesa85) -- preparing for first ship -- updated required modules -- fixed multiple vm naming finally... -- added support for pwsh 5 and 7 -- added ability to reset the config file -- new config function added -- removing expansion -- added support for powershell 5 and 7 -- added additional argumentcompleter +- Fixed various minor issues (@hkystar35) +- Fixed legacy variable reference +- Added cmdlet autocomplete functionality +- Standardized parameter values +- Improved cmdlet naming consistency +- Enhanced cmdlet usability +- Fixed PowerShell 7 module dependencies +- Added required Path parameter to Initialize-HVTools +- Updated module description and notes +- Released initial stable version (#3) +- Added VM serial number to notes (@brucesa85) +- Prepared for initial release +- Updated module dependencies +- Fixed multiple VM naming issues +- Added PowerShell 5 and 7 compatibility +- Added config reset capability +- Added configuration management function +- Removed unnecessary expansion +- Added PowerShell version compatibility +- Added parameter auto-completion ### 1.0.0.203 -- Initial commit +- Initial release diff --git a/Tests/codecheck.ps1 b/Tests/codecheck.ps1 index ac4cccb..b988845 100644 --- a/Tests/codecheck.ps1 +++ b/Tests/codecheck.ps1 @@ -1,5 +1,5 @@ -if (!(Test-Path $fp\.tests\)) { +if (-not(Test-Path $fp\.tests\)) { new-item $fp\.tests -ItemType Directory -Force } Import-Module Pester -RequiredVersion 4.10.1 -Force -Invoke-Pester -Script "$PSScriptRoot\codecheck.test.ps1" -OutputFile "$fp\.tests\pester.codecheck.test.xml" -OutputFormat 'NUnitXml' \ No newline at end of file +Invoke-Pester -Script "$PSScriptRoot\codecheck.test.ps1" -OutputFile "$fp\.tests\pester.codecheck.test.xml" -OutputFormat 'NUnitXml' diff --git a/Tests/codecheck.test.ps1 b/Tests/codecheck.test.ps1 index 2bd68cc..1b9003a 100644 --- a/Tests/codecheck.test.ps1 +++ b/Tests/codecheck.test.ps1 @@ -1,10 +1,10 @@ -[cmdletbinding()] +[CmdletBinding()] param ( [System.IO.FileInfo]$filePath ) $excludeRule = @( - "PSAvoidUsingWriteHost", - "PSAvoidUsingConvertToSecureStringWithPlainText" + 'PSAvoidUsingWriteHost', + 'PSAvoidUsingConvertToSecureStringWithPlainText' ) $fp = Split-Path $PSScriptRoot -Parent if (Test-Path $fp\localenv.ps1 -ErrorAction SilentlyContinue) { @@ -12,13 +12,12 @@ if (Test-Path $fp\localenv.ps1 -ErrorAction SilentlyContinue) { } $fp = "$fp\bin\release\$env:BUILD_BUILDID" $fp -Describe "Checking content exists" { +Describe 'Checking content exists' { if ($filePath) { $scripts = Get-ChildItem $filePath - } - else { + } else { $scripts = Get-ChildItem -Path "$fp\$env:MODULENAME" -Recurse -Include *.ps1 - $scope = @("Private", "Public") + $scope = @('Private', 'Public') foreach ($s in $scope) { Context "Checking for files in $s.." { It "$s scripts folder not empty" { ($scripts | Where-Object { $_.Directory.Name -eq $s }).count | Should -BeGreaterOrEqual 1 } @@ -26,18 +25,18 @@ Describe "Checking content exists" { } } } -if (!($filePath)) { - Describe "Manifest" { - Context "Checking module manifest" { +if (-not($filePath)) { + Describe 'Manifest' { + Context 'Checking module manifest' { $manifest = Test-ModuleManifest -Path "$fp\$env:MODULENAME\$env:MODULENAME`.psd1" - It "Has a valid module manifest" { $manifest | Should -Not -BeNullOrEmpty } + It 'Has a valid module manifest' { $manifest | Should -Not -BeNullOrEmpty } } } } -Describe "Checking Code Quality" { +Describe 'Checking Code Quality' { $scripts = Get-ChildItem -Path "$fp\$env:MODULENAME" -Recurse -Include *.ps1 $scripts.ForEach{ - Context "PSSA Quality Check: $($_.name)" { + Context "PSScriptAnalyzer Quality Check: $($_.name)" { $pssaIssues = Invoke-ScriptAnalyzer -Path "$_" -ExcludeRule $excludeRule $pssaRuleNames = Get-ScriptAnalyzerRule | Select-Object -ExpandProperty RuleName foreach ($rule in $pssaRuleNames) { @@ -51,4 +50,4 @@ Describe "Checking Code Quality" { } } } -} \ No newline at end of file +} diff --git a/build.ps1 b/build.ps1 index a49e899..950ce37 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,6 @@ -[cmdletbinding()] +[CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [System.IO.FileInfo]$modulePath, [parameter(Mandatory = $false)] @@ -20,7 +20,7 @@ if ($buildLocal) { } try { - if (!($moduleName)) { + if (-not($moduleName)) { $moduleName = Split-Path $modulePath -Leaf } #region Generate a new version number @@ -32,22 +32,22 @@ try { "Module Path is $modulePath" "Module Name is $moduleName" "Release Path is $relPath" - if (!(Test-Path $relPath)) { + if (-not (Test-Path $relPath)) { New-Item -Path $relPath -ItemType Directory -Force | Out-Null } - Copy-Item "$modulePath\*" -Destination "$relPath" -Recurse -Exclude ".gitKeep" + Copy-Item "$modulePath\*" -Destination "$relPath" -Recurse -Exclude '.gitKeep' #endregion #region Generate a list of public functions and update the module manifest $functions = @(Get-ChildItem -Path $relPath\Public\*.ps1 -ErrorAction SilentlyContinue).basename $params = @{ - Path = "$relPath\$ModuleName.psd1" - ModuleVersion = $newVersion - Description = (Get-Content $relPath\description.txt -raw).ToString() + Path = "$relPath\$ModuleName.psd1" + ModuleVersion = $newVersion + Description = (Get-Content $relPath\description.txt -Raw).ToString() FunctionsToExport = $functions - ReleaseNotes = @((git log --oneline --decorate -- './Intune.HV.Tools/*.*') -join "`n") + ReleaseNotes = @((git log --oneline --decorate -- './Intune.HV.Tools/*.*') -join "`n") } Update-ModuleManifest @params - $moduleManifest = Get-Content $relPath\$ModuleName.psd1 -raw | Invoke-Expression + $moduleManifest = Get-Content $relPath\$ModuleName.psd1 -Raw | Invoke-Expression #endregion #region Generate the nuspec manifest $t = [xml](Get-Content $PSScriptRoot\module.nuspec -Raw) @@ -55,8 +55,8 @@ try { $t.package.metadata.version = $newVersion.ToString() $t.package.metadata.authors = $moduleManifest.author.ToString() $t.package.metadata.owners = $moduleManifest.author.ToString() - $t.package.metadata.requireLicenseAcceptance = "false" - $t.package.metadata.description = (Get-Content $relPath\description.txt -raw).ToString() + $t.package.metadata.requireLicenseAcceptance = 'false' + $t.package.metadata.description = (Get-Content $relPath\description.txt -Raw).ToString() $t.package.metadata.description $t.package.metadata.releaseNotes = @((git log --oneline --decorate -- './Intune.HV.Tools/*.*') -join "`n") $t.package.metadata.releaseNotes @@ -64,7 +64,6 @@ try { $t.package.metadata.tags = ($moduleManifest.PrivateData.PSData.Tags -join ',').ToString() $t.Save("$PSScriptRoot\$moduleName`.nuspec") #endregion -} -catch { +} catch { $_ -} \ No newline at end of file +} diff --git a/pr-pipeline.yml b/pr-pipeline.yml index 412d62b..12f7fc5 100644 --- a/pr-pipeline.yml +++ b/pr-pipeline.yml @@ -23,9 +23,9 @@ stages: targetType: 'inline' script: | Install-Module -Name Pester -Verbose -Scope CurrentUser -SkipPublisherCheck -Force - Install-Module WindowsAutoPilotIntune -Scope CurrentUser -Force - Install-Module Microsoft.Graph.Intune -Scope CurrentUser -Force - Install-Module Hyper-ConvertImage -Scope CurrentUser -Force + Install-Module -Name WindowsAutoPilotIntune -Scope CurrentUser -Force + Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser -Force + Install-Module -Name Hyper-ConvertImage -Scope CurrentUser -Force pwsh: true - task: PowerShell@2 @@ -48,4 +48,4 @@ stages: testResultsFiles: 'pester.codecheck.test.xml' searchFolder: '$(System.DefaultWorkingDirectory)/.tests' failTaskOnFailedTests: true - testRunTitle: 'Code Quality Test' \ No newline at end of file + testRunTitle: 'Code Quality Test' diff --git a/prod-pipeline.yml b/prod-pipeline.yml index c8356f6..d5fa6dc 100644 --- a/prod-pipeline.yml +++ b/prod-pipeline.yml @@ -9,7 +9,7 @@ trigger: - /.gitignore - /readme.md - /.tests - + pr: none stages: @@ -25,10 +25,10 @@ stages: targetType: 'inline' script: | Install-Module -Name Pester -MaximumVersion 4.10.1 -Verbose -Scope CurrentUser -SkipPublisherCheck -Force - Install-Module WindowsAutoPilotIntune -Scope CurrentUser -Force - Install-Module Microsoft.Graph.Intune -Scope CurrentUser -Force - Install-Module Hyper-ConvertImage -Scope CurrentUser -Force - Install-Module PSScriptAnalyzer -Scope CurrentUser -SkipPublisherCheck -ErrorAction SilentlyContinue -Force + Install-Module -Name WindowsAutoPilotIntune -Scope CurrentUser -Force + Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser -Force + Install-Module -Name Hyper-ConvertImage -Scope CurrentUser -Force + Install-Module -Name PSScriptAnalyzer -Scope CurrentUser -SkipPublisherCheck -ErrorAction SilentlyContinue -Force pwsh: true - task: PowerShell@2 @@ -89,15 +89,15 @@ stages: "Hyper-ConvertImage" ) foreach ($m in $modules) { - write-host "Installing module: $m.." - Install-Module $m -Scope CurrentUser -Force + Write-Host "Installing module: $m.." + Install-Module -Name $m -Scope CurrentUser -Force } - write-host "Publishing module from: $env:Pipeline_Workspace\release\$env:MODULENAME" + Write-Host "Publishing module from: $env:Pipeline_Workspace\release\$env:MODULENAME" Publish-Module -Path "$env:Pipeline_Workspace\release\$env:MODULENAME" -NuGetApiKey $env:APIKEY } catch { - write-warning $_ + Write-Warning $_ } pwsh: true env: - APIKEY: $(apiKey) \ No newline at end of file + APIKEY: $(apiKey) From a4258cb246db5d695309eca31c2d6c123147f3d7 Mon Sep 17 00:00:00 2001 From: Captain Firmware Date: Tue, 4 Mar 2025 17:50:58 -0500 Subject: [PATCH 2/3] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 39584f6..140524f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Created in collaboration with: - [Microsoft.Graph.Intune](https://www.powershellgallery.com/packages/Microsoft.Graph.Intune/) - [Hyper-ConvertImage](https://www.powershellgallery.com/packages/Hyper-ConvertImage/) - [PowerShell 7](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-windows?view=powershell-7) -- A copy of Windows 10 (Multi-format ISO recommended) +- A copy of Windows ISO, you can use [Fido](https://github.com/pbatard/Fido) to grab them ## How to use @@ -129,7 +129,7 @@ Allows you to access the environment configuration file. ### Create a virtual machine ``` PowerShell -New-ClientVM -TenantName 'Powers-Hell' -OSBuild 2004 -NumberOfVMs 10 -CPUsPerVM 2 -VMMemory 8gb +New-ClientVM -TenantName 'Powers-Hell' -OSBuild 2004 -NumberOfVMs 10 -CPUsPerVM 2 -VMMemory 8gb -DynamicMemory ``` The example above will create 10 VMs using the reference image from the environment config named '2004' with 2 CPUs per VM and 8gb of ram. @@ -145,7 +145,7 @@ Once this Autopilot configuration is captured locally, you will not be required ### Create a virtual machine without Autopilot offline injection ``` PowerShell -New-ClientVM -TenantName 'Powers-Hell' -OSBuild 2004 -NumberOfVMs 10 -CPUsPerVM 2 -VMMemory 8gb -SkipAutopilot +New-ClientVM -TenantName 'Powers-Hell' -OSBuild 2004 -NumberOfVMs 10 -CPUsPerVM 2 -VMMemory 8gb -DynamicMemory -SkipAutopilot ``` Exactly the same as the previous step. Using the parameter `SkipAutopilot` allows you to build VMs without injecting the Autopilot configuration file into the *.VHDX. @@ -158,7 +158,7 @@ If you find a problem and want to contribute - please do! I love community invol ## Release Notes -## 1.0.0.320 +### 1.0.0.320 - Fixed VMIntegrationService error on non-English systems [#24](https://github.com/tabs-not-spaces/Intune.HV.Tools/pull/24) - Fixed authentication error to use MgGraph [#29](https://github.com/tabs-not-spaces/Intune.HV.Tools/issues/29) From 779d5c13574b65c04771fed495fdac3f058ec02e Mon Sep 17 00:00:00 2001 From: Captain Firmware Date: Tue, 4 Mar 2025 18:22:55 -0500 Subject: [PATCH 3/3] Update ModuleVersion --- Intune.HV.Tools/Intune.HV.Tools.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Intune.HV.Tools/Intune.HV.Tools.psd1 b/Intune.HV.Tools/Intune.HV.Tools.psd1 index 812635e..9daa13e 100644 --- a/Intune.HV.Tools/Intune.HV.Tools.psd1 +++ b/Intune.HV.Tools/Intune.HV.Tools.psd1 @@ -4,7 +4,7 @@ RootModule = 'Intune.HV.Tools.psm1' # Version number of this module. - ModuleVersion = '1.0.0.313' + ModuleVersion = '1.0.0.320' # Supported PSEditions # CompatiblePSEditions = @()