mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
feat: add Import-PveVmDisk cmdlet for disk image import
Imports disk images (qcow2, raw, vmdk) and OVA archives into VMs
via the PVE API using POST config with import-from syntax.
- VmService.ImportDisk() method using POST /nodes/{node}/qemu/{vmid}/config
- Import-PveVmDisk cmdlet with -Source, -Disk, -TargetStorage, -Format, -Wait
- Pester unit tests
- prepare-test-vm.sh updated to use Import-PveVmDisk instead of SSH qm importdisk
- Set-PveNetwork: add required Type parameter (fixes PVE API 400 error)
- Integration test fix: pass -Type bridge to Set-PveNetwork
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -227,6 +227,7 @@ SDN management requires Proxmox VE 8.0 or later. Connected server is version 7.4
|
|||||||
| `Get-PveVmConfig` | Get VM configuration |
|
| `Get-PveVmConfig` | Get VM configuration |
|
||||||
| `Set-PveVmConfig` | Modify VM configuration |
|
| `Set-PveVmConfig` | Modify VM configuration |
|
||||||
| `Resize-PveVmDisk` | Resize a VM disk |
|
| `Resize-PveVmDisk` | Resize a VM disk |
|
||||||
|
| `Import-PveVmDisk` | Import a disk image (qcow2, raw, vmdk, OVA) into a VM |
|
||||||
|
|
||||||
### Containers
|
### Containers
|
||||||
| Cmdlet | Description |
|
| Cmdlet | Description |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This document tracks which PVE API areas are implemented in PSProxmoxVE and whic
|
|||||||
|
|
||||||
**Last updated:** 2026-03-20
|
**Last updated:** 2026-03-20
|
||||||
**Module version:** 0.1.0-preview
|
**Module version:** 0.1.0-preview
|
||||||
**Total cmdlets:** 74
|
**Total cmdlets:** 75
|
||||||
|
|
||||||
## Implemented
|
## Implemented
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ This document tracks which PVE API areas are implemented in PSProxmoxVE and whic
|
|||||||
|------|---------|---------------|
|
|------|---------|---------------|
|
||||||
| **Connection** | 3 | `POST /access/ticket`, `DELETE /access/ticket` |
|
| **Connection** | 3 | `POST /access/ticket`, `DELETE /access/ticket` |
|
||||||
| **Nodes** | 2 | `GET /nodes`, `GET /nodes/{node}/status` |
|
| **Nodes** | 2 | `GET /nodes`, `GET /nodes/{node}/status` |
|
||||||
| **VMs (QEMU)** | 18 | `/nodes/{node}/qemu/*` (CRUD, lifecycle, clone, migrate, resize, config, guest agent) |
|
| **VMs (QEMU)** | 19 | `/nodes/{node}/qemu/*` (CRUD, lifecycle, clone, migrate, resize, disk import, config, guest agent) |
|
||||||
| **Containers (LXC)** | 14 | `/nodes/{node}/lxc/*` (CRUD, lifecycle, clone, migrate, config, snapshots) |
|
| **Containers (LXC)** | 14 | `/nodes/{node}/lxc/*` (CRUD, lifecycle, clone, migrate, config, snapshots) |
|
||||||
| **Storage** | 6 | `/storage`, `/nodes/{node}/storage/{storage}/*` (CRUD, content, upload, download) |
|
| **Storage** | 6 | `/storage`, `/nodes/{node}/storage/{storage}/*` (CRUD, content, upload, download) |
|
||||||
| **Snapshots** | 4 | `/nodes/{node}/qemu/{vmid}/snapshot/*` (CRUD, rollback) |
|
| **Snapshots** | 4 | `/nodes/{node}/qemu/{vmid}/snapshot/*` (CRUD, rollback) |
|
||||||
|
|||||||
@@ -123,6 +123,61 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Disk import
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Imports a disk image into a VM by setting a disk config key with the import-from syntax.
|
||||||
|
/// Uses POST (not PUT) because the import is an async background operation.
|
||||||
|
/// Returns the task UPID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
|
/// <param name="node">The cluster node name.</param>
|
||||||
|
/// <param name="vmid">The VM ID.</param>
|
||||||
|
/// <param name="disk">The disk key (e.g. "scsi0", "sata0", "virtio0").</param>
|
||||||
|
/// <param name="targetStorage">The target storage for the imported disk (e.g. "local-lvm").</param>
|
||||||
|
/// <param name="importFrom">
|
||||||
|
/// The import source in PVE format. Examples:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>"local:iso/image.img" — import from a file already on storage</item>
|
||||||
|
/// <item>"local:import/myvm.ova/disk.vmdk" — import a disk from within an OVA</item>
|
||||||
|
/// <item>"/var/lib/vz/images/disk.qcow2" — import from an absolute path on the node</item>
|
||||||
|
/// </list>
|
||||||
|
/// </param>
|
||||||
|
/// <param name="format">Optional target format (e.g. "qcow2", "raw"). Defaults to storage default.</param>
|
||||||
|
public PveTask ImportDisk(
|
||||||
|
PveSession session,
|
||||||
|
string node,
|
||||||
|
int vmid,
|
||||||
|
string disk,
|
||||||
|
string targetStorage,
|
||||||
|
string importFrom,
|
||||||
|
string? format = null)
|
||||||
|
{
|
||||||
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
if (string.IsNullOrWhiteSpace(disk)) throw new ArgumentNullException(nameof(disk));
|
||||||
|
if (string.IsNullOrWhiteSpace(targetStorage)) throw new ArgumentNullException(nameof(targetStorage));
|
||||||
|
if (string.IsNullOrWhiteSpace(importFrom)) throw new ArgumentNullException(nameof(importFrom));
|
||||||
|
|
||||||
|
// Build the disk value: "storage:0,import-from=source[,format=fmt]"
|
||||||
|
var diskValue = $"{targetStorage}:0,import-from={importFrom}";
|
||||||
|
if (!string.IsNullOrEmpty(format))
|
||||||
|
diskValue += $",format={format}";
|
||||||
|
|
||||||
|
var formData = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
[disk] = diskValue
|
||||||
|
};
|
||||||
|
|
||||||
|
using var client = new PveHttpClient(session);
|
||||||
|
// POST (not PUT) because import-from triggers a background task
|
||||||
|
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/config", formData)
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
|
return ParseTask(response, node);
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ namespace PSProxmoxVE.Cmdlets.Network
|
|||||||
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The network interface name.")]
|
[Parameter(Mandatory = true, Position = 1, HelpMessage = "The network interface name.")]
|
||||||
public string Iface { get; set; } = string.Empty;
|
public string Iface { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Network interface type (e.g. bridge, bond, eth, vlan, OVSBridge).</summary>
|
||||||
|
[Parameter(Mandatory = true, Position = 2, HelpMessage = "Interface type (bridge, bond, eth, vlan, OVSBridge).")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>IPv4 address for the interface.</summary>
|
/// <summary>IPv4 address for the interface.</summary>
|
||||||
[Parameter(Mandatory = false, HelpMessage = "IPv4 address for the interface.")]
|
[Parameter(Mandatory = false, HelpMessage = "IPv4 address for the interface.")]
|
||||||
public string? Address { get; set; }
|
public string? Address { get; set; }
|
||||||
@@ -75,7 +79,10 @@ namespace PSProxmoxVE.Cmdlets.Network
|
|||||||
using var client = new PveHttpClient(session);
|
using var client = new PveHttpClient(session);
|
||||||
|
|
||||||
WriteVerbose($"Updating network interface '{Iface}' on node '{Node}'...");
|
WriteVerbose($"Updating network interface '{Iface}' on node '{Node}'...");
|
||||||
var data = new Dictionary<string, string>();
|
var data = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["type"] = Type
|
||||||
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(Address)) data["address"] = Address!;
|
if (!string.IsNullOrEmpty(Address)) data["address"] = Address!;
|
||||||
if (!string.IsNullOrEmpty(Netmask)) data["netmask"] = Netmask!;
|
if (!string.IsNullOrEmpty(Netmask)) data["netmask"] = Netmask!;
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using System.Management.Automation;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Models.Vms;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Cmdlets.Vms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <para type="synopsis">Imports a disk image into a Proxmox VE virtual machine.</para>
|
||||||
|
/// <para type="description">
|
||||||
|
/// Imports a disk image file into the specified VM by setting a disk configuration key
|
||||||
|
/// with the PVE import-from syntax. The image can be:
|
||||||
|
///
|
||||||
|
/// - A file already on PVE storage (e.g. a cloud image downloaded via Invoke-PveStorageDownload)
|
||||||
|
/// - A VMDK inside an uploaded OVA archive
|
||||||
|
/// - An absolute path to an image file on the PVE node
|
||||||
|
///
|
||||||
|
/// The import runs as a background task on the PVE node. Use -Wait to block until complete.
|
||||||
|
///
|
||||||
|
/// Examples:
|
||||||
|
/// # Import a cloud image from local ISO storage
|
||||||
|
/// Import-PveVmDisk -Node pve1 -VmId 100 -Disk scsi0 -TargetStorage local-lvm `
|
||||||
|
/// -Source 'local:iso/noble-server-cloudimg-amd64.img' -Wait
|
||||||
|
///
|
||||||
|
/// # Import a VMDK from an OVA (uploaded with content=import)
|
||||||
|
/// Import-PveVmDisk -Node pve1 -VmId 100 -Disk sata0 -TargetStorage local-lvm `
|
||||||
|
/// -Source 'local:import/appliance.ova/appliance-disk1.vmdk' -Wait
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[Cmdlet(VerbsData.Import, "PveVmDisk", SupportsShouldProcess = true)]
|
||||||
|
[OutputType(typeof(PveTask))]
|
||||||
|
public sealed class ImportPveVmDiskCmdlet : PveCmdletBase
|
||||||
|
{
|
||||||
|
/// <summary>The Proxmox VE node name.</summary>
|
||||||
|
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The PVE node name.")]
|
||||||
|
public string Node { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>The VM identifier. Accepts pipeline input from Get-PveVm.</summary>
|
||||||
|
[Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "The VM identifier.")]
|
||||||
|
[ValidateRange(100, 999999999)]
|
||||||
|
public int VmId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The disk bus and index to import to (e.g. "scsi0", "sata0", "virtio0", "ide0").
|
||||||
|
/// </summary>
|
||||||
|
[Parameter(Mandatory = true, Position = 0, HelpMessage = "Target disk slot (e.g. scsi0, sata0, virtio0).")]
|
||||||
|
[ValidatePattern(@"^(scsi|sata|virtio|ide)\d+$")]
|
||||||
|
public string Disk { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>The target storage for the imported disk (e.g. "local-lvm", "ceph-pool").</summary>
|
||||||
|
[Parameter(Mandatory = true, HelpMessage = "Target storage for the imported disk.")]
|
||||||
|
public string TargetStorage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The import source. Accepted formats:
|
||||||
|
/// - Storage reference: "local:iso/image.img", "local:import/vm.ova/disk.vmdk"
|
||||||
|
/// - Absolute path on the node: "/var/lib/vz/images/disk.qcow2"
|
||||||
|
/// </summary>
|
||||||
|
[Parameter(Mandatory = true, HelpMessage = "Import source (e.g. local:iso/image.img or local:import/vm.ova/disk.vmdk).")]
|
||||||
|
public string Source { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Target disk format (e.g. "qcow2", "raw"). If omitted, uses the storage default.</summary>
|
||||||
|
[Parameter(Mandatory = false, HelpMessage = "Target disk format (qcow2, raw, vmdk).")]
|
||||||
|
[ValidateSet("qcow2", "raw", "vmdk")]
|
||||||
|
public string? Format { get; set; }
|
||||||
|
|
||||||
|
/// <summary>When specified, waits for the import task to complete before returning.</summary>
|
||||||
|
[Parameter(Mandatory = false, HelpMessage = "Wait for the import task to complete.")]
|
||||||
|
public SwitchParameter Wait { get; set; }
|
||||||
|
|
||||||
|
protected override void ProcessRecord()
|
||||||
|
{
|
||||||
|
if (!ShouldProcess($"VM {VmId} disk {Disk} on node '{Node}'", $"Import disk from '{Source}' to '{TargetStorage}'"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var session = GetSession();
|
||||||
|
var vmService = new VmService();
|
||||||
|
|
||||||
|
WriteVerbose($"Importing disk to VM {VmId} slot {Disk}: {Source} -> {TargetStorage}...");
|
||||||
|
var task = vmService.ImportDisk(session, Node, VmId, Disk, TargetStorage, Source, Format);
|
||||||
|
|
||||||
|
if (Wait.IsPresent && !string.IsNullOrEmpty(task.Upid))
|
||||||
|
{
|
||||||
|
var taskService = new TaskService();
|
||||||
|
task = taskService.WaitForTask(session, Node, task.Upid, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteObject(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@
|
|||||||
'Get-PveVmConfig',
|
'Get-PveVmConfig',
|
||||||
'Set-PveVmConfig',
|
'Set-PveVmConfig',
|
||||||
'Resize-PveVmDisk',
|
'Resize-PveVmDisk',
|
||||||
|
'Import-PveVmDisk',
|
||||||
|
|
||||||
# QEMU Guest Agent
|
# QEMU Guest Agent
|
||||||
'Test-PveVmGuestAgent',
|
'Test-PveVmGuestAgent',
|
||||||
|
|||||||
@@ -575,6 +575,7 @@ Describe 'Integration Tests' -Tag 'Integration' {
|
|||||||
{ Set-PveNetwork `
|
{ Set-PveNetwork `
|
||||||
-Node $script:Node `
|
-Node $script:Node `
|
||||||
-Iface 'vmbr99' `
|
-Iface 'vmbr99' `
|
||||||
|
-Type 'bridge' `
|
||||||
-Comments 'Created by Pester integration test' `
|
-Comments 'Created by Pester integration test' `
|
||||||
-ErrorAction Stop } | Should -Not -Throw
|
-ErrorAction Stop } | Should -Not -Throw
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#Requires -Module Pester
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Pester 5 tests for Import-PveVmDisk.
|
||||||
|
|
||||||
|
All tests are fully offline — no live Proxmox VE target is required.
|
||||||
|
#>
|
||||||
|
|
||||||
|
BeforeAll {
|
||||||
|
. $PSScriptRoot/../_TestHelper.ps1
|
||||||
|
|
||||||
|
$script:Cmd = Get-Command 'Import-PveVmDisk' -ErrorAction SilentlyContinue
|
||||||
|
$script:Available = $null -ne $script:Cmd
|
||||||
|
|
||||||
|
function Skip-IfMissing {
|
||||||
|
if (-not $script:Available) {
|
||||||
|
Set-ItResult -Skipped -Because "Import-PveVmDisk is not yet implemented in this build"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe 'Import-PveVmDisk — manifest' {
|
||||||
|
BeforeAll {
|
||||||
|
$manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1'
|
||||||
|
$script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Should be declared in CmdletsToExport' {
|
||||||
|
if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return }
|
||||||
|
$script:Manifest.CmdletsToExport | Should -Contain 'Import-PveVmDisk'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe 'Import-PveVmDisk' {
|
||||||
|
|
||||||
|
Context 'Command existence' {
|
||||||
|
It 'Should be available after module import' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$script:Cmd | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Should be a CmdletInfo (binary cmdlet)' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$script:Cmd.CommandType | Should -Be 'Cmdlet'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'ShouldProcess support' {
|
||||||
|
It 'Should support WhatIf' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Required parameters' {
|
||||||
|
It 'Node should be Mandatory' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values |
|
||||||
|
Where-Object { $_.IsMandatory }
|
||||||
|
$isMandatory | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'VmId should be Mandatory' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values |
|
||||||
|
Where-Object { $_.IsMandatory }
|
||||||
|
$isMandatory | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Disk should be Mandatory' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$isMandatory = $script:Cmd.Parameters['Disk'].ParameterSets.Values |
|
||||||
|
Where-Object { $_.IsMandatory }
|
||||||
|
$isMandatory | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'TargetStorage should be Mandatory' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$isMandatory = $script:Cmd.Parameters['TargetStorage'].ParameterSets.Values |
|
||||||
|
Where-Object { $_.IsMandatory }
|
||||||
|
$isMandatory | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Source should be Mandatory' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$isMandatory = $script:Cmd.Parameters['Source'].ParameterSets.Values |
|
||||||
|
Where-Object { $_.IsMandatory }
|
||||||
|
$isMandatory | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Optional parameters' {
|
||||||
|
It 'Should have Format parameter' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$script:Cmd.Parameters.ContainsKey('Format') | Should -BeTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Should have Wait switch parameter' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$script:Cmd.Parameters.ContainsKey('Wait') | Should -BeTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Should have Session parameter' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Disk parameter validation' {
|
||||||
|
It 'Should accept valid disk names' {
|
||||||
|
Skip-IfMissing
|
||||||
|
# ValidatePattern should accept these — test by checking the attribute exists
|
||||||
|
$attrs = $script:Cmd.Parameters['Disk'].Attributes |
|
||||||
|
Where-Object { $_ -is [System.Management.Automation.ValidatePatternAttribute] }
|
||||||
|
$attrs | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Pipeline support' {
|
||||||
|
It 'VmId should accept pipeline input by property name' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$attr = $script:Cmd.Parameters['VmId'].ParameterSets.Values |
|
||||||
|
Where-Object { $_.ValueFromPipelineByPropertyName }
|
||||||
|
$attr | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'Node should accept pipeline input by property name' {
|
||||||
|
Skip-IfMissing
|
||||||
|
$attr = $script:Cmd.Parameters['Node'].ParameterSets.Values |
|
||||||
|
Where-Object { $_.ValueFromPipelineByPropertyName }
|
||||||
|
$attr | Should -Not -BeNullOrEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Context 'Without active session' {
|
||||||
|
It 'Should throw when no session is active' {
|
||||||
|
Skip-IfMissing
|
||||||
|
{ Import-PveVmDisk -Node 'pve1' -VmId 100 -Disk 'scsi0' `
|
||||||
|
-TargetStorage 'local-lvm' -Source 'local:iso/test.img' `
|
||||||
|
-Confirm:$false -ErrorAction Stop } |
|
||||||
|
Should -Throw '*No active Proxmox VE session*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,15 @@
|
|||||||
# Uses PSProxmoxVE cmdlets for all supported operations:
|
# Uses PSProxmoxVE cmdlets for all supported operations:
|
||||||
# - Invoke-PveStorageDownload (cloud image download)
|
# - Invoke-PveStorageDownload (cloud image download)
|
||||||
# - New-PveVm (VM creation)
|
# - New-PveVm (VM creation)
|
||||||
# - Set-PveVmConfig -AdditionalConfig (disk/boot/agent/cloud-init config)
|
# - Import-PveVmDisk (disk import from storage)
|
||||||
|
# - Set-PveVmConfig -AdditionalConfig (boot/agent/cloud-init config)
|
||||||
# - Set-PveCloudInitConfig (user/password/IP)
|
# - Set-PveCloudInitConfig (user/password/IP)
|
||||||
# - Start-PveVm (boot)
|
# - Start-PveVm (boot)
|
||||||
# - Test-PveVmGuestAgent (agent ping)
|
# - Test-PveVmGuestAgent (agent ping)
|
||||||
#
|
#
|
||||||
# SSH/SCP only for operations without API support:
|
# SSH/SCP only for operations without API support:
|
||||||
# - pvesm set (enable snippets content type)
|
# - pvesm set (enable snippets content type)
|
||||||
# - SCP snippet upload (no snippet API)
|
# - SCP snippet upload (no snippet upload API — PVE limitation, not even the web UI supports this)
|
||||||
# - qm importdisk (no disk import API)
|
|
||||||
#
|
#
|
||||||
# Usage: prepare-test-vm.sh <nested-pve-ip> <root-password> <vm-id> <node> <api-token>
|
# Usage: prepare-test-vm.sh <nested-pve-ip> <root-password> <vm-id> <node> <api-token>
|
||||||
#
|
#
|
||||||
@@ -72,9 +72,14 @@ pwsh -NoProfile -Command "
|
|||||||
-Memory 512 -Cores 1 -OsType 'l26' -Wait
|
-Memory 512 -Cores 1 -OsType 'l26' -Wait
|
||||||
"
|
"
|
||||||
|
|
||||||
# ── Step 4: Import disk (SSH — no importdisk API) ────────────────────
|
# ── Step 4: Import disk (module cmdlet) ────────────────────────────
|
||||||
echo "Importing disk image..."
|
echo "Importing disk image via Import-PveVmDisk..."
|
||||||
${SSH_CMD} "qm importdisk ${VMID} /var/lib/vz/template/iso/${CLOUD_IMAGE_FILENAME} local-lvm 2>&1 | tail -1"
|
pwsh -NoProfile -Command "
|
||||||
|
Import-Module PSProxmoxVE; ${CONNECT_CMD}
|
||||||
|
Import-PveVmDisk -Node '${NODE}' -VmId ${VMID} -Disk 'scsi0' \
|
||||||
|
-TargetStorage 'local-lvm' \
|
||||||
|
-Source 'local:iso/${CLOUD_IMAGE_FILENAME}' -Wait
|
||||||
|
"
|
||||||
|
|
||||||
# ── Step 5: Configure VM (module cmdlet — AdditionalConfig) ──────────
|
# ── Step 5: Configure VM (module cmdlet — AdditionalConfig) ──────────
|
||||||
echo "Configuring VM via Set-PveVmConfig -AdditionalConfig..."
|
echo "Configuring VM via Set-PveVmConfig -AdditionalConfig..."
|
||||||
@@ -82,7 +87,6 @@ pwsh -NoProfile -Command "
|
|||||||
Import-Module PSProxmoxVE; ${CONNECT_CMD}
|
Import-Module PSProxmoxVE; ${CONNECT_CMD}
|
||||||
Set-PveVmConfig -Node '${NODE}' -VmId ${VMID} -AdditionalConfig @{
|
Set-PveVmConfig -Node '${NODE}' -VmId ${VMID} -AdditionalConfig @{
|
||||||
scsihw = 'virtio-scsi-single'
|
scsihw = 'virtio-scsi-single'
|
||||||
scsi0 = 'local-lvm:vm-${VMID}-disk-0'
|
|
||||||
boot = 'order=scsi0'
|
boot = 'order=scsi0'
|
||||||
serial0 = 'socket'
|
serial0 = 'socket'
|
||||||
agent = '1'
|
agent = '1'
|
||||||
|
|||||||
Reference in New Issue
Block a user