diff --git a/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageCmdlet.cs b/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageCmdlet.cs
index d4797a9..95a67a2 100644
--- a/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageCmdlet.cs
+++ b/src/PSProxmoxVE/Cmdlets/Storage/NewPveStorageCmdlet.cs
@@ -63,6 +63,14 @@ namespace PSProxmoxVE.Cmdlets.Storage
[Parameter(Mandatory = false, HelpMessage = "Ceph monitor host list.")]
public string? MonHost { get; set; }
+ /// iSCSI target IQN (for "iscsi" type).
+ [Parameter(Mandatory = false, HelpMessage = "iSCSI target IQN (e.g. iqn.2024-01.com.example:storage).")]
+ public string? Target { get; set; }
+
+ /// iSCSI portal address (for "iscsi" type). Defaults to server:3260 if not specified.
+ [Parameter(Mandatory = false, HelpMessage = "iSCSI portal address (host:port).")]
+ public string? Portal { get; set; }
+
/// Whether this storage is shared across cluster nodes.
[Parameter(Mandatory = false, HelpMessage = "Storage is shared across cluster nodes.")]
public SwitchParameter Shared { get; set; }
@@ -75,6 +83,12 @@ namespace PSProxmoxVE.Cmdlets.Storage
[Parameter(Mandatory = false, HelpMessage = "Limit access to these nodes (comma-separated).")]
public string? Nodes { get; set; }
+ private static void AddIfNotEmpty(Dictionary data, string key, string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ data[key] = value!;
+ }
+
protected override void ProcessRecord()
{
if (!ShouldProcess(Storage, "Create PVE Storage"))
@@ -98,16 +112,17 @@ namespace PSProxmoxVE.Cmdlets.Storage
["type"] = Type
};
- if (!string.IsNullOrEmpty(Content)) data["content"] = Content!;
- if (!string.IsNullOrEmpty(Path)) data["path"] = Path!;
- if (!string.IsNullOrEmpty(Server)) data["server"] = Server!;
- if (!string.IsNullOrEmpty(Export)) data["export"] = Export!;
- if (!string.IsNullOrEmpty(VgName)) data["vgname"] = VgName!;
- if (!string.IsNullOrEmpty(ThinPool)) data["thinpool"] = ThinPool!;
- if (!string.IsNullOrEmpty(Pool)) data["pool"] = Pool!;
- if (!string.IsNullOrEmpty(CephPool)) data["pool"] = CephPool!;
- if (!string.IsNullOrEmpty(MonHost)) data["monhost"] = MonHost!;
- if (!string.IsNullOrEmpty(Nodes)) data["nodes"] = Nodes!;
+ AddIfNotEmpty(data, "content", Content);
+ AddIfNotEmpty(data, "path", Path);
+ AddIfNotEmpty(data, "server", Server);
+ AddIfNotEmpty(data, "export", Export);
+ AddIfNotEmpty(data, "vgname", VgName);
+ AddIfNotEmpty(data, "thinpool", ThinPool);
+ AddIfNotEmpty(data, "pool", Pool ?? CephPool);
+ AddIfNotEmpty(data, "monhost", MonHost);
+ AddIfNotEmpty(data, "target", Target);
+ AddIfNotEmpty(data, "portal", Portal);
+ AddIfNotEmpty(data, "nodes", Nodes);
if (Shared.IsPresent) data["shared"] = "1";
if (Disable.IsPresent) data["disable"] = "1";
diff --git a/tests/PSProxmoxVE.Tests/Integration/SharedStorage.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/SharedStorage.Tests.ps1
new file mode 100644
index 0000000..3409a3a
--- /dev/null
+++ b/tests/PSProxmoxVE.Tests/Integration/SharedStorage.Tests.ps1
@@ -0,0 +1,179 @@
+#Requires -Module Pester
+<#
+.SYNOPSIS
+ Pester 5 integration tests for shared storage backends (NFS, iSCSI).
+
+ These tests require the multi-node integration test infrastructure with
+ Docker-based storage services (iSCSI target + NFS server). They are
+ SKIPPED when the storage env vars are not set.
+
+ Required environment variables (in addition to base integration vars):
+ PVETEST_STORAGE_VM_IP - IP of the storage host running iSCSI/NFS
+ PVETEST_ISCSI_IQN - iSCSI target IQN
+ PVETEST_NFS_EXPORT - NFS export path (e.g. 10.0.0.1:/srv/nfs/shared)
+
+ WARNING: These tests CREATE and DESTROY storage definitions on the target
+ PVE node. Never run against a production cluster.
+#>
+
+BeforeAll {
+ . $PSScriptRoot/../_TestHelper.ps1
+
+ # --- Base integration vars ---
+ $baseVars = @('PVETEST_HOST', 'PVETEST_PORT', 'PVETEST_APITOKEN', 'PVETEST_NODE')
+ $script:SkipReason = $null
+
+ foreach ($var in $baseVars) {
+ if (-not [System.Environment]::GetEnvironmentVariable($var)) {
+ $script:SkipReason = "No live Proxmox VE target configured. Set: $($baseVars -join ', ')"
+ break
+ }
+ }
+
+ # --- Shared storage vars ---
+ $script:StorageVmIp = [System.Environment]::GetEnvironmentVariable('PVETEST_STORAGE_VM_IP')
+ $script:IscsiIqn = [System.Environment]::GetEnvironmentVariable('PVETEST_ISCSI_IQN')
+ $script:NfsExport = [System.Environment]::GetEnvironmentVariable('PVETEST_NFS_EXPORT')
+
+ $script:Host_ = [System.Environment]::GetEnvironmentVariable('PVETEST_HOST')
+ $portEnv = [System.Environment]::GetEnvironmentVariable('PVETEST_PORT')
+ $script:Port = [int]$(if ($portEnv) { $portEnv } else { '8006' })
+ $script:ApiToken = [System.Environment]::GetEnvironmentVariable('PVETEST_APITOKEN')
+ $script:Node = [System.Environment]::GetEnvironmentVariable('PVETEST_NODE')
+
+ # Track created storages for cleanup
+ $script:CreatedStorages = [System.Collections.Generic.List[string]]::new()
+
+ function script:Skip-IfNoTarget {
+ if ($script:SkipReason) {
+ Set-ItResult -Skipped -Because $script:SkipReason
+ return $true
+ }
+ return $false
+ }
+
+ function script:Skip-IfNoSharedStorage {
+ if (Skip-IfNoTarget) { return $true }
+ if (-not $script:StorageVmIp -or -not $script:IscsiIqn -or -not $script:NfsExport) {
+ Set-ItResult -Skipped -Because 'Shared storage env vars not set (PVETEST_STORAGE_VM_IP, PVETEST_ISCSI_IQN, PVETEST_NFS_EXPORT)'
+ return $true
+ }
+ return $false
+ }
+}
+
+AfterAll {
+ # Best-effort cleanup of any storages we created
+ foreach ($name in $script:CreatedStorages) {
+ try { Remove-PveStorage -Storage $name -Confirm:$false -ErrorAction Stop }
+ catch { Write-Warning "Cleanup: failed to remove storage '$name': $_" }
+ }
+}
+
+Describe 'Shared Storage — Integration' -Tag 'Integration' {
+
+ # -------------------------------------------------------------------
+ Context 'Connection' {
+ It 'Should connect to PVE node' {
+ if (Skip-IfNoTarget) { return }
+
+ $session = Connect-PveServer `
+ -Server $script:Host_ `
+ -Port $script:Port `
+ -ApiToken $script:ApiToken `
+ -SkipCertificateCheck `
+ -PassThru
+
+ $session | Should -Not -BeNullOrEmpty
+ }
+ }
+
+ # -------------------------------------------------------------------
+ Context 'NFS Storage' {
+ It 'Should create NFS storage' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ # Parse server and export from the full NFS export path (e.g. "10.0.0.1:/srv/nfs/shared")
+ $nfsParts = $script:NfsExport -split ':', 2
+ $nfsServer = $nfsParts[0]
+ $nfsExportPath = $nfsParts[1]
+
+ $result = New-PveStorage -Storage 'pester-nfs' -Type 'nfs' `
+ -Server $nfsServer `
+ -Export $nfsExportPath `
+ -Content 'images,iso,backup' `
+ -Shared `
+ -ErrorAction Stop
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Storage | Should -Be 'pester-nfs'
+ $script:CreatedStorages.Add('pester-nfs')
+ }
+
+ It 'Should list and find the NFS storage' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ $storages = Get-PveStorage -Node $script:Node
+ $nfs = $storages | Where-Object { $_.Storage -eq 'pester-nfs' }
+ $nfs | Should -Not -BeNullOrEmpty
+ $nfs.Type | Should -Be 'nfs'
+ }
+
+ It 'Should get NFS storage status' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ $status = Get-PveStorageStatus -Node $script:Node -Storage 'pester-nfs' -ErrorAction Stop
+ $status | Should -Not -BeNullOrEmpty
+ $status.Active | Should -Be 1
+ }
+
+ It 'Should remove NFS storage' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ { Remove-PveStorage -Storage 'pester-nfs' -Confirm:$false -ErrorAction Stop } |
+ Should -Not -Throw
+ $script:CreatedStorages.Remove('pester-nfs')
+ }
+ }
+
+ # -------------------------------------------------------------------
+ Context 'iSCSI Storage' {
+ It 'Should create iSCSI storage with -Target parameter' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ $result = New-PveStorage -Storage 'pester-iscsi' -Type 'iscsi' `
+ -Server $script:StorageVmIp `
+ -Target $script:IscsiIqn `
+ -Shared `
+ -ErrorAction Stop
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Storage | Should -Be 'pester-iscsi'
+ $script:CreatedStorages.Add('pester-iscsi')
+ }
+
+ It 'Should list and find the iSCSI storage' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ $storages = Get-PveStorage -Node $script:Node
+ $iscsi = $storages | Where-Object { $_.Storage -eq 'pester-iscsi' }
+ $iscsi | Should -Not -BeNullOrEmpty
+ $iscsi.Type | Should -Be 'iscsi'
+ }
+
+ It 'Should get iSCSI storage status' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ $status = Get-PveStorageStatus -Node $script:Node -Storage 'pester-iscsi' -ErrorAction Stop
+ $status | Should -Not -BeNullOrEmpty
+ }
+
+ It 'Should remove iSCSI storage' {
+ if (Skip-IfNoSharedStorage) { return }
+
+ { Remove-PveStorage -Storage 'pester-iscsi' -Confirm:$false -ErrorAction Stop } |
+ Should -Not -Throw
+ $script:CreatedStorages.Remove('pester-iscsi')
+ }
+ }
+}
diff --git a/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1 b/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1
index 8e40591..e66f721 100644
--- a/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1
+++ b/tests/PSProxmoxVE.Tests/Storage/Get-PveStorage.Tests.ps1
@@ -171,6 +171,48 @@ Describe 'New-PveStorage' {
$isMandatory | Should -Not -BeNullOrEmpty
}
}
+
+ Context 'iSCSI/NFS parameters' {
+ It 'Should have Target parameter for iSCSI IQN' {
+ Skip-IfMissing 'New-PveStorage'
+ $script:Cmd.Parameters.ContainsKey('Target') | Should -BeTrue
+ }
+
+ It 'Target should not be Mandatory' {
+ Skip-IfMissing 'New-PveStorage'
+ $isMandatory = $script:Cmd.Parameters['Target'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -BeNullOrEmpty
+ }
+
+ It 'Should have Portal parameter for iSCSI portal' {
+ Skip-IfMissing 'New-PveStorage'
+ $script:Cmd.Parameters.ContainsKey('Portal') | Should -BeTrue
+ }
+
+ It 'Portal should not be Mandatory' {
+ Skip-IfMissing 'New-PveStorage'
+ $isMandatory = $script:Cmd.Parameters['Portal'].ParameterSets.Values |
+ Where-Object { $_.IsMandatory }
+ $isMandatory | Should -BeNullOrEmpty
+ }
+
+ It 'Should have Server parameter for NFS/iSCSI server' {
+ Skip-IfMissing 'New-PveStorage'
+ $script:Cmd.Parameters.ContainsKey('Server') | Should -BeTrue
+ }
+
+ It 'Should have Export parameter for NFS export path' {
+ Skip-IfMissing 'New-PveStorage'
+ $script:Cmd.Parameters.ContainsKey('Export') | Should -BeTrue
+ }
+
+ It 'Should have Shared switch parameter' {
+ Skip-IfMissing 'New-PveStorage'
+ $script:Cmd.Parameters.ContainsKey('Shared') | Should -BeTrue
+ $script:Cmd.Parameters['Shared'].ParameterType | Should -Be ([System.Management.Automation.SwitchParameter])
+ }
+ }
}
# ---------------------------------------------------------------------------