1
0

Auto-commit: 2025-10-31 08:58:35

This commit is contained in:
David Wuibaille
2025-10-31 08:58:36 +01:00
parent 7d94414992
commit 7cc3011354
1088 changed files with 193455 additions and 0 deletions

View File

@@ -0,0 +1,381 @@
#requires -RunAsAdministrator
<#
GUI to:
- List running Hyper-V VMs
- Fetch .ps1 scripts from https://nas.wuibaille.fr/WS/postype/
- Prompt for credentials
- Execute selected script inside selected VMs via PowerShell Direct
- Optional: run gpupdate /force afterwards
Tested on Windows 11 + PowerShell 5/7.
#>
$ErrorActionPreference = 'Stop'
# ---------- Config ----------
$BaseUrl = 'https://nas.wuibaille.fr/WS/postype/'
# ---------- Setup ----------
try { Import-Module Hyper-V -ErrorAction Stop } catch {
Write-Error "Hyper-V PowerShell module not available. Install/enable Hyper-V Management Tools."
return
}
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor 0x3000 # include TLS1.3 if available
} catch {
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
}
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
function Get-RunningVMs {
Get-VM | Where-Object { $_.State -eq 'Running' } | Sort-Object Name | Select-Object -ExpandProperty Name
}
function Get-ScriptsFromUrl {
param([string]$Url)
# Fetch directory HTML and extract .ps1 links (robust for PS5/PS7)
$resp = Invoke-WebRequest -Uri $Url -UseBasicParsing
$html = $resp.Content
# Regex: href="something.ps1" or href='something.ps1'
$matches = [System.Text.RegularExpressions.Regex]::Matches($html, 'href\s*=\s*["' + "'" + ']([^"' + "'" + ']+\.ps1)["' + "'" + ']', 'IgnoreCase')
$links = @()
$baseUri = [Uri]$Url
foreach ($m in $matches) {
$href = $m.Groups[1].Value
# Skip parent links etc.
if ($href -match '^\.\./?$') { continue }
$full = if ([Uri]::IsWellFormedUriString($href, [UriKind]::Absolute)) {
[Uri]$href
} else {
[Uri]::new($baseUri, $href)
}
$name = [System.IO.Path]::GetFileName($full.AbsolutePath)
if (-not [string]::IsNullOrWhiteSpace($name)) {
$links += [PSCustomObject]@{ Name = $name; Url = $full.AbsoluteUri }
}
}
$links | Sort-Object Name -Unique
}
function Write-Log {
param([string]$Message)
$ts = (Get-Date).ToString('HH:mm:ss')
$LogTextBox.AppendText("[$ts] $Message`r`n")
$LogTextBox.SelectionStart = $LogTextBox.Text.Length
$LogTextBox.ScrollToCaret()
}
function Invoke-GuestScript {
param(
[string]$VMName,
[pscredential]$Credential,
[string]$ScriptUrl,
[bool]$RunGpupdate
)
$fileName = [IO.Path]::GetFileName($ScriptUrl)
Invoke-Command -VMName $VMName -Credential $Credential -ScriptBlock {
param($Url, $FileName, $DoGp)
# Prefer TLS12/13 in guest
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor 0x3000
} catch {
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
}
$dest = Join-Path $env:TEMP $FileName
$out = [PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
Downloaded = $false
DownloadErr = $null
ScriptExit = $null
ScriptErr = $null
GPExit = $null
GPErr = $null
}
try {
Invoke-WebRequest -Uri $Url -OutFile $dest -UseBasicParsing -ErrorAction Stop
$out.Downloaded = Test-Path -LiteralPath $dest
} catch {
$out.DownloadErr = $_.Exception.Message
}
if ($out.Downloaded) {
try {
$p = Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File', $dest) -WindowStyle Hidden -Wait -PassThru
$out.ScriptExit = $p.ExitCode
} catch {
$out.ScriptExit = -1
$out.ScriptErr = $_.Exception.Message
}
}
if ($DoGp) {
try {
$p2 = Start-Process -FilePath 'gpupdate.exe' -ArgumentList '/force' -WindowStyle Hidden -Wait -PassThru
$out.GPExit = $p2.ExitCode
} catch {
$out.GPExit = -1
$out.GPErr = $_.Exception.Message
}
}
$out
} -ArgumentList $ScriptUrl, $fileName, $RunGpupdate -ErrorAction Stop
}
# ---------- GUI ----------
$form = New-Object System.Windows.Forms.Form
$form.Text = 'Hyper-V Guest Script Runner'
$form.StartPosition = 'CenterScreen'
$form.Size = New-Object System.Drawing.Size(820, 640)
$form.MaximizeBox = $true
# Credentials
$grpCred = New-Object System.Windows.Forms.GroupBox
$grpCred.Text = 'Credentials (Guest OS)'
$grpCred.Location = New-Object System.Drawing.Point(12, 12)
$grpCred.Size = New-Object System.Drawing.Size(390, 90)
$lblUser = New-Object System.Windows.Forms.Label
$lblUser.Text = 'Username'
$lblUser.Location = New-Object System.Drawing.Point(12, 28)
$lblUser.AutoSize = $true
$txtUser = New-Object System.Windows.Forms.TextBox
$txtUser.Location = New-Object System.Drawing.Point(100, 24)
$txtUser.Size = New-Object System.Drawing.Size(270, 23)
$txtUser.Text = '.\Administrator'
$lblPass = New-Object System.Windows.Forms.Label
$lblPass.Text = 'Password'
$lblPass.Location = New-Object System.Drawing.Point(12, 58)
$lblPass.AutoSize = $true
$txtPass = New-Object System.Windows.Forms.TextBox
$txtPass.Location = New-Object System.Drawing.Point(100, 54)
$txtPass.Size = New-Object System.Drawing.Size(270, 23)
$txtPass.UseSystemPasswordChar = $true
$grpCred.Controls.AddRange(@($lblUser,$txtUser,$lblPass,$txtPass))
$form.Controls.Add($grpCred)
# Scripts
$grpScripts = New-Object System.Windows.Forms.GroupBox
$grpScripts.Text = 'Available Scripts'
$grpScripts.Location = New-Object System.Drawing.Point(12, 110)
$grpScripts.Size = New-Object System.Drawing.Size(780, 80)
$lblScript = New-Object System.Windows.Forms.Label
$lblScript.Text = 'Script'
$lblScript.Location = New-Object System.Drawing.Point(12, 35)
$lblScript.AutoSize = $true
$cmbScripts = New-Object System.Windows.Forms.ComboBox
$cmbScripts.Location = New-Object System.Drawing.Point(70, 31)
$cmbScripts.Size = New-Object System.Drawing.Size(620, 23)
$cmbScripts.DropDownStyle = 'DropDownList'
$btnRefreshScripts = New-Object System.Windows.Forms.Button
$btnRefreshScripts.Text = 'Refresh'
$btnRefreshScripts.Location = New-Object System.Drawing.Point(700, 30)
$btnRefreshScripts.Size = New-Object System.Drawing.Size(65, 25)
$grpScripts.Controls.AddRange(@($lblScript,$cmbScripts,$btnRefreshScripts))
$form.Controls.Add($grpScripts)
# VMs
$grpVMs = New-Object System.Windows.Forms.GroupBox
$grpVMs.Text = 'Running VMs'
$grpVMs.Location = New-Object System.Drawing.Point(12, 200)
$grpVMs.Size = New-Object System.Drawing.Size(390, 320)
$vmList = New-Object System.Windows.Forms.CheckedListBox
$vmList.Location = New-Object System.Drawing.Point(12, 24)
$vmList.Size = New-Object System.Drawing.Size(360, 244)
$vmList.CheckOnClick = $true
$btnSelAll = New-Object System.Windows.Forms.Button
$btnSelAll.Text = 'Select All'
$btnSelAll.Location = New-Object System.Drawing.Point(12, 276)
$btnSelAll.Size = New-Object System.Drawing.Size(90, 25)
$btnClearSel = New-Object System.Windows.Forms.Button
$btnClearSel.Text = 'Clear'
$btnClearSel.Location = New-Object System.Drawing.Point(108, 276)
$btnClearSel.Size = New-Object System.Drawing.Size(90, 25)
$btnRefreshVMs = New-Object System.Windows.Forms.Button
$btnRefreshVMs.Text = 'Refresh'
$btnRefreshVMs.Location = New-Object System.Drawing.Point(204, 276)
$btnRefreshVMs.Size = New-Object System.Drawing.Size(90, 25)
$grpVMs.Controls.AddRange(@($vmList,$btnSelAll,$btnClearSel,$btnRefreshVMs))
$form.Controls.Add($grpVMs)
# Actions / Options
$grpActions = New-Object System.Windows.Forms.GroupBox
$grpActions.Text = 'Actions'
$grpActions.Location = New-Object System.Drawing.Point(414, 200)
$grpActions.Size = New-Object System.Drawing.Size(378, 100)
$chkGp = New-Object System.Windows.Forms.CheckBox
$chkGp.Text = 'Run gpupdate /force after script'
$chkGp.Location = New-Object System.Drawing.Point(16, 28)
$chkGp.AutoSize = $true
$chkGp.Checked = $false
$btnRun = New-Object System.Windows.Forms.Button
$btnRun.Text = 'Run on Selected VMs'
$btnRun.Location = New-Object System.Drawing.Point(16, 58)
$btnRun.Size = New-Object System.Drawing.Size(340, 28)
$grpActions.Controls.AddRange(@($chkGp,$btnRun))
$form.Controls.Add($grpActions)
# Log
$grpLog = New-Object System.Windows.Forms.GroupBox
$grpLog.Text = 'Log'
$grpLog.Location = New-Object System.Drawing.Point(414, 304)
$grpLog.Size = New-Object System.Drawing.Size(378, 216)
$LogTextBox = New-Object System.Windows.Forms.TextBox
$LogTextBox.Multiline = $true
$LogTextBox.ScrollBars = 'Vertical'
$LogTextBox.Location = New-Object System.Drawing.Point(16, 24)
$LogTextBox.Size = New-Object System.Drawing.Size(346, 176)
$LogTextBox.Font = New-Object System.Drawing.Font('Consolas', 9)
$grpLog.Controls.Add($LogTextBox)
$form.Controls.Add($grpLog)
# Progress bar
$prg = New-Object System.Windows.Forms.ProgressBar
$prg.Location = New-Object System.Drawing.Point(12, 532)
$prg.Size = New-Object System.Drawing.Size(780, 20)
$prg.Minimum = 0
$prg.Step = 1
$form.Controls.Add($prg)
# ---------- Handlers ----------
$populateVMs = {
$vmList.Items.Clear()
$vms = Get-RunningVMs
foreach ($v in $vms) { [void]$vmList.Items.Add($v, $false) }
Write-Log ("Found {0} running VMs." -f $vms.Count)
}
$populateScripts = {
try {
$cmbScripts.Items.Clear()
$scripts = Get-ScriptsFromUrl -Url $BaseUrl
foreach ($s in $scripts) {
# store PSCustomObject directly; display Name
[void]$cmbScripts.Items.Add($s)
}
if ($cmbScripts.Items.Count -gt 0) { $cmbScripts.SelectedIndex = 0 }
Write-Log ("Loaded {0} scripts from {1}" -f $cmbScripts.Items.Count, $BaseUrl)
} catch {
Write-Log "ERROR fetching scripts: $($_.Exception.Message)"
[System.Windows.Forms.MessageBox]::Show("Failed to fetch scripts.`r`n$($_.Exception.Message)", 'Error',
[System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | Out-Null
}
}
$btnRefreshVMs.Add_Click({ & $populateVMs })
$btnRefreshScripts.Add_Click({ & $populateScripts })
$btnSelAll.Add_Click({
for ($i=0; $i -lt $vmList.Items.Count; $i++) { $vmList.SetItemChecked($i, $true) }
})
$btnClearSel.Add_Click({
for ($i=0; $i -lt $vmList.Items.Count; $i++) { $vmList.SetItemChecked($i, $false) }
})
$btnRun.Add_Click({
# Validate inputs
$user = $txtUser.Text.Trim()
$pass = $txtPass.Text
if ([string]::IsNullOrWhiteSpace($user) -or [string]::IsNullOrWhiteSpace($pass)) {
[System.Windows.Forms.MessageBox]::Show('Please enter username and password.', 'Missing credentials',
[System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning) | Out-Null
return
}
$selScript = $cmbScripts.SelectedItem
if (-not $selScript) {
[System.Windows.Forms.MessageBox]::Show('Please select a script.', 'No script selected',
[System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning) | Out-Null
return
}
$selectedVMs = @()
foreach ($item in $vmList.CheckedItems) { $selectedVMs += [string]$item }
if (-not $selectedVMs -or $selectedVMs.Count -eq 0) {
[System.Windows.Forms.MessageBox]::Show('Please select at least one VM.', 'No VMs selected',
[System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Warning) | Out-Null
return
}
# Build credential
try {
$sec = ConvertTo-SecureString $pass -AsPlainText -Force
$cred = [pscredential]::new($user, $sec)
} catch {
[System.Windows.Forms.MessageBox]::Show("Invalid credentials: $($_.Exception.Message)", 'Error',
[System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | Out-Null
return
}
# Disable controls during run
foreach ($ctrl in @($form.Controls + $grpCred.Controls + $grpScripts.Controls + $grpVMs.Controls + $grpActions.Controls)) {
try { $ctrl.Enabled = $false } catch {}
}
$prg.Value = 0
$prg.Maximum = $selectedVMs.Count
Write-Log ("Executing '{0}' on {1} VM(s)..." -f $selScript.Name, $selectedVMs.Count)
foreach ($vm in $selectedVMs) {
try {
Write-Log "[$vm] Starting..."
$res = Invoke-GuestScript -VMName $vm -Credential $cred -ScriptUrl $selScript.Url -RunGpupdate $chkGp.Checked
# Summarize
$dl = if ($res.Downloaded) { 'OK' } else { 'FAIL' }
$sx = if ($null -ne $res.ScriptExit) { $res.ScriptExit } else { 'N/A' }
$gx = if ($null -ne $res.GPExit) { $res.GPExit } else { 'N/A' }
Write-Log ("[$vm] Download={0}; ScriptExit={1}; GPExit={2}" -f $dl, $sx, $gx)
if ($res.DownloadErr) { Write-Log ("[$vm] DownloadError: {0}" -f $res.DownloadErr) }
if ($res.ScriptErr) { Write-Log ("[$vm] ScriptError: {0}" -f $res.ScriptErr) }
if ($res.GPErr) { Write-Log ("[$vm] GPError: {0}" -f $res.GPErr) }
}
catch {
Write-Log ("[$vm] ERROR: {0}" -f $_.Exception.Message)
}
finally {
$prg.PerformStep()
}
}
Write-Log 'Done.'
# Re-enable controls
foreach ($ctrl in @($form.Controls + $grpCred.Controls + $grpScripts.Controls + $grpVMs.Controls + $grpActions.Controls)) {
try { $ctrl.Enabled = $true } catch {}
}
})
# ---------- Init ----------
& $populateVMs
& $populateScripts
[void]$form.ShowDialog()

View File

@@ -0,0 +1,44 @@
# Hyper-V Guest Script Runner (GUI)
![Hyper-V Script Runner](readme.png)
A PowerShell **WinForms GUI** to run `.ps1` scripts inside selected Hyper-V VMs using **PowerShell Direct**.
It fetches scripts from a remote repository and executes them in guest OSes with provided credentials.
## ✨ Features
- List all **running Hyper-V VMs**
- Fetch `.ps1` scripts dynamically from a remote URL
*(default: `https://nas.wuibaille.fr/WS/postype/`)*
- Prompt for **guest credentials**
- Run the selected script in one or multiple VMs
- Option to run **gpupdate /force** after script execution
- Logs output for each VM in a GUI textbox with timestamps
## 📌 Requirements
- Windows 10/11 with **Hyper-V** role enabled
- PowerShell 5.1 or 7+
- PowerShell Direct enabled (run on the Hyper-V host)
- Access to the remote URL hosting `.ps1` scripts
- Run script as **Administrator**
## 🚀 Usage
1. Launch the script on the **Hyper-V host**:
```powershell
powershell -ExecutionPolicy Bypass -File .\HyperV-GuestScriptRunner.ps1
```
- Enter guest **username and password**.
- Refresh and select **scripts** from the remote repository.
- Select one or multiple **running VMs**.
- (Optional) Check **Run gpupdate /force**.
- Click **Run on Selected VMs**.
The **log panel** will display progress and results per VM.
⚠️ Notes
- Requires **network access** to the script repository.
- Guest VMs must allow login with the specified **credentials**.
- If a VM is not accessible via **PowerShell Direct**, execution will fail for that VM.
- Script errors and exit codes are **logged in the GUI**.