Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions tests/_util.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib
import tempfile
import subprocess
import logging
Expand All @@ -6,6 +7,22 @@

log = logging.getLogger()


def generate_test_name(subsystem, context):
random_hex = os.urandom(4).hex()
prefix = f"snapm_{subsystem}_{context}_{random_hex}"
checksum = hashlib.sha256(prefix.encode("ascii")).hexdigest()[:4]
return f"{prefix}_{checksum}"


def validate_test_name(name):
if len(name) < 6 or name[-5] != "_":
return False
prefix = name[:-5]
checksum = name[-4:]
return hashlib.sha256(prefix.encode("ascii")).hexdigest()[:4] == checksum


_LOSETUP_CMD = "losetup"

_UDEVADM = "udevadm"
Expand All @@ -27,9 +44,9 @@
_MKFS_EXT4_CMD = "mkfs.ext4"

# LVM2
_VG_NAME = "test_vg0"
_VG_NAME = generate_test_name("lm", "vg")

_THIN_POOL_NAME = "pool0"
_THIN_POOL_NAME = generate_test_name("lm", "pool")

# 1GiB
_LV_SIZE = 1024**3
Expand All @@ -52,7 +69,7 @@
_NAME_ARG = "--name"
_SIZE_ARG = "--size"

_POOL_NAME = "pool1"
_POOL_NAME = generate_test_name("st", "pool")

_FS_SIZE = "1GiB"

Expand Down
63 changes: 46 additions & 17 deletions tests/bin/cleanup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,54 @@ if [ -f /tmp/fstab ]; then
rm -f /tmp/fstab
fi

