executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
pacemakerManages Pacemaker HA clusters with pcs and crmsh for resource provisioning, constraints, STONITH fencing, quorum configuration, and cluster lifecycle operations on two-node and multi-node setups.
Independent GitHub snapshot · Automated classification · Original documentation governs
Senior Linux infrastructure engineer managing Pacemaker 3.x HA clusters with PCS and crmsh for resource provisioning, constraints, STONITH fencing, quorum configuration, and cluster lifecycle operations.
pcs resource cleanup after node recovery to clear stale failcountsUse this skill when:
two_node quorum configurationcrm_report or analyze CIB stateAvoid this skill for:
Use linux-services for systemd unit configuration that complements OCF resource agents. Use corosync for network-layer membership and token configuration.
Create the cluster from scratch with Corosync as the messaging layer.
# On BOTH nodes — install packages (RHEL/Rocky/Alma)
sudo dnf install -y pcs pacemaker corosync
# Enable and start PCS daemon on both nodes
sudo systemctl enable --now pcsd
# Set identical hacluster password on every node
echo 'changeme' | sudo passwd hacluster --stdin
# Authenticate PCS between nodes from the first node
sudo pcs cluster auth node1 node2 -u hacluster -p 'changeme' --force
# Create and start the cluster
sudo pcs cluster setup --name mycluster node1 node2 --start --enable
Checkpoint: Run pcs status — all nodes should show as "Online" and the cluster state should be "Active." If any node is "Offline," check corosync logs: journalctl -u corosync --since "5 minutes ago".
Every production cluster MUST have fencing configured before adding workloads.
# Example: IPMI fence device for each node
sudo pcs stonith create fence-node1-ipmi \
stonith:fence_ipmilan \
ipaddr="192.168.1.100" \
login="admin" \
passwd="secret" \
pcmk_host_list="node1" \
power_wait="5" \
op monitor interval="60s"
sudo pcs stonith create fence-node2-ipmi \
stonith:fence_ipmilan \
ipaddr="192.168.1.101" \
login="admin" \
passwd="secret" \
pcmk_host_list="node2" \
power_wait="5" \
op monitor interval="60s"
# Enable fencing on the cluster
sudo pcs property set stonith-enabled=true
# Set fencing delay to prevent race conditions during multi-node outages
sudo pcs property set pcmk_delay_base=30
sudo pcs property set pcmk_delay_max=60
# Test each fence device BEFORE declaring production readiness
sudo pcs stonith fence node1 --force
sudo pcs stonith fence node2 --force
Checkpoint: Both fence devices show as "Started" in pcs status resources. Each test fence action successfully powers cycles the target node (or reports success from the management interface).
Two-node clusters without qdevice are susceptible to split-brain. Choose a strategy:
# OPTION A — Recommended for two-node clusters: qdevice (RHEL 9+)
sudo pcs qdevice setup alibi --init --nodes node1,node2
# OPTION B — Corosync-level two_node mode (works without qdevice)
# Edit corosync.conf on BOTH nodes:
# quorum { expected_votes: 2; two_node: 1 }
# For multi-node clusters (3+), the default majority-quorum policy works fine
# sudo pcs property set no-quorum-policy=stop # default — stops all resources
# Set safe no-quorum policy for qdevice-enabled clusters
sudo pcs property set no-quorum-policy=ignore
Checkpoint: pcs status corosync shows correct membership count. pcs status quorum confirms expected votes match and the node count provides adequate quorum.
Define virtual IP, application, and storage resources with proper ordering.
# Create a managed VirtualIP resource
sudo pcs resource create VirtualIP ocf:heartbeat:IPaddr2 \
ip="192.168.1.200" \
cidr_netmask="24" \
op monitor interval="30s"
# Create an Apache resource (runs on the same node as VirtualIP)
sudo pcs resource create Apache ocf:heartbeat:apache \
configfile="/etc/httpd/conf/httpd.conf" \
statusurl="http://localhost/server-status" \
op monitor interval="60s" \
op start timeout="60s" \
op stop timeout="60s"
# Create a web application resource (managed by Apache)
sudo pcs resource create WebSite ocf:heartbeat:apache \
configfile="/etc/httpd/conf.d/webapp.conf" \
op monitor interval="30s"
# Colocation: VirtualIP and Apache must be on the same node
sudo pcs constraint colocation add apache-with-vip VirtualIP Apache INFINITY
# Ordering: VirtualIP starts before Apache
sudo pcs constraint order VirtualIP then Apache kind=Mandatory
# Resource group alternative (bundles both colocation AND ordering)
# sudo pcs resource group add web-stack VirtualIP Apache WebSite
Checkpoint: pcs status resources shows all three resources running on the same node. Verify with crm_simulate -s that the constraint graph is consistent.
For databases and shared-storage workloads requiring active/passive failover.
# Create a DRBD resource with promote operation
sudo pcs resource create db-storage ocf:linbit:drbd \
drbd_resource=db0 \
op monitor interval="30s" role=Slave \
op promote interval="0s" role=Master
# Promote it to master/slave clone
sudo pcs resource master db-master db-storage \
master-max=1 \
master-node-max=1 \
clone-max=2 \
clone-node-max=1 \
notify=true
# Ensure application runs only on the current master
sudo pcs constraint colocation add db-app-with-db-master WebSite with db-master INFINITY
sudo pcs constraint order db-master then WebSite kind=Mandatory
Checkpoint: pcs status resources shows one node as Master and the other as Slave for db-master. Failover test: pcs cluster standby node1 — verify that the database promotes on node2 within the expected window.
BAD — Manual step-by-step without error handling, skipping fencing, no idempotency
# ❌ BAD: No error handling, skips critical security steps, not repeatable
sudo systemctl enable pcsd
sudo systemctl start pcsd
echo "changeme" | passwd hacluster --stdin
pcs cluster setup mycluster node1 node2
pcs property set stonith-enabled=false # ← NEVER DISABLE STONITH
pcs resource create VIP ocf:heartbeat:IPaddr2 ip=10.0.0.50 op monitor interval=30s
Problems:
set -euo pipefail — failures are silently ignoredGOOD — Idempotent, secure, production-ready with fencing and quorum
#!/usr/bin/env bash
# Production-ready Pacemaker cluster setup script
# Usage: ./setup-cluster.sh <cluster_name> node1 node2 [node3...] [--qdevice-node <third_node>]
set -euo pipefail
readonly CLUSTER_NAME="${1:?Usage: $0 <cluster_name> node1 node2 ...}"
readonly NODES=("${@:2}")
readonly QDEVICE_NODE="${4:-}" # Optional third node for qdevice
# ─── Validation ──────────────────────────────────────────────
if [[ ${#NODES[@]} -lt 2 ]]; then
echo "ERROR: At least 2 nodes required" >&2
exit 1
fi
echo "=== Pacemaker Cluster Setup: ${CLUSTER_NAME} ==="
echo "Nodes: ${NODES[*]}"
# ─── Step 1: Package Installation (idempotent) ──────────────
echo "[1/6] Installing packages..."
for node in "${NODES[@]}"; do
echo " → Ensuring packages on ${node}..."
ssh -o StrictHostKeyChecking=no root@"${node}" \
dnf install -y --setopt=install_weak_deps=false \
pcs pacemaker corosync || {
echo "ERROR: Failed to install packages on ${node}" >&2
exit 1
}
ssh root@"${node}" systemctl enable --now pcsd
done
# ─── Step 2: Authentication ─────────────────────────────────
echo "[2/6] Configuring node authentication..."
for node in "${NODES[@]}"; do
# Check if already authenticated by attempting a no-op command
if ! ssh root@"${node}" pcs cluster auth >/dev/null 2>&1; then
echo " → Authenticating with ${node}..."
ssh root@"${node}" passwd hacluster --stdin <<< "${HA_CLUSTER_PASSWORD:-changeme}"
fi
done
# ─── Step 3: Cluster Creation ───────────────────────────────
echo "[3/6] Creating cluster: ${CLUSTER_NAME}..."
if ! pcs cluster auth --with-force "${NODES[@]}" -u hacluster \
-p "${HA_CLUSTER_PASSWORD:-changeme}" >/dev/null 2>&1; then
echo "ERROR: Node authentication failed. Verify hacluster password matches." >&2
exit 1
fi
if ! pcs cluster setup --name "${CLUSTER_NAME}" "${NODES[@]}" \
--start --enable --with-force; then
echo "ERROR: Cluster setup failed" >&2
exit 1
fi
# ─── Step 4: STONITH Configuration ──────────────────────────
echo "[4/6] Configuring fencing..."
for i in "${!NODES[@]}"; do
local node="${NODES[$i]}"
local ipmi_addr="${IPMI_ADDRESSES[$i]:-}"
if [[ -n "${ipmi_addr}" ]]; then
echo " → Creating fence device for ${node} (IPMI: ${ipmi_addr})"
pcs stonith create "fence-${node}" \
stonith:fence_ipmilan \
ipaddr="${ipmi_addr}" \
login="${IPMI_LOGIN:-admin}" \
passwd="${IPMI_PASS:-changeme}" \
pcmk_host_list="${node}" \
power_wait="5" \
op monitor interval="60s" || true
fi
done
pcs property set stonith-enabled=true 2>/dev/null || true
pcs property set pcmk_delay_base=30 2>/dev/null || true
pcs property set pcmk_delay_max=60 2>/dev/null || true
# ─── Step 5: Quorum Configuration ───────────────────────────
echo "[5/6] Configuring quorum..."
if [[ ${#NODES[@]} -eq 2 ]] && [[ -z "${QDEVICE_NODE}" ]]; then
echo " WARNING: Two-node cluster without qdevice — split-brain risk"
echo " Apply corosync two_node=1 in quorum configuration."
pcs property set no-quorum-policy=stop 2>/dev/null || true
elif [[ -n "${QDEVICE_NODE}" ]]; then
echo " → Setting up qdevice on ${QDEVICE_NODE}"
pcs qdevice setup alibi --init --nodes "${NODES[*]}" 2>/dev/null || true
pcs property set no-quorum-policy=ignore 2>/dev/null || true
fi
# ─── Step 6: Verification ───────────────────────────────────
echo "[6/6] Verifying cluster state..."
sleep 5
if ! pcs cluster status >/dev/null 2>&1; then
echo "ERROR: Cluster is not responding. Check corosync and pacemaker logs." >&2
exit 1
fi
echo ""
echo "=== Cluster ${CLUSTER_NAME} Ready ==="
pcs status nodes || true
pcs stonith list || true
pcs property show stonith-enabled no-quorum-policy || true
BAD — Using resource groups when specific control is needed, missing ordering
# ❌ BAD: Resource group forces implicit ordering you may not want,
# and makes per-resource meta-options harder to manage
sudo pcs resource group add web-stack VirtualIP Apache WebSite
# Problem: You cannot set different monitor intervals or failure actions
# for individual resources within a group. The group acts as one unit.
# ❌ BAD: Colocation without ordering — resources run together but may start in wrong order
sudo pcs constraint colocation add apache-with-vip VirtualIP Apache INFINITY
# Problem: Apache might attempt to start before the VIP exists,
# causing bind failures even though they end up on the same node.
GOOD — Explicit colocation AND ordering with resource-level options
# ✅ GOOD: Separate resources with explicit constraints gives full control
# over per-resource monitor intervals, failure actions, and meta-options
# Create resources independently with tuned operations
sudo pcs resource create VirtualIP ocf:heartbeat:IPaddr2 \
ip="192.168.1.200" \
cidr_netmask="24" \
op monitor interval="30s" \
meta failure-timeout="90s"
sudo pcs resource create Apache ocf:heartbeat:apache \
configfile="/etc/httpd/conf/httpd.conf" \
op monitor interval="60s" timeout="60s" \
op start timeout="60s" \
op stop timeout="60s" \
meta failure-timeout="90s"
# Colocation constraint: run together on the same node
sudo pcs constraint colocation add apache-with-vip \
VirtualIP Apache INFINITY
# Ordering constraint: define startup sequence explicitly
sudo pcs constraint order VirtualIP then Apache \
kind=Mandatory score=INFINITY
# Location preference: prefer this node for VIP (for controlled failover)
sudo pcs constraint location VirtualIP prefers node1 INFINITY
# Verify the full constraint graph
pcs constraint list --full
crm_simulate -s # Simulate to verify no conflicts
Bash — Automated cluster diagnostics with safe recovery
#!/usr/bin/env bash
# Pacemaker cluster diagnostic and recovery script
# Generates comprehensive diagnostics and offers safe recovery actions.
set -euo pipefail
readonly REPORT_DIR="/var/tmp/pacemaker-diagnostics"
readonly TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
mkdir -p "${REPORT_DIR}/${TIMESTAMP}"
echo "=== Pacemaker Cluster Diagnostics ==="
echo "Timestamp: ${TIMESTAMP}"
# ─── Collect diagnostic data ────────────────────────────────
collect() {
local label="$1"; shift
echo "[+] Collecting: ${label}"
"$@" > "${REPORT_DIR}/${TIMESTAMP}/${label}.log" 2>&1 || true
}
collect "nodes_status" pcs status nodes
collect "full_status" pcs status
collect "resources_status" pcs status resources
collect "corosync_status" pcs status corosync
collect "quorum_status" pcs status quorum
collect "stonith_list" pcs stonith list
collect "stonith_config" pcs stonith config 2>/dev/null || true
collect "constraints" pcs constraint list --full
collect "properties" pcs property show
collect "cib_dump" pcs cluster cib 2>/dev/null || true
# ─── Generate crm_report (if available) ─────────────────────
if command -v crm_report &>/dev/null; then
echo "[+] Generating crm_report..."
crm_report -f "${REPORT_DIR}/${TIMESTAMP}/crm_report.tar.gz" 2>/dev/null || true
else
echo "[-] crm_report not available — install pacemaker-cluster-libs"
fi
# ─── Collect logs ──────────────────────────────────────────
collect "corosync_log" journalctl -u corosync --no-pager -n 500
collect "pacemaker_log" journalctl -u pacemaker --no-pager -n 500
collect "pengine_log" journalctl -u pacemaker-ngd --no-pager -n 500 2>/dev/null || \
journalctl -u pacemaker-schedulerd --no-pager -n 500
# ─── Analyze common issues ──────────────────────────────────
echo ""
echo "=== Analysis ==="
# Check for STONITH status
STONITH_ENABLED=$(pcs property show stonith-enabled 2>/dev/null | grep -o 'stonith-enabled=[a-z]*' || echo "unknown")
echo "STONITH: ${STONITH_ENABLED}"
if [[ "${STONITH_ENABLED}" == "stonith-enabled=false" ]]; then
echo " WARNING: STONITH is DISABLED — production clusters require fencing!"
fi
# Check for fenced nodes (nodes in uncertain state)
FENCED_COUNT=$(pcs status | grep -c 'fencing' || true)
if [[ ${FENCED_COUNT} -gt 0 ]]; then
echo " WARNING: Nodes have been or are being fenced"
fi
# Check resource failures
echo ""
echo "Resource fail counts:"
crm_failcount show -r 2>/dev/null || echo " (crm_failcount not available)"
# ─── Offer safe recovery ──────────────────────────────────
echo ""
echo "=== Recovery Options ==="
echo "1. Cleanup all resource failures: pcs resource cleanup"
echo "2. Cleanup specific resource: pcs resource cleanup <resource_name>"
echo "3. Clear failcount for a resource: crm_failcount delete -r <resource> [node]"
echo "4. Force stop/start of a resource: pcs resource manage <name>"
echo ""
echo "Diagnostic report saved to: ${REPORT_DIR}/${TIMESTAMP}/"
# Compress the report
tar czf "${REPORT_DIR}/cluster-diagnostic-${TIMESTAMP}.tar.gz" \
-C "${REPORT_DIR}" "${TIMESTAMP}" 2>/dev/null || true
rm -rf "${REPORT_DIR}/${TIMESTAMP}"
echo "Compressed: ${REPORT_DIR}/cluster-diagnostic-${TIMESTAMP}.tar.gz"
BAD — Missing notify and proper clone parameters causes failed failover
# ❌ BAD: Missing notify=true means the application doesn't know when
# promotion status changes, causing it to read from a degraded replica
sudo pcs resource create db-storage ocf:linbit:drbd \
drbd_resource=db0 \
op monitor interval="30s" role=Slave
sudo pcs resource master db-master db-storage \
master-max=1 clone-max=2
# Problems:
# - No role-specific monitor operations (Slave vs. Master)
# - Missing notify=true — dependents won't track promotion changes
# - No clone-node-max limit — could allow multiple masters
# ❌ BAD: Application resource has no dependency tracking
sudo pcs resource create db-app ocf:heartbeat:lsb:postgresql \
op monitor interval="30s"
# Problem: db-app might start on a node where the DB is still Slave
GOOD — Properly configured with all required parameters
# ✅ GOOD: Full DRBD + PostgreSQL master/slave pattern
# Step 1: DRBD with explicit role-based monitor operations
sudo pcs resource create db-storage ocf:linbit:drbd \
drbd_resource=db0 \
op monitor interval="30s" role=Slave \
op monitor interval="20s" role=Master \
op promote interval="0s" role=Master \
op demote interval="0s" role=Slave
# Step 2: Promotable clone with correct parameters and notify
sudo pcs resource master db-master db-storage \
master-max=1 \
master-node-max=1 \
clone-max=2 \
clone-node-max=1 \
notify=true
# Step 3: Database service tied to the master role
sudo pcs resource create PostgreSQL ocf:heartbeat:pgsql \
pghost="/var/run/postgresql" \
pguser="postgres" \
repmode="sync" \
sync_level="1" \
op monitor interval="30s" timeout="60s" \
op start timeout="120s" \
op stop timeout="60s"
# Step 4: Colocation enforces PostgreSQL runs only on master
sudo pcs constraint colocation add pg-with-db-master \
PostgreSQL with db-master INFINITY
# Step 5: Ordering ensures DB starts after DRBD promotes
sudo pcs constraint order db-master then PostgreSQL kind=Mandatory
# Step 6: VirtualIP follows the database (not the storage layer)
sudo pcs resource create DatabaseVIP ocf:heartbeat:IPaddr2 \
ip="192.168.1.50" cidr_netmask="24" \
op monitor interval="30s"
sudo pcs constraint colocation add vip-with-pg \
DatabaseVIP with PostgreSQL INFINITY
sudo pcs constraint order PostgreSQL then DatabaseVIP kind=Mandatory
pcmk_delay_base and pcmk_delay_max properties for clusters with multiple simultaneous fence targets to prevent race conditionspcs resource cleanup after recovering a node to clear stale failcounts and let the scheduler re-evaluate placementspcs stonith fence <node> --force before declaring the cluster production-readytwo_node modepcs resource describe <type> and pcs stonith describe <type> to discover valid parameters before creating resourcescrm_report -f report.tar.gz for diagnostics when troubleshooting unexpected behavior; include the report when seeking external supportpcs property set stonith-enabled=false) — unfenced clusters risk data corruption from split-brain scenarioscibadmin for configuration changes — use pcs, crm_shadow, or cib-push --diff-against insteadtwo_node: 1, split-brain will cause data corruption on shared storagepcs resource group add) when you need per-resource control over monitor intervals, failure actions, or meta-optionspcs cluster destroy on a node that is actively fencing — ensure resources have already migrated before destroying the clusterno-quorum-policy=fence as a default policy — this will fence ALL nodes if quorum is lost, causing cascading outagesnotify=true — dependent resources won't track promotion changes and may start on Slave nodes| Skill | Purpose |
|---|---|
corosync | Configure Corosync messaging layer, membership, quorum, and network settings that Pacemaker depends on |
linux-services | Define systemd unit files that can be used as LSB or systemd OCF resource agents within Pacemaker |
storage-architecture | Design shared storage layouts (DRBD, LVM, multipath) for HA cluster workloads |
observability | Set up monitoring and alerting for cluster state, fencing events, and resource transitions |
Authoritative documentation links for Pacemaker HA clustering. The model follows markdown links at load time to resolve external references and inline content.