#!/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) 2015 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 <http://www.gnu.org/licenses/>.

import math
import os
import unittest

import storagelib
import testlib


# We use basename a lot, let's reduce the noise
def bn(path):
    return os.path.basename(path)


@testlib.nondestructive
class TestStorageLvm2(storagelib.StorageCase):

    def can_do_layouts(self):
        return self.storaged_version >= [2, 10]

    def skip_if_no_layouts(self):
        if not self.can_do_layouts():
            raise unittest.SkipTest("raid layouts not supported")

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

        mount_point_one = "/run/one"
        mount_point_thin = "/run/thin"

        self.login_and_go("/storage")

        dev_1 = self.add_ram_disk()
        dev_2 = self.add_loopback_disk()
        b.wait_in_text("#drives", dev_1)
        b.wait_in_text("#others", dev_2)

        # Create a volume group out of two disks
        m.execute(f"vgcreate TEST1 {dev_1} {dev_2}")
        # just in case the test fails
        self.addCleanup(m.execute, "vgremove --force TEST1 2>/dev/null || true")
        b.wait_in_text("#devices", "TEST1")
        b.wait_in_text("#devices", "/dev/TEST1/")
        b.click('.sidepanel-row:contains("TEST1")')
        b.wait_visible("#storage-detail")
        b.wait_in_text("#detail-sidebar", "scsi_debug")
        b.wait_in_text("#detail-sidebar", dev_2)

        # Both of them should be empty and removable
        b.wait_not_present('#detail-sidebar tr:nth-child(1) button.disabled')
        b.wait_not_present('#detail-sidebar tr:nth-child(2) button.disabled')

        # Create two logical volumes
        m.execute("lvcreate TEST1 -n one -L 20m")
        self.content_row_wait_in_col(1, 1, "one")
        m.execute("lvcreate TEST1 -n two -L 20m")
        self.content_row_wait_in_col(2, 1, "two")

        # Deactivate one
        m.execute("lvchange TEST1/one -a n")
        self.content_row_wait_in_col(1, 2, "Inactive volume")

        # and remove it
        m.execute("until lvremove -f TEST1/one; do sleep 5; done")
        b.wait_not_in_text("#detail-content", "one")

        # remove a disk from the volume group
        m.execute(f"pvmove {dev_2} &>/dev/null || true")
        m.execute(f"vgreduce TEST1 {dev_2}")
        b.wait_not_in_text("#detail-sidebar", dev_2)

        # The remaining lone disk is not removable
        b.wait_visible('#detail-sidebar .sidepanel-row:nth-child(1) button:disabled')

        # Wipe the disk and make sure lvmetad forgets about it.  This
        # might help with reusing it in the second half of this test.
        #
        # HACK - the pvscan is necessary because of
        # https://bugzilla.redhat.com/show_bug.cgi?id=1063813
        #
        m.execute(f"wipefs -a {dev_2}")
        m.execute(f"pvscan --cache {dev_2}")

        # Thin volumes
        m.execute("lvcreate TEST1 --thinpool pool -L 20m")
        self.content_row_wait_in_col(1, 1, "pool")
        m.execute("lvcreate -T TEST1/pool -n thin -V 100m")
        self.content_row_wait_in_col(2, 1, "thin")
        m.execute("dd if=/dev/urandom of=/dev/mapper/TEST1-thin bs=1M count=10 status=none")
        self.content_tab_wait_in_info(1, 1, "Data used", "50%")
        m.execute("until lvremove -f TEST1/thin; do sleep 5; done")
        self.content_tab_wait_in_info(1, 1, "Data used", "0%")

        # remove the volume group
        b.go("#/")
        b.wait_visible("#storage")
        m.execute("vgremove -f TEST1")
        b.wait_not_in_text("#devices", "TEST1")

        # create volume group in the UI

        self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create LVM2 volume group"),
                               expect=lambda: (self.dialog_is_present('disks', dev_1) and
                                               self.dialog_is_present('disks', dev_2) and
                                               self.dialog_check({"name": "vgroup0"})),
                               values={"disks": {dev_1: True,
                                                 dev_2: True}})

        # just in case the test fails
        self.addCleanup(m.execute, "vgremove --force vgroup0 2>/dev/null || true")
        b.wait_in_text("#devices", "vgroup0")

        # Check that the next name is "vgroup1"
        self.devices_dropdown("Create LVM2 volume group")
        self.dialog_wait_open()
        self.dialog_wait_val("name", "vgroup1")
        self.dialog_cancel()
        self.dialog_wait_close()

        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')

        # create a logical volume
        b.click("button:contains(Create new logical volume)")
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block",
                            "size": 20})
        self.content_row_wait_in_col(1, 1, "lvol0")

        # check that the next default name is "lvol1"
        b.click("button:contains(Create new logical volume)")
        self.dialog_wait_open()
        self.dialog_wait_val("name", "lvol1")
        self.dialog_cancel()
        self.dialog_wait_close()

        # rename the lvol0
        self.content_tab_action(1, 1, "edit")
        self.dialog_wait_open()
        self.dialog({"name": "lvol1"})
        self.content_row_wait_in_col(1, 1, "lvol1")

        if self.can_do_layouts():
            # check that it is stored on dev_2
            self.content_tab_wait_in_info(1, 1, "Physical volumes", bn(dev_2))

        # grow it
        self.content_tab_action(1, 1, "Grow")
        self.dialog({"size": 30})

        # format and mount it
        self.content_row_action(1, "Format")
        self.dialog({"type": "ext4",
                     "mount_point": mount_point_one})
        self.content_row_wait_in_col(1, 2, "ext4 filesystem")
        self.assert_in_configuration("/dev/vgroup0/lvol1", "fstab", "dir", mount_point_one)
        self.content_tab_wait_in_info(1, 2, "Mount point", mount_point_one)

        # unmount it
        self.content_dropdown_action(1, "Unmount")
        self.confirm()
        self.content_tab_wait_in_info(1, 2, "Mount point", "The filesystem is not mounted.")

        # shrink it, this time with a filesystem.
        self.content_tab_action(1, 1, "Shrink")
        self.dialog({"size": 10})

        # delete it
        self.content_dropdown_action(1, "Delete")
        self.confirm()
        b.wait_not_in_text("#detail-content", "/dev/vgroup0/lvol1")
        self.assertEqual(m.execute(f"grep {mount_point_one} /etc/fstab || true"), "")

        # remove disk2
        b.click(f'#detail-sidebar .sidepanel-row:contains({dev_2}) button.pf-m-secondary')
        b.wait_not_in_text("#detail-sidebar", dev_2)
        b.wait_in_text("#detail-header dt:contains(Capacity) + dd", "50.3 MB")

        # create thin pool and volume
        # the pool will be maximum size, 50.3 MB
        b.click("button:contains(Create new logical volume)")
        self.dialog(expect={"size": 50.3},
                    values={"purpose": "pool",
                            "name": "pool",
                            "size": 38})
        self.content_row_wait_in_col(1, 1, "pool")

        self.content_row_action(1, "Create thin volume")
        self.dialog(expect={"name": "lvol0"},
                    values={"name": "thin",
                            "size": 50})
        self.content_row_wait_in_col(2, 1, "thin")

        # add a disk and resize the pool
        b.click('#detail-sidebar .pf-v5-c-card__header button')
        self.dialog_wait_open()
        self.dialog_set_val('disks', {dev_2: True})
        self.dialog_apply()
        self.dialog_wait_close()
        b.wait_in_text("#detail-sidebar", dev_2)
        # this is sometimes 96, sometimes 100 MB
        b.wait_js_cond("Number(ph_text('#detail-header dt:contains(Capacity) + dd').split(' ')[0]) >= 96")
        b.wait_js_cond("Number(ph_text('#detail-header dt:contains(Capacity) + dd').split(' ')[0]) <= 101")

        self.content_tab_action(1, 1, "Grow")
        self.dialog({"size": 70})

        # There is not enough free space to remove any of the disks.
        b.wait_visible('#detail-sidebar .sidepanel-row:nth-child(1) button:disabled')
        b.wait_visible('#detail-sidebar .sidepanel-row:nth-child(2) button:disabled')

        # use almost all of the pool by erasing the thin volume
        self.content_row_action(2, "Format")
        self.dialog({"erase.on": True,
                     "type": "ext4",
                     "mount_point": mount_point_thin})
        self.assert_in_configuration("/dev/vgroup0/thin", "fstab", "dir", mount_point_thin)
        self.content_row_wait_in_col(2, 2, "ext4 filesystem")

        # remove pool
        self.content_dropdown_action(1, "Delete")
        self.confirm()
        b.wait_not_in_text("#detail-content", "pool")
        self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")

        # make another logical volume and format it, just so that we
        # can see whether deleting the volume group will clean it all
        # up.
        b.click("button:contains(Create new logical volume)")
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block"})
        self.content_row_wait_in_col(1, 1, "lvol0")
        self.content_row_action(1, "Format")
        self.dialog({"type": "ext4",
                     "mount_point": mount_point_one})
        self.assert_in_configuration("/dev/vgroup0/lvol0", "fstab", "dir", mount_point_one)
        self.content_row_wait_in_col(1, 2, "ext4 filesystem")
        self.content_tab_wait_in_info(1, 2, "Mount point", mount_point_one)

        # rename volume group
        b.click('.pf-v5-c-card__header:contains("LVM2 volume group") button:contains("Rename")')
        self.dialog_wait_open()
        self.dialog_set_val("name", "vgroup1")
        self.dialog_apply()
        self.dialog_wait_close()

        # remove volume group
        b.click('.pf-v5-c-card__header:contains("LVM2 volume group vgroup1")')
        b.click('.pf-v5-c-card__header:contains("LVM2 volume group") button:contains("Delete")')
        self.confirm()
        b.wait_visible("#storage")
        b.wait_not_in_text("#devices", "vgroup1")
        self.assertEqual(m.execute(f"grep {mount_point_thin} /etc/fstab || true"), "")

        # FIXME: Grow/Shrink buttons are in a dd > div > dd without dl wrapper
        self.allow_browser_errors("validateDOMNesting.*cannot appear as a descendant.*<dd> dd")

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

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk()
        b.wait_in_text("#drives", disk1)
        b.wait_in_text("#others", disk2)

        # Put a partition table on disk1 and disk2
        m.execute(f'parted -s {disk1} mktable gpt')
        m.execute(f'parted -s {disk2} mktable gpt')

        # Create a volume group out of disk1
        self.dialog_with_retry(trigger=lambda: self.devices_dropdown('Create LVM2 volume group'),
                               expect=lambda: self.dialog_is_present('disks', disk1) and
                               self.dialog_is_present('disks', "unpartitioned space on Linux scsi_debug"),
                               values={"disks": {disk1: True}})

        b.wait_in_text("#devices", "vgroup0")
        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')

        # Check the we are really using a partition on disk1 now
        b.wait_in_text("#detail-sidebar", "Partition of Linux scsi_debug")

        # Add the unused space of disk2
        self.dialog_with_retry(trigger=lambda: b.click('#detail-sidebar .pf-v5-c-card__header button'),
                               expect=lambda: self.dialog_is_present(
                                   'disks', "unpartitioned space on " + disk2),
                               values={"disks": {disk2: True}})
        b.wait_in_text("#detail-sidebar", "Partition of " + disk2)

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

        self.login_and_go("/storage")

        disk = self.add_ram_disk(100)
        b.wait_in_text("#drives", disk)

        m.execute(f"vgcreate TEST {disk}")

        self.addCleanup(m.execute, "vgremove --force TEST")
        b.wait_in_text("#devices", "TEST")
        b.click('.sidepanel-row:contains("TEST")')
        b.wait_visible("#storage-detail")

        # Create a thinpool and a thin volume in it

        m.execute("lvcreate TEST --thinpool pool -L 10m")
        m.execute("lvcreate -T TEST/pool -n thin -V 30m")
        self.content_row_wait_in_col(2, 1, "thin")

        # Create a logical volume and take a snapshot of it.  We will
        # later check that the snapshot isn't shown.

        m.execute("lvcreate TEST -n lvol0 -L 10m")
        m.execute("lvcreate -s -n snap0 -L 10m TEST/lvol0")

        # the above lvol0 will be the new first entry in the table, so
        # TEST/thin moves to he third row
        self.content_row_wait_in_col(1, 1, "lvol0")
        self.content_row_wait_in_col(2, 1, "pool")
        self.content_row_wait_in_col(3, 1, "thin")

        # Take a snapshot of the thin volume and check that it appears

        self.content_dropdown_action(3, "Create snapshot")
        self.dialog({"name": "mysnapshot"})
        self.content_row_wait_in_col(3, 1, "mysnapshot")

        # Now check that the traditional snapshot is not shown.  We do
        # this here to be sure that Cockpit is fully caught up and has
        # actually ignored it instead of just not having gotten around
        # to showing it.

        self.content_row_wait_in_col(1, 1, "lvol0")
        b.wait_not_in_text("#storage-detail", "snap0")

        # FIXME: Grow/Shrink buttons are in a dd > div > dd without dl wrapper
        self.allow_browser_errors("validateDOMNesting.*cannot appear as a descendant.*<dd> dd")

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

        self.skip_if_no_layouts()

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk(name="loop10")
        disk3 = self.add_loopback_disk(name="loop11")
        disk4 = self.add_loopback_disk(name="loop12")

        # Make a volume group with four physical volumes

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True}})

        self.addCleanup(m.execute, "vgremove --force vgroup0 2>/dev/null || true")
        b.wait_in_text("#devices", "vgroup0")

        # Make a raid5 on three PVs, using about half of each

        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')
        b.click("button:contains(Create new logical volume)")
        self.dialog_wait_open()
        self.dialog_wait_val("name", "lvol0")
        self.dialog_set_val("purpose", "block")
        self.dialog_set_val("layout", "raid5")
        self.dialog_set_val("pvs", {disk1: False})
        self.dialog_set_val("size", 40)
        b.assert_pixels("#dialog", "create-raid5")
        self.dialog_apply()
        self.dialog_wait_close()

        self.content_row_wait_in_col(1, 1, "lvol0")

        self.content_tab_wait_in_info(1, 1, "Layout", "Distributed parity (RAID 5)")
        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk2))
        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk3))
        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk4))

        tab = self.content_tab_expand(1, 1)
        b.assert_pixels(tab, "raid5-tab")

        # Make linear volume to fully use second PV

        b.click("button:contains(Create new logical volume)")
        self.dialog(expect={"name": "lvol1"},
                    values={"purpose": "block",
                            "layout": "linear",
                            "pvs": {disk1: False, disk3: False, disk4: False}})
        self.content_row_wait_in_col(2, 1, "lvol1")
        self.content_tab_wait_in_info(2, 1, "Physical volumes", bn(disk2))

        # Grow raid5 to about maximum
        self.content_tab_action(1, 1, "Grow")
        self.dialog_wait_open()
        self.dialog_set_val("size", 80)
        b.assert_pixels("#dialog", "grow-raid5")
        self.dialog_apply()
        self.dialog_wait_close()

        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk1))

        # Check that each PV is used exactly once.
        tab = self.content_tab_expand(1, 1)
        b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)
        b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)
        b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)
        b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)
        b.assert_pixels(tab, "raid5-tab2")

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

        self.skip_if_no_layouts()

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk(name="loop10")
        disk3 = self.add_loopback_disk(name="loop11")
        disk4 = self.add_loopback_disk(name="loop12")

        # Make a volume group with three physical volumes

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True}})

        self.addCleanup(m.execute, "vgremove --force vgroup0 2>/dev/null || true")
        b.wait_in_text("#devices", "vgroup0")

        # Make a raid5 on the three PVs

        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')
        b.click("button:contains(Create new logical volume)")
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block",
                            "layout": "raid5"})
        self.content_row_wait_in_col(1, 1, "lvol0")

        self.content_tab_wait_in_info(1, 1, "Layout", "Distributed parity (RAID 5)")
        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk1))
        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk2))
        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk3))

        # Kill one PV

        self.force_remove_disk(disk1)

        b.wait_in_text(".pf-v5-c-alert", "This volume group is missing some physical volumes.")
        b.wait_visible("button:contains(Rename):disabled")

        self.content_tab_wait_in_info(1, 1, "Stripes", "This logical volume has lost some of its physical volumes but has not lost any data yet.")
        self.content_tab_wait_in_info(1, 1, "Stripes", cond=lambda sel: bn(disk1) not in b.text(sel))

        # Repair with fourth

        self.content_row_action(1, "Repair")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "There is not enough space available")
        self.dialog_cancel()
        self.dialog_wait_close()

        b.click('#detail-sidebar .pf-v5-c-card__header button')
        self.dialog_wait_open()
        self.dialog_set_val('disks', {disk4: True})
        self.dialog_apply()
        self.dialog_wait_close()
        b.wait_in_text("#detail-sidebar", disk4)

        self.content_row_action(1, "Repair")
        self.dialog_wait_open()
        self.dialog_apply()
        self.dialog_wait_error("pvs", "An additional 46.1 MB must be selected")
        self.dialog_set_val("pvs", {disk4: True})
        self.dialog_apply()
        self.dialog_wait_close()

        self.content_tab_wait_in_info(1, 1, "Stripes",
                                      cond=lambda sel: "This logical volume has lost some" not in b.text(sel))
        self.content_tab_wait_in_info(1, 1, "Stripes", bn(disk4))

        # Dismiss alert

        b.click(".pf-v5-c-alert button:contains(Dismiss)")
        self.dialog({})
        b.wait_not_present(".pf-v5-c-alert")

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

        self.skip_if_no_layouts()

        self.login_and_go("/storage")

        disk1 = self.add_ram_disk()
        disk2 = self.add_loopback_disk()
        disk3 = self.add_loopback_disk()

        # Make a volume group with three physical volumes

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True}})

        self.addCleanup(m.execute, "vgremove --force vgroup0 2>/dev/null || true")
        b.wait_in_text("#devices", "vgroup0")

        # Make a linear volume on two of them

        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')
        b.click("button:contains(Create new logical volume)")
        self.dialog(expect={"name": "lvol0"},
                    values={"purpose": "block",
                            "layout": "linear",
                            "pvs": {disk2: False}})
        self.content_row_wait_in_col(1, 1, "lvol0")

        self.content_tab_wait_in_info(1, 1, "Physical volumes", bn(disk1))
        self.content_tab_wait_in_info(1, 1, "Physical volumes", bn(disk3))

        # Kill one PV

        self.force_remove_disk(disk1)
        b.wait_in_text(".pf-v5-c-alert", "This volume group is missing some physical volumes.")
        b.wait_visible("button:contains(Rename):disabled")

        self.content_tab_wait_in_info(1, 1, "Physical volumes", "This logical volume has lost some of its physical volumes and can no longer be used.")
        self.content_tab_wait_in_info(1, 1, "Physical volumes", cond=lambda sel: bn(disk1) not in b.text(sel))

        # Dismiss alert, this will delete the volume

        b.click(".pf-v5-c-alert button:contains(Dismiss)")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "/dev/vgroup0/lvol0")
        self.dialog_apply()
        self.dialog_wait_close()

        b.wait_not_present(".pf-v5-c-alert")
        b.wait_in_text("#detail-content", "No logical volumes")

    def testMaxLayoutSizes(self):
        b = self.browser

        self.skip_if_no_layouts()

        # Make sure that Cockpit gets the computation of the maximum
        # size right.

        self.login_and_go("/storage")

        disk1 = self.add_loopback_disk(name="loop10")
        disk2 = self.add_loopback_disk(name="loop11")
        disk3 = self.add_loopback_disk(name="loop12")
        disk4 = self.add_loopback_disk(name="loop13")
        disk5 = self.add_loopback_disk(name="loop14")
        disk6 = self.add_loopback_disk(name="loop15")

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_is_present('disks', disk5) and
                                                   self.dialog_is_present('disks', disk6) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True,
                                                     disk5: True,
                                                     disk6: True}})
        self.addCleanup(self.machine.execute, "vgremove --force vgroup0 2>/dev/null || true")

        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')

        def mb(size):
            if size < 100e6:
                return str(round(size / 1e6, 1)) + " MB"
            else:
                return str(round(size / 1e6)) + " MB"

        def test(layout, expected_size):
            b.click("button:contains(Create new logical volume)")
            self.dialog_wait_open()
            self.dialog_wait_val("name", "lvol0")
            self.dialog_set_val("purpose", "block")
            self.dialog_set_val("layout", layout)
            if layout == "raid10":
                self.dialog_set_val("pvs", {disk6: False})
                self.dialog_apply()
                self.dialog_wait_error("pvs", "an even number")
                self.dialog_set_val("pvs", {disk6: True})
            self.dialog_apply()
            self.dialog_wait_close()

            self.content_row_wait_in_col(1, 1, "lvol0")

            field = "Physical volumes" if layout.startswith("linear") else "Stripes"

            self.content_tab_wait_in_info(1, 1, field, bn(disk1))
            self.content_tab_wait_in_info(1, 1, field, bn(disk2))
            self.content_tab_wait_in_info(1, 1, field, bn(disk3))
            self.content_tab_wait_in_info(1, 1, field, bn(disk4))
            self.content_tab_wait_in_info(1, 1, field, bn(disk5))
            self.content_tab_wait_in_info(1, 1, field, bn(disk6))

            self.content_tab_wait_in_info(1, 1, "Size", mb(expected_size))

            self.content_dropdown_action(1, "Delete")
            self.confirm()
            b.wait_not_in_text("#detail-content", "/dev/vgroup0/lvol0")

        ext_size = 4 * 1024 * 1024
        pv_size = math.floor(50e6 / ext_size) * ext_size
        n_pvs = 6

        test("linear", n_pvs * pv_size)
        test("raid0", n_pvs * pv_size)
        test("raid1", pv_size - ext_size)
        test("raid10", (n_pvs / 2) * (pv_size - ext_size))
        test("raid5", (n_pvs - 1) * (pv_size - ext_size))
        test("raid6", (n_pvs - 2) * (pv_size - ext_size))

    def testMaxLayoutGrowth(self):
        b = self.browser

        self.skip_if_no_layouts()

        # Make sure that Cockpit gets the computation of the maximum
        # size right when growing a logical volume.

        self.login_and_go("/storage")

        disk1 = self.add_loopback_disk(name="loop10")
        disk2 = self.add_loopback_disk(name="loop11")
        disk3 = self.add_loopback_disk(name="loop12")
        disk4 = self.add_loopback_disk(name="loop13")
        disk5 = self.add_loopback_disk(name="loop14")
        disk6 = self.add_loopback_disk(name="loop15")

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_is_present('disks', disk5) and
                                                   self.dialog_is_present('disks', disk6) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True,
                                                     disk5: True,
                                                     disk6: True}})
        self.addCleanup(self.machine.execute, "vgremove --force vgroup0 2>/dev/null || true")

        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')

        def mb(size):
            if size < 100e6:
                return str(round(size / 1e6, 1)) + " MB"
            else:
                return str(round(size / 1e6)) + " MB"

        def test(layout, expected_size):
            b.click("button:contains(Create new logical volume)")
            self.dialog(expect={"name": "lvol0"},
                        values={"purpose": "block",
                                "layout": layout,
                                "size": round(expected_size / 2e6)})

            self.content_row_wait_in_col(1, 1, "lvol0")

            field = "Stripes"

            self.content_tab_wait_in_info(1, 1, field, bn(disk1))
            self.content_tab_wait_in_info(1, 1, field, bn(disk2))
            self.content_tab_wait_in_info(1, 1, field, bn(disk3))
            self.content_tab_wait_in_info(1, 1, field, bn(disk4))
            self.content_tab_wait_in_info(1, 1, field, bn(disk5))
            self.content_tab_wait_in_info(1, 1, field, bn(disk6))

            # Grow to max with default pvs
            self.content_tab_action(1, 1, "Grow")
            self.dialog_wait_open()
            slider = self.dialog_field("size") + " .pf-v5-c-slider .pf-v5-c-slider__rail"
            width = b.call_js_func('(function (sel) { return ph_find(sel).offsetWidth; })', slider)
            b.mouse(slider, "click", width, 0)
            self.dialog_apply()
            self.dialog_wait_close()

            tab = self.content_tab_expand(1, 1)
            b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk1)}')", 1)
            b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk2)}')", 1)
            b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk3)}')", 1)
            b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk4)}')", 1)
            b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk5)}')", 1)
            b.wait_js_func("ph_count_check", tab + f" .storage-stripe-pv-box-dev:contains('{bn(disk6)}')", 1)

            self.content_tab_wait_in_info(1, 1, "Size", mb(expected_size))

            self.content_dropdown_action(1, "Delete")
            self.confirm()
            b.wait_not_in_text("#detail-content", "/dev/vgroup0/lvol0")

        ext_size = 4 * 1024 * 1024
        pv_size = math.floor(50e6 / ext_size) * ext_size
        n_pvs = 6

        test("raid0", n_pvs * pv_size)
        test("raid1", pv_size - ext_size)
        test("raid10", (n_pvs / 2) * (pv_size - ext_size))
        test("raid5", (n_pvs - 1) * (pv_size - ext_size))
        test("raid6", (n_pvs - 2) * (pv_size - ext_size))

    @testlib.skipImage("No targetd in Arch Linux", "arch")
    def testDegradation(self):
        b = self.browser

        self.skip_if_no_layouts()

        # Make one (very small) logical volume for each RAID type and
        # then break them.

        self.login_and_go("/storage")

        disk1 = self.add_targetd_loopback_disk(index=1, size=100)
        disk2 = self.add_targetd_loopback_disk(index=2, size=100)
        disk3 = self.add_targetd_loopback_disk(index=3, size=100)
        disk4 = self.add_targetd_loopback_disk(index=4, size=100)
        disk5 = self.add_targetd_loopback_disk(index=5, size=100)

        with b.wait_timeout(60):
            self.dialog_with_retry(trigger=lambda: self.devices_dropdown("Create LVM2 volume group"),
                                   expect=lambda: (self.dialog_is_present('disks', disk1) and
                                                   self.dialog_is_present('disks', disk2) and
                                                   self.dialog_is_present('disks', disk3) and
                                                   self.dialog_is_present('disks', disk4) and
                                                   self.dialog_is_present('disks', disk5) and
                                                   self.dialog_check({"name": "vgroup0"})),
                                   values={"disks": {disk1: True,
                                                     disk2: True,
                                                     disk3: True,
                                                     disk4: True,
                                                     disk5: True}})
        self.addCleanup(self.machine.execute, "vgremove --force vgroup0 2>/dev/null || true")

        b.click('.sidepanel-row:contains("vgroup0")')
        b.wait_visible('#storage-detail')

        def create(row, layout, expected_name):
            b.click("button:contains(Create new logical volume)")
            self.dialog_wait_open()
            self.dialog_wait_val("name", expected_name)
            self.dialog_set_val("layout", layout)
            if layout == "raid10":
                self.dialog_set_val("pvs", {disk5: False})
            elif layout == "linear":
                self.dialog_set_val("pvs", {disk2: False, disk3: False, disk4: False, disk5: False})
            self.dialog_set_val("size", 10)
            self.dialog_apply()
            self.dialog_wait_close()
            self.content_row_wait_in_col(row, 1, expected_name)

        create(1, "linear", "lvol0")
        create(2, "raid0", "lvol1")
        create(3, "raid1", "lvol2")
        create(4, "raid10", "lvol3")
        create(5, "raid5", "lvol4")
        create(6, "raid6", "lvol5")

        def wait_msg(row, msg):
            self.content_tab_wait_in_info(row, 1, "Physical volumes" if row == 1 else "Stripes", msg)

        def wait_partial(row):
            wait_msg(row, "This logical volume has lost some of its physical volumes and can no longer be used.")

        def wait_degraded(row):
            wait_msg(row, "This logical volume has lost some of its physical volumes but has not lost any data yet.")

        def wait_maybe_partial(row):
            wait_msg(row, "This logical volume has lost some of its physical volumes but might not have lost any data yet.")

        self.force_remove_disk(disk1)

        wait_partial(1)         # linear is broken now
        wait_partial(2)         # striped as well
        wait_degraded(3)        # mirror is fine
        wait_degraded(4)        # striped mirror as well
        wait_degraded(5)        # raid5 and ...
        wait_degraded(6)        # ... raid6 are doing their job

        self.force_remove_disk(disk2)

        wait_degraded(3)        # mirror is still fine
        wait_maybe_partial(4)   # striped mirror is not sure anymore
        wait_partial(5)         # raid5 is now toast
        wait_degraded(6)        # but raid6 still hangs on

        self.force_remove_disk(disk3)

        wait_degraded(3)        # mirror is _still_ fine
        wait_partial(4)         # striped mirror has lost more than half and is kaputt
        wait_partial(6)         # raid6 is finally toast as well

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

        self.skip_if_no_layouts()

        # Make sure that Cockpit gets the layout description right for
        # encrypted physical volumes

        self.login_and_go("/storage")

        disk = self.add_loopback_disk()
        b.wait_in_text("#others", disk)
        m.execute(f"echo einszweidrei | cryptsetup luksFormat --pbkdf-memory 32768 {disk}")
        m.execute(f"echo einszweidrei | cryptsetup luksOpen {disk} dm-test")
        self.addCleanup(m.execute, "cryptsetup close dm-test || true")
        m.execute("vgcreate vgroup0 /dev/mapper/dm-test")
        self.addCleanup(m.execute, "vgremove --force vgroup0 2>/dev/null || true")
        m.execute("lvcreate vgroup0 -n lvol0 -l100%FREE")

        b.wait_in_text("#devices", "vgroup0")
        b.click('#devices .sidepanel-row:contains("vgroup0")')

        self.content_tab_wait_in_info(1, 1, "Physical volumes", bn(disk))


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