mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-09-04 03:05:32 +00:00
Merge pull request #93 from GoodOlClint/test/cluster-join-link0
fix: New-PveCluster -Wait blocks until quorate; make cluster failures diagnosable Adds ClusterConfigService.WaitForQuorum with a -Timeout parameter (D014), so Add-PveClusterMember no longer hits "cluster not ready - no quorum?" after a completed create task. Adds diagnose-cluster.sh, which captures corosync and pmxcfs state plus PVE task logs from both nodes before cleanup destroys them.
This commit is contained in:
@@ -53,6 +53,11 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
keep_vms:
|
||||||
|
description: 'Skip cleanup so the nested PVE VMs survive for inspection'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -183,6 +188,13 @@ jobs:
|
|||||||
PVETEST_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }}
|
PVETEST_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }}
|
||||||
run: bash ${SCRIPTS_DIR}/run-integration.sh test ${{ matrix.pve_version }}
|
run: bash ${SCRIPTS_DIR}/run-integration.sh test ${{ matrix.pve_version }}
|
||||||
|
|
||||||
|
- name: Diagnose cluster state
|
||||||
|
if: failure()
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PVE_PASSWORD: ${{ secrets.PVE_TEST_PASSWORD }}
|
||||||
|
run: bash ${SCRIPTS_DIR}/diagnose-cluster.sh ${{ matrix.pve_version }}
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -193,7 +205,7 @@ jobs:
|
|||||||
# ── Cleanup: destroy all VMs (always runs) ──────────────────────
|
# ── Cleanup: destroy all VMs (always runs) ──────────────────────
|
||||||
cleanup:
|
cleanup:
|
||||||
needs: [provision, test]
|
needs: [provision, test]
|
||||||
if: always() && needs.provision.result != 'skipped' && github.actor != 'dependabot[bot]'
|
if: always() && needs.provision.result != 'skipped' && github.actor != 'dependabot[bot]' && !inputs.keep_vms
|
||||||
runs-on: psproxmoxve
|
runs-on: psproxmoxve
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
container:
|
container:
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `New-PveCluster -Wait` now blocks until the cluster reports quorum, not merely until the creation task finishes. PVE's create task returns before corosync converges (~6s earlier in testing), so the natural `New-PveCluster -Wait` → `Add-PveClusterMember` sequence failed with `cluster not ready - no quorum?`. Adds `-Timeout` (seconds, default 60, range 1-3600) following the `-Wait` timeout convention used by `Stop-PveContainer` and `Reset-PveVm`. See `DECISIONS.md` D014.
|
||||||
|
|
||||||
## [0.2.0] - 2026-05-22
|
## [0.2.0] - 2026-05-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -407,3 +407,57 @@ public List<Dictionary<string, object?>>? Members { get; set; }
|
|||||||
// or
|
// or
|
||||||
[OutputType(typeof(PSObject))]
|
[OutputType(typeof(PSObject))]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D014 — New-PveCluster -Wait blocks until the cluster is quorate
|
||||||
|
|
||||||
|
**Status**: Active
|
||||||
|
**Finding refs**: (none — found via integration run 172, 2026-09-01)
|
||||||
|
**Resolved in scan**: n/a
|
||||||
|
|
||||||
|
### Decision
|
||||||
|
`New-PveCluster -Wait` returns only after the cluster reports quorum, not merely when the
|
||||||
|
create task completes. `ClusterConfigService.WaitForQuorum` implements the wait; it polls
|
||||||
|
`GET /cluster/status` for the `cluster` entry with `quorate = 1` and tolerates transient API
|
||||||
|
errors while corosync and pmxcfs restart.
|
||||||
|
|
||||||
|
The wait is bounded and throws `TimeoutException` on expiry. It follows the `-Wait` timeout
|
||||||
|
convention already used by `Stop-PveContainer`, `Reset-PveVm` and `New-PveBackup`:
|
||||||
|
`[ValidateRange(1, 3600)] public int Timeout` with a default (60 here), **no `0 = infinite`**.
|
||||||
|
|
||||||
|
Note there are two distinct timeout conventions in this module; do not mix them:
|
||||||
|
- **`-Wait` waits** (`Timeout`, `int` with a default, range 1-3600, no infinite) — task/state waits.
|
||||||
|
- **HTTP client timeouts** (`TimeoutSeconds`, `int?`, range 0-int.MaxValue, `0 = infinite`) —
|
||||||
|
`Connect-PveServer`, `Send-PveFile`, `Invoke-PveStorageDownload`, which set `HttpClient.Timeout`.
|
||||||
|
|
||||||
|
A single-node cluster reaches quorum in seconds (~6 s observed), so a node still not quorate
|
||||||
|
after 60 s is broken rather than slow.
|
||||||
|
|
||||||
|
`-Wait` on every other cmdlet still means "wait for the task". Cluster creation is the
|
||||||
|
exception because the task completing does not make the cluster usable.
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
PVE's cluster-create task returns before corosync converges. Until the node is quorate it
|
||||||
|
rejects a join with `cluster not ready - no quorum?`, so the natural sequence
|
||||||
|
`New-PveCluster -Wait` → `Add-PveClusterMember` fails intermittently for every caller.
|
||||||
|
|
||||||
|
Observed on node A in integration run 172: the create task returned, corosync started ~1 s
|
||||||
|
later, and `node has quorum` appeared ~6 s after that. The integration test had guarded this
|
||||||
|
with `Start-Sleep -Seconds 5` — a fixed sleep against a longer, variable convergence — which
|
||||||
|
is why the cluster tests had never passed.
|
||||||
|
|
||||||
|
### Anti-pattern (do not reintroduce)
|
||||||
|
```powershell
|
||||||
|
# NEVER guard cluster convergence with a fixed sleep
|
||||||
|
New-PveCluster -ClusterName 'c1' -Wait
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
Add-PveClusterMember ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Correct pattern
|
||||||
|
```powershell
|
||||||
|
# -Wait already guarantees quorum; join immediately
|
||||||
|
New-PveCluster -ClusterName 'c1' -Wait
|
||||||
|
Add-PveClusterMember ...
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using PSProxmoxVE.Core.Authentication;
|
using PSProxmoxVE.Core.Authentication;
|
||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Exceptions;
|
||||||
using PSProxmoxVE.Core.Models.Cluster;
|
using PSProxmoxVE.Core.Models.Cluster;
|
||||||
using PSProxmoxVE.Core.Utilities;
|
using PSProxmoxVE.Core.Utilities;
|
||||||
|
|
||||||
@@ -16,6 +18,9 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
{
|
{
|
||||||
private readonly IPveHttpClient? _injectedClient;
|
private readonly IPveHttpClient? _injectedClient;
|
||||||
|
|
||||||
|
private static readonly TimeSpan DefaultQuorumTimeout = TimeSpan.FromSeconds(60);
|
||||||
|
private static readonly TimeSpan QuorumPollInterval = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ClusterConfigService"/> class.
|
/// Initializes a new instance of the <see cref="ClusterConfigService"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -378,6 +383,48 @@ namespace PSProxmoxVE.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blocks until the cluster reports quorum (GET /cluster/status, quorate = 1).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="session">The authenticated PVE session.</param>
|
||||||
|
/// <param name="timeout">Maximum time to wait. Defaults to 60 seconds.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// The cluster-create task completes before corosync converges; until the node
|
||||||
|
/// is quorate it rejects joins with "cluster not ready - no quorum?". API errors
|
||||||
|
/// during that window are transient and are retried until the deadline.
|
||||||
|
/// </remarks>
|
||||||
|
/// <exception cref="TimeoutException">Quorum was not reached before the deadline.</exception>
|
||||||
|
public void WaitForQuorum(PveSession session, TimeSpan? timeout = null)
|
||||||
|
{
|
||||||
|
if (session == null) throw new ArgumentNullException(nameof(session));
|
||||||
|
|
||||||
|
var effectiveTimeout = timeout ?? DefaultQuorumTimeout;
|
||||||
|
var deadline = DateTime.UtcNow.Add(effectiveTimeout);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var entry in GetClusterStatus(session))
|
||||||
|
{
|
||||||
|
if (string.Equals(entry.Type, "cluster", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& entry.Quorate == 1)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (PveApiException)
|
||||||
|
{
|
||||||
|
// pmxcfs and corosync restart while the cluster forms.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow >= deadline)
|
||||||
|
throw new TimeoutException(
|
||||||
|
$"Cluster did not reach quorum within {effectiveTimeout.TotalSeconds:0} seconds.");
|
||||||
|
|
||||||
|
Thread.Sleep(QuorumPollInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the next available VM/CT ID (GET /cluster/nextid).
|
/// Returns the next available VM/CT ID (GET /cluster/nextid).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Management.Automation;
|
using System.Management.Automation;
|
||||||
using PSProxmoxVE.Core.Models.Vms;
|
using PSProxmoxVE.Core.Models.Vms;
|
||||||
using PSProxmoxVE.Core.Services;
|
using PSProxmoxVE.Core.Services;
|
||||||
@@ -36,10 +37,15 @@ namespace PSProxmoxVE.Cmdlets.Cluster
|
|||||||
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
|
[Parameter(Mandatory = false, HelpMessage = "Corosync link addresses as key=value strings (e.g. 'link0=10.0.0.1').")]
|
||||||
public string[]? Links { get; set; }
|
public string[]? Links { get; set; }
|
||||||
|
|
||||||
/// <summary>Wait for the cluster creation task to complete.</summary>
|
/// <summary>Wait for the cluster creation task to complete and the cluster to reach quorum.</summary>
|
||||||
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete before returning.")]
|
[Parameter(Mandatory = false, HelpMessage = "Wait for the task to complete and the cluster to reach quorum before returning.")]
|
||||||
public SwitchParameter Wait { get; set; }
|
public SwitchParameter Wait { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Seconds to wait for the cluster to reach quorum when -Wait is used.</summary>
|
||||||
|
[Parameter(Mandatory = false, HelpMessage = "Timeout in seconds for -Wait (default 60).")]
|
||||||
|
[ValidateRange(1, 3600)]
|
||||||
|
public int Timeout { get; set; } = 60;
|
||||||
|
|
||||||
protected override void ProcessRecord()
|
protected override void ProcessRecord()
|
||||||
{
|
{
|
||||||
if (!ShouldProcess($"cluster '{ClusterName}'", "Create new cluster"))
|
if (!ShouldProcess($"cluster '{ClusterName}'", "Create new cluster"))
|
||||||
@@ -61,6 +67,11 @@ namespace PSProxmoxVE.Cmdlets.Cluster
|
|||||||
{
|
{
|
||||||
var taskService = new TaskService();
|
var taskService = new TaskService();
|
||||||
task = taskService.WaitForTask(session, node, upid);
|
task = taskService.WaitForTask(session, node, upid);
|
||||||
|
|
||||||
|
// The create task returns before corosync converges; the cluster
|
||||||
|
// rejects joins until it is quorate.
|
||||||
|
WriteVerbose("Waiting for cluster to reach quorum...");
|
||||||
|
service.WaitForQuorum(session, TimeSpan.FromSeconds(Timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteObject(task);
|
WriteObject(task);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using PSProxmoxVE.Core.Authentication;
|
using PSProxmoxVE.Core.Authentication;
|
||||||
using PSProxmoxVE.Core.Client;
|
using PSProxmoxVE.Core.Client;
|
||||||
|
using PSProxmoxVE.Core.Exceptions;
|
||||||
using PSProxmoxVE.Core.Services;
|
using PSProxmoxVE.Core.Services;
|
||||||
|
|
||||||
namespace PSProxmoxVE.Core.Tests.Services
|
namespace PSProxmoxVE.Core.Tests.Services
|
||||||
@@ -466,5 +468,49 @@ namespace PSProxmoxVE.Core.Tests.Services
|
|||||||
{
|
{
|
||||||
Assert.Throws<ArgumentNullException>(() => new ClusterConfigService(null!));
|
Assert.Throws<ArgumentNullException>(() => new ClusterConfigService(null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WaitForQuorum_ReturnsOnceClusterIsQuorate()
|
||||||
|
{
|
||||||
|
var notQuorate = @"{""data"": [{""type"": ""cluster"", ""name"": ""c1"", ""quorate"": 0}]}";
|
||||||
|
var quorate = @"{""data"": [{""type"": ""cluster"", ""name"": ""c1"", ""quorate"": 1}]}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.SetupSequence(c => c.GetAsync("cluster/status"))
|
||||||
|
.ReturnsAsync(notQuorate)
|
||||||
|
.ReturnsAsync(quorate);
|
||||||
|
var service = new ClusterConfigService(mockClient.Object);
|
||||||
|
|
||||||
|
service.WaitForQuorum(CreateSession(), TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
mockClient.Verify(c => c.GetAsync("cluster/status"), Times.Exactly(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WaitForQuorum_ThrowsWhenQuorumNeverReached()
|
||||||
|
{
|
||||||
|
var notQuorate = @"{""data"": [{""type"": ""cluster"", ""name"": ""c1"", ""quorate"": 0}]}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.Setup(c => c.GetAsync("cluster/status")).ReturnsAsync(notQuorate);
|
||||||
|
var service = new ClusterConfigService(mockClient.Object);
|
||||||
|
|
||||||
|
Assert.Throws<TimeoutException>(() =>
|
||||||
|
service.WaitForQuorum(CreateSession(), TimeSpan.FromMilliseconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WaitForQuorum_RetriesWhileTheApiIsRestarting()
|
||||||
|
{
|
||||||
|
var quorate = @"{""data"": [{""type"": ""cluster"", ""name"": ""c1"", ""quorate"": 1}]}";
|
||||||
|
var mockClient = new Mock<IPveHttpClient>();
|
||||||
|
mockClient.SetupSequence(c => c.GetAsync("cluster/status"))
|
||||||
|
.ThrowsAsync(new PveApiException(HttpStatusCode.InternalServerError,
|
||||||
|
"cluster not ready", "cluster/status", "GET"))
|
||||||
|
.ReturnsAsync(quorate);
|
||||||
|
var service = new ClusterConfigService(mockClient.Object);
|
||||||
|
|
||||||
|
service.WaitForQuorum(CreateSession(), TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
|
mockClient.Verify(c => c.GetAsync("cluster/status"), Times.Exactly(2));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,8 +112,11 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
|||||||
$result | Should -Not -BeNullOrEmpty
|
$result | Should -Not -BeNullOrEmpty
|
||||||
$script:ClusterCreated = $true
|
$script:ClusterCreated = $true
|
||||||
|
|
||||||
# Allow corosync to fully stabilize
|
# New-PveCluster -Wait returns only once the cluster is quorate (D014),
|
||||||
Start-Sleep -Seconds 5
|
# which is what makes the join below safe without a sleep.
|
||||||
|
$cluster = @(Get-PveClusterStatus -ErrorAction Stop) |
|
||||||
|
Where-Object { $_.Type -eq 'cluster' } | Select-Object -First 1
|
||||||
|
$cluster.Quorate | Should -Be 1 -Because 'node A must be quorate before node B can join'
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'Get-PveClusterStatus shows cluster formed' {
|
It 'Get-PveClusterStatus shows cluster formed' {
|
||||||
@@ -216,6 +219,12 @@ Describe 'Cluster Config & HA Lifecycle — Integration' -Tag 'Integration' {
|
|||||||
Start-Sleep -Seconds 3
|
Start-Sleep -Seconds 3
|
||||||
} while ([DateTime]::UtcNow -lt $deadline)
|
} while ([DateTime]::UtcNow -lt $deadline)
|
||||||
|
|
||||||
|
$cluster = @($status) | Where-Object { $_.Type -eq 'cluster' } | Select-Object -First 1
|
||||||
|
Write-Host "cluster: quorate=$($cluster.Quorate) nodes=$($cluster.Nodes); node B answers on $($script:HostB)"
|
||||||
|
foreach ($n in $nodeEntries) {
|
||||||
|
Write-Host " node=$($n.Name) nodeid=$($n.NodeId) ring0=$($n.Ip) online=$($n.Online) local=$($n.Local)"
|
||||||
|
}
|
||||||
|
|
||||||
@($nodeEntries).Count | Should -BeGreaterOrEqual 2
|
@($nodeEntries).Count | Should -BeGreaterOrEqual 2
|
||||||
@($onlineNodes).Count | Should -BeGreaterOrEqual 2
|
@($onlineNodes).Count | Should -BeGreaterOrEqual 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Dump corosync state from both nested PVE nodes after a cluster test failure.
|
||||||
|
#
|
||||||
|
# Usage: diagnose-cluster.sh [8|9]
|
||||||
|
#
|
||||||
|
# The PVE API reports a joined-but-offline node as online=0 with no further
|
||||||
|
# detail; corosync's own view lives only on the nodes, which the cleanup job
|
||||||
|
# destroys minutes later. Best-effort: never fails the caller.
|
||||||
|
#
|
||||||
|
# Required env vars:
|
||||||
|
# PVE_PASSWORD Root password for the nested PVE instances
|
||||||
|
#
|
||||||
|
# Optional env vars:
|
||||||
|
# CONFIG_FILE Test config JSON (default: $CACHE_DIR/work/config.json)
|
||||||
|
# CACHE_DIR Shared cache mount (default: /opt/pve-integration)
|
||||||
|
|
||||||
|
VERSION="${1:-9}"
|
||||||
|
CACHE_DIR="${CACHE_DIR:-/opt/pve-integration}"
|
||||||
|
CONFIG_FILE="${CONFIG_FILE:-$CACHE_DIR/work/config.json}"
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
echo "diagnose-cluster: no config at $CONFIG_FILE — nothing to inspect"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${PVE_PASSWORD:-}" ]]; then
|
||||||
|
echo "diagnose-cluster: PVE_PASSWORD unset — cannot reach the nodes"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=10)
|
||||||
|
|
||||||
|
dump_node() {
|
||||||
|
local label="$1" ip="$2"
|
||||||
|
echo
|
||||||
|
echo "══════════ $label ($ip) ══════════"
|
||||||
|
if [[ -z "$ip" || "$ip" == "null" ]]; then
|
||||||
|
echo " no address in $CONFIG_FILE"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
sshpass -p "$PVE_PASSWORD" ssh "${SSH_OPTS[@]}" "root@${ip}" bash -s <<'REMOTE' 2>&1 || echo " ssh to $ip failed (rc=$?)"
|
||||||
|
set +e
|
||||||
|
echo "--- hostname / resolution ---"
|
||||||
|
hostname -f
|
||||||
|
echo "hostname -i: $(hostname -i 2>&1)"
|
||||||
|
grep -vE '^\s*#' /etc/hosts | grep -vE '^\s*$'
|
||||||
|
echo
|
||||||
|
echo "--- addresses ---"
|
||||||
|
ip -4 -o addr show scope global
|
||||||
|
echo
|
||||||
|
echo "--- pmxcfs mode: cluster or local? ---"
|
||||||
|
# /etc/pve/corosync.conf is database-backed; pmxcfs only creates it when it
|
||||||
|
# starts with no config.db and imports /etc/corosync/corosync.conf. A surviving
|
||||||
|
# standalone config.db means silent local mode with corosync otherwise healthy.
|
||||||
|
dpkg-query -W pve-cluster corosync 2>&1
|
||||||
|
tr '\0' ' ' < "/proc/$(systemctl show pve-cluster -p MainPID --value)/cmdline" 2>&1; echo
|
||||||
|
findmnt --target /etc/pve --output TARGET,SOURCE,FSTYPE 2>&1
|
||||||
|
echo ".members: $(cat /etc/pve/.members 2>&1 | tr -d '\n')"
|
||||||
|
ls -la /var/lib/pve-cluster/ 2>&1
|
||||||
|
ls -la /var/lib/pve-cluster/backup/ 2>&1
|
||||||
|
if command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
sqlite3 -readonly /var/lib/pve-cluster/config.db \
|
||||||
|
"PRAGMA quick_check; SELECT name,version,writer,mtime,length(data) FROM tree WHERE name='corosync.conf';" 2>&1
|
||||||
|
else
|
||||||
|
echo "sqlite3 absent; corosync.conf occurrences in config.db: $(strings /var/lib/pve-cluster/config.db 2>/dev/null | grep -c '^corosync\.conf$')"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "--- corosync-cpgtool (pmxcfs joins dcdb/status CPG groups when clustered) ---"
|
||||||
|
corosync-cpgtool 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- corosync.conf ---"
|
||||||
|
cat /etc/pve/corosync.conf 2>&1 || cat /etc/corosync/corosync.conf 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- corosync-cfgtool -s ---"
|
||||||
|
corosync-cfgtool -s 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- pvecm status ---"
|
||||||
|
pvecm status 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- corosync service ---"
|
||||||
|
systemctl is-active corosync pve-cluster 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- journalctl -u corosync (last 60) ---"
|
||||||
|
journalctl -u corosync -n 60 --no-pager 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- journalctl -u pve-cluster (last 30) ---"
|
||||||
|
journalctl -u pve-cluster -n 30 --no-pager 2>&1
|
||||||
|
echo
|
||||||
|
echo "--- cluster task logs ---"
|
||||||
|
# "Cluster join aborted!" is generic; the reason is only in the task log.
|
||||||
|
find /var/log/pve/tasks -type f \( -name '*clusterjoin*' -o -name '*clustercreate*' \) \
|
||||||
|
-exec echo "== {} ==" \; -exec cat {} \; 2>&1 | tail -80
|
||||||
|
REMOTE
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Cluster diagnostics for PVE $VERSION ==="
|
||||||
|
node_a="$(jq -r ".pve${VERSION}.nodes.a.host // empty" "$CONFIG_FILE")"
|
||||||
|
node_b="$(jq -r ".pve${VERSION}.nodes.b.host // empty" "$CONFIG_FILE")"
|
||||||
|
|
||||||
|
dump_node "node A" "$node_a"
|
||||||
|
dump_node "node B" "$node_b"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== End cluster diagnostics ==="
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user