Visitar URL original
kvm: fix SharedMountPoint heartbeat when pool path is a subdirectory of a mount by danivogel90 · Pull Request #14331 · apache/cloudstack · GitHub
Skip to content

kvm: fix SharedMountPoint heartbeat when pool path is a subdirectory of a mount - #14331

Open
danivogel90 wants to merge 3 commits into
apache:4.22from
danivogel90:fix-smp-heartbeat-14326
Open

danivogel90 wants to merge 3 commits into
apache:4.22from
danivogel90:fix-smp-heartbeat-14326

Conversation

@danivogel90

Copy link
Copy Markdown

Description

This PR fixes the KVM HA heartbeat for SharedMountPoint pools whose path is a subdirectory of a mounted filesystem, which is common with clustered/parallel filesystems (IBM Storage Scale/GPFS, WEKA, Quobyte, GFS2, OCFS2, CephFS). It also fixes two related problems in kvmsmpheartbeat.sh (introduced in #12773):

  1. Mount point check – mountpoint -q "$MountPoint" rejects valid pools such as /data01/vol01 on a GPFS filesystem mounted at /data01. Every heartbeat write fails with Mount point is not a mounted filesystem. A new helper is_on_mounted_fs() accepts any path on a mounted filesystem other than / (findmnt -n -o TARGET -T, fallback df -P). The safety intent is kept: if the shared filesystem is not mounted, the heartbeat is not written to the local root filesystem.
    The same helper now also replaces the /proc/mounts check in front of deleteVMs. Without this, relaxing only the first check would make every heartbeat write call deleteVMs, which kill -9s all qemu processes using the pool.
  2. False DEAD on read – check_hbLog used diff=`expr $now - $hb` followed by if [ $? -ne 0 ]. expr exits with 1 when the result is 0, so a host whose heartbeat was written in the same second as the check was reported as DEAD (999997). The timestamp is now validated and the difference computed with shell arithmetic.
  3. Temp file left behind – if the script is killed (e.g. on timeout while the clustered FS is recovering), the hb-<ip>.<pid> temp file remained. A trap now removes it.

Fixes: #14326

Broader context: #14327

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI
  • Test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

How Has This Been Tested?

Environment: 3 KVM hosts, Ubuntu 24.04, CloudStack 4.23.0.0 (script identical on 4.22 and main), hyperconverged IBM Storage Scale 5.2.3-5 (3-way replication). SharedMountPoint pool /data01/vol01 (subdirectory of GPFS mount /data01), plus an NFS primary storage pool.

Script tests (copy of the script with kill -9 replaced by an echo):

Case Expected Result
unpatched, subdirectory of GPFS mount fails exit 1
patched, subdirectory of GPFS mount, write + read ALIVE, no deleteVMs OK
patched, real mount point (tmpfs) ALIVE OK
patched, NFS pool mount point (read) ALIVE OK
patched, path on root FS refuse, nothing written exit 1, dir empty
patched, path does not exist refuse exit 1
unpatched, read right after write (diff=0) ALIVE DEAD
patched, read right after write / after 3 s, -t 60 ALIVE OK / OK
patched, after 3 s with -t 1 / corrupt hb file DEAD OK / OK
patched, SIGTERM during write temp file removed OK

Live: the patched script has been running in the agent on one host since 2026-10-07. Heartbeat writes succeed, there are no errors in agent.log, and nothing was killed.

HA failover test: HA-enabled VM (offering with offerha=true) on the SharedMountPoint pool on host A. Agent autostart disabled on A, then A hard-reset via sysrq. Results:

  • after ~85 s the neighbour with the patched script returned "Heart is not beating" for both pools (NFS + SharedMountPoint), and the host was marked Down
  • after ~110 s the VM was running on another host
  • host A rejoined normally after the agent was re-enabled

How did you try to break this feature and the system with this change?

  • Path on the local root filesystem / non-existent path: refused, nothing written, no deleteVMs.
  • Corrupt and stale heartbeat files: reported as DEAD as before.
  • Script interrupted during write: no leftover temp file.
  • Known limitation (unchanged from before): a path on a different local filesystem (e.g. a separate /var LV) is not detected as "not shared". The previous mountpoint -q check did not cover this either.

@boring-cyborg

boring-cyborg Bot commented Oct 7, 2026

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache CloudStack community! If you have any issues or are unsure about any anything please check our Contribution Guide (https://github.com/apache/cloudstack/blob/main/CONTRIBUTING.md)
Here are some useful points:

Comment thread scripts/vm/hypervisor/kvm/kvmsmpheartbeat.sh Outdated
…of a mount

kvmsmpheartbeat.sh required the pool path itself to be a mount point
(mountpoint -q). A SharedMountPoint pool is "a file system path local to
each server" and is commonly a subdirectory of a mounted clustered or
parallel filesystem (e.g. /data01/vol01 on IBM Storage Scale mounted at
/data01). For such pools every heartbeat write failed.

- add is_on_mounted_fs(): the path must reside on a mounted filesystem
  other than / (findmnt -T, fallback df -P). Use it for both the initial
  check and the /proc/mounts check in front of deleteVMs, so relaxing the
  first check cannot lead to deleteVMs (kill -9) being run on every write.
- check_hbLog: 'expr' exits with 1 when the result is 0, so a heartbeat
  read in the same second it was written reported the host as DEAD.
  Validate the timestamp and use shell arithmetic instead.
- remove the temporary heartbeat file when the script is interrupted
  (e.g. killed on timeout).

Fixes apache#14326
The initial check already exits when the path is not on a mounted
filesystem, so the deleteVMs branch could not be reached. Remove the
branch together with the now unused deleteVMs function.

This keeps the behaviour of 4.22.1/4.23: there the earlier
'mountpoint -q' check exited first as well, so deleteVMs never ran in
practice. Unlike kvmheartbeat.sh (NFS), which kills VMs only after it
remounted a lost NFS share, a SharedMountPoint cannot be remounted by
the script. Killing VMs on a failed local check is left to the existing
HA/fencing logic.

@weizhouapache weizhouapache left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code lgtm

thanks @danivogel90

@weizhouapache

Copy link
Copy Markdown
Member

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@weizhouapache a [SL] Jenkins job has been kicked to build packages. It will be bundled with KVM, XenServer and VMware SystemVM templates. I'll keep you posted as I make progress.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Mount-loss safety, forced-timeout cleanup, and numeric timestamp parsing have unresolved correctness issues.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity

Open (3)
What changed in this PR

Fixes KVM HA heartbeats for SharedMountPoint pools located below a filesystem mount.

Changes:

  • Resolves the containing filesystem for pool paths.
  • Adds temporary heartbeat-file cleanup.
  • Validates timestamps and uses shell arithmetic.
File Description
scripts/​vm/​hypervisor/​kvm/​kvmsmpheartbeat.sh Updates mount validation, heartbeat writes, and timestamp checks.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

else
target=$(df -P "$1" 2>/dev/null | awk 'NR==2 {print $6}')
fi
[ -n "$target" ] && [ "$target" != "/" ]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, valid point for an exact pool mount nested below a separate local filesystem. Fixed in d089836:

  • If the path is a mount point itself, it is accepted as before (mountpoint -q).
  • Otherwise a subdirectory is only accepted if its filesystem is neither / nor a local disk filesystem (ext*, xfs, btrfs, tmpfs, …).

The script does not receive the expected mount source for SharedMountPoint (-i/-p are unused here), so it cannot compare against it; the filesystem type is the closest signal available.

Tested: pool mounted exactly below a separate local filesystem → accepted; after unmounting the pool → refused, nothing written locally. With acd5ba4 the same case returned 0 and wrote the heartbeat to the local filesystem.

Comment on lines +128 to +130
# remove the temporary file if the script is interrupted (e.g. on timeout)
trap 'rm -f "$tmpfile"' EXIT
trap 'exit 1' INT TERM

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the trap cannot run after destroyForcibly(). Rather than changing Script.execute() for all scripts, d089836 removes leftover hb-<host>.* temp files older than one minute at the start of the next write. The trap stays for normal exits and TERM.

Tested: a 5-minute-old leftover is removed, a fresh one is kept.

Comment on lines +154 to +160
case "$hb" in
''|*[!0-9]*)
hb_diff=999997
return 1
;;
esac
diff=$((now - hb))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d089836: the timestamp is limited to 12 digits and evaluated as base 10 (10#$hb). 08, a 19-digit value and abc now report DEAD; with acd5ba4, 08 failed with value too great for base.

@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 18.02%. Comparing base (2974af8) to head (acd5ba4).

Additional details and impacted files
@@             Coverage Diff              @@
##               4.22   #14331      +/-   ##
============================================
- Coverage     18.02%   18.02%   -0.01%     
+ Complexity    16250    16246       -4     
============================================
  Files          5936     5936              
  Lines        535823   535823              
  Branches      65612    65612              
============================================
- Hits          96582    96568      -14     
- Misses       428242   428253      +11     
- Partials      10999    11002       +3     
Flag Coverage Δ
uitests 4.04% <ø> (ø)
unittests 19.09% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✖️ el8 ✖️ el9 ✔️ debian ✖️ suse15. SL-JID 19450

Address the review comments on the heartbeat script:

- is_on_mounted_fs(): accept the path if it is a mount point itself
  (as with the former 'mountpoint -q' check). Otherwise accept a
  subdirectory only if its filesystem is neither / nor a local disk
  filesystem (ext*, xfs, btrfs, tmpfs, ...). An exact pool mount nested
  below a separate local filesystem (e.g. /var) that is lost no longer
  passes: its empty directory belongs to the local parent filesystem,
  so the heartbeat is not written locally.
- write_hbLog: Script.execute() ends a timed out run with
  destroyForcibly(), so the EXIT/TERM trap cannot remove the temporary
  file. Remove leftover hb-<host>.* files older than one minute on the
  next run.
- check_hbLog: limit the timestamp to 12 digits and evaluate it as
  base 10, so a corrupt value with a leading zero (e.g. "08") reports
  DEAD instead of failing with "value too great for base".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@danivogel90

Copy link
Copy Markdown
Author

@weizhouapache I pushed d089836 to address the Copilot review (exact mount check, temp file cleanup after a forced kill, base-10 parsing). Tested with 17 script cases and live in the agent on one host (Ubuntu 24.04, 4.23.0.0, IBM Storage Scale); details are in the review threads. Could you take another quick look?

The packaging run reported failures for el8/el9/suse15 (debian OK). Since the PR only changes a shell script, I assume this is unrelated. Could you re-trigger it?

The changes in this PR were prepared with the help of Claude (AI assistant); I reviewed and tested them. Thanks!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants