382 lines
14 KiB
PowerShell
382 lines
14 KiB
PowerShell
#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()
|