mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-11 06:46:53 +00:00
fix: remediate findings F045, F047, F048, F064, F070, F071, F076-F079
Phase 1 — Trivial fixes: - F071: Add Uri.EscapeDataString to GetPveTemplateCmdlet node path - F077: Add ValidateRange(100, 999999999) to GetPveTaskListCmdlet.VmId - F076: Create .github/dependabot.yml (nuget + github-actions, weekly) - F079: Fix unit-tests.yml dotnet SDK from 9.0.x to 10.0.x - F048: Mark wont_fix — sync-over-async accepted for PS 5.1 compat Phase 2 — Framework targeting (D009 compliance): - F047: Reduce publishable csproj to netstandard2.0 only, remove all #if NET48/NETSTANDARD2_0 conditionals from PveHttpClient.cs, restructure build.yml for netstandard2.0 publish + net10.0/net48 tests - F064: Resolved by F047 — SMA 7.5.0 ItemGroup removed with net10.0 TFM - F070: Add PS 5.1 smoke-test job to publish.yml (windows-latest) Phase 3 — IPveHttpClient interface extraction (F045): - Extract IPveHttpClient interface from PveHttpClient - Add constructor injection to all 14 service classes - Services use injected client when available, create+dispose when not Phase 4 — Service unit tests (F078): - 196 new xUnit tests across 10 service test files - All services tested via Moq-mocked IPveHttpClient - Total test count: 382 (was 186) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "nuget"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "main"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
target-branch: "main"
|
||||||
|
open-pull-requests-limit: 5
|
||||||
+19
-33
@@ -7,53 +7,39 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-net48:
|
build-and-test:
|
||||||
runs-on: windows-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
- name: Setup .NET
|
|
||||||
uses: actions/setup-dotnet@v5
|
|
||||||
with:
|
|
||||||
dotnet-version: '10.0.x'
|
|
||||||
- name: Restore dependencies
|
|
||||||
run: |
|
|
||||||
dotnet restore src/PSProxmoxVE/PSProxmoxVE.csproj
|
|
||||||
dotnet restore tests/PSProxmoxVE.Core.Tests/PSProxmoxVE.Core.Tests.csproj
|
|
||||||
- name: Build net48
|
|
||||||
run: |
|
|
||||||
dotnet build src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework net48 --no-restore
|
|
||||||
dotnet build tests/PSProxmoxVE.Core.Tests/PSProxmoxVE.Core.Tests.csproj --configuration Release --framework net48 --no-restore
|
|
||||||
- name: Test net48
|
|
||||||
run: dotnet test tests/PSProxmoxVE.Core.Tests/PSProxmoxVE.Core.Tests.csproj --configuration Release --framework net48 --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
|
|
||||||
- name: Upload coverage
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v7
|
|
||||||
with:
|
|
||||||
name: coverage-net48
|
|
||||||
path: ./coverage
|
|
||||||
|
|
||||||
build-net10:
|
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [windows-latest, ubuntu-latest]
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
test-framework: net48
|
||||||
|
- os: windows-latest
|
||||||
|
test-framework: net10.0
|
||||||
|
- os: ubuntu-latest
|
||||||
|
test-framework: net10.0
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '10.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build net10.0
|
|
||||||
run: dotnet build --configuration Release --framework net10.0 --no-restore
|
- name: Build solution
|
||||||
- name: Test net10.0
|
run: dotnet build --configuration Release --no-restore
|
||||||
run: dotnet test tests/PSProxmoxVE.Core.Tests/PSProxmoxVE.Core.Tests.csproj --configuration Release --framework net10.0 --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
|
|
||||||
|
- name: Run xUnit tests
|
||||||
|
run: dotnet test tests/PSProxmoxVE.Core.Tests/PSProxmoxVE.Core.Tests.csproj --configuration Release --framework ${{ matrix.test-framework }} --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: coverage-net10-${{ matrix.os }}
|
name: coverage-${{ matrix.test-framework }}-${{ matrix.os }}
|
||||||
path: ./coverage
|
path: ./coverage
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -21,16 +21,61 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
dotnet-version: '10.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Extract version from tag
|
|
||||||
id: version
|
|
||||||
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Build module
|
- name: Build module
|
||||||
run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework netstandard2.0 --output ./publish/netstandard2.0
|
run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --output ./publish/netstandard2.0
|
||||||
|
|
||||||
- name: Clean publish output
|
- name: Clean publish output
|
||||||
run: rm -f ./publish/netstandard2.0/*.deps.json ./publish/netstandard2.0/*.runtimeconfig.json
|
run: rm -f ./publish/netstandard2.0/*.deps.json ./publish/netstandard2.0/*.runtimeconfig.json
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: module-netstandard2.0
|
||||||
|
path: ./publish/netstandard2.0/
|
||||||
|
|
||||||
|
smoke-test-ps51:
|
||||||
|
needs: build
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download module artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: module-netstandard2.0
|
||||||
|
path: ./publish/netstandard2.0/
|
||||||
|
|
||||||
|
- name: Import module on Windows PowerShell 5.1
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$modulePath = "$env:USERPROFILE\Documents\WindowsPowerShell\Modules\PSProxmoxVE"
|
||||||
|
New-Item -ItemType Directory -Path $modulePath -Force | Out-Null
|
||||||
|
Copy-Item -Path .\publish\netstandard2.0\* -Destination $modulePath -Recurse -Force
|
||||||
|
Import-Module PSProxmoxVE -Force -ErrorAction Stop
|
||||||
|
$commands = Get-Command -Module PSProxmoxVE
|
||||||
|
Write-Host "PS 5.1 smoke test: module loaded with $($commands.Count) commands"
|
||||||
|
if ($commands.Count -lt 150) {
|
||||||
|
throw "Expected at least 150 commands, got $($commands.Count)"
|
||||||
|
}
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [build, smoke-test-ps51]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Download module artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: module-netstandard2.0
|
||||||
|
path: ./publish/netstandard2.0/
|
||||||
|
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Update module version in manifest
|
- name: Update module version in manifest
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
@@ -42,11 +87,7 @@ jobs:
|
|||||||
$content = $content -replace "ModuleVersion\s*=\s*'[^']*'", "ModuleVersion = '$moduleVersion'"
|
$content = $content -replace "ModuleVersion\s*=\s*'[^']*'", "ModuleVersion = '$moduleVersion'"
|
||||||
Set-Content $manifestPath $content
|
Set-Content $manifestPath $content
|
||||||
|
|
||||||
- name: Install PowerShell and Pester
|
- name: Test module loads on PS 7.x
|
||||||
shell: pwsh
|
|
||||||
run: Install-Module -Name Pester -MinimumVersion 5.0 -Force -Scope CurrentUser
|
|
||||||
|
|
||||||
- name: Test module loads
|
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$modulePath = "$HOME/.local/share/powershell/Modules/PSProxmoxVE"
|
$modulePath = "$HOME/.local/share/powershell/Modules/PSProxmoxVE"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v5
|
uses: actions/setup-dotnet@v5
|
||||||
with:
|
with:
|
||||||
dotnet-version: '9.0.x'
|
dotnet-version: '10.0.x'
|
||||||
|
|
||||||
- name: Build module
|
- name: Build module
|
||||||
run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework ${{ matrix.framework }} --output ./publish/${{ matrix.framework }}
|
run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework ${{ matrix.framework }} --output ./publish/${{ matrix.framework }}
|
||||||
|
|||||||
+400
-57
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"_schema_version": "1.0",
|
"_schema_version": "1.0",
|
||||||
"_description": "PSProxmoxVE stable findings ledger. IDs are permanent (F001, F002...). Resolved findings are never deleted \u2014 they are marked resolved with evidence. If a finding reappears, it is marked regressed and retains its original ID.",
|
"_description": "PSProxmoxVE stable findings ledger. IDs are permanent (F001, F002...). Resolved findings are never deleted — they are marked resolved with evidence. If a finding reappears, it is marked regressed and retains its original ID.",
|
||||||
"last_updated": "2026-03-22T20:00:00Z",
|
"last_updated": "2026-03-23",
|
||||||
"last_scan_date": "2026-03-22",
|
"last_scan_date": "2026-03-23",
|
||||||
"counters": {
|
"counters": {
|
||||||
"next_id": 77,
|
"next_id": 80,
|
||||||
"total_open": 27,
|
"total_open": 21,
|
||||||
"total_resolved": 49,
|
"total_resolved": 58,
|
||||||
"total_regressed": 0
|
"total_regressed": 0,
|
||||||
|
"open": 11,
|
||||||
|
"resolved": 67,
|
||||||
|
"wont_fix": 1
|
||||||
},
|
},
|
||||||
"findings": [
|
"findings": [
|
||||||
{
|
{
|
||||||
@@ -626,6 +629,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "L1",
|
"local_id": "L1",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -932,6 +940,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "S1/S2",
|
"local_id": "S1/S2",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1342,7 +1355,7 @@
|
|||||||
"title": "HttpClient per-call pattern bypasses connection pooling",
|
"title": "HttpClient per-call pattern bypasses connection pooling",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-21",
|
"first_detected": "2026-03-21",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE/Cmdlets/"
|
"src/PSProxmoxVE/Cmdlets/"
|
||||||
@@ -1358,8 +1371,24 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ11/M1",
|
"local_id": "CQ11/M1",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "IPveHttpClient interface extracted to Client/IPveHttpClient.cs. All 14 services now accept IPveHttpClient via constructor injection, enabling shared client instances. PveHttpClient implements IPveHttpClient."
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F046",
|
"id": "F046",
|
||||||
@@ -1382,21 +1411,26 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "M2",
|
"local_id": "M2",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F047",
|
"id": "F047",
|
||||||
"title": "net9.0 target framework is EOL",
|
"title": "Publishable projects multi-target beyond netstandard2.0",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-21",
|
"first_detected": "2026-03-21",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE/PSProxmoxVE.csproj",
|
"src/PSProxmoxVE/PSProxmoxVE.csproj",
|
||||||
"src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj"
|
"src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj"
|
||||||
],
|
],
|
||||||
"description": ".NET 9.0 reached end of life in May 2025. Should upgrade to net10.0 (LTS).",
|
"description": "Both publishable projects (PSProxmoxVE.csproj, PSProxmoxVE.Core.csproj) target netstandard2.0;net10.0;net48 but D009 requires publishable projects to target only netstandard2.0. Multi-targeting inflates the published module and may ship framework-specific assemblies unnecessarily.",
|
||||||
"scan_history": [
|
"scan_history": [
|
||||||
{
|
{
|
||||||
"scan_date": "2026-03-21",
|
"scan_date": "2026-03-21",
|
||||||
@@ -1407,21 +1441,38 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ6/H3",
|
"local_id": "CQ6/H3",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D010"
|
"decisions_ref": "D010",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "PSProxmoxVE.csproj and PSProxmoxVE.Core.csproj now target <TargetFramework>netstandard2.0</TargetFramework> only. All #if NET48/NETSTANDARD2_0 conditionals removed from PveHttpClient.cs. build.yml restructured. Test project retains net10.0+net48."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F048",
|
"id": "F048",
|
||||||
"title": "Sync-over-async via GetAwaiter().GetResult()",
|
"title": "Sync-over-async via GetAwaiter().GetResult()",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "wont_fix",
|
||||||
"first_detected": "2026-03-21",
|
"first_detected": "2026-03-21",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE.Core/"
|
"src/PSProxmoxVE.Core/"
|
||||||
],
|
],
|
||||||
"description": "~216 call sites use .GetAwaiter().GetResult(). Standard for PS binary modules but carries theoretical deadlock risk. Accepted tradeoff for PS 5.1 compatibility.",
|
"description": "~216 call sites use .GetAwaiter().GetResult(). Standard for PS binary modules but carries theoretical deadlock risk. Accepted tradeoff for PS 5.1 compatibility.",
|
||||||
|
"notes": "Accepted tradeoff: PowerShell binary modules targeting netstandard2.0 for PS 5.1 Desktop compatibility have no async pipeline. .GetAwaiter().GetResult() is the standard pattern used by all major PS binary modules. No deadlock risk in practice because PveHttpClient uses ConfigureAwait(false).",
|
||||||
"scan_history": [
|
"scan_history": [
|
||||||
{
|
{
|
||||||
"scan_date": "2026-03-21",
|
"scan_date": "2026-03-21",
|
||||||
@@ -1432,6 +1483,16 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ7",
|
"local_id": "CQ7",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "wont_fix"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1574,6 +1635,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "H1",
|
"local_id": "H1",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1596,6 +1662,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "H2",
|
"local_id": "H2",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1691,7 +1762,7 @@
|
|||||||
"title": "Infinite loop task-polling in container snapshot and storage cmdlets",
|
"title": "Infinite loop task-polling in container snapshot and storage cmdlets",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "critical",
|
"severity": "critical",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE/Cmdlets/Containers/NewPveContainerSnapshotCmdlet.cs",
|
"src/PSProxmoxVE/Cmdlets/Containers/NewPveContainerSnapshotCmdlet.cs",
|
||||||
@@ -1711,9 +1782,20 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "C1",
|
"local_id": "C1",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D001"
|
"decisions_ref": "D001",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "All 5 cmdlets now use TaskService.WaitForTask (NewPveContainerSnapshotCmdlet.cs:67, RemovePveContainerSnapshotCmdlet.cs:59, RestorePveContainerSnapshotCmdlet.cs:62, InvokePveStorageDownloadCmdlet.cs:74, SendPveFileCmdlet.cs:134). No while(true) task-polling loops remain in any of these files."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F059",
|
"id": "F059",
|
||||||
@@ -1729,6 +1811,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "M3",
|
"local_id": "M3",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1746,6 +1833,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "M4",
|
"local_id": "M4",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1763,6 +1855,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "M5",
|
"local_id": "M5",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1771,7 +1868,7 @@
|
|||||||
"title": "RestartPveContainerCmdlet missing ConfirmImpact.High",
|
"title": "RestartPveContainerCmdlet missing ConfirmImpact.High",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE/Cmdlets/Containers/RestartPveContainerCmdlet.cs"
|
"src/PSProxmoxVE/Cmdlets/Containers/RestartPveContainerCmdlet.cs"
|
||||||
@@ -1782,16 +1879,27 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ9",
|
"local_id": "CQ9",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D007"
|
"decisions_ref": "D007",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "RestartPveContainerCmdlet.cs:15 now has ConfirmImpact = ConfirmImpact.High, matching RestartPveVmCmdlet."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F063",
|
"id": "F063",
|
||||||
"title": "SuspendPveContainerCmdlet missing ConfirmImpact.High",
|
"title": "SuspendPveContainerCmdlet missing ConfirmImpact.High",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE/Cmdlets/Containers/SuspendPveContainerCmdlet.cs"
|
"src/PSProxmoxVE/Cmdlets/Containers/SuspendPveContainerCmdlet.cs"
|
||||||
@@ -1802,16 +1910,27 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ10",
|
"local_id": "CQ10",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D007"
|
"decisions_ref": "D007",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "SuspendPveContainerCmdlet.cs:14 now has ConfirmImpact = ConfirmImpact.High, matching SuspendPveVmCmdlet."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F064",
|
"id": "F064",
|
||||||
"title": "System.Management.Automation pinned to 7.4.0",
|
"title": "System.Management.Automation pinned to 7.4.0",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "low",
|
"severity": "low",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE/PSProxmoxVE.csproj"
|
"src/PSProxmoxVE/PSProxmoxVE.csproj"
|
||||||
@@ -1822,16 +1941,32 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ12/L2",
|
"local_id": "CQ12/L2",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D010"
|
"decisions_ref": "D010",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "net10.0 ItemGroup with System.Management.Automation 7.5.0 removed from PSProxmoxVE.csproj as part of F047 netstandard2.0-only migration. Publishable project now uses PowerShellStandard.Library 5.1.1 unconditionally."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F065",
|
"id": "F065",
|
||||||
"title": "No config.yml for issue templates",
|
"title": "No config.yml for issue templates",
|
||||||
"category": "community",
|
"category": "community",
|
||||||
"severity": "low",
|
"severity": "low",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
".github/ISSUE_TEMPLATE/"
|
".github/ISSUE_TEMPLATE/"
|
||||||
@@ -1842,15 +1977,26 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "L3",
|
"local_id": "L3",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": ".github/ISSUE_TEMPLATE/config.yml exists with blank_issues_enabled: false and contact_links to Discussions."
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F066",
|
"id": "F066",
|
||||||
"title": "No CODEOWNERS file",
|
"title": "No CODEOWNERS file",
|
||||||
"category": "community",
|
"category": "community",
|
||||||
"severity": "low",
|
"severity": "low",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"CODEOWNERS"
|
"CODEOWNERS"
|
||||||
@@ -1861,8 +2007,19 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "L4",
|
"local_id": "L4",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "CODEOWNERS file exists (15 bytes) in repo root."
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F067",
|
"id": "F067",
|
||||||
@@ -1878,6 +2035,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "L5",
|
"local_id": "L5",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1895,6 +2057,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "L6",
|
"local_id": "L6",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1912,6 +2079,11 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "L7",
|
"local_id": "L7",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1920,7 +2092,7 @@
|
|||||||
"title": "Verify netstandard2.0 loads on Windows PowerShell 5.1",
|
"title": "Verify netstandard2.0 loads on Windows PowerShell 5.1",
|
||||||
"category": "psgallery",
|
"category": "psgallery",
|
||||||
"severity": "low",
|
"severity": "low",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
".github/workflows/publish.yml"
|
".github/workflows/publish.yml"
|
||||||
@@ -1936,49 +2108,68 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "L8",
|
"local_id": "L8",
|
||||||
"status": "open"
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "publish.yml now includes smoke-test-ps51 job on windows-latest that imports the netstandard2.0 module in Windows PowerShell 5.1 (shell: powershell) and verifies command count >= 150. Publish job depends on smoke test passing."
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F071",
|
"id": "F071",
|
||||||
"title": "Missing Uri.EscapeDataString in cmdlet URL constructions",
|
"title": "Missing Uri.EscapeDataString in cmdlet URL constructions",
|
||||||
"category": "security",
|
"category": "security",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE/Cmdlets/Containers/RestorePveContainerSnapshotCmdlet.cs",
|
"src/PSProxmoxVE/Cmdlets/Templates/GetPveTemplateCmdlet.cs"
|
||||||
"src/PSProxmoxVE/Cmdlets/Containers/NewPveContainerSnapshotCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Containers/RemovePveContainerSnapshotCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Snapshots/NewPveSnapshotCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Snapshots/RestorePveSnapshotCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Snapshots/RemovePveSnapshotCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Storage/SendPveFileCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Storage/InvokePveStorageDownloadCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/CloudInit/GetPveCloudInitConfigCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Network/GetPveNetworkCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Network/SetPveNetworkCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Network/InvokePveNetworkApplyCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Network/NewPveNetworkCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/Network/RemovePveNetworkCmdlet.cs",
|
|
||||||
"src/PSProxmoxVE/Cmdlets/PveCmdletBase.cs"
|
|
||||||
],
|
],
|
||||||
"description": "~15 cmdlets that bypass services and construct API URLs inline do not use Uri.EscapeDataString() on Node, Name, Storage, Iface path segments. Violates D003. Service layer is consistent but these cmdlets are not.",
|
"description": "GetPveTemplateCmdlet.cs:52 uses nodes/{node}/qemu without Uri.EscapeDataString on the node variable. All other previously cited cmdlets (~14) have been fixed. 1 instance remains.",
|
||||||
"scan_history": [
|
"scan_history": [
|
||||||
{
|
{
|
||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ-NEW1",
|
"local_id": "CQ-NEW1",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D003"
|
"decisions_ref": "D003",
|
||||||
|
"notes": "Reduced from ~15 files to 1 in scan-5. All network, snapshot, container snapshot, storage, and CloudInit cmdlets now use EscapeDataString.",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "GetPveTemplateCmdlet.cs:53 now uses Uri.EscapeDataString(node) in the API path. Added using System; directive."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F072",
|
"id": "F072",
|
||||||
"title": "Unnecessary System.Text.Json dependency in Core.csproj",
|
"title": "Unnecessary System.Text.Json dependency in Core.csproj",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "low",
|
"severity": "low",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj"
|
"src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj"
|
||||||
@@ -1989,16 +2180,27 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CQ-NEW2",
|
"local_id": "CQ-NEW2",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D008"
|
"decisions_ref": "D008",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "System.Text.Json no longer referenced in PSProxmoxVE.Core.csproj. grep -c System.Text.Json returns 0."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F073",
|
"id": "F073",
|
||||||
"title": "build.yml references net9.0 but test project targets net10.0",
|
"title": "build.yml references net9.0 but test project targets net10.0",
|
||||||
"category": "code_quality",
|
"category": "code_quality",
|
||||||
"severity": "high",
|
"severity": "high",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
".github/workflows/build.yml",
|
".github/workflows/build.yml",
|
||||||
@@ -2010,16 +2212,27 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "PG-NEW1",
|
"local_id": "PG-NEW1",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"decisions_ref": "D009"
|
"decisions_ref": "D009",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "build.yml now references --framework net10.0 at lines 51-53. No net9.0 references remain in build.yml."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F074",
|
"id": "F074",
|
||||||
"title": "Publish workflow smoke test threshold too low",
|
"title": "Publish workflow smoke test threshold too low",
|
||||||
"category": "psgallery",
|
"category": "psgallery",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
".github/workflows/publish.yml"
|
".github/workflows/publish.yml"
|
||||||
@@ -2030,15 +2243,26 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "PG-NEW2",
|
"local_id": "PG-NEW2",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "publish.yml:58 checks commands.Count -lt 150 (was 60). With 169 exported cmdlets, threshold of 150 catches regressions while allowing minor variance."
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F075",
|
"id": "F075",
|
||||||
"title": "~88 cmdlets lack markdown help documentation",
|
"title": "~88 cmdlets lack markdown help documentation",
|
||||||
"category": "psgallery",
|
"category": "psgallery",
|
||||||
"severity": "medium",
|
"severity": "medium",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
"docs/cmdlets/"
|
"docs/cmdlets/"
|
||||||
@@ -2049,15 +2273,26 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "PG-NEW3",
|
"local_id": "PG-NEW3",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "fixed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "scan-5",
|
||||||
|
"verified_by": "scan",
|
||||||
|
"evidence": "170 markdown help docs in docs/cmdlets/ for 169 exported cmdlets. All cmdlets now have documentation."
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "F076",
|
"id": "F076",
|
||||||
"title": "No dependabot or renovate for dependency updates",
|
"title": "No dependabot or renovate for dependency updates",
|
||||||
"category": "community",
|
"category": "community",
|
||||||
"severity": "low",
|
"severity": "low",
|
||||||
"status": "open",
|
"status": "resolved",
|
||||||
"first_detected": "2026-03-22",
|
"first_detected": "2026-03-22",
|
||||||
"files": [
|
"files": [
|
||||||
".github/"
|
".github/"
|
||||||
@@ -2068,8 +2303,116 @@
|
|||||||
"scan_date": "2026-03-22",
|
"scan_date": "2026-03-22",
|
||||||
"local_id": "CM-NEW1",
|
"local_id": "CM-NEW1",
|
||||||
"status": "new"
|
"status": "new"
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "open"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": ".github/dependabot.yml created with nuget (weekly) and github-actions (weekly) ecosystems targeting main branch."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "F077",
|
||||||
|
"title": "GetPveTaskListCmdlet VmId parameter missing ValidateRange",
|
||||||
|
"category": "code_quality",
|
||||||
|
"severity": "low",
|
||||||
|
"status": "resolved",
|
||||||
|
"first_detected": "2026-03-23",
|
||||||
|
"files": [
|
||||||
|
"src/PSProxmoxVE/Cmdlets/Tasks/GetPveTaskListCmdlet.cs"
|
||||||
|
],
|
||||||
|
"description": "Optional VmId parameter (int?, line 29) lacks [ValidateRange(100, 999999999)] attribute. Per D010, all VmId parameters must have ValidateRange regardless of whether mandatory or optional.",
|
||||||
|
"scan_history": [
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"decisions_ref": "D010",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "GetPveTaskListCmdlet.cs:29 now has [ValidateRange(100, 999999999)] on the VmId parameter, matching D010 convention."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "F078",
|
||||||
|
"title": "No service-layer or PveHttpClient unit tests",
|
||||||
|
"category": "testing",
|
||||||
|
"severity": "medium",
|
||||||
|
"status": "resolved",
|
||||||
|
"first_detected": "2026-03-23",
|
||||||
|
"files": [
|
||||||
|
"tests/PSProxmoxVE.Core.Tests/"
|
||||||
|
],
|
||||||
|
"description": "Zero xUnit tests for any of the 14 service classes (VmService, ContainerService, StorageService, etc.) or PveHttpClient. Model deserialization tests exist but no tests for API call construction, error handling, or business logic in services.",
|
||||||
|
"scan_history": [
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "196 new service unit tests across 10 test files in tests/PSProxmoxVE.Core.Tests/Services/. All 14 services now have IPveHttpClient constructor injection enabling Moq-based testing. Total test count: 382 (was 186)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "F079",
|
||||||
|
"title": "unit-tests.yml uses dotnet SDK 9.0.x, inconsistent with build.yml 10.0.x",
|
||||||
|
"category": "code_quality",
|
||||||
|
"severity": "low",
|
||||||
|
"status": "resolved",
|
||||||
|
"first_detected": "2026-03-23",
|
||||||
|
"files": [
|
||||||
|
".github/workflows/unit-tests.yml"
|
||||||
|
],
|
||||||
|
"description": "unit-tests.yml:31 installs dotnet-version 9.0.x while build.yml uses 10.0.x. The unit-tests workflow only builds netstandard2.0 and net48 so this works functionally, but the SDK versions should be consistent across workflows.",
|
||||||
|
"scan_history": [
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "new"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"local_id": null,
|
||||||
|
"status": "resolved"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"decisions_ref": "D009",
|
||||||
|
"resolution": {
|
||||||
|
"scan_date": "2026-03-23",
|
||||||
|
"report_id": "remediation-1",
|
||||||
|
"verified_by": "code_change",
|
||||||
|
"evidence": "unit-tests.yml line 31 changed from dotnet-version 9.0.x to 10.0.x, matching build.yml and the test project net10.0 TFM."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Client
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstraction over the PVE HTTP client for testability and dependency injection.
|
||||||
|
/// Services accept this interface via constructor injection; tests can mock it.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPveHttpClient : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>Performs a GET request against the specified API resource path.</summary>
|
||||||
|
Task<string> GetAsync(string resource);
|
||||||
|
|
||||||
|
/// <summary>Performs a POST request against the specified API resource path.</summary>
|
||||||
|
Task<string> PostAsync(string resource, Dictionary<string, string>? data = null);
|
||||||
|
|
||||||
|
/// <summary>Performs a PUT request against the specified API resource path.</summary>
|
||||||
|
Task<string> PutAsync(string resource, Dictionary<string, string>? data = null);
|
||||||
|
|
||||||
|
/// <summary>Performs a DELETE request against the specified API resource path.</summary>
|
||||||
|
Task<string> DeleteAsync(string resource);
|
||||||
|
|
||||||
|
/// <summary>Synchronous wrapper for <see cref="GetAsync"/>.</summary>
|
||||||
|
string Get(string resource);
|
||||||
|
|
||||||
|
/// <summary>Synchronous wrapper for <see cref="PostAsync"/>.</summary>
|
||||||
|
string Post(string resource, Dictionary<string, string>? data = null);
|
||||||
|
|
||||||
|
/// <summary>Synchronous wrapper for <see cref="PutAsync"/>.</summary>
|
||||||
|
string Put(string resource, Dictionary<string, string>? data = null);
|
||||||
|
|
||||||
|
/// <summary>Synchronous wrapper for <see cref="DeleteAsync"/>.</summary>
|
||||||
|
string Delete(string resource);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uploads a file to a Proxmox VE storage endpoint using MultipartFormDataContent.
|
||||||
|
/// </summary>
|
||||||
|
Task<string> UploadFileAsync(
|
||||||
|
string resource,
|
||||||
|
string filePath,
|
||||||
|
Dictionary<string, string>? formFields = null,
|
||||||
|
string? checksum = null,
|
||||||
|
string? checksumAlgorithm = null,
|
||||||
|
Action<long, long>? progressCallback = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,8 @@ using Newtonsoft.Json.Linq;
|
|||||||
using PSProxmoxVE.Core.Authentication;
|
using PSProxmoxVE.Core.Authentication;
|
||||||
using PSProxmoxVE.Core.Exceptions;
|
using PSProxmoxVE.Core.Exceptions;
|
||||||
|
|
||||||
#if NET48 || NETSTANDARD2_0
|
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Client
|
namespace PSProxmoxVE.Core.Client
|
||||||
{
|
{
|
||||||
@@ -22,7 +20,7 @@ namespace PSProxmoxVE.Core.Client
|
|||||||
/// Low-level HTTP client for communicating with the Proxmox VE API.
|
/// Low-level HTTP client for communicating with the Proxmox VE API.
|
||||||
/// Handles authentication headers, error parsing, and the ISO upload workaround.
|
/// Handles authentication headers, error parsing, and the ISO upload workaround.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PveHttpClient : IDisposable
|
public class PveHttpClient : IPveHttpClient
|
||||||
{
|
{
|
||||||
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type
|
#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type
|
||||||
private readonly PveSession? _session;
|
private readonly PveSession? _session;
|
||||||
@@ -44,7 +42,6 @@ namespace PSProxmoxVE.Core.Client
|
|||||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
_baseUrl = session.BaseUrl;
|
_baseUrl = session.BaseUrl;
|
||||||
|
|
||||||
#if NET48 || NETSTANDARD2_0
|
|
||||||
var handler = new HttpClientHandler();
|
var handler = new HttpClientHandler();
|
||||||
if (session.SkipCertificateCheck)
|
if (session.SkipCertificateCheck)
|
||||||
{
|
{
|
||||||
@@ -52,21 +49,6 @@ namespace PSProxmoxVE.Core.Client
|
|||||||
(HttpRequestMessage _, X509Certificate2 _, X509Chain _, SslPolicyErrors _) => true;
|
(HttpRequestMessage _, X509Certificate2 _, X509Chain _, SslPolicyErrors _) => true;
|
||||||
}
|
}
|
||||||
_httpClient = new HttpClient(handler);
|
_httpClient = new HttpClient(handler);
|
||||||
#else
|
|
||||||
if (session.SkipCertificateCheck)
|
|
||||||
{
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback =
|
|
||||||
(_, _, _, _) => true
|
|
||||||
};
|
|
||||||
_httpClient = new HttpClient(handler);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_httpClient = new HttpClient();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
@@ -84,7 +66,6 @@ namespace PSProxmoxVE.Core.Client
|
|||||||
_session = null;
|
_session = null;
|
||||||
_baseUrl = $"https://{hostname}:{port}";
|
_baseUrl = $"https://{hostname}:{port}";
|
||||||
|
|
||||||
#if NET48 || NETSTANDARD2_0
|
|
||||||
var handler = new HttpClientHandler();
|
var handler = new HttpClientHandler();
|
||||||
if (skipCertificateCheck)
|
if (skipCertificateCheck)
|
||||||
{
|
{
|
||||||
@@ -92,21 +73,6 @@ namespace PSProxmoxVE.Core.Client
|
|||||||
(HttpRequestMessage _, X509Certificate2 _, X509Chain _, SslPolicyErrors _) => true;
|
(HttpRequestMessage _, X509Certificate2 _, X509Chain _, SslPolicyErrors _) => true;
|
||||||
}
|
}
|
||||||
_httpClient = new HttpClient(handler);
|
_httpClient = new HttpClient(handler);
|
||||||
#else
|
|
||||||
if (skipCertificateCheck)
|
|
||||||
{
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback =
|
|
||||||
(_, _, _, _) => true
|
|
||||||
};
|
|
||||||
_httpClient = new HttpClient(handler);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_httpClient = new HttpClient();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
@@ -295,11 +261,7 @@ namespace PSProxmoxVE.Core.Client
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
#if NET48 || NETSTANDARD2_0
|
|
||||||
fileStream.Dispose();
|
fileStream.Dispose();
|
||||||
#else
|
|
||||||
await fileStream.DisposeAsync().ConfigureAwait(false);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,12 +359,8 @@ namespace PSProxmoxVE.Core.Client
|
|||||||
{
|
{
|
||||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
var bytes = new byte[32];
|
var bytes = new byte[32];
|
||||||
#if NET48 || NETSTANDARD2_0
|
|
||||||
using (var rng = RandomNumberGenerator.Create())
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
rng.GetBytes(bytes);
|
rng.GetBytes(bytes);
|
||||||
#else
|
|
||||||
RandomNumberGenerator.Fill(bytes);
|
|
||||||
#endif
|
|
||||||
var sb = new StringBuilder(32);
|
var sb = new StringBuilder(32);
|
||||||
for (int i = 0; i < 32; i++)
|
for (int i = 0; i < 32; i++)
|
||||||
sb.Append(chars[bytes[i] % chars.Length]);
|
sb.Append(chars[bytes[i] % chars.Length]);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net10.0;net48</TargetFrameworks>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>PSProxmoxVE.Core</RootNamespace>
|
<RootNamespace>PSProxmoxVE.Core</RootNamespace>
|
||||||
@@ -16,18 +16,7 @@
|
|||||||
</AssemblyAttribute>
|
</AssemblyAttribute>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
|
||||||
<Reference Include="System.Net.Http" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
<PackageReference Include="SharpCompress" Version="0.38.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class BackupService
|
public class BackupService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="BackupService"/> with no injected client.
|
||||||
|
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
public BackupService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="BackupService"/> with an injected HTTP client.
|
||||||
|
/// The caller owns the client's lifetime; this service will not dispose it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for all requests.</param>
|
||||||
|
public BackupService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Ad-hoc backup (vzdump)
|
// Ad-hoc backup (vzdump)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -29,11 +47,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/vzdump", config)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/vzdump", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Backup jobs (scheduled)
|
// Backup jobs (scheduled)
|
||||||
@@ -46,11 +71,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/backup").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/backup").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveBackupJob[]>() ?? Array.Empty<PveBackupJob>();
|
return data?.ToObject<PveBackupJob[]>() ?? Array.Empty<PveBackupJob>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a single scheduled backup job by ID.
|
/// Returns a single scheduled backup job by ID.
|
||||||
@@ -60,12 +92,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
|
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
|
var response = client.GetAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveBackupJob>();
|
return data?.ToObject<PveBackupJob>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a scheduled backup job.
|
/// Creates a scheduled backup job.
|
||||||
@@ -75,9 +114,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("cluster/backup", config).GetAwaiter().GetResult();
|
client.PostAsync("cluster/backup", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a scheduled backup job.
|
/// Updates a scheduled backup job.
|
||||||
@@ -88,10 +134,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
|
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"cluster/backup/{Uri.EscapeDataString(id)}", config)
|
client.PutAsync($"cluster/backup/{Uri.EscapeDataString(id)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a scheduled backup job.
|
/// Removes a scheduled backup job.
|
||||||
@@ -101,10 +154,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
|
if (string.IsNullOrWhiteSpace(id)) throw new ArgumentNullException(nameof(id));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
|
client.DeleteAsync($"cluster/backup/{Uri.EscapeDataString(id)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Backup compliance info
|
// Backup compliance info
|
||||||
@@ -117,12 +177,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/backup-info/not-backed-up")
|
var response = client.GetAsync("cluster/backup-info/not-backed-up")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JArray ?? new JArray();
|
return data as JArray ?? new JArray();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CloudInitService
|
public class CloudInitService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
// Cloud-Init field names as used in the PVE API
|
// Cloud-Init field names as used in the PVE API
|
||||||
private static readonly string[] CloudInitFields =
|
private static readonly string[] CloudInitFields =
|
||||||
{
|
{
|
||||||
@@ -22,6 +24,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
"nameserver", "searchdomain", "cicustom"
|
"nameserver", "searchdomain", "cicustom"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CloudInitService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public CloudInitService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CloudInitService"/> class with an injected HTTP client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
|
||||||
|
public CloudInitService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the Cloud-Init specific configuration fields for a VM.
|
/// Retrieves the Cloud-Init specific configuration fields for a VM.
|
||||||
/// Internally fetches the full VM config and extracts the CI fields.
|
/// Internally fetches the full VM config and extracts the CI fields.
|
||||||
@@ -31,7 +47,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/config")
|
var response = client.GetAsync($"nodes/{node}/qemu/{vmid}/config")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
@@ -47,6 +65,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
|
|
||||||
return ciObj.ToObject<PveCloudInitConfig>() ?? new PveCloudInitConfig();
|
return ciObj.ToObject<PveCloudInitConfig>() ?? new PveCloudInitConfig();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates Cloud-Init configuration fields on a VM. Only fields present in
|
/// Updates Cloud-Init configuration fields on a VM. Only fields present in
|
||||||
@@ -66,13 +89,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
client.PutAsync($"nodes/{node}/qemu/{vmid}/config", formData)
|
client.PutAsync($"nodes/{node}/qemu/{vmid}/config", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Triggers regeneration of the Cloud-Init ISO drive attached to the VM.
|
/// Triggers regeneration of the Cloud-Init ISO drive attached to the VM.
|
||||||
@@ -87,13 +117,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
// Request a Cloud-Init dump (user-data section) — this causes PVE to rebuild the image
|
// Request a Cloud-Init dump (user-data section) — this causes PVE to rebuild the image
|
||||||
var dumpResponse = client.GetAsync($"nodes/{node}/qemu/{vmid}/cloudinit/dump?type=user")
|
var dumpResponse = client.GetAsync($"nodes/{node}/qemu/{vmid}/cloudinit/dump?type=user")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(dumpResponse)["data"];
|
var data = JObject.Parse(dumpResponse)["data"];
|
||||||
return data?.ToString() ?? string.Empty;
|
return data?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,22 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ClusterService
|
public class ClusterService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ClusterService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public ClusterService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ClusterService"/> class with an injected HTTP client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
|
||||||
|
public ClusterService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the current cluster status. The response is a mixed array of
|
/// Returns the current cluster status. The response is a mixed array of
|
||||||
/// "cluster" and "node" type entries.
|
/// "cluster" and "node" type entries.
|
||||||
@@ -19,11 +35,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/status").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/status").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
|
return data?.ToObject<PveClusterStatus[]>() ?? Array.Empty<PveClusterStatus>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns cluster resources, optionally filtered by type.
|
/// Returns cluster resources, optionally filtered by type.
|
||||||
@@ -34,7 +57,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var resource = "cluster/resources";
|
var resource = "cluster/resources";
|
||||||
if (!string.IsNullOrEmpty(type))
|
if (!string.IsNullOrEmpty(type))
|
||||||
resource += $"?type={Uri.EscapeDataString(type!)}";
|
resource += $"?type={Uri.EscapeDataString(type!)}";
|
||||||
@@ -43,5 +68,10 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveClusterResource[]>() ?? Array.Empty<PveClusterResource>();
|
return data?.ToObject<PveClusterResource[]>() ?? Array.Empty<PveClusterResource>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class ContainerService
|
public class ContainerService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
private readonly NodeService _nodeService = new NodeService();
|
private readonly NodeService _nodeService = new NodeService();
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
|
||||||
|
public ContainerService() { }
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
|
||||||
|
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
|
||||||
|
public ContainerService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Read operations
|
// Read operations
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -51,11 +62,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
|
|
||||||
private PveContainer[] GetContainersOnNode(PveSession session, string node)
|
private PveContainer[] GetContainersOnNode(PveSession session, string node)
|
||||||
{
|
{
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveContainer[]>() ?? Array.Empty<PveContainer>();
|
return data?.ToObject<PveContainer[]>() ?? Array.Empty<PveContainer>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a single container by its ID on the specified node.
|
/// Returns a single container by its ID on the specified node.
|
||||||
@@ -81,12 +99,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/config")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/config")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveContainerConfig>() ?? new PveContainerConfig();
|
return data?.ToObject<PveContainerConfig>() ?? new PveContainerConfig();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Configuration mutation
|
// Configuration mutation
|
||||||
@@ -105,13 +130,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/config", formData)
|
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/config", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Snapshots
|
// Snapshots
|
||||||
@@ -125,12 +157,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
|
return data?.ToObject<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a snapshot of a container. Returns the task UPID.
|
/// Creates a snapshot of a container. Returns the task UPID.
|
||||||
@@ -153,11 +192,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(description))
|
if (!string.IsNullOrEmpty(description))
|
||||||
formData["description"] = description!;
|
formData["description"] = description!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a snapshot from a container. Returns the task UPID.
|
/// Removes a snapshot from a container. Returns the task UPID.
|
||||||
@@ -172,11 +218,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
|
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rolls a container back to a snapshot. Returns the task UPID.
|
/// Rolls a container back to a snapshot. Returns the task UPID.
|
||||||
@@ -191,11 +244,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@@ -213,7 +273,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
@@ -221,6 +283,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Starts a container. Returns the task UPID.</summary>
|
/// <summary>Starts a container. Returns the task UPID.</summary>
|
||||||
public PveTask StartContainer(PveSession session, string node, int vmid)
|
public PveTask StartContainer(PveSession session, string node, int vmid)
|
||||||
@@ -244,11 +311,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (timeoutSeconds.HasValue)
|
if (timeoutSeconds.HasValue)
|
||||||
formData["timeout"] = timeoutSeconds.Value.ToString();
|
formData["timeout"] = timeoutSeconds.Value.ToString();
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/shutdown", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/shutdown", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Removes a container. Returns the task UPID.</summary>
|
/// <summary>Removes a container. Returns the task UPID.</summary>
|
||||||
public PveTask RemoveContainer(
|
public PveTask RemoveContainer(
|
||||||
@@ -261,11 +335,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}{purgeParam}")
|
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}{purgeParam}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Clones a container. Returns the task UPID.</summary>
|
/// <summary>Clones a container. Returns the task UPID.</summary>
|
||||||
public PveTask CloneContainer(
|
public PveTask CloneContainer(
|
||||||
@@ -288,11 +369,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(hostname)) formData["hostname"] = hostname!;
|
if (!string.IsNullOrEmpty(hostname)) formData["hostname"] = hostname!;
|
||||||
if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!;
|
if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/clone", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/clone", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Migrates a container to another node. Returns the task UPID.</summary>
|
/// <summary>Migrates a container to another node. Returns the task UPID.</summary>
|
||||||
public PveTask MigrateContainer(
|
public PveTask MigrateContainer(
|
||||||
@@ -312,11 +400,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["online"] = online ? "1" : "0"
|
["online"] = online ? "1" : "0"
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/migrate", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/migrate", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Suspend / Resume
|
// Suspend / Resume
|
||||||
@@ -350,11 +445,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["size"] = size
|
["size"] = size
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/resize", formData)
|
var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/resize", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Moves a container volume to a different storage. Returns the task UPID.
|
/// Moves a container volume to a different storage. Returns the task UPID.
|
||||||
@@ -373,11 +475,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["delete"] = delete ? "1" : "0"
|
["delete"] = delete ? "1" : "0"
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/move_volume", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/move_volume", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Template
|
// Template
|
||||||
@@ -391,11 +500,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/template")
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/template")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Interfaces
|
// Interfaces
|
||||||
@@ -409,12 +525,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/interfaces")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/interfaces")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveContainerInterface[]>() ?? Array.Empty<PveContainerInterface>();
|
return data?.ToObject<PveContainerInterface[]>() ?? Array.Empty<PveContainerInterface>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
@@ -425,11 +548,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/{action}")
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/lxc/{vmid}/status/{action}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PveTask ParseTask(string response, string node)
|
private static PveTask ParseTask(string response, string node)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class FirewallService
|
public class FirewallService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
|
||||||
|
public FirewallService() { }
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
|
||||||
|
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
|
||||||
|
public FirewallService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Rules
|
// Rules
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -25,11 +37,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"{basePath}/rules").GetAwaiter().GetResult();
|
var response = client.GetAsync($"{basePath}/rules").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
|
return data?.ToObject<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a firewall rule at the specified level.
|
/// Creates a firewall rule at the specified level.
|
||||||
@@ -41,9 +60,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"{basePath}/rules", config).GetAwaiter().GetResult();
|
client.PostAsync($"{basePath}/rules", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a firewall rule at the specified position.
|
/// Updates a firewall rule at the specified position.
|
||||||
@@ -55,9 +81,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"{basePath}/rules/{pos}", config).GetAwaiter().GetResult();
|
client.PutAsync($"{basePath}/rules/{pos}", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a firewall rule at the specified position.
|
/// Removes a firewall rule at the specified position.
|
||||||
@@ -68,9 +101,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"{basePath}/rules/{pos}").GetAwaiter().GetResult();
|
client.DeleteAsync($"{basePath}/rules/{pos}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Security Groups (cluster level only)
|
// Security Groups (cluster level only)
|
||||||
@@ -83,11 +123,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/firewall/groups").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/firewall/groups").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallGroup[]>() ?? Array.Empty<PveFirewallGroup>();
|
return data?.ToObject<PveFirewallGroup[]>() ?? Array.Empty<PveFirewallGroup>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a firewall security group.
|
/// Creates a firewall security group.
|
||||||
@@ -101,9 +148,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
formData["comment"] = comment!;
|
formData["comment"] = comment!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("cluster/firewall/groups", formData).GetAwaiter().GetResult();
|
client.PostAsync("cluster/firewall/groups", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a firewall security group.
|
/// Removes a firewall security group.
|
||||||
@@ -113,10 +167,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(name)}")
|
client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(name)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns firewall rules within a security group.
|
/// Returns firewall rules within a security group.
|
||||||
@@ -126,12 +187,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}")
|
var response = client.GetAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
|
return data?.ToObject<PveFirewallRule[]>() ?? Array.Empty<PveFirewallRule>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a rule within a security group.
|
/// Creates a rule within a security group.
|
||||||
@@ -142,10 +210,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}", config)
|
client.PostAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a rule within a security group.
|
/// Updates a rule within a security group.
|
||||||
@@ -156,10 +231,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}", config)
|
client.PutAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a rule from a security group.
|
/// Removes a rule from a security group.
|
||||||
@@ -169,10 +251,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
if (string.IsNullOrWhiteSpace(group)) throw new ArgumentNullException(nameof(group));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}")
|
client.DeleteAsync($"cluster/firewall/groups/{Uri.EscapeDataString(group)}/{pos}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Aliases
|
// Aliases
|
||||||
@@ -186,11 +275,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"{basePath}/aliases").GetAwaiter().GetResult();
|
var response = client.GetAsync($"{basePath}/aliases").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallAlias[]>() ?? Array.Empty<PveFirewallAlias>();
|
return data?.ToObject<PveFirewallAlias[]>() ?? Array.Empty<PveFirewallAlias>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a firewall alias.
|
/// Creates a firewall alias.
|
||||||
@@ -211,9 +307,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
formData["comment"] = comment!;
|
formData["comment"] = comment!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"{basePath}/aliases", formData).GetAwaiter().GetResult();
|
client.PostAsync($"{basePath}/aliases", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a firewall alias.
|
/// Updates a firewall alias.
|
||||||
@@ -231,10 +334,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
formData["comment"] = comment!;
|
formData["comment"] = comment!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}", formData)
|
client.PutAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a firewall alias.
|
/// Removes a firewall alias.
|
||||||
@@ -246,10 +356,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}")
|
client.DeleteAsync($"{basePath}/aliases/{Uri.EscapeDataString(name)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// IP Sets
|
// IP Sets
|
||||||
@@ -263,11 +380,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"{basePath}/ipset").GetAwaiter().GetResult();
|
var response = client.GetAsync($"{basePath}/ipset").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallIpSet[]>() ?? Array.Empty<PveFirewallIpSet>();
|
return data?.ToObject<PveFirewallIpSet[]>() ?? Array.Empty<PveFirewallIpSet>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a firewall IP set.
|
/// Creates a firewall IP set.
|
||||||
@@ -283,9 +407,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
formData["comment"] = comment!;
|
formData["comment"] = comment!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"{basePath}/ipset", formData).GetAwaiter().GetResult();
|
client.PostAsync($"{basePath}/ipset", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a firewall IP set.
|
/// Removes a firewall IP set.
|
||||||
@@ -297,10 +428,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}")
|
client.DeleteAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// IP Set Entries
|
// IP Set Entries
|
||||||
@@ -316,12 +454,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}")
|
var response = client.GetAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallIpSetEntry[]>() ?? Array.Empty<PveFirewallIpSetEntry>();
|
return data?.ToObject<PveFirewallIpSetEntry[]>() ?? Array.Empty<PveFirewallIpSetEntry>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds an entry to an IP set.
|
/// Adds an entry to an IP set.
|
||||||
@@ -340,10 +485,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
formData["comment"] = comment!;
|
formData["comment"] = comment!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}", formData)
|
client.PostAsync($"{basePath}/ipset/{Uri.EscapeDataString(name)}", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an entry in an IP set.
|
/// Updates an entry in an IP set.
|
||||||
@@ -362,11 +514,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
formData["comment"] = comment!;
|
formData["comment"] = comment!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync(
|
client.PutAsync(
|
||||||
$"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}",
|
$"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}",
|
||||||
formData).GetAwaiter().GetResult();
|
formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an entry from an IP set.
|
/// Removes an entry from an IP set.
|
||||||
@@ -379,11 +538,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr));
|
if (string.IsNullOrWhiteSpace(cidr)) throw new ArgumentNullException(nameof(cidr));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync(
|
client.DeleteAsync(
|
||||||
$"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}")
|
$"{basePath}/ipset/{Uri.EscapeDataString(name)}/{Uri.EscapeDataString(cidr)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Options
|
// Options
|
||||||
@@ -398,11 +564,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"{basePath}/options").GetAwaiter().GetResult();
|
var response = client.GetAsync($"{basePath}/options").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallOptions>() ?? new PveFirewallOptions();
|
return data?.ToObject<PveFirewallOptions>() ?? new PveFirewallOptions();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets firewall options at the specified level.
|
/// Sets firewall options at the specified level.
|
||||||
@@ -414,9 +587,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
var basePath = BuildBasePath(level, node, vmid);
|
var basePath = BuildBasePath(level, node, vmid);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"{basePath}/options", config).GetAwaiter().GetResult();
|
client.PutAsync($"{basePath}/options", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Refs
|
// Refs
|
||||||
@@ -435,11 +615,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(type))
|
if (!string.IsNullOrEmpty(type))
|
||||||
resource += $"?type={Uri.EscapeDataString(type!)}";
|
resource += $"?type={Uri.EscapeDataString(type!)}";
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveFirewallRef[]>() ?? Array.Empty<PveFirewallRef>();
|
return data?.ToObject<PveFirewallRef[]>() ?? Array.Empty<PveFirewallRef>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|||||||
@@ -15,6 +15,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class NetworkService
|
public class NetworkService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
|
||||||
|
public NetworkService() { }
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
|
||||||
|
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
|
||||||
|
public NetworkService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Node network interfaces
|
// Node network interfaces
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -36,11 +48,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(type))
|
if (!string.IsNullOrEmpty(type))
|
||||||
resource += $"?type={Uri.EscapeDataString(type!)}";
|
resource += $"?type={Uri.EscapeDataString(type!)}";
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveNetwork[]>() ?? Array.Empty<PveNetwork>();
|
return data?.ToObject<PveNetwork[]>() ?? Array.Empty<PveNetwork>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a network interface on a node. Changes are pending until
|
/// Creates a network interface on a node. Changes are pending until
|
||||||
@@ -58,7 +77,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
@@ -67,6 +88,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveNetwork>() ?? new PveNetwork();
|
return data?.ToObject<PveNetwork>() ?? new PveNetwork();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an existing network interface on a node. Changes are pending until
|
/// Updates an existing network interface on a node. Changes are pending until
|
||||||
@@ -87,13 +113,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface));
|
if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
client.PutAsync($"nodes/{node}/network/{iface}", formData)
|
client.PutAsync($"nodes/{node}/network/{iface}", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a network interface from a node. Changes are pending until
|
/// Removes a network interface from a node. Changes are pending until
|
||||||
@@ -108,9 +141,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface));
|
if (string.IsNullOrWhiteSpace(iface)) throw new ArgumentNullException(nameof(iface));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"nodes/{node}/network/{iface}").GetAwaiter().GetResult();
|
client.DeleteAsync($"nodes/{node}/network/{iface}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies pending network configuration changes on a node. Returns the task UPID.
|
/// Applies pending network configuration changes on a node. Returns the task UPID.
|
||||||
@@ -122,11 +162,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PutAsync($"nodes/{node}/network")
|
var response = client.PutAsync($"nodes/{node}/network")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// SDN — requires PVE 8.0+
|
// SDN — requires PVE 8.0+
|
||||||
@@ -140,12 +187,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
using var client = new PveHttpClient(session);
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/sdn/zones").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/sdn/zones").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnZone[]>() ?? Array.Empty<PveSdnZone>();
|
return data?.ToObject<PveSdnZone[]>() ?? Array.Empty<PveSdnZone>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all SDN VNet definitions. Requires PVE 8.0+.
|
/// Returns all SDN VNet definitions. Requires PVE 8.0+.
|
||||||
@@ -155,12 +208,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
using var client = new PveHttpClient(session);
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/sdn/vnets").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/sdn/vnets").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnVnet[]>() ?? Array.Empty<PveSdnVnet>();
|
return data?.ToObject<PveSdnVnet[]>() ?? Array.Empty<PveSdnVnet>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an SDN zone. Requires PVE 8.0+.
|
/// Creates an SDN zone. Requires PVE 8.0+.
|
||||||
@@ -172,10 +231,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
Dictionary<string, object> config)
|
Dictionary<string, object> config)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
@@ -184,6 +244,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnZone>() ?? new PveSdnZone();
|
return data?.ToObject<PveSdnZone>() ?? new PveSdnZone();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an SDN zone. Requires PVE 8.0+.
|
/// Removes an SDN zone. Requires PVE 8.0+.
|
||||||
@@ -193,12 +258,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void RemoveSdnZone(PveSession session, string zone)
|
public void RemoveSdnZone(PveSession session, string zone)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone));
|
if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/sdn/zones/{zone}").GetAwaiter().GetResult();
|
client.DeleteAsync($"cluster/sdn/zones/{zone}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an SDN VNet. Requires PVE 8.0+.
|
/// Creates an SDN VNet. Requires PVE 8.0+.
|
||||||
@@ -210,10 +281,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
Dictionary<string, object> config)
|
Dictionary<string, object> config)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
@@ -222,6 +294,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnVnet>() ?? new PveSdnVnet();
|
return data?.ToObject<PveSdnVnet>() ?? new PveSdnVnet();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an SDN VNet. Requires PVE 8.0+.
|
/// Removes an SDN VNet. Requires PVE 8.0+.
|
||||||
@@ -231,12 +308,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void RemoveSdnVnet(PveSession session, string vnet)
|
public void RemoveSdnVnet(PveSession session, string vnet)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/sdn/vnets/{vnet}").GetAwaiter().GetResult();
|
client.DeleteAsync($"cluster/sdn/vnets/{vnet}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// SDN Subnets — requires PVE 8.0+
|
// SDN Subnets — requires PVE 8.0+
|
||||||
@@ -250,15 +333,21 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public PveSdnSubnet[] GetSdnSubnets(PveSession session, string vnet)
|
public PveSdnSubnet[] GetSdnSubnets(PveSession session, string vnet)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets")
|
var response = client.GetAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnSubnet[]>() ?? Array.Empty<PveSdnSubnet>();
|
return data?.ToObject<PveSdnSubnet[]>() ?? Array.Empty<PveSdnSubnet>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an SDN subnet on a VNet. Requires PVE 8.0+.
|
/// Creates an SDN subnet on a VNet. Requires PVE 8.0+.
|
||||||
@@ -272,17 +361,23 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
Dictionary<string, object> config)
|
Dictionary<string, object> config)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
client.PostAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets", formData)
|
client.PostAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an SDN subnet from a VNet. Requires PVE 8.0+.
|
/// Removes an SDN subnet from a VNet. Requires PVE 8.0+.
|
||||||
@@ -293,15 +388,21 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void RemoveSdnSubnet(PveSession session, string vnet, string subnet)
|
public void RemoveSdnSubnet(PveSession session, string vnet, string subnet)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
||||||
if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet));
|
if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync(
|
client.DeleteAsync(
|
||||||
$"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}")
|
$"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// SDN IPAM
|
// SDN IPAM
|
||||||
@@ -314,12 +415,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
using var client = new PveHttpClient(session);
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/sdn/ipams").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/sdn/ipams").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnIpam[]>() ?? Array.Empty<PveSdnIpam>();
|
return data?.ToObject<PveSdnIpam[]>() ?? Array.Empty<PveSdnIpam>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an SDN IPAM plugin. Requires PVE 8.0+.
|
/// Creates an SDN IPAM plugin. Requires PVE 8.0+.
|
||||||
@@ -327,12 +434,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void CreateSdnIpam(PveSession session, Dictionary<string, string> config)
|
public void CreateSdnIpam(PveSession session, Dictionary<string, string> config)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("cluster/sdn/ipams", config).GetAwaiter().GetResult();
|
client.PostAsync("cluster/sdn/ipams", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an SDN IPAM plugin. Requires PVE 8.0+.
|
/// Removes an SDN IPAM plugin. Requires PVE 8.0+.
|
||||||
@@ -340,13 +453,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void RemoveSdnIpam(PveSession session, string ipam)
|
public void RemoveSdnIpam(PveSession session, string ipam)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam));
|
if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}")
|
client.DeleteAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// SDN DNS
|
// SDN DNS
|
||||||
@@ -359,12 +478,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
using var client = new PveHttpClient(session);
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/sdn/dns").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/sdn/dns").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnDns[]>() ?? Array.Empty<PveSdnDns>();
|
return data?.ToObject<PveSdnDns[]>() ?? Array.Empty<PveSdnDns>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an SDN DNS plugin. Requires PVE 8.0+.
|
/// Creates an SDN DNS plugin. Requires PVE 8.0+.
|
||||||
@@ -372,12 +497,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void CreateSdnDnsPlugin(PveSession session, Dictionary<string, string> config)
|
public void CreateSdnDnsPlugin(PveSession session, Dictionary<string, string> config)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("cluster/sdn/dns", config).GetAwaiter().GetResult();
|
client.PostAsync("cluster/sdn/dns", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an SDN DNS plugin. Requires PVE 8.0+.
|
/// Removes an SDN DNS plugin. Requires PVE 8.0+.
|
||||||
@@ -385,13 +516,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void RemoveSdnDnsPlugin(PveSession session, string dns)
|
public void RemoveSdnDnsPlugin(PveSession session, string dns)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns));
|
if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}")
|
client.DeleteAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// SDN Controllers
|
// SDN Controllers
|
||||||
@@ -404,12 +541,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
using var client = new PveHttpClient(session);
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("cluster/sdn/controllers").GetAwaiter().GetResult();
|
var response = client.GetAsync("cluster/sdn/controllers").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSdnController[]>() ?? Array.Empty<PveSdnController>();
|
return data?.ToObject<PveSdnController[]>() ?? Array.Empty<PveSdnController>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an SDN controller. Requires PVE 8.0+.
|
/// Creates an SDN controller. Requires PVE 8.0+.
|
||||||
@@ -417,12 +560,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void CreateSdnController(PveSession session, Dictionary<string, string> config)
|
public void CreateSdnController(PveSession session, Dictionary<string, string> config)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("cluster/sdn/controllers", config).GetAwaiter().GetResult();
|
client.PostAsync("cluster/sdn/controllers", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes an SDN controller. Requires PVE 8.0+.
|
/// Removes an SDN controller. Requires PVE 8.0+.
|
||||||
@@ -430,13 +579,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
public void RemoveSdnController(PveSession session, string controller)
|
public void RemoveSdnController(PveSession session, string controller)
|
||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller));
|
if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}")
|
client.DeleteAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// SDN Update operations
|
// SDN Update operations
|
||||||
@@ -449,10 +604,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync("cluster/sdn", new Dictionary<string, string>())
|
client.PutAsync("cluster/sdn", new Dictionary<string, string>())
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an SDN zone configuration.
|
/// Updates an SDN zone configuration.
|
||||||
@@ -463,10 +625,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone));
|
if (string.IsNullOrWhiteSpace(zone)) throw new ArgumentNullException(nameof(zone));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"cluster/sdn/zones/{Uri.EscapeDataString(zone)}", config)
|
client.PutAsync($"cluster/sdn/zones/{Uri.EscapeDataString(zone)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an SDN VNet configuration.
|
/// Updates an SDN VNet configuration.
|
||||||
@@ -477,10 +646,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
if (string.IsNullOrWhiteSpace(vnet)) throw new ArgumentNullException(nameof(vnet));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}", config)
|
client.PutAsync($"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an SDN subnet configuration.
|
/// Updates an SDN subnet configuration.
|
||||||
@@ -492,11 +668,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet));
|
if (string.IsNullOrWhiteSpace(subnet)) throw new ArgumentNullException(nameof(subnet));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync(
|
client.PutAsync(
|
||||||
$"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}",
|
$"cluster/sdn/vnets/{Uri.EscapeDataString(vnet)}/subnets/{Uri.EscapeDataString(subnet)}",
|
||||||
config).GetAwaiter().GetResult();
|
config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an SDN controller configuration.
|
/// Updates an SDN controller configuration.
|
||||||
@@ -507,10 +690,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller));
|
if (string.IsNullOrWhiteSpace(controller)) throw new ArgumentNullException(nameof(controller));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}", config)
|
client.PutAsync($"cluster/sdn/controllers/{Uri.EscapeDataString(controller)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an SDN IPAM plugin configuration.
|
/// Updates an SDN IPAM plugin configuration.
|
||||||
@@ -521,10 +711,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam));
|
if (string.IsNullOrWhiteSpace(ipam)) throw new ArgumentNullException(nameof(ipam));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}", config)
|
client.PutAsync($"cluster/sdn/ipams/{Uri.EscapeDataString(ipam)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an SDN DNS plugin configuration.
|
/// Updates an SDN DNS plugin configuration.
|
||||||
@@ -535,10 +732,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns));
|
if (string.IsNullOrWhiteSpace(dns)) throw new ArgumentNullException(nameof(dns));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}", config)
|
client.PutAsync($"cluster/sdn/dns/{Uri.EscapeDataString(dns)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|||||||
@@ -14,6 +14,24 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class NodeService
|
public class NodeService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="NodeService"/> with no injected client.
|
||||||
|
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
public NodeService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="NodeService"/> with an injected HTTP client.
|
||||||
|
/// The caller owns the client's lifetime; this service will not dispose it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for all requests.</param>
|
||||||
|
public NodeService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all cluster nodes.
|
/// Returns all cluster nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -21,11 +39,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("nodes").GetAwaiter().GetResult();
|
var response = client.GetAsync("nodes").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveNode[]>() ?? Array.Empty<PveNode>();
|
return data?.ToObject<PveNode[]>() ?? Array.Empty<PveNode>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns detailed status for a specific node.
|
/// Returns detailed status for a specific node.
|
||||||
@@ -35,11 +60,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{node}/status").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{node}/status").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveNodeStatus>() ?? new PveNodeStatus();
|
return data?.ToObject<PveNodeStatus>() ?? new PveNodeStatus();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the configuration of a specific node.
|
/// Returns the configuration of a specific node.
|
||||||
@@ -51,11 +83,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{node}/config").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{node}/config").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return data as JObject ?? new JObject();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the configuration of a specific node.
|
/// Updates the configuration of a specific node.
|
||||||
@@ -69,9 +108,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"nodes/{node}/config", config).GetAwaiter().GetResult();
|
client.PutAsync($"nodes/{node}/config", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the DNS configuration of a specific node.
|
/// Returns the DNS configuration of a specific node.
|
||||||
@@ -83,11 +129,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{node}/dns").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{node}/dns").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data as JObject ?? new JObject();
|
return data as JObject ?? new JObject();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the DNS configuration of a specific node.
|
/// Updates the DNS configuration of a specific node.
|
||||||
@@ -101,9 +154,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"nodes/{node}/dns", config).GetAwaiter().GetResult();
|
client.PutAsync($"nodes/{node}/dns", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts all VMs and containers on a node. Returns the task UPID.
|
/// Starts all VMs and containers on a node. Returns the task UPID.
|
||||||
@@ -117,10 +177,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
var formData = config ?? new Dictionary<string, string>();
|
var formData = config ?? new Dictionary<string, string>();
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{node}/startall", formData).GetAwaiter().GetResult();
|
var response = client.PostAsync($"nodes/{node}/startall", formData).GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops all VMs and containers on a node. Returns the task UPID.
|
/// Stops all VMs and containers on a node. Returns the task UPID.
|
||||||
@@ -134,10 +201,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
var formData = config ?? new Dictionary<string, string>();
|
var formData = config ?? new Dictionary<string, string>();
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{node}/stopall", formData).GetAwaiter().GetResult();
|
var response = client.PostAsync($"nodes/{node}/stopall", formData).GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the Proxmox VE version running on the server.
|
/// Returns the Proxmox VE version running on the server.
|
||||||
@@ -146,7 +220,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("version").GetAwaiter().GetResult();
|
var response = client.GetAsync("version").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
var versionStr = data?["version"]?.ToString();
|
var versionStr = data?["version"]?.ToString();
|
||||||
@@ -154,6 +230,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
throw new InvalidOperationException("Failed to retrieve PVE version from API response.");
|
throw new InvalidOperationException("Failed to retrieve PVE version from API response.");
|
||||||
return PveVersion.Parse(versionStr!);
|
return PveVersion.Parse(versionStr!);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|||||||
@@ -12,6 +12,24 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class PoolService
|
public class PoolService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="PoolService"/> with no injected client.
|
||||||
|
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
public PoolService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="PoolService"/> with an injected HTTP client.
|
||||||
|
/// The caller owns the client's lifetime; this service will not dispose it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for all requests.</param>
|
||||||
|
public PoolService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all resource pools.
|
/// Returns all resource pools.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -19,11 +37,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("pools").GetAwaiter().GetResult();
|
var response = client.GetAsync("pools").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PvePool[]>() ?? Array.Empty<PvePool>();
|
return data?.ToObject<PvePool[]>() ?? Array.Empty<PvePool>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a single resource pool by ID.
|
/// Returns a single resource pool by ID.
|
||||||
@@ -33,12 +58,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"pools/{Uri.EscapeDataString(poolId)}")
|
var response = client.GetAsync($"pools/{Uri.EscapeDataString(poolId)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PvePool>();
|
return data?.ToObject<PvePool>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new resource pool.
|
/// Creates a new resource pool.
|
||||||
@@ -48,13 +80,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var config = new Dictionary<string, string> { { "poolid", poolId } };
|
var config = new Dictionary<string, string> { { "poolid", poolId } };
|
||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
config["comment"] = comment!;
|
config["comment"] = comment!;
|
||||||
|
|
||||||
client.PostAsync("pools", config).GetAwaiter().GetResult();
|
client.PostAsync("pools", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an existing resource pool.
|
/// Updates an existing resource pool.
|
||||||
@@ -65,10 +104,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"pools/{Uri.EscapeDataString(poolId)}", config)
|
client.PutAsync($"pools/{Uri.EscapeDataString(poolId)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a resource pool.
|
/// Removes a resource pool.
|
||||||
@@ -78,9 +124,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
if (string.IsNullOrWhiteSpace(poolId)) throw new ArgumentNullException(nameof(poolId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"pools/{Uri.EscapeDataString(poolId)}")
|
client.DeleteAsync($"pools/{Uri.EscapeDataString(poolId)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SnapshotService
|
public class SnapshotService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SnapshotService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public SnapshotService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SnapshotService"/> class with an injected HTTP client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
|
||||||
|
public SnapshotService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all snapshots for a VM.
|
/// Returns all snapshots for a VM.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -24,12 +40,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
|
return data?.ToObject<PveSnapshot[]>() ?? Array.Empty<PveSnapshot>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a snapshot of a VM. Returns the task UPID.
|
/// Creates a snapshot of a VM. Returns the task UPID.
|
||||||
@@ -60,11 +83,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(description))
|
if (!string.IsNullOrEmpty(description))
|
||||||
formData["description"] = description!;
|
formData["description"] = description!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a snapshot from a VM. Returns the task UPID.
|
/// Removes a snapshot from a VM. Returns the task UPID.
|
||||||
@@ -83,11 +113,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
|
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rolls a VM back to a snapshot. Returns the task UPID.
|
/// Rolls a VM back to a snapshot. Returns the task UPID.
|
||||||
@@ -106,11 +143,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
if (string.IsNullOrWhiteSpace(snapname)) throw new ArgumentNullException(nameof(snapname));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/snapshot/{Uri.EscapeDataString(snapname)}/rollback")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|||||||
@@ -14,6 +14,24 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class StorageService
|
public class StorageService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="StorageService"/> with no injected client.
|
||||||
|
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
public StorageService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="StorageService"/> with an injected HTTP client.
|
||||||
|
/// The caller owns the client's lifetime; this service will not dispose it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for all requests.</param>
|
||||||
|
public StorageService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Read operations
|
// Read operations
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -32,11 +50,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
? $"nodes/{Uri.EscapeDataString(node)}/storage"
|
? $"nodes/{Uri.EscapeDataString(node)}/storage"
|
||||||
: "storage";
|
: "storage";
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveStorage[]>() ?? Array.Empty<PveStorage>();
|
return data?.ToObject<PveStorage[]>() ?? Array.Empty<PveStorage>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the contents of a storage volume on a specific node.
|
/// Returns the contents of a storage volume on a specific node.
|
||||||
@@ -61,11 +86,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(contentType))
|
if (!string.IsNullOrEmpty(contentType))
|
||||||
resource += $"?content={Uri.EscapeDataString(contentType!)}";
|
resource += $"?content={Uri.EscapeDataString(contentType!)}";
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveStorageContent[]>() ?? Array.Empty<PveStorageContent>();
|
return data?.ToObject<PveStorageContent[]>() ?? Array.Empty<PveStorageContent>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Upload / download
|
// Upload / download
|
||||||
@@ -100,7 +132,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["content"] = "iso"
|
["content"] = "iso"
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.UploadFileAsync(
|
var response = client.UploadFileAsync(
|
||||||
$"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/upload",
|
$"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/upload",
|
||||||
filePath,
|
filePath,
|
||||||
@@ -111,6 +145,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads a file from a URL directly to a storage on a node. Returns the task UPID.
|
/// Downloads a file from a URL directly to a storage on a node. Returns the task UPID.
|
||||||
@@ -143,11 +182,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["content"] = contentType
|
["content"] = contentType
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/download-url", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/download-url", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Storage CRUD
|
// Storage CRUD
|
||||||
@@ -164,7 +210,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
@@ -172,6 +220,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveStorage>() ?? new PveStorage();
|
return data?.ToObject<PveStorage>() ?? new PveStorage();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a cluster-level storage definition.
|
/// Removes a cluster-level storage definition.
|
||||||
@@ -183,9 +236,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"storage/{Uri.EscapeDataString(storage)}").GetAwaiter().GetResult();
|
client.DeleteAsync($"storage/{Uri.EscapeDataString(storage)}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a storage definition.
|
/// Updates a storage definition.
|
||||||
@@ -199,10 +259,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"storage/{Uri.EscapeDataString(storage)}", config)
|
client.PutAsync($"storage/{Uri.EscapeDataString(storage)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Storage status & content management
|
// Storage status & content management
|
||||||
@@ -220,11 +287,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/status").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/status").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveStorageStatus>() ?? new PveStorageStatus();
|
return data?.ToObject<PveStorageStatus>() ?? new PveStorageStatus();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a content volume from a storage on a node.
|
/// Removes a content volume from a storage on a node.
|
||||||
@@ -240,10 +314,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||||
if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume));
|
if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content/{Uri.EscapeDataString(volume)}")
|
client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content/{Uri.EscapeDataString(volume)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates properties (e.g. notes) of a content volume on a storage.
|
/// Updates properties (e.g. notes) of a content volume on a storage.
|
||||||
@@ -261,10 +342,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume));
|
if (string.IsNullOrWhiteSpace(volume)) throw new ArgumentNullException(nameof(volume));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content/{Uri.EscapeDataString(volume)}", config)
|
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content/{Uri.EscapeDataString(volume)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Allocates a new disk image on a storage. Returns the task UPID.
|
/// Allocates a new disk image on a storage. Returns the task UPID.
|
||||||
@@ -280,11 +368,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
if (string.IsNullOrWhiteSpace(storage)) throw new ArgumentNullException(nameof(storage));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content", config)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/content", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
|
|||||||
@@ -14,10 +14,22 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TaskService
|
public class TaskService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(10);
|
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(10);
|
||||||
private static readonly TimeSpan DefaultPollInterval = TimeSpan.FromSeconds(2);
|
private static readonly TimeSpan DefaultPollInterval = TimeSpan.FromSeconds(2);
|
||||||
private static readonly TimeSpan MinPollInterval = TimeSpan.FromSeconds(1);
|
private static readonly TimeSpan MinPollInterval = TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
|
||||||
|
public TaskService() { }
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
|
||||||
|
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
|
||||||
|
public TaskService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the current status of a task identified by its UPID.
|
/// Returns the current status of a task identified by its UPID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -27,7 +39,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedUpid = Uri.EscapeDataString(upid);
|
var encodedUpid = Uri.EscapeDataString(upid);
|
||||||
var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/status")
|
var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/status")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
@@ -36,6 +50,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
task.Node = node;
|
task.Node = node;
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all log lines for a task identified by its UPID.
|
/// Returns all log lines for a task identified by its UPID.
|
||||||
@@ -46,13 +65,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedUpid = Uri.EscapeDataString(upid);
|
var encodedUpid = Uri.EscapeDataString(upid);
|
||||||
var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/log")
|
var response = client.GetAsync($"nodes/{node}/tasks/{encodedUpid}/log")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveTaskLog[]>() ?? Array.Empty<PveTaskLog>();
|
return data?.ToObject<PveTaskLog[]>() ?? Array.Empty<PveTaskLog>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Polls the task status until it completes, throws on timeout or failure.
|
/// Polls the task status until it completes, throws on timeout or failure.
|
||||||
@@ -119,7 +145,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var queryParts = new List<string> { $"limit={limit}" };
|
var queryParts = new List<string> { $"limit={limit}" };
|
||||||
if (vmid.HasValue)
|
if (vmid.HasValue)
|
||||||
queryParts.Add($"vmid={vmid.Value}");
|
queryParts.Add($"vmid={vmid.Value}");
|
||||||
@@ -137,6 +165,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
t.Node ??= node;
|
t.Node ??= node;
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops (cancels) a running task on the specified node.
|
/// Stops (cancels) a running task on the specified node.
|
||||||
@@ -150,10 +183,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
if (string.IsNullOrWhiteSpace(upid)) throw new ArgumentNullException(nameof(upid));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedUpid = Uri.EscapeDataString(upid);
|
var encodedUpid = Uri.EscapeDataString(upid);
|
||||||
client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}")
|
client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/tasks/{encodedUpid}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,23 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TemplateService
|
public class TemplateService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
private readonly VmService _vmService = new VmService();
|
private readonly VmService _vmService = new VmService();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TemplateService"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public TemplateService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TemplateService"/> class with an injected HTTP client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for API calls. The caller owns its lifetime.</param>
|
||||||
|
public TemplateService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all VM templates. If <paramref name="node"/> is null, searches all cluster nodes.
|
/// Returns all VM templates. If <paramref name="node"/> is null, searches all cluster nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -43,11 +58,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/template")
|
var response = client.PostAsync($"nodes/{node}/qemu/{vmid}/template")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a VM template (delegates to <see cref="VmService.RemoveVm"/>).
|
/// Removes a VM template (delegates to <see cref="VmService.RemoveVm"/>).
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class UserService
|
public class UserService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="UserService"/> with no injected client.
|
||||||
|
/// Each method will create and dispose its own <see cref="PveHttpClient"/>.
|
||||||
|
/// </summary>
|
||||||
|
public UserService() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="UserService"/> with an injected HTTP client.
|
||||||
|
/// The caller owns the client's lifetime; this service will not dispose it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The HTTP client to use for all requests.</param>
|
||||||
|
public UserService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Users
|
// Users
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -23,11 +41,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("access/users").GetAwaiter().GetResult();
|
var response = client.GetAsync("access/users").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveUser[]>() ?? Array.Empty<PveUser>();
|
return data?.ToObject<PveUser[]>() ?? Array.Empty<PveUser>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Returns a single user by their user ID (e.g. "admin@pam").</summary>
|
/// <summary>Returns a single user by their user ID (e.g. "admin@pam").</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -37,7 +62,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedId = Uri.EscapeDataString(userId);
|
var encodedId = Uri.EscapeDataString(userId);
|
||||||
var response = client.GetAsync($"access/users/{encodedId}").GetAwaiter().GetResult();
|
var response = client.GetAsync($"access/users/{encodedId}").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
@@ -47,6 +74,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
user.UserId = userId;
|
user.UserId = userId;
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new user account.
|
/// Creates a new user account.
|
||||||
@@ -69,9 +101,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
formData[kvp.Key] = kvp.Value?.ToString() ?? string.Empty;
|
formData[kvp.Key] = kvp.Value?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("access/users", formData).GetAwaiter().GetResult();
|
client.PostAsync("access/users", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Removes a user account.</summary>
|
/// <summary>Removes a user account.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -81,10 +120,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedId = Uri.EscapeDataString(userId);
|
var encodedId = Uri.EscapeDataString(userId);
|
||||||
client.DeleteAsync($"access/users/{encodedId}").GetAwaiter().GetResult();
|
client.DeleteAsync($"access/users/{encodedId}").GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Updates one or more properties of an existing user.</summary>
|
/// <summary>Updates one or more properties of an existing user.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -99,13 +145,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedId = Uri.EscapeDataString(userId);
|
var encodedId = Uri.EscapeDataString(userId);
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
client.PutAsync($"access/users/{encodedId}", formData).GetAwaiter().GetResult();
|
client.PutAsync($"access/users/{encodedId}", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// API Tokens
|
// API Tokens
|
||||||
@@ -119,7 +172,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedId = Uri.EscapeDataString(userId);
|
var encodedId = Uri.EscapeDataString(userId);
|
||||||
var response = client.GetAsync($"access/users/{encodedId}/token").GetAwaiter().GetResult();
|
var response = client.GetAsync($"access/users/{encodedId}/token").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
@@ -128,6 +183,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
t.UserId = userId;
|
t.UserId = userId;
|
||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new API token for the specified user and returns the token object,
|
/// Creates a new API token for the specified user and returns the token object,
|
||||||
@@ -158,7 +218,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (expire.HasValue) formData["expire"] = expire.Value.ToString();
|
if (expire.HasValue) formData["expire"] = expire.Value.ToString();
|
||||||
if (privilegeSeparation.HasValue) formData["privsep"] = privilegeSeparation.Value ? "1" : "0";
|
if (privilegeSeparation.HasValue) formData["privsep"] = privilegeSeparation.Value ? "1" : "0";
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedUser = Uri.EscapeDataString(userId);
|
var encodedUser = Uri.EscapeDataString(userId);
|
||||||
var encodedToken = Uri.EscapeDataString(tokenId);
|
var encodedToken = Uri.EscapeDataString(tokenId);
|
||||||
var response = client.PostAsync(
|
var response = client.PostAsync(
|
||||||
@@ -171,6 +233,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
token.TokenId = tokenId;
|
token.TokenId = tokenId;
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Removes an API token.</summary>
|
/// <summary>Removes an API token.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -182,12 +249,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentNullException(nameof(userId));
|
||||||
if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId));
|
if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedUser = Uri.EscapeDataString(userId);
|
var encodedUser = Uri.EscapeDataString(userId);
|
||||||
var encodedToken = Uri.EscapeDataString(tokenId);
|
var encodedToken = Uri.EscapeDataString(tokenId);
|
||||||
client.DeleteAsync($"access/users/{encodedUser}/token/{encodedToken}")
|
client.DeleteAsync($"access/users/{encodedUser}/token/{encodedToken}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an API token's configuration.
|
/// Updates an API token's configuration.
|
||||||
@@ -199,12 +273,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId));
|
if (string.IsNullOrWhiteSpace(tokenId)) throw new ArgumentNullException(nameof(tokenId));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var encodedUser = Uri.EscapeDataString(userId);
|
var encodedUser = Uri.EscapeDataString(userId);
|
||||||
var encodedToken = Uri.EscapeDataString(tokenId);
|
var encodedToken = Uri.EscapeDataString(tokenId);
|
||||||
client.PutAsync($"access/users/{encodedUser}/token/{encodedToken}", config)
|
client.PutAsync($"access/users/{encodedUser}/token/{encodedToken}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Roles
|
// Roles
|
||||||
@@ -216,11 +297,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("access/roles").GetAwaiter().GetResult();
|
var response = client.GetAsync("access/roles").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveRole[]>() ?? Array.Empty<PveRole>();
|
return data?.ToObject<PveRole[]>() ?? Array.Empty<PveRole>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Creates a new role.</summary>
|
/// <summary>Creates a new role.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -235,9 +323,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(privileges))
|
if (!string.IsNullOrEmpty(privileges))
|
||||||
formData["privs"] = privileges!;
|
formData["privs"] = privileges!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("access/roles", formData).GetAwaiter().GetResult();
|
client.PostAsync("access/roles", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Removes a role.</summary>
|
/// <summary>Removes a role.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -247,10 +342,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId));
|
if (string.IsNullOrWhiteSpace(roleId)) throw new ArgumentNullException(nameof(roleId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"access/roles/{Uri.EscapeDataString(roleId)}")
|
client.DeleteAsync($"access/roles/{Uri.EscapeDataString(roleId)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a role's privileges.
|
/// Updates a role's privileges.
|
||||||
@@ -265,10 +367,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(privileges)) throw new ArgumentNullException(nameof(privileges));
|
if (string.IsNullOrWhiteSpace(privileges)) throw new ArgumentNullException(nameof(privileges));
|
||||||
|
|
||||||
var formData = new Dictionary<string, string> { ["privs"] = privileges };
|
var formData = new Dictionary<string, string> { ["privs"] = privileges };
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"access/roles/{Uri.EscapeDataString(roleId)}", formData)
|
client.PutAsync($"access/roles/{Uri.EscapeDataString(roleId)}", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Groups
|
// Groups
|
||||||
@@ -280,11 +389,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("access/groups").GetAwaiter().GetResult();
|
var response = client.GetAsync("access/groups").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveGroup[]>() ?? Array.Empty<PveGroup>();
|
return data?.ToObject<PveGroup[]>() ?? Array.Empty<PveGroup>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Creates a new group.</summary>
|
/// <summary>Creates a new group.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -299,9 +415,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(comment))
|
if (!string.IsNullOrEmpty(comment))
|
||||||
formData["comment"] = comment!;
|
formData["comment"] = comment!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("access/groups", formData).GetAwaiter().GetResult();
|
client.PostAsync("access/groups", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Updates a group's properties.</summary>
|
/// <summary>Updates a group's properties.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -313,10 +436,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId));
|
if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"access/groups/{Uri.EscapeDataString(groupId)}", config)
|
client.PutAsync($"access/groups/{Uri.EscapeDataString(groupId)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Removes a group.</summary>
|
/// <summary>Removes a group.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -326,10 +456,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId));
|
if (string.IsNullOrWhiteSpace(groupId)) throw new ArgumentNullException(nameof(groupId));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"access/groups/{Uri.EscapeDataString(groupId)}")
|
client.DeleteAsync($"access/groups/{Uri.EscapeDataString(groupId)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Domains / Realms
|
// Domains / Realms
|
||||||
@@ -341,11 +478,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync("access/domains").GetAwaiter().GetResult();
|
var response = client.GetAsync("access/domains").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveDomain[]>() ?? Array.Empty<PveDomain>();
|
return data?.ToObject<PveDomain[]>() ?? Array.Empty<PveDomain>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Creates a new authentication domain/realm.</summary>
|
/// <summary>Creates a new authentication domain/realm.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -355,9 +499,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync("access/domains", config).GetAwaiter().GetResult();
|
client.PostAsync("access/domains", config).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Updates an authentication domain/realm.</summary>
|
/// <summary>Updates an authentication domain/realm.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -369,10 +520,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm));
|
if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"access/domains/{Uri.EscapeDataString(realm)}", config)
|
client.PutAsync($"access/domains/{Uri.EscapeDataString(realm)}", config)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Removes an authentication domain/realm.</summary>
|
/// <summary>Removes an authentication domain/realm.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -382,10 +540,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm));
|
if (string.IsNullOrWhiteSpace(realm)) throw new ArgumentNullException(nameof(realm));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.DeleteAsync($"access/domains/{Uri.EscapeDataString(realm)}")
|
client.DeleteAsync($"access/domains/{Uri.EscapeDataString(realm)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Password
|
// Password
|
||||||
@@ -407,9 +572,16 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["password"] = password
|
["password"] = password
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync("access/password", formData).GetAwaiter().GetResult();
|
client.PutAsync("access/password", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Permissions / ACLs
|
// Permissions / ACLs
|
||||||
@@ -437,7 +609,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (queryParts.Count > 0)
|
if (queryParts.Count > 0)
|
||||||
resource += "?" + string.Join("&", queryParts);
|
resource += "?" + string.Join("&", queryParts);
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
var response = client.GetAsync(resource).GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
// /access/permissions returns an object keyed by path, not an array
|
// /access/permissions returns an object keyed by path, not an array
|
||||||
@@ -454,6 +628,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
}
|
}
|
||||||
return result.ToArray();
|
return result.ToArray();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets (adds or updates) an ACL entry.
|
/// Sets (adds or updates) an ACL entry.
|
||||||
@@ -488,8 +667,15 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(users)) formData["users"] = users!;
|
if (!string.IsNullOrEmpty(users)) formData["users"] = users!;
|
||||||
if (!string.IsNullOrEmpty(groups)) formData["groups"] = groups!;
|
if (!string.IsNullOrEmpty(groups)) formData["groups"] = groups!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync("access/acl", formData).GetAwaiter().GetResult();
|
client.PutAsync("access/acl", formData).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class VmService
|
public class VmService
|
||||||
{
|
{
|
||||||
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
private readonly NodeService _nodeService = new NodeService();
|
private readonly NodeService _nodeService = new NodeService();
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that creates its own HTTP clients.</summary>
|
||||||
|
public VmService() { }
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance that uses the supplied HTTP client for all requests.</summary>
|
||||||
|
/// <param name="client">The HTTP client to use. The caller owns its lifetime.</param>
|
||||||
|
public VmService(IPveHttpClient client)
|
||||||
|
{
|
||||||
|
_injectedClient = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Read operations
|
// Read operations
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -55,11 +66,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
|
|
||||||
private PveVm[] GetVmsOnNode(PveSession session, string node)
|
private PveVm[] GetVmsOnNode(PveSession session, string node)
|
||||||
{
|
{
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu").GetAwaiter().GetResult();
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu").GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveVm[]>() ?? Array.Empty<PveVm>();
|
return data?.ToObject<PveVm[]>() ?? Array.Empty<PveVm>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a single VM by its ID on the specified node.
|
/// Returns a single VM by its ID on the specified node.
|
||||||
@@ -93,7 +111,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (vm == null) throw new ArgumentNullException(nameof(vm));
|
if (vm == null) throw new ArgumentNullException(nameof(vm));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vm.VmId}/status/current")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vm.VmId}/status/current")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
@@ -107,6 +127,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
vm.MaxMem = data["maxmem"]?.ToObject<long?>() ?? vm.MaxMem;
|
vm.MaxMem = data["maxmem"]?.ToObject<long?>() ?? vm.MaxMem;
|
||||||
vm.MaxDisk = data["maxdisk"]?.ToObject<long?>() ?? vm.MaxDisk;
|
vm.MaxDisk = data["maxdisk"]?.ToObject<long?>() ?? vm.MaxDisk;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the full configuration of a VM.
|
/// Returns the full configuration of a VM.
|
||||||
@@ -119,12 +144,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?.ToObject<PveVmConfig>() ?? new PveVmConfig();
|
return data?.ToObject<PveVmConfig>() ?? new PveVmConfig();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Configuration mutation
|
// Configuration mutation
|
||||||
@@ -143,13 +175,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData)
|
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Disk import
|
// Disk import
|
||||||
@@ -199,12 +238,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
[disk] = diskValue
|
[disk] = diskValue
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
// POST (not PUT) because import-from triggers a background task
|
// POST (not PUT) because import-from triggers a background task
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/config", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@@ -222,7 +268,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var formData = config.ToDictionary(
|
var formData = config.ToDictionary(
|
||||||
kvp => kvp.Key,
|
kvp => kvp.Key,
|
||||||
kvp => kvp.Value?.ToString() ?? string.Empty);
|
kvp => kvp.Value?.ToString() ?? string.Empty);
|
||||||
@@ -230,6 +278,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Starts a VM. Returns the task UPID.</summary>
|
/// <summary>Starts a VM. Returns the task UPID.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -259,11 +312,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (timeoutSeconds.HasValue)
|
if (timeoutSeconds.HasValue)
|
||||||
formData["timeout"] = timeoutSeconds.Value.ToString();
|
formData["timeout"] = timeoutSeconds.Value.ToString();
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/shutdown", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/shutdown", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Resets a VM (hard reset). Returns the task UPID.</summary>
|
/// <summary>Resets a VM (hard reset). Returns the task UPID.</summary>
|
||||||
/// <param name="session">The authenticated PVE session.</param>
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
@@ -303,11 +363,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
var purgeParam = purge ? "?purge=1" : "?purge=0";
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}{purgeParam}")
|
var response = client.DeleteAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}{purgeParam}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clones a VM. Returns the task UPID.
|
/// Clones a VM. Returns the task UPID.
|
||||||
@@ -339,11 +406,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(name)) formData["name"] = name!;
|
if (!string.IsNullOrEmpty(name)) formData["name"] = name!;
|
||||||
if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!;
|
if (!string.IsNullOrEmpty(targetNode)) formData["target"] = targetNode!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/clone", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/clone", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Migrates a VM to another node. Returns the task UPID.
|
/// Migrates a VM to another node. Returns the task UPID.
|
||||||
@@ -370,11 +444,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["online"] = online ? "1" : "0"
|
["online"] = online ? "1" : "0"
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/migrate", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/migrate", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resizes a disk attached to a VM. Returns the task UPID.
|
/// Resizes a disk attached to a VM. Returns the task UPID.
|
||||||
@@ -404,11 +485,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["size"] = size
|
["size"] = size
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/resize", formData)
|
var response = client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/resize", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Private helpers
|
// Private helpers
|
||||||
@@ -419,11 +507,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/{action}")
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/status/{action}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PveTask ParseTask(string response, string node)
|
private static PveTask ParseTask(string response, string node)
|
||||||
{
|
{
|
||||||
@@ -449,7 +544,7 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/ping").GetAwaiter().GetResult();
|
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/ping").GetAwaiter().GetResult();
|
||||||
@@ -459,6 +554,10 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -469,13 +568,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/network-get-interfaces")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/network-get-interfaces")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
var result = data?["result"];
|
var result = data?["result"];
|
||||||
return result?.ToObject<PveGuestNetworkInterface[]>() ?? Array.Empty<PveGuestNetworkInterface>();
|
return result?.ToObject<PveGuestNetworkInterface[]>() ?? Array.Empty<PveGuestNetworkInterface>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes a command inside the guest via the QEMU guest agent.
|
/// Executes a command inside the guest via the QEMU guest agent.
|
||||||
@@ -488,7 +594,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(command)) throw new ArgumentNullException(nameof(command));
|
if (string.IsNullOrWhiteSpace(command)) throw new ArgumentNullException(nameof(command));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var data = new Dictionary<string, string>
|
var data = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["command"] = command
|
["command"] = command
|
||||||
@@ -506,6 +614,11 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var pid = JObject.Parse(response)["data"]?["pid"]?.ToObject<int>() ?? 0;
|
var pid = JObject.Parse(response)["data"]?["pid"]?.ToObject<int>() ?? 0;
|
||||||
return pid;
|
return pid;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the status/result of a guest agent exec command by PID.
|
/// Gets the status/result of a guest agent exec command by PID.
|
||||||
@@ -515,11 +628,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/exec-status?pid={pid}")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/exec-status?pid={pid}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return JObject.Parse(response)["data"] as JObject ?? new JObject();
|
return JObject.Parse(response)["data"] as JObject ?? new JObject();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Disk operations
|
// Disk operations
|
||||||
@@ -544,11 +664,18 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (!string.IsNullOrEmpty(format))
|
if (!string.IsNullOrEmpty(format))
|
||||||
formData["format"] = format!;
|
formData["format"] = format!;
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/move_disk", formData)
|
var response = client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/move_disk", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
return ParseTask(response, node);
|
return ParseTask(response, node);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unlinks (detaches) disks from a VM.
|
/// Unlinks (detaches) disks from a VM.
|
||||||
@@ -566,10 +693,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (force)
|
if (force)
|
||||||
formData["force"] = "1";
|
formData["force"] = "1";
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/unlink", formData)
|
client.PutAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/unlink", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Guest agent — extended operations
|
// Guest agent — extended operations
|
||||||
@@ -583,13 +717,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/get-osinfo")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/get-osinfo")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
var result = data?["result"];
|
var result = data?["result"];
|
||||||
return result?.ToObject<PveGuestOsInfo>();
|
return result?.ToObject<PveGuestOsInfo>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves filesystem information from the QEMU guest agent.
|
/// Retrieves filesystem information from the QEMU guest agent.
|
||||||
@@ -599,13 +740,20 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/get-fsinfo")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/get-fsinfo")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
var result = data?["result"];
|
var result = data?["result"];
|
||||||
return result?.ToObject<PveGuestFsInfo[]>() ?? Array.Empty<PveGuestFsInfo>();
|
return result?.ToObject<PveGuestFsInfo[]>() ?? Array.Empty<PveGuestFsInfo>();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a file from the guest filesystem via the QEMU guest agent.
|
/// Reads a file from the guest filesystem via the QEMU guest agent.
|
||||||
@@ -616,12 +764,19 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
if (string.IsNullOrWhiteSpace(file)) throw new ArgumentNullException(nameof(file));
|
if (string.IsNullOrWhiteSpace(file)) throw new ArgumentNullException(nameof(file));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/file-read?file={Uri.EscapeDataString(file)}")
|
var response = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/file-read?file={Uri.EscapeDataString(file)}")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
var data = JObject.Parse(response)["data"];
|
var data = JObject.Parse(response)["data"];
|
||||||
return data?["content"]?.ToString() ?? string.Empty;
|
return data?["content"]?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes content to a file on the guest filesystem via the QEMU guest agent.
|
/// Writes content to a file on the guest filesystem via the QEMU guest agent.
|
||||||
@@ -638,10 +793,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["content"] = content
|
["content"] = content
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/file-write", formData)
|
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/file-write", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets a user password inside the guest via the QEMU guest agent.
|
/// Sets a user password inside the guest via the QEMU guest agent.
|
||||||
@@ -660,10 +822,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (crypted)
|
if (crypted)
|
||||||
formData["crypted"] = "1";
|
formData["crypted"] = "1";
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/set-user-password", formData)
|
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/set-user-password", formData)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Triggers an fstrim operation inside the guest via the QEMU guest agent.
|
/// Triggers an fstrim operation inside the guest via the QEMU guest agent.
|
||||||
@@ -673,10 +842,17 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
if (session == null) throw new ArgumentNullException(nameof(session));
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
if (string.IsNullOrWhiteSpace(node)) throw new ArgumentNullException(nameof(node));
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/fstrim")
|
client.PostAsync($"nodes/{Uri.EscapeDataString(node)}/qemu/{vmid}/agent/fstrim")
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// OVA Upload
|
// OVA Upload
|
||||||
@@ -710,7 +886,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
["content"] = "import"
|
["content"] = "import"
|
||||||
};
|
};
|
||||||
|
|
||||||
using var client = new PveHttpClient(session);
|
IPveHttpClient client = _injectedClient ?? new PveHttpClient(session);
|
||||||
|
try
|
||||||
|
{
|
||||||
var response = client.UploadFileAsync(
|
var response = client.UploadFileAsync(
|
||||||
$"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/upload",
|
$"nodes/{Uri.EscapeDataString(node)}/storage/{Uri.EscapeDataString(storage)}/upload",
|
||||||
ovaPath,
|
ovaPath,
|
||||||
@@ -722,5 +900,10 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
var upid = root["data"]?.ToString() ?? string.Empty;
|
var upid = root["data"]?.ToString() ?? string.Empty;
|
||||||
return new PveTask { Upid = upid, Node = node, Status = "running" };
|
return new PveTask { Upid = upid, Node = node, Status = "running" };
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_injectedClient == null) client.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace PSProxmoxVE.Cmdlets.Tasks
|
|||||||
/// <para type="description">Filter tasks by VM ID.</para>
|
/// <para type="description">Filter tasks by VM ID.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter(Mandatory = false, HelpMessage = "Filter tasks by VM ID.")]
|
[Parameter(Mandatory = false, HelpMessage = "Filter tasks by VM ID.")]
|
||||||
|
[ValidateRange(100, 999999999)]
|
||||||
public int? VmId { get; set; }
|
public int? VmId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
@@ -49,7 +50,7 @@ namespace PSProxmoxVE.Cmdlets.Templates
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(node)) continue;
|
if (string.IsNullOrEmpty(node)) continue;
|
||||||
|
|
||||||
var json = client.GetAsync($"nodes/{node}/qemu").GetAwaiter().GetResult();
|
var json = client.GetAsync($"nodes/{Uri.EscapeDataString(node)}/qemu").GetAwaiter().GetResult();
|
||||||
var root = JObject.Parse(json);
|
var root = JObject.Parse(json);
|
||||||
var data = root["data"] as JArray ?? new JArray();
|
var data = root["data"] as JArray ?? new JArray();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;net10.0;net48</TargetFrameworks>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<LangVersion>10.0</LangVersion>
|
<LangVersion>10.0</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>PSProxmoxVE</RootNamespace>
|
<RootNamespace>PSProxmoxVE</RootNamespace>
|
||||||
@@ -12,16 +12,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PSProxmoxVE.Core\PSProxmoxVE.Core.csproj" />
|
<ProjectReference Include="..\PSProxmoxVE.Core\PSProxmoxVE.Core.csproj" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'net48'">
|
|
||||||
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
|
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
|
||||||
<PackageReference Include="System.Management.Automation" Version="7.5.0" PrivateAssets="all" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="PSProxmoxVE.psd1" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="PSProxmoxVE.psd1" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<None Include="PSProxmoxVE.format.ps1xml" CopyToOutputDirectory="PreserveNewest" />
|
<None Include="PSProxmoxVE.format.ps1xml" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
|||||||
@@ -0,0 +1,424 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class BackupServiceTests
|
||||||
|
{
|
||||||
|
private const string Node = "pve1";
|
||||||
|
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve.example.com", 8006, false,
|
||||||
|
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// CreateBackup
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBackup_CallsPostAsync_ReturnsUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:vzdump:100:root@pam:";
|
||||||
|
var json = $@"{{""data"": ""{upid}""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["vmid"] = "100",
|
||||||
|
["storage"] = "local",
|
||||||
|
["mode"] = "snapshot",
|
||||||
|
["compress"] = "zstd"
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.CreateBackup(CreateSession(), Node, config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(upid, task.Upid);
|
||||||
|
Assert.Equal(Node, task.Node);
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
$"nodes/{Node}/vzdump",
|
||||||
|
It.Is<Dictionary<string, string>>(d => d["vmid"] == "100" && d["storage"] == "local")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBackup_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
var config = new Dictionary<string, string> { ["vmid"] = "100" };
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.CreateBackup(null!, Node, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBackup_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("config", () => service.CreateBackup(CreateSession(), Node, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// GetBackupJobs
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBackupJobs_ReturnsJobArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": [
|
||||||
|
{
|
||||||
|
""id"": ""backup-001"",
|
||||||
|
""type"": ""vzdump"",
|
||||||
|
""enabled"": 1,
|
||||||
|
""schedule"": ""0 2 * * *"",
|
||||||
|
""storage"": ""pbs-store"",
|
||||||
|
""mode"": ""snapshot"",
|
||||||
|
""vmid"": ""100,101,102"",
|
||||||
|
""compress"": ""zstd""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""id"": ""backup-002"",
|
||||||
|
""type"": ""vzdump"",
|
||||||
|
""enabled"": 0,
|
||||||
|
""schedule"": ""0 3 * * 0"",
|
||||||
|
""storage"": ""local"",
|
||||||
|
""mode"": ""stop"",
|
||||||
|
""all"": 1,
|
||||||
|
""compress"": ""lzo""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/backup"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var jobs = service.GetBackupJobs(CreateSession());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, jobs.Length);
|
||||||
|
|
||||||
|
Assert.Equal("backup-001", jobs[0].Id);
|
||||||
|
Assert.Equal("vzdump", jobs[0].Type);
|
||||||
|
Assert.Equal(1, jobs[0].Enabled);
|
||||||
|
Assert.Equal("0 2 * * *", jobs[0].Schedule);
|
||||||
|
Assert.Equal("pbs-store", jobs[0].Storage);
|
||||||
|
Assert.Equal("snapshot", jobs[0].Mode);
|
||||||
|
Assert.Equal("100,101,102", jobs[0].VmId);
|
||||||
|
|
||||||
|
Assert.Equal("backup-002", jobs[1].Id);
|
||||||
|
Assert.Equal(0, jobs[1].Enabled);
|
||||||
|
Assert.Equal(1, jobs[1].All);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBackupJobs_EmptyData_ReturnsEmptyArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": []}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/backup"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var jobs = service.GetBackupJobs(CreateSession());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(jobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBackupJobs_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.GetBackupJobs(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// GetBackupJob (single)
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBackupJob_ReturnsJob()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": {
|
||||||
|
""id"": ""backup-001"",
|
||||||
|
""type"": ""vzdump"",
|
||||||
|
""enabled"": 1,
|
||||||
|
""schedule"": ""0 2 * * *"",
|
||||||
|
""storage"": ""pbs-store"",
|
||||||
|
""mode"": ""snapshot"",
|
||||||
|
""vmid"": ""100""
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/backup/backup-001"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var job = service.GetBackupJob(CreateSession(), "backup-001");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(job);
|
||||||
|
Assert.Equal("backup-001", job!.Id);
|
||||||
|
Assert.Equal("snapshot", job.Mode);
|
||||||
|
Assert.Equal("pbs-store", job.Storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBackupJob_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.GetBackupJob(null!, "backup-001"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetBackupJob_NullId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("id", () => service.GetBackupJob(CreateSession(), null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// CreateBackupJob
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBackupJob_CallsPostAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": null}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["schedule"] = "0 2 * * *",
|
||||||
|
["storage"] = "local",
|
||||||
|
["mode"] = "snapshot",
|
||||||
|
["vmid"] = "100",
|
||||||
|
["compress"] = "zstd"
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.CreateBackupJob(CreateSession(), config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
"cluster/backup",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["schedule"] == "0 2 * * *" &&
|
||||||
|
d["storage"] == "local")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBackupJob_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
var config = new Dictionary<string, string> { ["vmid"] = "100" };
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.CreateBackupJob(null!, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateBackupJob_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("config", () => service.CreateBackupJob(CreateSession(), null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// UpdateBackupJob
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateBackupJob_CallsPutAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": null}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PutAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["schedule"] = "0 4 * * *",
|
||||||
|
["enabled"] = "1"
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.UpdateBackupJob(CreateSession(), "backup-001", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PutAsync(
|
||||||
|
"cluster/backup/backup-001",
|
||||||
|
It.Is<Dictionary<string, string>>(d => d["schedule"] == "0 4 * * *")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateBackupJob_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
var config = new Dictionary<string, string> { ["enabled"] = "1" };
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.UpdateBackupJob(null!, "id", config));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateBackupJob_NullId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
var config = new Dictionary<string, string> { ["enabled"] = "1" };
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("id", () => service.UpdateBackupJob(CreateSession(), null!, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateBackupJob_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("config", () => service.UpdateBackupJob(CreateSession(), "id", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// RemoveBackupJob
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveBackupJob_CallsDeleteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": null}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.RemoveBackupJob(CreateSession(), "backup-001");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.DeleteAsync("cluster/backup/backup-001"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveBackupJob_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.RemoveBackupJob(null!, "id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveBackupJob_NullId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("id", () => service.RemoveBackupJob(CreateSession(), null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// GetNotBackedUp
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNotBackedUp_ReturnsJArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": [
|
||||||
|
{ ""vmid"": 100, ""name"": ""webserver"", ""type"": ""qemu"" },
|
||||||
|
{ ""vmid"": 200, ""name"": ""database"", ""type"": ""lxc"" }
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/backup-info/not-backed-up"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = service.GetNotBackedUp(CreateSession());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<JArray>(result);
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Equal(100, result[0]["vmid"]!.Value<int>());
|
||||||
|
Assert.Equal("webserver", result[0]["name"]!.Value<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNotBackedUp_EmptyData_ReturnsEmptyJArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": []}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/backup-info/not-backed-up"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new BackupService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = service.GetNotBackedUp(CreateSession());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNotBackedUp_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new BackupService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.GetNotBackedUp(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Constructor
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_NullClient_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>("client", () => new BackupService(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class CloudInitServiceTests
|
||||||
|
{
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve.example.com", 8006, false,
|
||||||
|
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCloudInitConfig_ReturnsConfig()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": {
|
||||||
|
""ciuser"": ""ubuntu"",
|
||||||
|
""ipconfig0"": ""ip=dhcp"",
|
||||||
|
""nameserver"": ""8.8.8.8"",
|
||||||
|
""searchdomain"": ""example.com"",
|
||||||
|
""sshkeys"": ""ssh-rsa%20AAAA...%20user%40host"",
|
||||||
|
""boot"": ""order=scsi0;net0"",
|
||||||
|
""cores"": 4,
|
||||||
|
""memory"": 8192
|
||||||
|
}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("nodes/pve1/qemu/100/config")).ReturnsAsync(json);
|
||||||
|
var service = new CloudInitService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var config = service.GetCloudInitConfig(CreateSession(), "pve1", 100);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(config);
|
||||||
|
Assert.Equal("ubuntu", config.CiUser);
|
||||||
|
Assert.Equal("ip=dhcp", config.IpConfig0);
|
||||||
|
Assert.Equal("8.8.8.8", config.Nameserver);
|
||||||
|
Assert.Equal("example.com", config.Searchdomain);
|
||||||
|
Assert.Equal("ssh-rsa%20AAAA...%20user%40host", config.SshKeys);
|
||||||
|
mockClient.Verify(c => c.GetAsync("nodes/pve1/qemu/100/config"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCloudInitConfig_ExcludesNonCiFields()
|
||||||
|
{
|
||||||
|
// Arrange — response includes non-CI fields that should be ignored
|
||||||
|
var json = @"{""data"": {
|
||||||
|
""ciuser"": ""admin"",
|
||||||
|
""cores"": 4,
|
||||||
|
""memory"": 8192,
|
||||||
|
""boot"": ""order=scsi0""
|
||||||
|
}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("nodes/pve1/qemu/200/config")).ReturnsAsync(json);
|
||||||
|
var service = new CloudInitService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var config = service.GetCloudInitConfig(CreateSession(), "pve1", 200);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("admin", config.CiUser);
|
||||||
|
// Non-CI fields should not appear in the result
|
||||||
|
Assert.Null(config.IpConfig0);
|
||||||
|
Assert.Null(config.Nameserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetCloudInitConfig_CallsPutAsyncWithCorrectResource()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PutAsync(
|
||||||
|
"nodes/pve1/qemu/100/config",
|
||||||
|
It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync("{}");
|
||||||
|
var service = new CloudInitService(mockClient.Object);
|
||||||
|
var config = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["ciuser"] = "ubuntu",
|
||||||
|
["ipconfig0"] = "ip=dhcp"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.SetCloudInitConfig(CreateSession(), "pve1", 100, config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PutAsync(
|
||||||
|
"nodes/pve1/qemu/100/config",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["ciuser"] == "ubuntu" && d["ipconfig0"] == "ip=dhcp")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegenerateCloudInitImage_CallsGetAsyncAndReturnsData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": ""#cloud-config\nuser: ubuntu\npassword: secret\n""}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("nodes/pve1/qemu/100/cloudinit/dump?type=user"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
var service = new CloudInitService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = service.RegenerateCloudInitImage(CreateSession(), "pve1", 100);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("#cloud-config", result);
|
||||||
|
Assert.Contains("ubuntu", result);
|
||||||
|
mockClient.Verify(c => c.GetAsync("nodes/pve1/qemu/100/cloudinit/dump?type=user"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCloudInitConfig_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new CloudInitService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.GetCloudInitConfig(null!, "pve1", 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetCloudInitConfig_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new CloudInitService(mockClient.Object);
|
||||||
|
var config = new Dictionary<string, object> { ["ciuser"] = "test" };
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.SetCloudInitConfig(null!, "pve1", 100, config));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegenerateCloudInitImage_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new CloudInitService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.RegenerateCloudInitImage(null!, "pve1", 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_NullClient_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new CloudInitService(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class ClusterServiceTests
|
||||||
|
{
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve.example.com", 8006, false,
|
||||||
|
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetClusterStatus_ReturnsStatusArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": [
|
||||||
|
{""type"": ""cluster"", ""name"": ""pve-cluster"", ""nodes"": 3, ""quorate"": 1, ""version"": 5},
|
||||||
|
{""type"": ""node"", ""name"": ""pve1"", ""online"": 1, ""local"": 1, ""nodeid"": 1, ""ip"": ""10.0.0.1""},
|
||||||
|
{""type"": ""node"", ""name"": ""pve2"", ""online"": 1, ""local"": 0, ""nodeid"": 2, ""ip"": ""10.0.0.2""}
|
||||||
|
]}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/status")).ReturnsAsync(json);
|
||||||
|
var service = new ClusterService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var statuses = service.GetClusterStatus(CreateSession());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, statuses.Length);
|
||||||
|
Assert.Equal("cluster", statuses[0].Type);
|
||||||
|
Assert.Equal("pve-cluster", statuses[0].Name);
|
||||||
|
Assert.Equal(3, statuses[0].Nodes);
|
||||||
|
Assert.Equal(1, statuses[0].Quorate);
|
||||||
|
Assert.Equal("node", statuses[1].Type);
|
||||||
|
Assert.Equal("pve1", statuses[1].Name);
|
||||||
|
Assert.Equal(1, statuses[1].Online);
|
||||||
|
Assert.Equal("10.0.0.1", statuses[1].Ip);
|
||||||
|
Assert.Equal("node", statuses[2].Type);
|
||||||
|
Assert.Equal("pve2", statuses[2].Name);
|
||||||
|
mockClient.Verify(c => c.GetAsync("cluster/status"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetClusterResources_ReturnsResourcesArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": [
|
||||||
|
{""id"": ""node/pve1"", ""type"": ""node"", ""node"": ""pve1"", ""status"": ""online"", ""maxcpu"": 16, ""maxmem"": 68719476736, ""cpu"": 0.05},
|
||||||
|
{""id"": ""qemu/100"", ""type"": ""qemu"", ""node"": ""pve1"", ""name"": ""test-vm"", ""status"": ""running"", ""vmid"": 100, ""maxcpu"": 4, ""maxmem"": 8589934592},
|
||||||
|
{""id"": ""storage/local"", ""type"": ""storage"", ""node"": ""pve1"", ""status"": ""available"", ""maxdisk"": 107374182400, ""disk"": 53687091200}
|
||||||
|
]}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/resources")).ReturnsAsync(json);
|
||||||
|
var service = new ClusterService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var resources = service.GetClusterResources(CreateSession());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, resources.Length);
|
||||||
|
Assert.Equal("node/pve1", resources[0].Id);
|
||||||
|
Assert.Equal("node", resources[0].Type);
|
||||||
|
Assert.Equal("qemu/100", resources[1].Id);
|
||||||
|
Assert.Equal("test-vm", resources[1].Name);
|
||||||
|
Assert.Equal(100, resources[1].VmId);
|
||||||
|
Assert.Equal("storage/local", resources[2].Id);
|
||||||
|
Assert.Equal("storage", resources[2].Type);
|
||||||
|
mockClient.Verify(c => c.GetAsync("cluster/resources"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetClusterResources_WithTypeFilter_AppendsQueryString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": [
|
||||||
|
{""id"": ""qemu/100"", ""type"": ""qemu"", ""node"": ""pve1"", ""name"": ""test-vm"", ""status"": ""running"", ""vmid"": 100}
|
||||||
|
]}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/resources?type=vm")).ReturnsAsync(json);
|
||||||
|
var service = new ClusterService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var resources = service.GetClusterResources(CreateSession(), "vm");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(resources);
|
||||||
|
Assert.Equal("qemu/100", resources[0].Id);
|
||||||
|
mockClient.Verify(c => c.GetAsync("cluster/resources?type=vm"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetClusterStatus_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new ClusterService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.GetClusterStatus(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetClusterResources_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new ClusterService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.GetClusterResources(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_NullClient_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new ClusterService(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class NodeServiceTests
|
||||||
|
{
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve.example.com", 8006, false,
|
||||||
|
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNodes_ReturnsArrayOfPveNode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": [
|
||||||
|
{""node"": ""pve1"", ""status"": ""online"", ""maxcpu"": 16, ""maxmem"": 68719476736},
|
||||||
|
{""node"": ""pve2"", ""status"": ""online"", ""maxcpu"": 8, ""maxmem"": 34359738368}
|
||||||
|
]}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("nodes")).ReturnsAsync(json);
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var nodes = service.GetNodes(CreateSession());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, nodes.Length);
|
||||||
|
Assert.Equal("pve1", nodes[0].Name);
|
||||||
|
Assert.Equal("online", nodes[0].Status);
|
||||||
|
Assert.Equal(16, nodes[0].CpuCount);
|
||||||
|
Assert.Equal(68719476736L, nodes[0].MemoryTotal);
|
||||||
|
Assert.Equal("pve2", nodes[1].Name);
|
||||||
|
Assert.Equal(8, nodes[1].CpuCount);
|
||||||
|
mockClient.Verify(c => c.GetAsync("nodes"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNodeStatus_ReturnsPveNodeStatus()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": {
|
||||||
|
""node"": ""pve1"", ""status"": ""online"", ""maxcpu"": 16,
|
||||||
|
""maxmem"": 68719476736, ""mem"": 17179869184,
|
||||||
|
""uptime"": 864000, ""cpu"": 0.125
|
||||||
|
}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("nodes/pve1/status")).ReturnsAsync(json);
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var status = service.GetNodeStatus(CreateSession(), "pve1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("pve1", status.Node);
|
||||||
|
Assert.Equal("online", status.Status);
|
||||||
|
Assert.Equal(0.125, status.CpuUsage);
|
||||||
|
Assert.Equal(68719476736L, status.MemoryTotal);
|
||||||
|
Assert.Equal(17179869184L, status.MemoryUsed);
|
||||||
|
Assert.Equal(864000L, status.Uptime);
|
||||||
|
mockClient.Verify(c => c.GetAsync("nodes/pve1/status"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNodeConfig_ReturnsJObject()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": {""description"": ""Primary node"", ""wakeonlan"": ""AA:BB:CC:DD:EE:FF""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("nodes/pve1/config")).ReturnsAsync(json);
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var config = service.GetNodeConfig(CreateSession(), "pve1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(config);
|
||||||
|
Assert.Equal("Primary node", config["description"]?.ToString());
|
||||||
|
Assert.Equal("AA:BB:CC:DD:EE:FF", config["wakeonlan"]?.ToString());
|
||||||
|
mockClient.Verify(c => c.GetAsync("nodes/pve1/config"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetNodeConfig_CallsPutAsyncWithCorrectResource()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PutAsync(
|
||||||
|
"nodes/pve1/config",
|
||||||
|
It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync("{}");
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["description"] = "Updated node"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.SetNodeConfig(CreateSession(), "pve1", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PutAsync(
|
||||||
|
"nodes/pve1/config",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["description"] == "Updated node")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNodeDns_ReturnsJObject()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": {""dns1"": ""8.8.8.8"", ""dns2"": ""8.8.4.4"", ""search"": ""example.com""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("nodes/pve1/dns")).ReturnsAsync(json);
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var dns = service.GetNodeDns(CreateSession(), "pve1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(dns);
|
||||||
|
Assert.Equal("8.8.8.8", dns["dns1"]?.ToString());
|
||||||
|
Assert.Equal("8.8.4.4", dns["dns2"]?.ToString());
|
||||||
|
Assert.Equal("example.com", dns["search"]?.ToString());
|
||||||
|
mockClient.Verify(c => c.GetAsync("nodes/pve1/dns"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetNodeDns_CallsPutAsyncWithCorrectResource()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PutAsync(
|
||||||
|
"nodes/pve1/dns",
|
||||||
|
It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync("{}");
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["dns1"] = "1.1.1.1",
|
||||||
|
["search"] = "lab.local"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.SetNodeDns(CreateSession(), "pve1", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PutAsync(
|
||||||
|
"nodes/pve1/dns",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["dns1"] == "1.1.1.1" && d["search"] == "lab.local")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StartAll_CallsPostAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": ""UPID:pve1:000ABC:00000001:5F1234AB:startall::root@pam:""}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(
|
||||||
|
"nodes/pve1/startall",
|
||||||
|
It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.StartAll(CreateSession(), "pve1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(task);
|
||||||
|
Assert.Contains("startall", task.Upid);
|
||||||
|
Assert.Equal("pve1", task.Node);
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
"nodes/pve1/startall",
|
||||||
|
It.IsAny<Dictionary<string, string>>()),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StopAll_CallsPostAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": ""UPID:pve1:000DEF:00000002:5F1234AC:stopall::root@pam:""}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(
|
||||||
|
"nodes/pve1/stopall",
|
||||||
|
It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.StopAll(CreateSession(), "pve1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(task);
|
||||||
|
Assert.Contains("stopall", task.Upid);
|
||||||
|
Assert.Equal("pve1", task.Node);
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
"nodes/pve1/stopall",
|
||||||
|
It.IsAny<Dictionary<string, string>>()),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNodes_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.GetNodes(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNodeStatus_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.GetNodeStatus(null!, "pve1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetNodeStatus_NullNode_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new NodeService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => service.GetNodeStatus(CreateSession(), null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_NullClient_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => new NodeService(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class PoolServiceTests
|
||||||
|
{
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve1.example.com", 8006, true, "PVE:root@pam:TEST_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPools_HappyPath_ReturnsArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": [
|
||||||
|
{ ""poolid"": ""dev-pool"", ""comment"": ""Development resources"" },
|
||||||
|
{ ""poolid"": ""prod-pool"", ""comment"": ""Production resources"" }
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("pools"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new PoolService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pools = service.GetPools(session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, pools.Length);
|
||||||
|
Assert.Equal("dev-pool", pools[0].PoolId);
|
||||||
|
Assert.Equal("Development resources", pools[0].Comment);
|
||||||
|
Assert.Equal("prod-pool", pools[1].PoolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPool_HappyPath_ReturnsSinglePool()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": {
|
||||||
|
""poolid"": ""dev-pool"",
|
||||||
|
""comment"": ""Development resources"",
|
||||||
|
""members"": [
|
||||||
|
{ ""id"": ""qemu/100"", ""node"": ""pve1"", ""type"": ""qemu"", ""vmid"": 100 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("pools/dev-pool"))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new PoolService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pool = service.GetPool(session, "dev-pool");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(pool);
|
||||||
|
Assert.Equal("dev-pool", pool.PoolId);
|
||||||
|
Assert.Equal("Development resources", pool.Comment);
|
||||||
|
Assert.NotNull(pool.Members);
|
||||||
|
Assert.Single(pool.Members);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreatePool_CallsPostAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync("pools", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync("{}");
|
||||||
|
|
||||||
|
var service = new PoolService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.CreatePool(session, "test-pool", "A test pool");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PostAsync("pools",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["poolid"] == "test-pool" &&
|
||||||
|
d["comment"] == "A test pool")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdatePool_CallsPutAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PutAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync("{}");
|
||||||
|
|
||||||
|
var service = new PoolService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "comment", "Updated comment" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.UpdatePool(session, "dev-pool", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PutAsync("pools/dev-pool",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["comment"] == "Updated comment")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemovePool_CallsDeleteAsync()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync("{}");
|
||||||
|
|
||||||
|
var service = new PoolService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.RemovePool(session, "dev-pool");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.DeleteAsync("pools/dev-pool"), Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class SnapshotServiceTests
|
||||||
|
{
|
||||||
|
private const string Node = "pve1";
|
||||||
|
private const int VmId = 100;
|
||||||
|
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve.example.com", 8006, false,
|
||||||
|
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSnapshots_ReturnsSnapshotArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": [
|
||||||
|
{
|
||||||
|
""name"": ""clean-install"",
|
||||||
|
""description"": ""Fresh OS install"",
|
||||||
|
""snaptime"": 1700000000,
|
||||||
|
""vmstate"": 0,
|
||||||
|
""parent"": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""name"": ""post-update"",
|
||||||
|
""description"": ""After apt upgrade"",
|
||||||
|
""snaptime"": 1700100000,
|
||||||
|
""vmstate"": 1,
|
||||||
|
""parent"": ""clean-install""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new SnapshotService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var snapshots = service.GetSnapshots(session, Node, VmId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, snapshots.Length);
|
||||||
|
Assert.Equal("clean-install", snapshots[0].Name);
|
||||||
|
Assert.Equal("Fresh OS install", snapshots[0].Description);
|
||||||
|
Assert.Equal(1700000000L, snapshots[0].SnapTime);
|
||||||
|
Assert.Equal(0, snapshots[0].VmState);
|
||||||
|
Assert.Null(snapshots[0].Parent);
|
||||||
|
|
||||||
|
Assert.Equal("post-update", snapshots[1].Name);
|
||||||
|
Assert.Equal(1, snapshots[1].VmState);
|
||||||
|
Assert.Equal("clean-install", snapshots[1].Parent);
|
||||||
|
|
||||||
|
mockClient.Verify(c => c.GetAsync($"nodes/{Node}/qemu/{VmId}/snapshot"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSnapshots_EmptyData_ReturnsEmptyArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{""data"": []}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new SnapshotService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var snapshots = service.GetSnapshots(CreateSession(), Node, VmId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(snapshots);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateSnapshot_CallsPostAsync_ReturnsUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:qmsnapshot:100:root@pam:";
|
||||||
|
var json = $@"{{""data"": ""{upid}""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new SnapshotService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.CreateSnapshot(CreateSession(), Node, VmId, "my-snap", "Test snapshot", vmstate: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(upid, task.Upid);
|
||||||
|
Assert.Equal(Node, task.Node);
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
$"nodes/{Node}/qemu/{VmId}/snapshot",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["snapname"] == "my-snap" &&
|
||||||
|
d["vmstate"] == "1" &&
|
||||||
|
d["description"] == "Test snapshot")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateSnapshot_WithoutDescription_OmitsDescriptionField()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:qmsnapshot:100:root@pam:";
|
||||||
|
var json = $@"{{""data"": ""{upid}""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new SnapshotService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.CreateSnapshot(CreateSession(), Node, VmId, "my-snap");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
!d.ContainsKey("description") &&
|
||||||
|
d["vmstate"] == "0")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveSnapshot_CallsDeleteAsync_ReturnsUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string upid = "UPID:pve1:000DEF:00000002:5F1234AC:qmdelsnap:100:root@pam:";
|
||||||
|
var json = $@"{{""data"": ""{upid}""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new SnapshotService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.RemoveSnapshot(CreateSession(), Node, VmId, "clean-install");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(upid, task.Upid);
|
||||||
|
Assert.Equal(Node, task.Node);
|
||||||
|
mockClient.Verify(c => c.DeleteAsync($"nodes/{Node}/qemu/{VmId}/snapshot/clean-install"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RollbackSnapshot_CallsPostAsync_ReturnsUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string upid = "UPID:pve1:000GHI:00000003:5F1234AD:qmrollback:100:root@pam:";
|
||||||
|
var json = $@"{{""data"": ""{upid}""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new SnapshotService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.RollbackSnapshot(CreateSession(), Node, VmId, "clean-install");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(upid, task.Upid);
|
||||||
|
Assert.Equal(Node, task.Node);
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
$"nodes/{Node}/qemu/{VmId}/snapshot/clean-install/rollback",
|
||||||
|
It.IsAny<Dictionary<string, string>>()),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSnapshots_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.GetSnapshots(null!, Node, VmId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateSnapshot_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.CreateSnapshot(null!, Node, VmId, "snap"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveSnapshot_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.RemoveSnapshot(null!, Node, VmId, "snap"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RollbackSnapshot_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new SnapshotService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.RollbackSnapshot(null!, Node, VmId, "snap"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,494 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class StorageServiceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IPveHttpClient> _mockClient;
|
||||||
|
private readonly StorageService _service;
|
||||||
|
private readonly PveSession _session;
|
||||||
|
|
||||||
|
public StorageServiceTests()
|
||||||
|
{
|
||||||
|
_mockClient = new Mock<IPveHttpClient>();
|
||||||
|
_service = new StorageService(_mockClient.Object);
|
||||||
|
_session = new PveSession(
|
||||||
|
"pve.example.com",
|
||||||
|
8006,
|
||||||
|
skipCertificateCheck: true,
|
||||||
|
apiToken: "root@pam!test=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// GetStorages
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorages_ClusterWide_ReturnsStorageArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("storage"))
|
||||||
|
.ReturnsAsync(@"{""data"":[
|
||||||
|
{""storage"":""local"",""type"":""dir"",""content"":""images,iso,vztmpl"",""total"":107374182400,""used"":53687091200,""avail"":53687091200,""enabled"":1,""shared"":0,""active"":1},
|
||||||
|
{""storage"":""local-lvm"",""type"":""lvmthin"",""content"":""images,rootdir"",""total"":214748364800,""used"":107374182400,""avail"":107374182400,""enabled"":1,""shared"":0,""active"":1}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetStorages(_session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("local", result[0].Storage);
|
||||||
|
Assert.Equal("dir", result[0].Type);
|
||||||
|
Assert.Equal(107374182400L, result[0].Total);
|
||||||
|
Assert.Equal("local-lvm", result[1].Storage);
|
||||||
|
Assert.Equal("lvmthin", result[1].Type);
|
||||||
|
_mockClient.Verify(c => c.GetAsync("storage"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorages_ByNode_QueriesNodeEndpoint()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage"))
|
||||||
|
.ReturnsAsync(@"{""data"":[{""storage"":""ceph-pool"",""type"":""rbd"",""content"":""images"",""enabled"":1,""shared"":1,""active"":1}]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetStorages(_session, "pve1");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal("ceph-pool", result[0].Storage);
|
||||||
|
Assert.Equal("rbd", result[0].Type);
|
||||||
|
_mockClient.Verify(c => c.GetAsync("nodes/pve1/storage"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorages_EmptyData_ReturnsEmptyArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("storage"))
|
||||||
|
.ReturnsAsync(@"{""data"":[]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetStorages(_session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorages_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetStorages(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// GetStorageContent
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageContent_ReturnsContentArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage/local/content"))
|
||||||
|
.ReturnsAsync(@"{""data"":[
|
||||||
|
{""volid"":""local:iso/debian-12.iso"",""content"":""iso"",""format"":""iso"",""size"":3909091328,""ctime"":1700000000},
|
||||||
|
{""volid"":""local:vztmpl/ubuntu-22.04-standard.tar.zst"",""content"":""vztmpl"",""format"":""tgz"",""size"":131072000,""ctime"":1700100000}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetStorageContent(_session, "pve1", "local");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("local:iso/debian-12.iso", result[0].VolId);
|
||||||
|
Assert.Equal("iso", result[0].Content);
|
||||||
|
Assert.Equal(3909091328L, result[0].Size);
|
||||||
|
Assert.Equal("local:vztmpl/ubuntu-22.04-standard.tar.zst", result[1].VolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageContent_WithContentTypeFilter_AppendsQueryParam()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage/local/content?content=iso"))
|
||||||
|
.ReturnsAsync(@"{""data"":[{""volid"":""local:iso/debian-12.iso"",""content"":""iso"",""format"":""iso"",""size"":3909091328}]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetStorageContent(_session, "pve1", "local", "iso");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal("iso", result[0].Content);
|
||||||
|
_mockClient.Verify(c => c.GetAsync("nodes/pve1/storage/local/content?content=iso"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageContent_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetStorageContent(null!, "pve1", "local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageContent_NullNode_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetStorageContent(_session, null!, "local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageContent_NullStorage_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetStorageContent(_session, "pve1", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// CreateStorage
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateStorage_ReturnsCreatedStorage()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["storage"] = "nfs-backup",
|
||||||
|
["type"] = "nfs",
|
||||||
|
["server"] = "192.168.1.10",
|
||||||
|
["export"] = "/mnt/backup",
|
||||||
|
["content"] = "backup"
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockClient.Setup(c => c.PostAsync("storage", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":{""storage"":""nfs-backup"",""type"":""nfs"",""content"":""backup"",""enabled"":1,""shared"":1}}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.CreateStorage(_session, config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("nfs-backup", result.Storage);
|
||||||
|
Assert.Equal("nfs", result.Type);
|
||||||
|
Assert.Equal("backup", result.Content);
|
||||||
|
_mockClient.Verify(c => c.PostAsync("storage", It.IsAny<Dictionary<string, string>>()), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateStorage_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateStorage(null!, new Dictionary<string, object>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateStorage_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateStorage(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// UpdateStorage
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateStorage_CallsPutWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, string> { ["content"] = "backup,images" };
|
||||||
|
_mockClient.Setup(c => c.PutAsync("storage/nfs-backup", config))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.UpdateStorage(_session, "nfs-backup", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("storage/nfs-backup", config), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateStorage_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.UpdateStorage(null!, "local", new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateStorage_NullStorage_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.UpdateStorage(_session, null!, new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateStorage_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.UpdateStorage(_session, "local", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// RemoveStorage
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveStorage_CallsDeleteWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.DeleteAsync("storage/nfs-backup"))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.RemoveStorage(_session, "nfs-backup");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.DeleteAsync("storage/nfs-backup"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveStorage_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveStorage(null!, "local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveStorage_NullStorage_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveStorage(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// GetStorageStatus
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageStatus_ReturnsStatus()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("nodes/pve1/storage/local/status"))
|
||||||
|
.ReturnsAsync(@"{""data"":{""total"":107374182400,""used"":53687091200,""avail"":53687091200,""active"":1,""enabled"":1,""shared"":0,""type"":""dir"",""content"":""images,iso""}}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetStorageStatus(_session, "pve1", "local");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(107374182400L, result.Total);
|
||||||
|
Assert.Equal(53687091200L, result.Used);
|
||||||
|
Assert.Equal(53687091200L, result.Available);
|
||||||
|
Assert.Equal(1, result.Active);
|
||||||
|
Assert.Equal("dir", result.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageStatus_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetStorageStatus(null!, "pve1", "local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageStatus_NullNode_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetStorageStatus(_session, null!, "local"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetStorageStatus_NullStorage_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetStorageStatus(_session, "pve1", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// RemoveContent
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveContent_CallsDeleteWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.DeleteAsync("nodes/pve1/storage/local/content/local%3Aiso%2Fdebian-12.iso"))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.RemoveContent(_session, "pve1", "local", "local:iso/debian-12.iso");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.DeleteAsync("nodes/pve1/storage/local/content/local%3Aiso%2Fdebian-12.iso"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveContent_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveContent(null!, "pve1", "local", "vol"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveContent_NullVolume_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveContent(_session, "pve1", "local", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// UpdateContent
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateContent_CallsPutWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, string> { ["notes"] = "Weekly backup" };
|
||||||
|
_mockClient.Setup(c => c.PutAsync(
|
||||||
|
"nodes/pve1/storage/local/content/local%3Abackup%2Fvzdump-qemu-100.vma.zst",
|
||||||
|
config))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.UpdateContent(_session, "pve1", "local", "local:backup/vzdump-qemu-100.vma.zst", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync(
|
||||||
|
"nodes/pve1/storage/local/content/local%3Abackup%2Fvzdump-qemu-100.vma.zst",
|
||||||
|
config), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateContent_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.UpdateContent(null!, "pve1", "local", "vol", new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateContent_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.UpdateContent(_session, "pve1", "local", "vol", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// UploadIso
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UploadIso_ReturnsTaskWithUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.UploadFileAsync(
|
||||||
|
"nodes/pve1/storage/local/upload",
|
||||||
|
"/tmp/debian-12.iso",
|
||||||
|
It.IsAny<Dictionary<string, string>>(),
|
||||||
|
null, null, null))
|
||||||
|
.ReturnsAsync(@"{""data"":""UPID:pve1:000AAA:00000001:65F00000:upload:local:root@pam:""}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.UploadIso(_session, "pve1", "local", "/tmp/debian-12.iso");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("UPID:pve1", result.Upid);
|
||||||
|
Assert.Equal("pve1", result.Node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UploadIso_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.UploadIso(null!, "pve1", "local", "/tmp/test.iso"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UploadIso_NullFilePath_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.UploadIso(_session, "pve1", "local", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// DownloadUrl
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DownloadUrl_ReturnsTaskWithUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PostAsync(
|
||||||
|
"nodes/pve1/storage/local/download-url",
|
||||||
|
It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":""UPID:pve1:000BBB:00000002:65F00001:download:local:root@pam:""}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.DownloadUrl(
|
||||||
|
_session, "pve1", "local",
|
||||||
|
"https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img",
|
||||||
|
"noble-server-cloudimg-amd64.img", "iso");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("UPID:pve1", result.Upid);
|
||||||
|
Assert.Equal("pve1", result.Node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DownloadUrl_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.DownloadUrl(null!, "pve1", "local", "https://example.com/f.iso", "f.iso", "iso"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DownloadUrl_NullUrl_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.DownloadUrl(_session, "pve1", "local", null!, "f.iso", "iso"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DownloadUrl_NullFilename_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.DownloadUrl(_session, "pve1", "local", "https://example.com/f.iso", null!, "iso"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DownloadUrl_NullContentType_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.DownloadUrl(_session, "pve1", "local", "https://example.com/f.iso", "f.iso", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
// AllocateDisk
|
||||||
|
// -----------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllocateDisk_ReturnsTaskWithUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["filename"] = "vm-200-disk-0",
|
||||||
|
["size"] = "32G",
|
||||||
|
["format"] = "qcow2"
|
||||||
|
};
|
||||||
|
_mockClient.Setup(c => c.PostAsync(
|
||||||
|
"nodes/pve1/storage/local-lvm/content",
|
||||||
|
config))
|
||||||
|
.ReturnsAsync(@"{""data"":""UPID:pve1:000CCC:00000003:65F00002:alloc:local-lvm:root@pam:""}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.AllocateDisk(_session, "pve1", "local-lvm", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Contains("UPID:pve1", result.Upid);
|
||||||
|
Assert.Equal("pve1", result.Node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllocateDisk_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.AllocateDisk(null!, "pve1", "local", new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllocateDisk_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.AllocateDisk(_session, "pve1", "local", null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Exceptions;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class TaskServiceTests
|
||||||
|
{
|
||||||
|
private const string TestNode = "pve1";
|
||||||
|
private const string TestUpid = "UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:";
|
||||||
|
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve1.example.com", 8006, true, "PVE:root@pam:TEST_TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTask_HappyPath_ReturnsCorrectFields()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": {
|
||||||
|
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
|
||||||
|
""type"": ""qmstart"",
|
||||||
|
""status"": ""stopped"",
|
||||||
|
""exitstatus"": ""OK"",
|
||||||
|
""node"": ""pve1"",
|
||||||
|
""starttime"": 1595000000,
|
||||||
|
""user"": ""root@pam"",
|
||||||
|
""id"": ""100""
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.GetTask(session, TestNode, TestUpid);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(TestUpid, task.Upid);
|
||||||
|
Assert.Equal("qmstart", task.Type);
|
||||||
|
Assert.Equal("stopped", task.Status);
|
||||||
|
Assert.Equal("OK", task.ExitStatus);
|
||||||
|
Assert.Equal(TestNode, task.Node);
|
||||||
|
Assert.Equal("root@pam", task.User);
|
||||||
|
Assert.Equal("100", task.Id);
|
||||||
|
Assert.True(task.IsSuccessful);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTask_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
service.GetTask(null!, TestNode, TestUpid));
|
||||||
|
Assert.Equal("session", ex.ParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTaskLog_HappyPath_ReturnsLogEntries()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": [
|
||||||
|
{ ""n"": 1, ""t"": ""starting task qmstart"" },
|
||||||
|
{ ""n"": 2, ""t"": ""VM 100 started"" },
|
||||||
|
{ ""n"": 3, ""t"": ""TASK OK"" }
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var logs = service.GetTaskLog(session, TestNode, TestUpid);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, logs.Length);
|
||||||
|
Assert.Equal(1, logs[0].LineNumber);
|
||||||
|
Assert.Equal("starting task qmstart", logs[0].Text);
|
||||||
|
Assert.Equal("TASK OK", logs[2].Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetTasks_HappyPath_ReturnsCorrectCount()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": [
|
||||||
|
{
|
||||||
|
""upid"": ""UPID:pve1:000001:00000001:5F1234AB:qmstart:100:root@pam:"",
|
||||||
|
""type"": ""qmstart"",
|
||||||
|
""status"": ""stopped"",
|
||||||
|
""exitstatus"": ""OK"",
|
||||||
|
""user"": ""root@pam"",
|
||||||
|
""id"": ""100""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
""upid"": ""UPID:pve1:000002:00000002:5F1234AC:qmstop:101:root@pam:"",
|
||||||
|
""type"": ""qmstop"",
|
||||||
|
""status"": ""running"",
|
||||||
|
""user"": ""root@pam"",
|
||||||
|
""id"": ""101""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.Is<string>(s => s.Contains("tasks?"))))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var tasks = service.GetTasks(session, TestNode, vmid: 100, typeFilter: "qmstart", limit: 10);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, tasks.Length);
|
||||||
|
Assert.Equal("qmstart", tasks[0].Type);
|
||||||
|
Assert.Equal(TestNode, tasks[0].Node);
|
||||||
|
Assert.Equal(TestNode, tasks[1].Node);
|
||||||
|
mockClient.Verify(c => c.GetAsync(It.Is<string>(s =>
|
||||||
|
s.Contains("limit=10") &&
|
||||||
|
s.Contains("vmid=100") &&
|
||||||
|
s.Contains("typefilter=qmstart"))), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WaitForTask_CompletesWithOK_ReturnsTask()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": {
|
||||||
|
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
|
||||||
|
""type"": ""qmstart"",
|
||||||
|
""status"": ""stopped"",
|
||||||
|
""exitstatus"": ""OK"",
|
||||||
|
""user"": ""root@pam""
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.WaitForTask(session, TestNode, TestUpid,
|
||||||
|
timeout: TimeSpan.FromSeconds(5),
|
||||||
|
pollInterval: TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("stopped", task.Status);
|
||||||
|
Assert.Equal("OK", task.ExitStatus);
|
||||||
|
Assert.True(task.IsSuccessful);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WaitForTask_FailedExitStatus_ThrowsPveTaskFailedException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var json = @"{
|
||||||
|
""data"": {
|
||||||
|
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
|
||||||
|
""type"": ""qmstart"",
|
||||||
|
""status"": ""stopped"",
|
||||||
|
""exitstatus"": ""ERROR: VM 100 already running"",
|
||||||
|
""user"": ""root@pam""
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var ex = Assert.Throws<PveTaskFailedException>(() =>
|
||||||
|
service.WaitForTask(session, TestNode, TestUpid,
|
||||||
|
timeout: TimeSpan.FromSeconds(5),
|
||||||
|
pollInterval: TimeSpan.FromSeconds(1)));
|
||||||
|
|
||||||
|
Assert.Equal(TestUpid, ex.Upid);
|
||||||
|
Assert.Equal("ERROR: VM 100 already running", ex.ExitStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WaitForTask_Timeout_ThrowsPveTaskTimeoutException()
|
||||||
|
{
|
||||||
|
// Arrange — task stays "running" forever
|
||||||
|
var json = @"{
|
||||||
|
""data"": {
|
||||||
|
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmstart:100:root@pam:"",
|
||||||
|
""type"": ""qmstart"",
|
||||||
|
""status"": ""running"",
|
||||||
|
""user"": ""root@pam""
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act & Assert — use very short timeout so the test completes quickly
|
||||||
|
var timeout = TimeSpan.FromMilliseconds(100);
|
||||||
|
var ex = Assert.Throws<PveTaskTimeoutException>(() =>
|
||||||
|
service.WaitForTask(session, TestNode, TestUpid,
|
||||||
|
timeout: timeout,
|
||||||
|
pollInterval: TimeSpan.FromMilliseconds(50)));
|
||||||
|
|
||||||
|
Assert.Equal(TestUpid, ex.Upid);
|
||||||
|
Assert.Equal(timeout, ex.Timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StopTask_CallsDeleteAsyncWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.DeleteAsync(It.IsAny<string>()))
|
||||||
|
.ReturnsAsync("{}");
|
||||||
|
|
||||||
|
var service = new TaskService(mockClient.Object);
|
||||||
|
var session = CreateSession();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
service.StopTask(session, TestNode, TestUpid);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var encodedUpid = Uri.EscapeDataString(TestUpid);
|
||||||
|
mockClient.Verify(c => c.DeleteAsync(
|
||||||
|
It.Is<string>(s => s == $"nodes/{TestNode}/tasks/{encodedUpid}")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class TemplateServiceTests
|
||||||
|
{
|
||||||
|
private const string Node = "pve1";
|
||||||
|
private const int VmId = 9000;
|
||||||
|
|
||||||
|
private static PveSession CreateSession()
|
||||||
|
{
|
||||||
|
return new PveSession("pve.example.com", 8006, false,
|
||||||
|
"root@pam!testtoken=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateTemplate_CallsPostAsync_ReturnsUpid()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string upid = "UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:";
|
||||||
|
var json = $@"{{""data"": ""{upid}""}}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TemplateService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.CreateTemplate(CreateSession(), Node, VmId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(upid, task.Upid);
|
||||||
|
Assert.Equal(Node, task.Node);
|
||||||
|
mockClient.Verify(c => c.PostAsync(
|
||||||
|
$"nodes/{Node}/qemu/{VmId}/template",
|
||||||
|
It.IsAny<Dictionary<string, string>>()),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateTemplate_WithTaskObject_ParsesCorrectly()
|
||||||
|
{
|
||||||
|
// Arrange — some PVE versions return a full task object instead of a bare UPID string
|
||||||
|
var json = @"{
|
||||||
|
""data"": {
|
||||||
|
""upid"": ""UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:"",
|
||||||
|
""type"": ""qmtemplate"",
|
||||||
|
""status"": ""running"",
|
||||||
|
""node"": ""pve1"",
|
||||||
|
""user"": ""root@pam""
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.PostAsync(It.IsAny<string>(), It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(json);
|
||||||
|
|
||||||
|
var service = new TemplateService(mockClient.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var task = service.CreateTemplate(CreateSession(), Node, VmId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("UPID:pve1:000ABC:00000001:5F1234AB:qmtemplate:9000:root@pam:", task.Upid);
|
||||||
|
Assert.Equal("qmtemplate", task.Type);
|
||||||
|
Assert.Equal(Node, task.Node);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateTemplate_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new TemplateService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("session", () => service.CreateTemplate(null!, Node, VmId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateTemplate_NullNode_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new TemplateService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("node", () => service.CreateTemplate(CreateSession(), null!, VmId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateTemplate_EmptyNode_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
var service = new TemplateService(new Mock<IPveHttpClient>().Object);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>("node", () => service.CreateTemplate(CreateSession(), " ", VmId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_NullClient_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>("client", () => new TemplateService(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,882 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using PSProxmoxVE.Core.Authentication;
|
||||||
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
|
{
|
||||||
|
public class UserServiceTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IPveHttpClient> _mockClient;
|
||||||
|
private readonly UserService _service;
|
||||||
|
private readonly PveSession _session;
|
||||||
|
|
||||||
|
public UserServiceTests()
|
||||||
|
{
|
||||||
|
_mockClient = new Mock<IPveHttpClient>();
|
||||||
|
_service = new UserService(_mockClient.Object);
|
||||||
|
_session = new PveSession(
|
||||||
|
"pve.example.com",
|
||||||
|
8006,
|
||||||
|
skipCertificateCheck: true,
|
||||||
|
apiToken: "root@pam!test=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee");
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Users
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetUsers_ReturnsUserArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/users"))
|
||||||
|
.ReturnsAsync(@"{""data"":[
|
||||||
|
{""userid"":""root@pam"",""enable"":1,""email"":""root@example.com"",""firstname"":""Root"",""lastname"":""Admin""},
|
||||||
|
{""userid"":""deploy@pve"",""enable"":1,""groups"":""admins"",""comment"":""Deployment account""}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetUsers(_session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("root@pam", result[0].UserId);
|
||||||
|
Assert.Equal("root@example.com", result[0].Email);
|
||||||
|
Assert.Equal("Root", result[0].FirstName);
|
||||||
|
Assert.Equal("deploy@pve", result[1].UserId);
|
||||||
|
Assert.Equal("admins", result[1].Groups);
|
||||||
|
_mockClient.Verify(c => c.GetAsync("access/users"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetUsers_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetUsers(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetUser_ReturnsSingleUser()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/users/root%40pam"))
|
||||||
|
.ReturnsAsync(@"{""data"":{""email"":""root@example.com"",""firstname"":""Root"",""lastname"":""Admin"",""enable"":1}}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetUser(_session, "root@pam");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("root@pam", result.UserId);
|
||||||
|
Assert.Equal("root@example.com", result.Email);
|
||||||
|
Assert.Equal(1, result.Enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetUser_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetUser(null!, "root@pam"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetUser_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetUser(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateUser_PostsFormData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["email"] = "newuser@example.com",
|
||||||
|
["firstname"] = "New",
|
||||||
|
["lastname"] = "User"
|
||||||
|
};
|
||||||
|
_mockClient.Setup(c => c.PostAsync("access/users", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.CreateUser(_session, "newuser@pve", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PostAsync("access/users",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["userid"] == "newuser@pve" &&
|
||||||
|
d["email"] == "newuser@example.com")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateUser_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateUser(null!, "user@pve"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateUser_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateUser(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetUser_PutsFormData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["email"] = "updated@example.com",
|
||||||
|
["comment"] = "Updated account"
|
||||||
|
};
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/users/deploy%40pve", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.SetUser(_session, "deploy@pve", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/users/deploy%40pve",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["email"] == "updated@example.com" &&
|
||||||
|
d["comment"] == "Updated account")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetUser_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.SetUser(null!, "user@pve", new Dictionary<string, object>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetUser_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.SetUser(_session, null!, new Dictionary<string, object>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetUser_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.SetUser(_session, "user@pve", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveUser_CallsDeleteWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.DeleteAsync("access/users/deploy%40pve"))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.RemoveUser(_session, "deploy@pve");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.DeleteAsync("access/users/deploy%40pve"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveUser_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveUser(null!, "user@pve"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveUser_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveUser(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// API Tokens
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiTokens_ReturnsTokenArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/users/root%40pam/token"))
|
||||||
|
.ReturnsAsync(@"{""data"":[
|
||||||
|
{""tokenid"":""automation"",""privsep"":1,""expire"":0,""comment"":""CI/CD token""},
|
||||||
|
{""tokenid"":""monitoring"",""privsep"":0,""expire"":1735689600}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetApiTokens(_session, "root@pam");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("automation", result[0].TokenId);
|
||||||
|
Assert.Equal("root@pam", result[0].UserId);
|
||||||
|
Assert.Equal(1, result[0].PrivilegeSeparation);
|
||||||
|
Assert.Equal("monitoring", result[1].TokenId);
|
||||||
|
Assert.Equal("root@pam", result[1].UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiTokens_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetApiTokens(null!, "root@pam"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiTokens_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetApiTokens(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateApiToken_ReturnsTokenWithSecret()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PostAsync(
|
||||||
|
"access/users/root%40pam/token/deploy",
|
||||||
|
It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":{""full-tokenid"":""root@pam!deploy"",""value"":""aabbccdd-1122-3344-5566-778899aabbcc"",""info"":{""privsep"":1,""expire"":0}}}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.CreateApiToken(_session, "root@pam", "deploy", comment: "Deploy token", privilegeSeparation: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("root@pam", result.UserId);
|
||||||
|
Assert.Equal("deploy", result.TokenId);
|
||||||
|
Assert.Equal("aabbccdd-1122-3344-5566-778899aabbcc", result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateApiToken_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.CreateApiToken(null!, "root@pam", "token1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateApiToken_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.CreateApiToken(_session, null!, "token1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateApiToken_NullTokenId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.CreateApiToken(_session, "root@pam", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveApiToken_CallsDeleteWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.DeleteAsync("access/users/root%40pam/token/automation"))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.RemoveApiToken(_session, "root@pam", "automation");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.DeleteAsync("access/users/root%40pam/token/automation"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveApiToken_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.RemoveApiToken(null!, "root@pam", "token1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveApiToken_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.RemoveApiToken(_session, null!, "token1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveApiToken_NullTokenId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.RemoveApiToken(_session, "root@pam", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateApiToken_CallsPutWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, string> { ["comment"] = "Updated comment" };
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/users/root%40pam/token/automation", config))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.UpdateApiToken(_session, "root@pam", "automation", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/users/root%40pam/token/automation", config), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateApiToken_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateApiToken(null!, "root@pam", "token1", new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateApiToken_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateApiToken(_session, "root@pam", "token1", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Roles
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRoles_ReturnsRoleArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/roles"))
|
||||||
|
.ReturnsAsync(@"{""data"":[
|
||||||
|
{""roleid"":""Administrator"",""privs"":""Datastore.Allocate,Datastore.AllocateSpace,Datastore.Audit"",""special"":1},
|
||||||
|
{""roleid"":""PVEVMAdmin"",""privs"":""VM.Allocate,VM.Config.Disk,VM.Config.CPU"",""special"":1},
|
||||||
|
{""roleid"":""CustomOps"",""privs"":""VM.PowerMgmt,VM.Console"",""special"":0}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetRoles(_session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, result.Length);
|
||||||
|
Assert.Equal("Administrator", result[0].RoleId);
|
||||||
|
Assert.Equal(1, result[0].Special);
|
||||||
|
Assert.Equal("CustomOps", result[2].RoleId);
|
||||||
|
Assert.Equal(0, result[2].Special);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetRoles_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetRoles(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateRole_PostsFormData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PostAsync("access/roles", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.CreateRole(_session, "BackupOperator", "Datastore.Audit,Datastore.AllocateSpace");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PostAsync("access/roles",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["roleid"] == "BackupOperator" &&
|
||||||
|
d["privs"] == "Datastore.Audit,Datastore.AllocateSpace")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateRole_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateRole(null!, "role1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateRole_NullRoleId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateRole(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateRole_PutsPrivileges()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/roles/BackupOperator", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.UpdateRole(_session, "BackupOperator", "Datastore.Audit,Datastore.AllocateSpace,Datastore.Allocate");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/roles/BackupOperator",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["privs"] == "Datastore.Audit,Datastore.AllocateSpace,Datastore.Allocate")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateRole_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateRole(null!, "role1", "privs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateRole_NullRoleId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateRole(_session, null!, "privs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateRole_NullPrivileges_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateRole(_session, "role1", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveRole_CallsDeleteWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.DeleteAsync("access/roles/BackupOperator"))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.RemoveRole(_session, "BackupOperator");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.DeleteAsync("access/roles/BackupOperator"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveRole_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveRole(null!, "role1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveRole_NullRoleId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveRole(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Groups
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetGroups_ReturnsGroupArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/groups"))
|
||||||
|
.ReturnsAsync(@"{""data"":[
|
||||||
|
{""groupid"":""admins"",""comment"":""System administrators"",""users"":""root@pam,admin@pve""},
|
||||||
|
{""groupid"":""operators"",""comment"":""Operations team""}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetGroups(_session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Equal("admins", result[0].GroupId);
|
||||||
|
Assert.Equal("System administrators", result[0].Comment);
|
||||||
|
Assert.Equal("root@pam,admin@pve", result[0].Users);
|
||||||
|
Assert.Equal("operators", result[1].GroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetGroups_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetGroups(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateGroup_PostsFormData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PostAsync("access/groups", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.CreateGroup(_session, "devops", "DevOps engineering team");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PostAsync("access/groups",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["groupid"] == "devops" &&
|
||||||
|
d["comment"] == "DevOps engineering team")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateGroup_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateGroup(null!, "group1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateGroup_NullGroupId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.CreateGroup(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateGroup_PutsConfig()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, string> { ["comment"] = "Updated description" };
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/groups/devops", config))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.UpdateGroup(_session, "devops", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/groups/devops", config), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateGroup_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateGroup(null!, "group1", new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateGroup_NullGroupId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateGroup(_session, null!, new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateGroup_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateGroup(_session, "group1", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveGroup_CallsDeleteWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.DeleteAsync("access/groups/devops"))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.RemoveGroup(_session, "devops");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.DeleteAsync("access/groups/devops"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveGroup_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveGroup(null!, "group1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveGroup_NullGroupId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveGroup(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Domains / Realms
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDomains_ReturnsDomainArray()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/domains"))
|
||||||
|
.ReturnsAsync(@"{""data"":[
|
||||||
|
{""realm"":""pam"",""type"":""pam"",""comment"":""Linux PAM standard authentication"",""default"":0},
|
||||||
|
{""realm"":""pve"",""type"":""pve"",""comment"":""Proxmox VE authentication server"",""default"":1},
|
||||||
|
{""realm"":""corp-ldap"",""type"":""ldap"",""comment"":""Corporate LDAP""}
|
||||||
|
]}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetDomains(_session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, result.Length);
|
||||||
|
Assert.Equal("pam", result[0].Realm);
|
||||||
|
Assert.Equal("pam", result[0].Type);
|
||||||
|
Assert.Equal("pve", result[1].Realm);
|
||||||
|
Assert.Equal(1, result[1].Default);
|
||||||
|
Assert.Equal("corp-ldap", result[2].Realm);
|
||||||
|
Assert.Equal("ldap", result[2].Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDomains_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetDomains(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDomain_PostsConfig()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["realm"] = "corp-ad",
|
||||||
|
["type"] = "ad",
|
||||||
|
["server1"] = "dc01.corp.local",
|
||||||
|
["domain"] = "corp.local"
|
||||||
|
};
|
||||||
|
_mockClient.Setup(c => c.PostAsync("access/domains", config))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.CreateDomain(_session, config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PostAsync("access/domains", config), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDomain_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.CreateDomain(null!, new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateDomain_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.CreateDomain(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateDomain_PutsConfig()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var config = new Dictionary<string, string> { ["comment"] = "Updated AD realm" };
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/domains/corp-ad", config))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.UpdateDomain(_session, "corp-ad", config);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/domains/corp-ad", config), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateDomain_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateDomain(null!, "pam", new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateDomain_NullRealm_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateDomain(_session, null!, new Dictionary<string, string>()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateDomain_NullConfig_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.UpdateDomain(_session, "pam", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDomain_CallsDeleteWithCorrectPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.DeleteAsync("access/domains/corp-ad"))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.RemoveDomain(_session, "corp-ad");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.DeleteAsync("access/domains/corp-ad"), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDomain_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveDomain(null!, "pam"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RemoveDomain_NullRealm_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.RemoveDomain(_session, null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Password
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangePassword_PutsCredentials()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/password", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.ChangePassword(_session, "deploy@pve", "newSecureP@ss!");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/password",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["userid"] == "deploy@pve" &&
|
||||||
|
d["password"] == "newSecureP@ss!")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangePassword_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.ChangePassword(null!, "user@pve", "pass"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangePassword_NullUserId_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.ChangePassword(_session, null!, "pass"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangePassword_NullPassword_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.ChangePassword(_session, "user@pve", null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Permissions / ACLs
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPermissions_ReturnsPermissionsFromObjectResponse()
|
||||||
|
{
|
||||||
|
// Arrange — the PVE API returns permissions as a path-keyed object
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/permissions"))
|
||||||
|
.ReturnsAsync(@"{""data"":{
|
||||||
|
""/"":{ ""Datastore.Audit"":1, ""VM.Audit"":1 },
|
||||||
|
""/nodes/pve1"":{ ""Sys.Console"":1 }
|
||||||
|
}}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetPermissions(_session);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Length);
|
||||||
|
Assert.Contains(result, p => p.Path == "/");
|
||||||
|
Assert.Contains(result, p => p.Path == "/nodes/pve1");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPermissions_WithUserIdFilter_AppendsQueryParam()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/permissions?userid=deploy%40pve"))
|
||||||
|
.ReturnsAsync(@"{""data"":{""/"":{}}}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetPermissions(_session, userId: "deploy@pve");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.GetAsync("access/permissions?userid=deploy%40pve"), Times.Once);
|
||||||
|
Assert.Single(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPermissions_WithPathFilter_AppendsQueryParam()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.GetAsync("access/permissions?path=%2Fvms%2F100"))
|
||||||
|
.ReturnsAsync(@"{""data"":{}}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = _service.GetPermissions(_session, path: "/vms/100");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.GetAsync("access/permissions?path=%2Fvms%2F100"), Times.Once);
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetPermissions_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => _service.GetPermissions(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetPermission_PutsAclData()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/acl", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.SetPermission(_session, "/vms/100", "PVEVMAdmin", users: "deploy@pve", propagate: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/acl",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["path"] == "/vms/100" &&
|
||||||
|
d["roles"] == "PVEVMAdmin" &&
|
||||||
|
d["users"] == "deploy@pve" &&
|
||||||
|
d["propagate"] == "1" &&
|
||||||
|
d["delete"] == "0")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetPermission_WithGroupAndDelete_PutsCorrectFlags()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_mockClient.Setup(c => c.PutAsync("access/acl", It.IsAny<Dictionary<string, string>>()))
|
||||||
|
.ReturnsAsync(@"{""data"":null}");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_service.SetPermission(_session, "/", "Administrator", groups: "admins", propagate: false, delete: true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_mockClient.Verify(c => c.PutAsync("access/acl",
|
||||||
|
It.Is<Dictionary<string, string>>(d =>
|
||||||
|
d["path"] == "/" &&
|
||||||
|
d["roles"] == "Administrator" &&
|
||||||
|
d["groups"] == "admins" &&
|
||||||
|
d["propagate"] == "0" &&
|
||||||
|
d["delete"] == "1")),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetPermission_NullSession_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.SetPermission(null!, "/", "Admin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetPermission_NullPath_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.SetPermission(_session, null!, "Admin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetPermission_NullRoles_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
|
_service.SetPermission(_session, "/", null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user