if [ -d /dev/test_vg0 ]; then
export LVM_SYSTEM_DIR="$PWD/tests/lvm"
vgremove --force --yes test_vg0 &> /dev/null || echo Failed to clean up test_vg0 || true
fi
_validate_snapm_name() {
local name="$1"
if [ ${#name} -lt 6 ]; then
return 1
fi
local sep="${name: -5:1}"
if [ "$sep" != "_" ]; then
return 1
fi
local prefix="${name:0:${#name}-5}"
local embedded="${name: -4}"
local computed
computed=$(printf '%s' "$prefix" | sha256sum | cut -c1-4)
[ "$embedded" = "$computed" ]
}
Comment on lines +25 to +39
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the ownership check match the advertised name format.

_validate_snapm_name() only verifies the trailing separator and checksum. Any snapm_lm_* / snapm_st_* name with a matching 4-character suffix is therefore treated as test-owned, even if it does not follow snapm_<subsystem>_<context>_<8hex>_<4hex>. That is too loose for the destructive vgremove / stratis ... destroy calls below.

Suggested tightening
 _validate_snapm_name() {
     local name="$1"
-    if [ ${`#name`} -lt 6 ]; then
-        return 1
-    fi
-    local sep="${name: -5:1}"
-    if [ "$sep" != "_" ]; then
-        return 1
-    fi
+    [[ "$name" =~ ^snapm_[a-z0-9]+_[a-z0-9]+_[0-9a-f]{8}_[0-9a-f]{4}$ ]] || return 1
     local prefix="${name:0:${`#name`}-5}"
     local embedded="${name: -4}"
     local computed
     computed=$(printf '%s' "$prefix" | sha256sum | cut -c1-4)
     [ "$embedded" = "$computed" ]
 }

Please keep the equivalent validators in tests/_util.py and tests/bin/snapm-test-name aligned with this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/bin/cleanup.sh` around lines 25 - 39, The _validate_snapm_name()
function currently only checks the trailing separator and 4-char checksum;
tighten it to validate the full advertised format by matching the name against
the pattern snapm_<subsystem>_<context>_<8hex>_<4hex> (validate
subsystem/context token structure and that the two hex fields are the correct
lengths and hex characters), then compute/verify the trailing 4-char checksum as
now; update the validators in tests/_util.py and tests/bin/snapm-test-name to
use the same strict regex and checksum logic so the ownership check used before
vgremove/stratis destroy only accepts properly formatted test-owned names.

Copy link
Copy Markdown
Contributor Author

@tasleson tasleson Apr 28, 2026

Choose a reason for hiding this comment

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

What we're trying to avoid is destroying something that the test didn't create. A human would have to create a name, then take the shasum of the same, grab the last 4 characters of the shasum and append it onto the end. I don't think any human would normally go through such a thing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tasleson, that's a fair point — the checksum is already a strong enough ownership signal. The probability of a coincidentally matching 4-character SHA-256 suffix on a non-test name is ~1 in 65 536, and as you note, deliberately constructing such a name would require intentionally running sha256sum on the prefix and appending the result — not something that happens by accident. The existing validation is sufficient for its purpose of guarding against accidental destruction of non-test resources. I'll withdraw my suggestion.

( ˶ˆ꒳ˆ˵ )🐇


🧠 Learnings used
Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-08-26T19:07:42.290Z
Learning: In the snapm virt_tests module, verify_lvm_setup() is intended as a lightweight sanity check for essential volumes, not comprehensive validation for every storage layout. The responsibility for testing new layout kickstart fragments lies with the developer submitting them. Adding extensive validation for each layout would be redundant and create clutter.

Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-08-27T21:01:32.324Z
Learning: In the snapm project, the design philosophy is to fail fast when expected files (like systemd units or tmpfiles.d configuration) don't exist during installation steps. This is intentional behavior - if these files are missing, it indicates a fundamental issue with the build/test setup that should abort the process immediately rather than continue with an incomplete installation.

Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-10-22T19:32:44.315Z
Learning: In systemd/tmpfiles.d configuration for snapm: use lowercase 'z' (non-recursive SELinux context restoration) instead of uppercase 'Z' (recursive) for /run/snapm and its subdirectories. Recursive context application is hazardous because /run/snapm/mounts contains mounted snapshot filesystems that have their own SELinux contexts which should not be modified. If a user runs systemd-tmpfiles --create, recursive relabeling would incorrectly change contexts on mounted content.

Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-10-22T16:43:42.894Z
Learning: In snapm/manager/_manager.py _check_snapm_dir: the correct order for directory validation is: 1) check if path exists and validate it's a directory (not a symlink), 2) os.makedirs() to create if needed, 3) os.chmod() to fix permissions if directory existed with wrong perms, 4) validate final mode matches expected. Do not attempt chmod after makedirs for a newly created directory since makedirs(mode=...) already sets permissions. Do not log errors when chmod fails but the mode is already correct.

Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 759
File: systemd/tmpfiles.d/snapm.conf:9-9
Timestamp: 2025-12-17T16:23:50.413Z
Learning: In systemd/tmpfiles.d configuration for snapm: use uppercase 'Z' (recursive SELinux context restoration) for /var/cache/snapm because it is a persistent cache directory tree (including subdirectories like /var/cache/snapm/diffcache) that should have proper labeling applied recursively. Unlike /run/snapm/mounts which contains mounted snapshot filesystems, /var/cache/snapm stores cache files and needs recursive context application.

Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-08-25T17:19:19.563Z
Learning: The man page changes mentioned in the AI summary for PR `#383` were rebasing artifacts and should be ignored when reviewing this branch - there are no actual man page changes on the bmr-vm-tests branch.

Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2026-01-23T20:10:43.866Z
Learning: PR `#954` in snapshotmanager/snapm removes a redundant `dest_path in used_dests` check at line 1236 of snapm/fsdiff/engine.py. This check became dead code after commit 77f1a3d2 which filters candidates in `used_dests` before the check runs. This is purely code cleanup/optimization with no functional changes, not a bug fix or enhancement.

Learnt from: bmr-cymru
Repo: snapshotmanager/snapm PR: 0
File: :0-0
Timestamp: 2025-09-14T02:14:30.316Z
Learning: PR `#478` in snapshotmanager/snapm fixes a regression in the LVM2 plugin where `can_snapshot()` method throws `SnapmCalloutError` when probing non-device-mapper devices. The fix adds device major number validation to prevent calling dmsetup on non-dm devices, allowing proper "no provider found" error messages instead of confusing dmsetup errors.


if [ -d /dev/stratis/pool1 ]; then
STRATIS_FILESYSTEMS=$(stratis filesystem list pool1 | awk '/pool1/{print $2}')
for FS in $STRATIS_FILESYSTEMS; do
if ! FAIL=$(stratis filesystem destroy pool1 "$FS" 2>&1); then
printf 'Failed to clean up pool1 %s: %s\n' "$FS" "$FAIL"
fi
done
fi
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"

if stratis pool list | grep -qE '^pool1([[:space:]]|$)'; then
if ! FAIL=$(stratis pool destroy pool1 2>&1); then
printf 'Failed to clean up pool1: %s\n' "$FAIL"
fi
fi
# Clean up LVM VGs matching snapm_lm_*
export LVM_SYSTEM_DIR="$REPO_ROOT/tests/lvm"
for vg_dir in /dev/snapm_lm_*; do
[ -d "$vg_dir" ] || continue
vg_name=$(basename "$vg_dir")
if _validate_snapm_name "$vg_name"; then
vgremove --force --yes "$vg_name" &> /dev/null \
|| printf 'Failed to clean up %s\n' "$vg_name"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
done

# Clean up Stratis pools matching snapm_st_*
for pool_dir in /dev/stratis/snapm_st_*; do
[ -d "$pool_dir" ] || continue
pool_name=$(basename "$pool_dir")
if _validate_snapm_name "$pool_name"; then
STRATIS_FILESYSTEMS=$(stratis filesystem list "$pool_name" 2>/dev/null \
| awk -v pool="$pool_name" '$0 ~ pool {print $2}')
for FS in $STRATIS_FILESYSTEMS; do
if ! FAIL=$(stratis filesystem destroy "$pool_name" "$FS" 2>&1); then
printf 'Failed to clean up %s %s: %s\n' "$pool_name" "$FS" "$FAIL"
fi
done
if stratis pool list 2>/dev/null | grep -qE "^${pool_name}([[:space:]]|$)"; then
if ! FAIL=$(stratis pool destroy "$pool_name" 2>&1); then
printf 'Failed to clean up %s: %s\n' "$pool_name" "$FAIL"
fi
fi
fi
done

LOOP_DEVICES=$(losetup --noheadings --list -Oname,back-file | awk '/_snapm_loop_back/{print $1}')
for loop in $LOOP_DEVICES; do
Expand Down
73 changes: 73 additions & 0 deletions tests/bin/snapm-test-name
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/bin/bash
# snapm-test-name - generate and validate snapm test resource names
#
# Format: snapm_<subsystem>_<context>_<8 hex random>_<4 hex checksum>
#
# Usage:
# snapm-test-name generate <subsystem> <context>
# snapm-test-name validate <name>
#
# Subsystem: lm (LVM), st (Stratis)
# Context: vg, pool, lv, vol

set -euo pipefail

_generate() {
local subsystem="$1"
local context="$2"
local random_hex
random_hex=$(od -An -tx1 -N4 /dev/urandom | tr -d ' \n')
local prefix="snapm_${subsystem}_${context}_${random_hex}"
local checksum
checksum=$(printf '%s' "$prefix" | sha256sum | cut -c1-4)
printf '%s_%s\n' "$prefix" "$checksum"
}

_validate() {
local name="$1"
if [ ${#name} -lt 6 ]; then
echo "INVALID: name too short"
return 1
fi
local sep="${name: -5:1}"
if [ "$sep" != "_" ]; then
echo "INVALID: missing checksum separator"
return 1
fi
local prefix="${name:0:${#name}-5}"
local embedded="${name: -4}"
local computed
computed=$(printf '%s' "$prefix" | sha256sum | cut -c1-4)
if [ "$embedded" = "$computed" ]; then
echo "VALID: $name"
return 0
else
echo "INVALID: checksum mismatch (expected $computed, got $embedded)"
return 1
fi
}

if [ $# -lt 2 ]; then
echo "Usage: $0 generate <subsystem> <context>" >&2
echo " $0 validate <name>" >&2
exit 2
fi

case "$1" in
generate)
if [ $# -ne 3 ]; then
echo "Usage: $0 generate <subsystem> <context>" >&2
exit 2
fi
_generate "$2" "$3"
;;
validate)
_validate "$2"
;;
*)
echo "Unknown command: $1" >&2
echo "Usage: $0 generate <subsystem> <context>" >&2
echo " $0 validate <name>" >&2
exit 2
;;
esac
4 changes: 2 additions & 2 deletions tests/test_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import boom

from tests import have_root, is_redhat, BOOT_ROOT_TEST
from ._util import LvmLoopBacked
from ._util import LvmLoopBacked, _VG_NAME

ETC_FSTAB = "/etc/fstab"
TMP_FSTAB = "/tmp/fstab"
Expand Down Expand Up @@ -100,7 +100,7 @@ def _set_fstab(self):
with open(TMP_FSTAB, "w", encoding="utf8") as file:
file.write("# Test fstab\n")
for origin, mp in self.boot_volumes:
file.write(f"/dev/test_vg0/{origin}\t{mp}\text4\tdefaults 0 0\n")
file.write(f"/dev/{_VG_NAME}/{origin}\t{mp}\text4\tdefaults 0 0\n")
run(["mount", "--bind", TMP_FSTAB, ETC_FSTAB], check=True)

def _clear_fstab(self):
Expand Down
22 changes: 11 additions & 11 deletions tests/test_lvm2.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from snapm import SnapmCalloutError

from tests import have_root
from ._util import LvmLoopBacked
from ._util import LvmLoopBacked, _VG_NAME, _THIN_POOL_NAME


class Lvm2TestsSimple(unittest.TestCase):
Expand Down Expand Up @@ -232,17 +232,17 @@ def test_lvm2cow_create_delete_origin_accounting(self):

# Create snapshot via Plugin.create_snapshot()
lvm2cow_plugin.start_transaction()
lvm2cow_plugin.check_create_snapshot("test_vg0/root", "test", 1721136677, "/", "1%SIZE")
lvm2cow_plugin.create_snapshot("test_vg0/root", "test", 1721136677, "/", "1%SIZE")
lvm2cow_plugin.check_create_snapshot(f"{_VG_NAME}/root", "test", 1721136677, "/", "1%SIZE")
lvm2cow_plugin.create_snapshot(f"{_VG_NAME}/root", "test", 1721136677, "/", "1%SIZE")
lvm2cow_plugin.end_transaction()

# Verify counter set to one
origin_count = lvm2cow_plugin.origins["/dev/test_vg0/root"]
origin_count = lvm2cow_plugin.origins[f"/dev/{_VG_NAME}/root"]
self.assertEqual(origin_count, 1)

# Delete snapshot & verify decrement
lvm2cow_plugin.delete_snapshot("test_vg0/root-snapset_test_1721136677_-")
origin_count = lvm2cow_plugin.origins["/dev/test_vg0/root"]
lvm2cow_plugin.delete_snapshot(f"{_VG_NAME}/root-snapset_test_1721136677_-")
origin_count = lvm2cow_plugin.origins[f"/dev/{_VG_NAME}/root"]
self.assertEqual(origin_count, 0)

def test_lvm2thin_create_delete_pool_accounting(self):
Expand All @@ -255,15 +255,15 @@ def test_lvm2thin_create_delete_pool_accounting(self):

# Create snapshot via Plugin.create_snapshot()
lvm2thin_plugin.start_transaction()
lvm2thin_plugin.check_create_snapshot("test_vg0/opt", "test", 1721136677, "/opt", "1%SIZE")
lvm2thin_plugin.create_snapshot("test_vg0/opt", "test", 1721136677, "/opt", "1%SIZE")
lvm2thin_plugin.check_create_snapshot(f"{_VG_NAME}/opt", "test", 1721136677, "/opt", "1%SIZE")
lvm2thin_plugin.create_snapshot(f"{_VG_NAME}/opt", "test", 1721136677, "/opt", "1%SIZE")
lvm2thin_plugin.end_transaction()

# Verify counter incremented
pool0_count = lvm2thin_plugin.pools["test_vg0/pool0"]
pool0_count = lvm2thin_plugin.pools[f"{_VG_NAME}/{_THIN_POOL_NAME}"]
self.assertEqual(pool0_count, 1)

# Delete snapshot & verify decrement
lvm2thin_plugin.delete_snapshot("test_vg0/opt-snapset_test_1721136677_-opt")
pool0_count = lvm2thin_plugin.pools["test_vg0/pool0"]
lvm2thin_plugin.delete_snapshot(f"{_VG_NAME}/opt-snapset_test_1721136677_-opt")
pool0_count = lvm2thin_plugin.pools[f"{_VG_NAME}/{_THIN_POOL_NAME}"]
self.assertEqual(pool0_count, 0)
4 changes: 2 additions & 2 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import boom

from tests import have_root, BOOT_ROOT_TEST, MockPlugin
from ._util import LvmLoopBacked, StratisLoopBacked
from ._util import LvmLoopBacked, StratisLoopBacked, _VG_NAME


log = logging.getLogger()
Expand Down Expand Up @@ -636,7 +636,7 @@ def test_activate_deactivate_snapsets(self):
self.assertEqual(snap.status, snapm.SnapStatus.ACTIVE)
self.manager.deactivate_snapshot_sets(selection=s)
for snap in sets[0].snapshots:
if snap.origin.removeprefix("test_vg0/") in self.thin_volumes:
if snap.origin.removeprefix(f"{_VG_NAME}/") in self.thin_volumes:
self.assertEqual(snap.status, snapm.SnapStatus.INACTIVE)
self.manager.delete_snapshot_sets(snapm.Selection(name="testset0"))

Expand Down
6 changes: 3 additions & 3 deletions tests/test_mounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from snapm.manager.plugins import format_snapshot_name, encode_mount_point

from tests import have_root, is_redhat, in_rh_ci
from ._util import LoopBackDevices, LvmLoopBacked
from ._util import LoopBackDevices, LvmLoopBacked, _VG_NAME

ETC_FSTAB = "/etc/fstab"

Expand Down Expand Up @@ -171,7 +171,7 @@ def _set_fstab(self):
with open(self._tmp_fstab, "w", encoding="utf8") as file:
file.write("# Test fstab\n")
for origin, mp in self.mount_volumes:
file.write(f"/dev/test_vg0/{origin}\t{mp}\text4\tdefaults 0 0\n")
file.write(f"/dev/{_VG_NAME}/{origin}\t{mp}\text4\tdefaults 0 0\n")
run(["mount", "--bind", self._tmp_fstab, ETC_FSTAB], check=True)

def _clear_fstab(self):
Expand Down Expand Up @@ -232,7 +232,7 @@ def setUp(self):

# ./~ Make it cheap & keep it shallow. Fill it up and make it hollow. ./~
with tempfile.TemporaryDirectory(prefix="snapm_root_prep_") as tempdir:
with mounted("/dev/test_vg0/root", tempdir):
with mounted(f"/dev/{_VG_NAME}/root", tempdir):
for dirpath in _root_dirs:
os.makedirs(os.path.join(tempdir, dirpath))

Expand Down
Loading
Loading