mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-08 13:33:12 +00:00
bc71ed4a12
ExecuteGuestCommand JSON-serialized the args array into the agent/exec 'input-data' field — which is the process's STDIN, not its arguments. So guest commands ran with no argv: cmd.exe started interactively and the JSON blob ['/c','echo',...] arrived at its prompt. PVE's agent/exec 'command' parameter is itself an array (element 0 = the executable, the rest = argv) sent as repeated form keys. The low-level client couldn't express repeated keys (Dictionary<string,string> only), so: - Add PostAsync(string, IEnumerable<KeyValuePair<string,string>>) to IPveHttpClient/PveHttpClient; BuildFormContent now emits one key=value field per pair, so a key may repeat. - ExecuteGuestCommand builds command = [exe] + args as repeated 'command' fields and no longer touches input-data. Tests: form-encoder repeated-key + per-value encoding cases; VmService tests asserting the command array, order, and absence of input-data; an integration regression guard that echoes an arg and checks it round-trips as stdout. Tracked as F091. Closes #68. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
287 lines
11 KiB
PowerShell
287 lines
11 KiB
PowerShell
#Requires -Module Pester
|
|
|
|
BeforeAll {
|
|
. $PSScriptRoot/_IntegrationHelper.ps1
|
|
Connect-TestPve
|
|
|
|
$script:LinuxVmId = $null
|
|
$script:CreatedVmIds = [System.Collections.Generic.List[int]]::new()
|
|
|
|
function script:Skip-IfNoLinuxVm {
|
|
if (Skip-IfNoTarget) { return $true }
|
|
if ($null -eq $script:LinuxVmId) {
|
|
Set-ItResult -Skipped -Because 'Linux VM was not provisioned (PVETEST_PASSWORD may not be set)'
|
|
return $true
|
|
}
|
|
return $false
|
|
}
|
|
}
|
|
|
|
AfterAll {
|
|
if ($null -eq $script:SkipReason) {
|
|
foreach ($vmId in $script:CreatedVmIds) {
|
|
try {
|
|
Stop-PveVm -Node $script:Node -VmId $vmId -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
|
|
Start-Sleep -Seconds 3
|
|
Remove-PveVm -Node $script:Node -VmId $vmId -Force -Purge -Confirm:$false -ErrorAction SilentlyContinue
|
|
}
|
|
catch { <# non-fatal #> }
|
|
}
|
|
}
|
|
Disconnect-TestPve
|
|
}
|
|
|
|
Describe 'Linux VM — Integration' -Tag 'Integration' {
|
|
|
|
Context 'Linux VM — Provisioning' {
|
|
It 'Should upload cloud image to PVE storage (Send-PveFile)' {
|
|
if (Skip-IfNoPassword) { return }
|
|
|
|
$task = Send-PveFile `
|
|
-Node $script:Node -Storage $script:Storage `
|
|
-Path $script:CloudImagePath `
|
|
-ContentType 'import' -Wait
|
|
|
|
$task | Should -Not -BeNullOrEmpty
|
|
$task.IsSuccessful | Should -BeTrue
|
|
}
|
|
|
|
It 'Should create a Linux VM (New-PveVm)' {
|
|
if (Skip-IfNoPassword) { return }
|
|
|
|
$task = New-PveVm `
|
|
-Node $script:Node `
|
|
-Name 'pester-linux-vm' `
|
|
-Memory 512 -Cores 1 -OsType 'l26' -Wait
|
|
|
|
$task | Should -Not -BeNullOrEmpty
|
|
|
|
$vm = Get-PveVm -Node $script:Node -Name 'pester-linux-vm' |
|
|
Select-Object -First 1
|
|
$vm | Should -Not -BeNullOrEmpty
|
|
|
|
$script:LinuxVmId = $vm.VmId
|
|
$script:CreatedVmIds.Add($vm.VmId)
|
|
}
|
|
|
|
It 'Should import cloud image disk (Import-PveVmDisk)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$cloudImageFilename = [System.IO.Path]::GetFileName($script:CloudImagePath)
|
|
$task = Import-PveVmDisk `
|
|
-Node $script:Node -VmId $script:LinuxVmId `
|
|
-Disk 'scsi0' -TargetStorage 'local-lvm' `
|
|
-Source "$($script:Storage):import/$cloudImageFilename" `
|
|
-Wait
|
|
|
|
$task | Should -Not -BeNullOrEmpty
|
|
$task.IsSuccessful | Should -BeTrue
|
|
}
|
|
|
|
It 'Should configure VM hardware (Set-PveVmConfig)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
{ Set-PveVmConfig -Node $script:Node -VmId $script:LinuxVmId `
|
|
-AdditionalConfig @{
|
|
scsihw = 'virtio-scsi-single'
|
|
boot = 'order=scsi0'
|
|
serial0 = 'socket'
|
|
agent = '1'
|
|
net0 = 'virtio,bridge=vmbr0'
|
|
ide2 = 'local-lvm:cloudinit'
|
|
cicustom = "user=$($script:Storage):snippets/test-vm-userdata.yml"
|
|
} -ErrorAction Stop } | Should -Not -Throw
|
|
}
|
|
|
|
It 'Should set cloud-init config (Set-PveCloudInitConfig)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
{ Set-PveCloudInitConfig -Node $script:Node -VmId $script:LinuxVmId `
|
|
-CiUser 'root' `
|
|
-Password (ConvertTo-SecureString $script:Password -AsPlainText -Force) `
|
|
-IpConfig0 'ip=dhcp' `
|
|
-ErrorAction Stop } | Should -Not -Throw
|
|
}
|
|
|
|
It 'Should start the Linux VM (Start-PveVm)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$task = Start-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30
|
|
$task | Should -Not -BeNullOrEmpty
|
|
}
|
|
|
|
It 'Should wait for guest agent (Test-PveVmGuestAgent)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$timeout = 300; $elapsed = 0
|
|
$agentReady = $false
|
|
while ($elapsed -lt $timeout) {
|
|
if (Test-PveVmGuestAgent -Node $script:Node -VmId $script:LinuxVmId) {
|
|
$agentReady = $true
|
|
break
|
|
}
|
|
Start-Sleep -Seconds 10
|
|
$elapsed += 10
|
|
}
|
|
|
|
$agentReady | Should -BeTrue -Because "Guest agent should respond within ${timeout}s"
|
|
}
|
|
}
|
|
|
|
Context 'Guest Agent — Cmdlets' {
|
|
It 'Should ping guest agent (Test-PveVmGuestAgent)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$result = Test-PveVmGuestAgent -Node $script:Node -VmId $script:LinuxVmId
|
|
$result | Should -BeTrue
|
|
}
|
|
|
|
It 'Should discover guest network interfaces (Get-PveVmGuestNetwork)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$interfaces = Get-PveVmGuestNetwork -Node $script:Node -VmId $script:LinuxVmId
|
|
$interfaces | Should -Not -BeNullOrEmpty
|
|
|
|
# Should have at least one interface with an IPv4 address
|
|
$withIp = $interfaces | Where-Object {
|
|
$_.IpAddresses | Where-Object { $_.Type -eq 'ipv4' -and $_.Address -ne '127.0.0.1' }
|
|
}
|
|
$withIp | Should -Not -BeNullOrEmpty
|
|
}
|
|
|
|
It 'Should execute a command in guest (Invoke-PveVmGuestExec)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$result = Invoke-PveVmGuestExec -Node $script:Node -VmId $script:LinuxVmId -Command 'hostname'
|
|
$result | Should -Not -BeNullOrEmpty
|
|
$result.ExitCode | Should -Be 0
|
|
$result.Stdout | Should -Not -BeNullOrEmpty
|
|
}
|
|
|
|
It 'Should pass -Args to the guest as argv (Invoke-PveVmGuestExec)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
# Regression guard for #68: args must reach the process as argv, not STDIN.
|
|
# With the old bug, echo got no arguments and stdout was empty.
|
|
$marker = 'PSPROXMOXVE_ARG_TEST'
|
|
$result = Invoke-PveVmGuestExec -Node $script:Node -VmId $script:LinuxVmId `
|
|
-Command 'echo' -Args @($marker)
|
|
$result.ExitCode | Should -Be 0
|
|
$result.Stdout.Trim() | Should -Be $marker
|
|
}
|
|
}
|
|
|
|
Context 'Guest Agent VM — Lifecycle' {
|
|
It 'Should have a running Linux VM with guest agent' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$vm = Get-PveVm -Node $script:Node |
|
|
Where-Object { $_.VmId -eq $script:LinuxVmId }
|
|
$vm | Should -Not -BeNullOrEmpty
|
|
$vm.Status | Should -Be 'running'
|
|
}
|
|
|
|
It 'Should suspend and resume a running VM (Suspend-PveVm / Resume-PveVm)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
# Suspend — -Wait -Timeout polls qmpstatus until 'paused'
|
|
$suspendTask = Suspend-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 -Confirm:$false
|
|
$suspendTask | Should -Not -BeNullOrEmpty
|
|
|
|
# Verify with -Detailed (fetches qmpstatus from status/current endpoint)
|
|
$vm = Get-PveVm -Node $script:Node -VmId $script:LinuxVmId -Detailed
|
|
$vm.EffectiveStatus | Should -Be 'paused'
|
|
|
|
# Resume — -Wait -Timeout polls qmpstatus until 'running'
|
|
$resumeTask = Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30
|
|
$resumeTask | Should -Not -BeNullOrEmpty
|
|
|
|
$vm = Get-PveVm -Node $script:Node -VmId $script:LinuxVmId -Detailed
|
|
$vm.EffectiveStatus | Should -Be 'running'
|
|
}
|
|
|
|
It 'Should gracefully restart a VM via ACPI (Restart-PveVm)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
# Ensure VM is running (resume if paused from a failed suspend test)
|
|
try { Resume-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 10 -ErrorAction SilentlyContinue | Out-Null }
|
|
catch { <# already running #> }
|
|
|
|
$task = Restart-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 60 -Confirm:$false
|
|
$task | Should -Not -BeNullOrEmpty
|
|
|
|
$vm = Get-PveVm -Node $script:Node |
|
|
Where-Object { $_.VmId -eq $script:LinuxVmId }
|
|
$vm.Status | Should -Be 'running'
|
|
}
|
|
|
|
It 'Should gracefully stop a VM via ACPI (Stop-PveVm)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$task = Stop-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 -Confirm:$false
|
|
$task | Should -Not -BeNullOrEmpty
|
|
|
|
$vm = Get-PveVm -Node $script:Node |
|
|
Where-Object { $_.VmId -eq $script:LinuxVmId }
|
|
$vm.Status | Should -Be 'stopped'
|
|
}
|
|
}
|
|
|
|
Context 'Templates — Convert and Clone' {
|
|
It 'Should convert the Linux VM to a template' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
# Ensure stopped first
|
|
$vm = Get-PveVm -Node $script:Node |
|
|
Where-Object { $_.VmId -eq $script:LinuxVmId }
|
|
if ($vm.Status -eq 'running') {
|
|
Stop-PveVm -Node $script:Node -VmId $script:LinuxVmId -Wait -Timeout 30 -Confirm:$false | Out-Null
|
|
}
|
|
|
|
{ New-PveTemplate -Node $script:Node -VmId $script:LinuxVmId -Confirm:$false -ErrorAction Stop } |
|
|
Should -Not -Throw
|
|
|
|
# Allow a moment for the template flag to propagate
|
|
Start-Sleep -Seconds 3
|
|
|
|
# Verify it appears as a template
|
|
$templates = Get-PveTemplate -Node $script:Node
|
|
$templates | Where-Object { $_.VmId -eq $script:LinuxVmId } |
|
|
Should -Not -BeNullOrEmpty
|
|
}
|
|
|
|
It 'Should clone a VM from the template (New-PveVmFromTemplate)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
$cloneId = $script:LinuxVmId + 1000
|
|
|
|
$task = New-PveVmFromTemplate `
|
|
-TemplateNode $script:Node `
|
|
-VmId $script:LinuxVmId `
|
|
-NewVmId $cloneId `
|
|
-NewName 'pester-from-template' `
|
|
-Full `
|
|
-Wait
|
|
|
|
$task | Should -Not -BeNullOrEmpty
|
|
|
|
$cloned = Get-PveVm -Node $script:Node -Name 'pester-from-template' |
|
|
Select-Object -First 1
|
|
$cloned | Should -Not -BeNullOrEmpty
|
|
|
|
# Track for cleanup
|
|
$script:CreatedVmIds.Add($cloned.VmId)
|
|
}
|
|
|
|
It 'Should remove a template (Remove-PveTemplate)' {
|
|
if (Skip-IfNoLinuxVm) { return }
|
|
|
|
{ Remove-PveTemplate -Node $script:Node -VmId $script:LinuxVmId `
|
|
-Confirm:$false -ErrorAction Stop } | Should -Not -Throw
|
|
|
|
$script:CreatedVmIds.Remove($script:LinuxVmId) | Out-Null
|
|
$script:LinuxVmId = $null
|
|
}
|
|
}
|
|
}
|