Skip to content

Commit

Permalink
Add some Python-based tests for filesystem revert
Browse files Browse the repository at this point in the history
Signed-off-by: mulhern <[email protected]>
  • Loading branch information
mulkieran committed Oct 30, 2024
1 parent 6ecd49b commit 5aae2f3
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 1 deletion.
3 changes: 2 additions & 1 deletion tests-fmf/python.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require:
- python3-dbus
- python3-dbus-client-gen
- python3-dbus-python-client-gen
- python3-justbytes
- python3-psutil
- python3-pyudev
- python3-tenacity
Expand Down Expand Up @@ -37,4 +38,4 @@ environment:

/v2/loop:
summary: Run Python tests that use loopbacked device framework
test: make -f Makefile tang-tests dump-metadata-tests startup-tests
test: make -f Makefile tang-tests dump-metadata-tests startup-tests revert-tests
4 changes: 4 additions & 0 deletions tests/client-dbus/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ filesystem-predict-tests:
.PHONY: dump-metadata-tests
dump-metadata-tests:
python3 -m unittest ${UNITTEST_OPTS} tests.udev.test_dump

.PHONY: revert-tests
revert-tests:
python3 -m unittest ${UNITTEST_OPTS} tests.udev.test_revert
1 change: 1 addition & 0 deletions tests/client-dbus/src/stratisd_client_dbus/_introspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
<property name="Devnode" type="s" access="read">
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="invalidates" />
</property>
<property name="MergeScheduled" type="b" access="readwrite" />
<property name="Name" type="s" access="read" />
<property name="Origin" type="(bs)" access="read" />
<property name="Pool" type="o" access="read">
Expand Down
296 changes: 296 additions & 0 deletions tests/client-dbus/tests/udev/test_revert.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
# Copyright 2024 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Test reverting a filesystem.
"""

# isort: STDLIB
import os
import subprocess
import tempfile

# isort: THIRDPARTY
from justbytes import Range

# isort: FIRSTPARTY
from dbus_python_client_gen import DPClientInvocationError

# isort: LOCAL
from stratisd_client_dbus import Filesystem
from stratisd_client_dbus._constants import TOP_OBJECT

from ._utils import (
Manager,
Pool,
ServiceContextManager,
UdevTest,
create_pool,
get_object,
random_string,
settle,
)


def write_file(mountdir, filename):
"""
Write a sentinel value derived from the filename to a file.
"""
with open(os.path.join(mountdir, filename), encoding="utf-8", mode="w") as fd:
print(filename, file=fd, end="")


class TestRevert(UdevTest):
"""
Test reverting a filesystem.
"""

def read_file(self, mountdir, filename):
"""
Read a file and verify that it contains the expected sentinel value.
"""
with open(os.path.join(mountdir, filename), encoding="utf-8") as fd:
self.assertEqual(fd.read(), filename)

def test_revert(self): # pylint: disable=too-many-locals
"""
Schedule a revert and verify that it has succeeded when the pool is
restarted.
First simply stop and start stratisd. In this way it is possible to
verify that when a revert fails, the pool is setup, without the revert.
"""
mountdir = tempfile.mkdtemp("_stratis_mnt")

with ServiceContextManager():
device_tokens = self._lb_mgr.create_devices(2)

pool_name = random_string(5)

(_, (pool_object_path, _)) = create_pool(
pool_name, self._lb_mgr.device_files(device_tokens)
)

fs_name = "fs1"
fs_size = Range(1024**3)
((_, fs_object_paths), return_code, message) = (
Pool.Methods.CreateFilesystems(
get_object(pool_object_path),
{"specs": [(fs_name, (True, str(fs_size.magnitude)), (False, ""))]},
)
)

if return_code != 0:
raise RuntimeError(
f"Failed to create a requested filesystem: {message}"
)

settle()

filepath = f"/dev/stratis/{pool_name}/{fs_name}"
subprocess.check_call(["mount", filepath, mountdir])

file1 = "file1.txt"
write_file(mountdir, file1)

snap_name = "snap1"
((_, snap_object_path), return_code, message) = (
Pool.Methods.SnapshotFilesystem(
get_object(pool_object_path),
{"origin": fs_object_paths[0][0], "snapshot_name": snap_name},
)
)

if return_code != 0:
raise RuntimeError(f"Failed to create requested snapshot: {message}")

file2 = "file2.txt"
write_file(mountdir, file2)

Filesystem.Properties.MergeScheduled.Set(get_object(snap_object_path), True)
subprocess.check_call(["umount", mountdir])

self.assertTrue(os.path.exists(f"/dev/stratis/{pool_name}/{snap_name}"))

# Do not stop the pool, but do stop stratisd. Since the devices were
# not torn down, the merge will fail and both filesystems will be set
# up as they were previously.
with ServiceContextManager():
self.wait_for_pools(1)

settle()

subprocess.check_call(["mount", filepath, mountdir])

self.read_file(mountdir, file1)
self.read_file(mountdir, file2)

subprocess.check_call(["umount", mountdir])

# Now stop the pool, which should tear down the devices
(_, return_code, message) = Manager.Methods.StopPool(
get_object(TOP_OBJECT),
{
"id": pool_name,
"id_type": "name",
},
)

if return_code != 0:
raise RuntimeError(f"Failed to stop the pool {pool_name}: {message}")

(_, return_code, message) = Manager.Methods.StartPool(
get_object(TOP_OBJECT),
{
"id": pool_name,
"id_type": "name",
"unlock_method": (False, ""),
"key_fd": (False, 0),
},
)

if return_code != 0:
raise RuntimeError(f"Failed to start the pool {pool_name}: {message}")

self.wait_for_pools(1)

settle()

subprocess.check_call(["mount", filepath, mountdir])

self.read_file(mountdir, file1)

self.assertFalse(os.path.exists(os.path.join(mountdir, file2)))
self.assertFalse(os.path.exists(f"/dev/stratis/{pool_name}/{snap_name}"))

subprocess.check_call(["umount", mountdir])

def test_revert_snapshot_chain(self): # pylint: disable=too-many-locals
"""
Make a chain of snapshots, schedule excess reverts and verify that
those yield an error, and then revert the middle link.
Verify that the snapshot link now points to the origin.
"""
mountdir = tempfile.mkdtemp("_stratis_mnt")

with ServiceContextManager():
device_tokens = self._lb_mgr.create_devices(2)

pool_name = random_string(5)

(_, (pool_object_path, _)) = create_pool(
pool_name, self._lb_mgr.device_files(device_tokens)
)

fs_name = "fs1"
fs_size = Range(1024**3)
((_, fs_object_paths), return_code, message) = (
Pool.Methods.CreateFilesystems(
get_object(pool_object_path),
{"specs": [(fs_name, (True, str(fs_size.magnitude)), (False, ""))]},
)
)

if return_code != 0:
raise RuntimeError(
f"Failed to create a requested filesystem: {message}"
)

settle()

filepath = f"/dev/stratis/{pool_name}/{fs_name}"
subprocess.check_call(["mount", filepath, mountdir])

file1 = "file1.txt"
write_file(mountdir, file1)

snap_name_1 = "snap1"
((_, snap_object_path_1), return_code, message) = (
Pool.Methods.SnapshotFilesystem(
get_object(pool_object_path),
{"origin": fs_object_paths[0][0], "snapshot_name": snap_name_1},
)
)

if return_code != 0:
raise RuntimeError(f"Failed to create requested snapshot: {message}")

file2 = "file2.txt"
write_file(mountdir, file2)

Filesystem.Properties.MergeScheduled.Set(
get_object(snap_object_path_1), True
)
subprocess.check_call(["umount", mountdir])

self.assertTrue(os.path.exists(f"/dev/stratis/{pool_name}/{snap_name_1}"))

snap_name_2 = "snap2"
((_, snap_object_path_2), return_code, message) = (
Pool.Methods.SnapshotFilesystem(
get_object(pool_object_path),
{"origin": snap_object_path_1, "snapshot_name": snap_name_2},
)
)

if return_code != 0:
raise RuntimeError(f"Failed to create requested snapshot: {message}")

settle()

self.assertTrue(os.path.exists(f"/dev/stratis/{pool_name}/{snap_name_2}"))
with self.assertRaises(DPClientInvocationError):
Filesystem.Properties.MergeScheduled.Set(
get_object(snap_object_path_2), True
)

# Now stop the pool, which should tear down the devices
(_, return_code, message) = Manager.Methods.StopPool(
get_object(TOP_OBJECT),
{
"id": pool_name,
"id_type": "name",
},
)

if return_code != 0:
raise RuntimeError(f"Failed to stop the pool {pool_name}: {message}")

(_, return_code, message) = Manager.Methods.StartPool(
get_object(TOP_OBJECT),
{
"id": pool_name,
"id_type": "name",
"unlock_method": (False, ""),
"key_fd": (False, 0),
},
)

if return_code != 0:
raise RuntimeError(f"Failed to start the pool {pool_name}: {message}")

self.wait_for_pools(1)

settle()

subprocess.check_call(["mount", filepath, mountdir])

self.read_file(mountdir, file1)

self.assertFalse(os.path.exists(os.path.join(mountdir, file2)))
self.assertFalse(os.path.exists(f"/dev/stratis/{pool_name}/{snap_name_1}"))
self.assertTrue(os.path.exists(f"/dev/stratis/{pool_name}/{snap_name_2}"))

subprocess.check_call(["umount", mountdir])

0 comments on commit 5aae2f3

Please sign in to comment.