#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)

# This file is part of Cockpit.
#
# Copyright (C) 2023 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <https://www.gnu.org/licenses/>.

import os
import os.path

import storagelib
import testlib


@testlib.nondestructive
@testlib.skipImage('no btrfs support', 'rhel-*', 'centos-*')
class TestStorageBtrfs(storagelib.StorageCase):
    def setUp(self):
        super().setUp()
        self.allow_browser_errors("unable to obtain subvolumes for mount point.*")
        self.allow_browser_errors("unable to obtain default subvolume for mount point.*")
        self.allow_browser_errors("error: unable to run findmnt.*")
        self.allow_browser_errors("error: findmnt.*")

    def checkTeardownAction(self, row, label, text):
        self.browser.wait_in_text(f".modal-footer-teardown tbody:nth-of-type({row}) td[data-label='{label}']", text)

    def testBasic(self):
        m = self.machine
        b = self.browser

        mount_point = "/run/butter"
        # btrfs requires a 128 MB
        dev_1 = self.add_ram_disk(size=128)

        self.login_and_go("/storage")
        with b.wait_timeout(30):
            b.wait_visible(self.card_row("Storage", name="sda"))

        self.click_dropdown(self.card_row("Storage", name=dev_1), "Format")
        self.dialog({"name": "butter", "type": "btrfs", "mount_point": mount_point, "mount_options.ro": True})

        # disk(s) are part of the volume card
        b.wait_visible(self.card_row("Storage", location=mount_point))
        self.assertIn("subvol=/", m.execute(f"findmnt --fstab -n -o OPTIONS {mount_point}"))
        self.click_card_row("Storage", name="sda")

        b.wait_text(self.card_desc("btrfs filesystem", "Label"), "butter")

        # Can't relabel a filesystem that is mounted read-only
        b.wait_visible(self.card_desc_action("btrfs filesystem", "Label") + ":disabled")

        # Unmount to change label
        self.click_dropdown(self.card_row("btrfs filesystem", name="top-level"), "Unmount")
        self.confirm()
        b.wait_visible(self.card_row("btrfs filesystem", location=f"{mount_point} (not mounted)"))

        label = "margarine"
        b.click(self.card_desc_action("btrfs filesystem", "Label"))
        self.dialog({"name": label})
        b.wait_text(self.card_desc("btrfs filesystem", "Label"), label)

        # Mount writable for the rest of the test
        self.click_dropdown(self.card_row("btrfs filesystem", name="top-level"), "Mount")
        self.dialog({"mount_options.ro": False})
        b.wait_visible(self.card_row("btrfs filesystem", location=f"{mount_point}"))

        # Change label
        label = "butter"
        b.click(self.card_desc_action("btrfs filesystem", "Label"))
        self.dialog({"name": label})
        b.wait_text(self.card_desc("btrfs filesystem", "Label"), label)

        # detect new subvolume
        subvol = "/run/butter/cake"
        m.execute(f"btrfs subvolume create {subvol}")
        b.wait_visible(self.card_row("btrfs filesystem", name=os.path.basename(subvol)))

        self.click_dropdown(self.card_row("btrfs filesystem", location=mount_point), "Unmount")
        self.confirm()

        b.wait_visible(self.card_row("btrfs filesystem", location=f"{mount_point} (not mounted)"))
        self.click_dropdown(self.card_row("btrfs filesystem", name="top-level"), "Mount")
        self.confirm()

        b.wait_visible(self.card_row("btrfs filesystem", location=mount_point))
        # try to mount a subvol
        subvol_mount_point = "/run/kitchen"
        self.click_dropdown(self.card_row("btrfs filesystem", name=os.path.basename(subvol)), "Mount")
        self.dialog({"mount_point": subvol_mount_point})

        b.wait_in_text(self.card_row("btrfs filesystem", location=subvol_mount_point), "cake")
        b.wait_visible(self.card_row("btrfs filesystem", location=mount_point))

        b.go("#/")
        b.wait_visible(self.card("Storage"))

        # podman subvolumes are hidden, Cockpit hides everything matching "containers/storage/btrfs/subvolumes"
        m.execute(f"""
        mkdir -p /run/kitchen/containers/storage/btrfs/subvolumes
        btrfs subvolume create /run/kitchen/containers/storage/btrfs/subvolumes/containername

        mkdir -p /run/kitchen/.local/share/containers/storage/btrfs/subvolumes
        btrfs subvolume create /run/kitchen/.local/share/containers/storage/btrfs/subvolumes/grafana

        btrfs subvolume create {subvol_mount_point}/pancake
        """)

        b.wait_in_text(self.card_row("Storage", name="pancake"), "btrfs subvolume")
        b.wait_not_in_text("div[data-test-card-title='Storage']", "containername")
        b.wait_not_in_text("div[data-test-card-title='Storage']", "grafana")

        # mount outside of fstab, should be cleaned up when re-formatting
        m.execute(f"""
            btrfs subvolume create {mount_point}/nonfstab
            mkdir -p /run/basement
            mount -o subvol=nonfstab {dev_1} /run/basement
        """)

        b.wait_visible(self.card_row("Storage", name="nonfstab"))

        # Format the btrfs device
        # The mount of /run/basement might take some time to be recognized by Cockpit
        self.dialog_open_with_retry(lambda: self.click_dropdown(self.card_row("Storage", name="sda"), "Format"),
                                    lambda: "/run/basement" in b.text("#dialog"))
        self.checkTeardownAction(1, "Device", os.path.basename(dev_1))
        self.checkTeardownAction(1, "Location", "/run/basement")
        self.checkTeardownAction(1, "Action", "unmount, format")
        self.checkTeardownAction(2, "Location", mount_point)
        self.checkTeardownAction(2, "Action", "unmount, format")
        self.checkTeardownAction(3, "Location", subvol_mount_point)
        self.checkTeardownAction(3, "Action", "unmount, format")
        self.dialog_set_val("type", "empty")
        self.dialog_apply()

        # subvolume is gone
        b.wait_not_present(self.card_row("Storage", name="nonfstab"))

        # All mounts should be gone
        m.execute(f"! findmnt -n | grep {dev_1}")

    def testCreateSubvolume(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk(size=140)
        label = "test_subvol"
        mount_point = "/run/butter"
        subvol = "cake"

        m.execute(f"mkfs.btrfs -L {label} {disk1}")
        self.login_and_go("/storage")

        # creation of btrfs partition can take a while on TF.
        with b.wait_timeout(30):
            b.wait_visible(self.card_row("Storage", name="sda"))

        root_sel = self.card_row("Storage", name="sda") + " + tr"
        b.click(self.dropdown_toggle(root_sel))
        b.wait_visible(self.dropdown_action(root_sel, "Create subvolume") + "[disabled]")
        b.wait_text(self.dropdown_description(root_sel, "Create subvolume"),
                    "At least one subvolume needs to be mounted")
        b.click(self.dropdown_toggle(root_sel))

        self.click_dropdown(root_sel, "Mount")
        self.dialog({"mount_point": mount_point})
        self.addCleanup(self.machine.execute, f"umount {mount_point} || true")
        b.wait_in_text(self.card_row("Storage", location=mount_point), "btrfs subvolume")
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")

        # Validation
        self.dialog_wait_open()
        self.dialog_apply_secondary()
        self.dialog_wait_error("name", "Name cannot be empty")
        self.dialog_set_val("name", "foo/bar")
        self.dialog_apply_secondary()
        self.dialog_wait_error("name", "Name cannot contain the character '/'.")
        self.dialog_set_val("name", "a" * 256)
        self.dialog_apply_secondary()
        self.dialog_wait_error("name", "Name cannot be longer than 255 characters.")

        # Without mount point
        self.dialog_set_val("name", subvol)
        self.dialog_apply_secondary()
        self.dialog_wait_close()
        b.wait_visible(self.card_row("Storage", name=subvol))
        # no fstab entry
        m.execute(f"! findmnt --fstab -n | grep subvol={subvol}")

        subvol_mount = "quiche"
        subvol_mount_point = "/run/oven"
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")

        # With mount point
        self.dialog_wait_open()
        self.dialog_set_val("name", subvol_mount)
        self.dialog_apply()
        self.dialog_wait_error("mount_point", "Mount point cannot be empty")
        self.dialog_set_val("mount_point", subvol_mount_point)
        self.dialog_apply()
        self.dialog_wait_close()
        self.addCleanup(self.machine.execute, f"umount {subvol_mount_point} || true")
        b.wait_in_text(self.card_row("Storage", location=subvol_mount_point), "btrfs subvolume")

        # Finding the correct subvolume parent from a non-mounted subvolume
        m.execute(f"btrfs subvolume create {subvol_mount_point}/pizza")
        self.click_dropdown(self.card_row("Storage", name="pizza"), "Create subvolume")
        self.dialog({"name": "pineapple"}, secondary=True)
        b.wait_in_text(self.card_row("Storage", name="pineapple"), "btrfs subvolume")

        left_subvol_mount = "/run/left"
        right_subvol_mount = "/run/right"

        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": os.path.basename(left_subvol_mount), "mount_point": left_subvol_mount})
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": os.path.basename(right_subvol_mount), "mount_point": right_subvol_mount})
        self.addCleanup(self.machine.execute, f"umount {left_subvol_mount} || true")
        self.addCleanup(self.machine.execute, f"umount {right_subvol_mount} || true")

        b.wait_in_text(self.card_row("Storage", location=left_subvol_mount), "btrfs subvolume")
        b.wait_in_text(self.card_row("Storage", location=right_subvol_mount), "btrfs subvolume")

        self.click_dropdown(self.card_row("Storage", location=left_subvol_mount), "Create subvolume")
        self.dialog({"name": "links"}, secondary=True)
        b.wait_in_text(self.card_row("Storage", name="links"), "btrfs subvolume")

        self.click_dropdown(self.card_row("Storage", location=right_subvol_mount), "Create subvolume")
        self.dialog({"name": "rechts"}, secondary=True)
        b.wait_in_text(self.card_row("Storage", name="rechts"), "btrfs subvolume")

        # Read only mount, cannot create subvolumes once /run/butter
        # is unmounted.

        ro_subvol = "/run/ro"
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": os.path.basename(ro_subvol), "mount_point": ro_subvol, "mount_options.ro": True})

        # We can always create subvolumes as long as a single one is
        # mounted, even read-only.

        self.click_dropdown(self.card_row("Storage", location="/run/butter"), "Unmount")
        self.confirm()

        self.click_dropdown(self.card_row("Storage", location=ro_subvol), "Create subvolume")
        self.dialog({"name": "bot"}, secondary=True)
        b.wait_visible(self.card_row("Storage", name="bot"))

    def testDeleteSubvolume(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk(size=140)
        label = "test_subvol"
        mount_point = "/run/butter"
        m.execute(f"mkfs.btrfs -L {label} {disk1}")
        self.login_and_go("/storage")

        # creation of btrfs partition can take a while on TF.
        with b.wait_timeout(30):
            b.wait_visible(self.card_row("Storage", name="sda"))

        root_sel = self.card_row("Storage", name="sda") + " + tr"
        self.click_dropdown(root_sel, "Mount")
        self.dialog({"mount_point": mount_point})
        self.addCleanup(self.machine.execute, f"umount {mount_point} || true")

        # No Delete button for the root subvolume
        b.click(self.dropdown_toggle(root_sel))
        b.wait_not_present(self.dropdown_action(root_sel, "Delete"))
        b.click(self.dropdown_toggle(root_sel))

        # Delete subvolume
        subvol = "subvol"
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": subvol}, secondary=True)
        b.wait_visible(self.card_row("Storage", name=subvol))

        self.click_dropdown(self.card_row("Storage", name=subvol), "Delete")
        self.checkTeardownAction(1, "Device", subvol)
        self.checkTeardownAction(1, "Action", "delete")
        self.confirm()
        b.wait_not_present(self.card_row("Storage", name=subvol))

        # Delete with subvolume with children
        child_subvol = "child-subvol"
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": subvol}, secondary=True)
        b.wait_visible(self.card_row("Storage", name=subvol))

        self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume")
        self.dialog({"name": child_subvol}, secondary=True)
        b.wait_visible(self.card_row("Storage", name=child_subvol))

        self.click_dropdown(self.card_row("Storage", name=subvol), "Delete")
        self.checkTeardownAction(1, "Device", child_subvol)
        self.checkTeardownAction(1, "Action", "delete")
        self.checkTeardownAction(2, "Device", subvol)
        self.checkTeardownAction(2, "Action", "delete")
        self.confirm()

        b.wait_not_present(self.card_row("Storage", name=child_subvol))
        b.wait_not_present(self.card_row("Storage", name=subvol))

        # Delete with subvolume with children and self mounted
        child_subvol = "child-subvol"
        subvol_mount_point = "/run/delete"
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": subvol, "mount_point": subvol_mount_point})
        b.wait_visible(self.card_row("Storage", location=subvol_mount_point))

        self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume")
        self.dialog({"name": child_subvol}, secondary=True)
        b.wait_visible(self.card_row("Storage", name=child_subvol))

        # Unmount root subvolume and delete child-subvol. That will use the mountpoint of "subvol"
        self.click_dropdown(root_sel, "Unmount")
        self.confirm()

        self.click_dropdown(self.card_row("Storage", name=child_subvol), "Delete")
        self.confirm()
        b.wait_not_present(self.card_row("Storage", name=child_subvol))

        # Creating inside subvol will also use its mountpoint now
        self.click_dropdown(self.card_row("Storage", name=subvol), "Create subvolume")
        self.dialog({"name": child_subvol}, secondary=True)
        b.wait_visible(self.card_row("Storage", name=child_subvol))

        # Mount the root subvolume again so that we can delete the lot
        self.click_dropdown(root_sel, "Mount")
        self.confirm()

        self.click_dropdown(self.card_row("Storage", name=subvol), "Delete")
        self.checkTeardownAction(1, "Device", child_subvol)
        self.checkTeardownAction(1, "Action", "delete")
        self.checkTeardownAction(1, "Device", subvol)
        self.checkTeardownAction(2, "Location", subvol_mount_point)
        self.checkTeardownAction(2, "Action", "unmount, delete")
        self.confirm()

        b.wait_not_present(self.card_row("Storage", location=subvol_mount_point))
        b.wait_not_present(self.card_row("Storage", name=subvol))

        # Delete with subvolume which is mounted and busy
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": subvol, "mount_point": subvol_mount_point})
        b.wait_visible(self.card_row("Storage", location=subvol_mount_point))
        sleep_pid = m.spawn(f"cd {subvol_mount_point}; sleep infinity", "sleep")

        self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Delete")
        self.checkTeardownAction(1, "Location", subvol_mount_point)
        self.checkTeardownAction(1, "Action", "unmount, delete")
        b.wait_in_text(".modal-footer-teardown tbody:nth-of-type(1)", "Currently in use")
        b.click(".modal-footer-teardown tbody:nth-of-type(1) button")
        b.wait_in_text(".pf-v5-c-popover", str(sleep_pid))

        self.confirm()
        b.wait_not_present(self.card_row("Storage", location=subvol_mount_point))

        subvol = "new-subvol"
        self.click_dropdown(self.card_row("Storage", location=mount_point), "Create subvolume")
        self.dialog({"name": subvol, "mount_point": subvol_mount_point})

        # We can not delete the last mounted subvolume

        self.click_dropdown(root_sel, "Unmount")
        self.confirm()

        sel = self.card_row("Storage", location=subvol_mount_point)
        b.click(self.dropdown_toggle(sel))
        b.wait_visible(self.dropdown_action(sel, "Delete") + "[disabled]")
        b.wait_text(self.dropdown_description(sel, "Delete"),
                    "The last mounted subvolume can not be deleted")
        b.click(self.dropdown_toggle(sel))

        subvol2 = "subvol2"
        subvol2_mount_point = "/run/subvol2"
        child_subvol = "child-subvol"
        self.click_dropdown(self.card_row("Storage", location=mount_point + " (not mounted)"), "Create subvolume")
        self.dialog({"name": subvol2, "mount_point": subvol2_mount_point, "mount_options.ro": True})
        b.wait_visible(self.card_row("Storage", location=subvol2_mount_point))
        self.assertIn("ro", m.execute(f"findmnt -s -n -o OPTIONS {subvol2_mount_point}"))

        self.click_dropdown(self.card_row("Storage", name=subvol2), "Create subvolume")
        self.dialog({"name": child_subvol}, secondary=True)
        b.wait_visible(self.card_row("Storage", name=child_subvol))

        # Now we can delete "new-subvol"
        self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Delete")
        self.confirm()

        # And also "child-subvol". The readonly-ness of "subvol2" should not matter
        self.click_dropdown(self.card_row("Storage", name=child_subvol), "Delete")
        self.confirm()

    def testMultiDevice(self):
        m = self.machine
        b = self.browser

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk(size=140)
        disk2 = self.add_loopback_disk(size=140)
        label = "raid1"
        mount_point = "/run/butter"
        subvol_mount_point = "/run/cake"
        subvol = "/run/butter/cake"
        subvol2 = "/run/butter/bread"
        subvol_name = os.path.basename(subvol)

        m.execute(f"mkfs.btrfs -L {label} -d raid1 {disk1} {disk2}")
        self.login_and_go("/storage")

        # creation of btrfs partition can take a while on TF.
        with b.wait_timeout(30):
            b.wait_visible(self.card_row("Storage", name=label))

        b.wait_in_text(self.card_row("Storage", name=os.path.basename(disk1)), "btrfs device")
        b.wait_in_text(self.card_row("Storage", name=os.path.basename(disk2)), "btrfs device")

        # We don't allow formatting of multi device btrfs filesystems
        self.click_dropdown(self.card_row("Storage", name=os.path.basename(disk1)), "Format")
        self.dialog_wait_open()
        self.dialog_wait_title(f"{disk1} is in use")
        self.dialog_cancel()
        self.dialog_wait_close()

        # mount /
        self.click_dropdown(self.card_row("Storage", name=label) + " + tr", "Mount")
        self.dialog({"mount_point": mount_point})
        b.wait_visible(self.card_row("Storage", location=mount_point))

        # create subvolume
        m.execute(f"""
            btrfs subvolume create {subvol}
            btrfs subvolume create {subvol2}
        """)
        b.wait_visible(self.card_row("Storage", name=os.path.basename(subvol)))

        self.click_dropdown(self.card_row("Storage", name=os.path.basename(subvol)), "Mount")
        self.dialog({"mount_point": subvol_mount_point})
        b.wait_visible(self.card_row("Storage", location=subvol_mount_point))

        # devices overview
        self.click_card_row("Storage", name=label)
        b.wait_visible(self.card_row("btrfs volume", name=disk1))
        b.wait_visible(self.card_row("btrfs volume", name=disk2))

        # unmount via main page
        b.go("#/")
        b.wait_visible(self.card("Storage"))

        self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Unmount")
        self.confirm()
        b.wait_visible(self.card_row("Storage", location=f"{subvol_mount_point} (not mounted)"))

        self.click_dropdown(self.card_row("Storage", name=os.path.basename(subvol)), "Mount")
        self.dialog({"mount_point": subvol_mount_point})
        b.wait_visible(self.card_row("Storage", location=subvol_mount_point))

        mount_options = m.execute(f"findmnt --fstab -n -o OPTIONS {subvol_mount_point}").strip()
        self.assertIn(f"subvol={subvol_name}", mount_options)
        self.assertEqual(mount_options.count(subvol_name), 1)

        self.click_dropdown(self.card_row("Storage", name=os.path.basename(subvol2)), "Mount")
        self.dialog_wait_open()
        self.dialog_set_val("mount_point", subvol_mount_point)
        self.dialog_apply()
        self.dialog_wait_error("mount_point", f"Mount point is already used for btrfs subvolume {os.path.basename(subvol)} of raid1")
        self.dialog_cancel()

        self.click_dropdown(self.card_row("Storage", location=subvol_mount_point), "Unmount")
        self.confirm()
        b.wait_visible(self.card_row("Storage", location=f"{subvol_mount_point} (not mounted)"))

    def testDefaultSubvolume(self):
        m = self.machine
        b = self.browser

        disk1 = self.add_ram_disk(size=140)
        label = "test_subvol"
        mount_point = "/run/butter"
        subvol = "cake"
        subvol_path = f"{mount_point}/{subvol}"

        m.execute(f"mkfs.btrfs -L {label} {disk1}")
        self.login_and_go("/storage")

        # creation of btrfs partition can take a while on TF.
        with b.wait_timeout(30):
            b.wait_visible(self.card_row("Storage", name="sda"))

        b.wait_in_text(self.card_row("Storage", name=os.path.basename(disk1)), "btrfs filesystem")

        # Create a new btrfs subvolume and set it as default and mount it.
        m.execute(f"""
            mkdir -p {mount_point}
            mount {disk1} {mount_point}
            btrfs subvolume create {subvol_path}
            btrfs subvolume set-default {subvol_path}
            while mountpoint -q {mount_point} && ! umount {mount_point}; do sleep 0.2; done;
            mount {disk1} {mount_point}
        """)

        # Show a warning for mismounting in details.
        b.wait_visible(self.card_row("Storage", name=subvol))
        b.wait_visible(self.card_row("Storage", name=subvol) + ' .ct-icon-exclamation-triangle')
        self.click_card_row("Storage", name=subvol)
        b.wait_text(self.card_desc("btrfs subvolume", "Name"), subvol)

        # Mount automatically on /run/butter on boot
        b.click(self.card_button("btrfs subvolume", f"Mount automatically on {mount_point} on boot"))
        b.wait_not_present(self.card_button("btrfs subvolume", f"Mount automatically on {mount_point} on boot"))

        # No warnings for either subvolume
        b.go("#/")
        self.click_card_row("Storage", name=disk1)
        b.wait_visible(self.card("btrfs filesystem"))
        b.wait_not_present(self.card_row("btrfs filesystem", name=subvol) + ' .ct-icon-exclamation-triangle')
        b.wait_not_present(self.card_row("btrfs filesystem", name="/") + ' .ct-icon-exclamation-triangle')

    def testLuksEncrypted(self):
        m = self.machine
        b = self.browser

        disk = self.add_ram_disk(size=128)
        label = "butter"
        mount_point = "/run/butter"
        passphrase = "einszweidrei"

        m.execute(f"""
            echo {passphrase} | cryptsetup luksFormat --pbkdf-memory 32768 {disk}
            echo {passphrase} | cryptsetup luksOpen {disk} btrfs-test
            mkfs.btrfs -L {label} /dev/mapper/btrfs-test
        """)

        self.login_and_go("/storage")
        # creation of btrfs partition can take a while on TF.
        with b.wait_timeout(30):
            b.wait_visible(self.card_row("Storage", name="sda"))
        b.wait_in_text(self.card_row("Storage", name="sda"), "btrfs filesystem (encrypted)")
        self.click_dropdown(self.card_row("Storage", name="sda") + " + tr", "Mount")
        self.dialog({"mount_point": mount_point})

        # Wait for Cockpit's own mount to go away
        b.wait(lambda: "/var/lib/cockpit/btrfs" not in m.execute("findmnt"))

        m.execute(f"""
            umount {mount_point}
            cryptsetup luksClose /dev/mapper/btrfs-test
        """)
        b.wait_in_text(self.card_row("Storage", name="sda"), "Locked data (encrypted)")
        self.click_dropdown(self.card_row("Storage", name="sda"), "Unlock")
        self.dialog({"passphrase": "einszweidrei"})
        b.wait_in_text(self.card_row("Storage", name="sda"), "btrfs filesystem (encrypted)")

        self.click_dropdown(self.card_row("Storage", name="sda") + " + tr", "Mount")
        self.confirm()
        b.wait_in_text(self.card_row("Storage", location=mount_point), "btrfs subvolume")

    def testNoSubvolMount(self):
        m = self.machine
        b = self.browser

        disk = self.add_ram_disk(size=128)
        mount_point = "/run/butter"

        m.execute(f"""
            mkfs.btrfs -L butter {disk}
            mkdir -p {mount_point}
            mount {disk} {mount_point}
            echo '{disk} {mount_point} auto defaults 0 0' >> /etc/fstab
        """)

        self.login_and_go("/storage")

        self.click_card_row("Storage", name="sda")
        b.wait_visible(self.card_row("btrfs filesystem", name="top-level"))

    def testNothingMounted(self):
        m = self.machine
        b = self.browser

        disk = self.add_ram_disk(size=128)

        m.execute(f"mkfs.btrfs -L butter {disk}; mount {disk} /mnt; btrfs subvolume create /mnt/home; btrfs subvolume create /mnt/home/backups")
        m.execute("while mountpoint -q /mnt && ! umount /mnt; do sleep 0.2; done;")

        self.login_and_go("/storage")

        self.click_card_row("Storage", name="sda")
        b.wait_not_present(self.card_row("btrfs filesystem", name="home"))
        b.wait_not_present(self.card_row("btrfs filesystem", name="backups"))

        # Add some fstab entries. Cockpit should pick up the
        # subvolumes mentioned in them and show them.

        m.execute(f"echo >>/etc/fstab '{disk} /mnt/home auto noauto,subvol=home 0 0'")
        m.execute(f"echo >>/etc/fstab '{disk} /mnt/backups auto noauto,subvol=home/backups 0 0'")

        b.wait_text(self.card_row_col("btrfs filesystem", row_name="home", col_index=3), "/mnt/home (not mounted)")
        b.wait_text(self.card_row_col("btrfs filesystem", row_name="backups", col_index=3), "/mnt/backups (not mounted)")

    def testSnapshot(self):
        m = self.machine
        b = self.browser

        disk = self.add_ram_disk(size=128)
        mount_point = "/run/butter"

        snapshot_dir = f"{mount_point}/snapshots"
        subdir = f"{mount_point}/subdir"

        m.execute(f"""
            mkfs.btrfs -L butter {disk}
            mkdir -p {mount_point}
            mount {disk} {mount_point}
            echo '{disk} {mount_point} auto defaults 0 0' >> /etc/fstab
            # Debian-testing/ubuntu-2204 btrfs-progrs version does not support creating multiple subvolumes at once
            btrfs subvolume create {subdir}
            btrfs subvolume create {snapshot_dir}
            btrfs subvolume create {subdir}/foo
            btrfs subvolume snapshot {subdir} {snapshot_dir}/snap-1
            btrfs subvolume snapshot {mount_point} {snapshot_dir}/snap-2
        """)

        self.login_and_go("/storage")

        self.click_card_row("Storage", name=os.path.basename(subdir))
        b.wait_text(self.card_desc("btrfs subvolume", "Name"), "subdir")
        b.wait_visible(self.card_row("Snapshots", name="snap-1"))

        # Normal subvolume does not show under snapshots
        b.wait_visible(self.card_row("btrfs subvolume", name="foo"))
        b.wait_not_present(self.card_row("Snapshots", name="foo"))

        # Snapshot details
        self.click_card_row("Snapshots", name="snap-1")
        b.wait_text(self.card_desc("btrfs subvolume", "Name"), "snapshots/snap-1")
        b.wait_text(self.card_desc("btrfs subvolume", "Snapshot origin"), "subdir")

        # Origin link works
        b.click(self.card_button("btrfs subvolume", "subdir"))
        b.wait_text(self.card_desc("btrfs subvolume", "Name"), "subdir")


if __name__ == '__main__':
    testlib.test_main()
