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

import json

import storagelib
import testlib


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

    def enterAnacondaMode(self, config):
        b = self.browser
        b.call_js_func("window.sessionStorage.setItem", "cockpit_anaconda", json.dumps(config))
        b.reload()
        b.enter_page("/storage")

    def expectExportedDevice(self, device, value):
        mpm = json.loads(self.browser.call_js_func("window.sessionStorage.getItem", "cockpit_mount_points"))
        self.assertIn(device, mpm)
        self.assertEqual(mpm[device], value)

    def expectExportedDevicePassphrase(self, device, value):
        pp = json.loads(self.browser.call_js_func("window.sessionStorage.getItem", "cockpit_passphrases"))
        self.assertIn(device, pp)
        self.assertEqual(pp[device], value)

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

        disk = self.add_ram_disk()

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk],
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        # There should be only one row, for our disk
        b.wait(lambda: b.call_js_func('ph_count', self.card("Storage") + " tbody tr") == 1)
        b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
        b.wait_not_present(self.card_row("Storage", location="/"))

        # Create a volume group with a logical volume
        self.click_devices_dropdown("Create LVM2 volume group")
        self.dialog_wait_open()
        b.wait(lambda: b.call_js_func('ph_count', "#dialog .select-space-name") == 1)
        self.dialog_set_val("disks", {disk: True})
        self.dialog_apply()
        self.dialog_wait_close()
        self.click_dropdown(self.card_row("Storage", name="vgroup0"), "Create new logical volume")
        self.dialog({})

        # Create an encrypted filesystem
        self.click_dropdown(self.card_row("Storage", name="lvol0"), "Format")
        self.dialog_wait_open()
        b.wait_not_present(self.dialog_field("at_boot"))
        b.wait_not_present(self.dialog_field("mount_options"))
        self.dialog_set_val("type", "ext4")
        self.dialog_set_val("mount_point", "/")
        self.dialog_set_val("crypto", self.default_crypto_type)
        self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
        self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
        self.dialog_apply()
        self.dialog_wait_close()

        self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))
        self.assertNotIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))

        self.click_card_row("Storage", name="lvol0")
        b.click(self.card_parent_link())
        b.click(self.card_parent_link())
        b.wait_visible(self.card("Storage"))

        b.assert_pixels("body", "page")

        # Edit mount point
        self.click_dropdown(self.card_row("Storage", name="lvol0"), "Edit mount point")
        self.dialog_wait_open()
        self.dialog_wait_val("mount_point", "/")
        self.dialog_set_val("mount_point", "/var")
        self.dialog_apply()
        self.dialog_wait_close()
        self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))

        # Mount/Unmount the filesystem
        self.click_card_row("Storage", location="/var")
        b.wait_visible(self.card("Encryption"))
        # LUKS should be open
        b.wait_in_text(self.card_desc("Encryption", "Cleartext device"), "/dev/mapper")
        b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "/var")

        b.click(self.card_button("ext4 filesystem", "Mount"))
        self.dialog_wait_open()
        self.dialog_wait_val("mount_point", "/var")
        self.dialog_apply()
        self.dialog_wait_close()
        self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))

        b.click(self.card_button("ext4 filesystem", "Unmount"))
        self.dialog_wait_open()
        b.wait_text("#dialog .pf-v5-c-modal-box__title-text", "Unmount filesystem /var")
        self.dialog_apply()
        self.dialog_wait_close()
        self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))

        # LUKS should still be open, close it so that we can deactivate the LV
        cleartext = b.text(self.card_desc("Encryption", "Cleartext device"))
        m.execute(f"cryptsetup close {cleartext}")
        b.wait_text(self.card_desc("Encryption", "Cleartext device"), "-")

        # Deactivate
        b.click(self.card_button("LVM2 logical volume", "Deactivate"))
        self.confirm()
        testlib.wait(lambda: m.execute("if ! test -e /dev/vgroup0/lvol0; then echo gone; fi").strip() == "gone")

        # Check exported information.
        self.expectExportedDevicePassphrase("/dev/vgroup0/lvol0", "vainu-reku-toma-rolle-kaja")
        self.expectExportedDevice("/dev/vgroup0/lvol0",
                                  {
                                      "type": "crypto",
                                      "content": {
                                          "type": "filesystem",
                                          "dir": "/var"
                                      }
                                  })

        # Activate and mount again, to check location in tear down information
        b.click(self.card_button("Inactive logical volume", "Activate"))
        b.click(self.card_button("Filesystem", "Mount"))
        self.dialog_wait_open()
        self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
        self.dialog_apply()
        self.dialog_wait_close()

        # Check and delete volume group
        b.click(self.card_parent_link())
        b.wait_visible(self.card_row("LVM2 volume group", name=disk))
        self.click_card_dropdown("LVM2 volume group", "Delete group")
        self.dialog_wait_open()
        b.wait_text("#dialog td[data-label='Location']", "/var")
        self.dialog_apply()
        self.dialog_wait_close()
        m.execute("! findmnt --fstab -n /sysroot/var")

        # Back to the beginning
        b.wait_visible(self.card("Storage"))
        b.wait(lambda: b.call_js_func('ph_count', self.card("Storage") + " tbody tr") == 1)
        b.wait_not_present(self.card_row("Storage", location="/var"))

    @testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
    def testStratis(self):
        m = self.machine
        b = self.browser

        m.execute("systemctl start stratisd")
        self.addCleanup(m.execute, "systemctl stop stratisd")

        PV_SIZE = 4000  # 4 GB in MB

        disk = self.add_loopback_disk(PV_SIZE, name="loop10")

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk],
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        # Create a Stratis pool
        self.click_devices_dropdown("Create Stratis pool")
        self.dialog_wait_open()
        b.wait(lambda: b.call_js_func('ph_count', "#dialog .select-space-name") == 1)
        self.dialog_set_val("disks", {disk: True})
        self.dialog_apply()
        self.dialog_wait_close()
        self.click_dropdown(self.card_row("Storage", name="pool0"), "Create new filesystem")
        self.dialog_wait_open()
        b.wait_not_present(self.dialog_field("at_boot"))
        b.wait_not_present(self.dialog_field("mount_options"))
        self.dialog_set_val("name", "root")
        self.dialog_set_val("mount_point", "/")
        self.dialog_apply()
        self.dialog_wait_close()

        self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))
        self.assertNotIn("nofail", m.execute("findmnt --fstab -n -o OPTIONS /sysroot"))

        b.wait_visible(self.card_row_col("Storage", 3, 5) + " .usage-bar[role=progressbar]")

        # Edit mount point
        self.click_dropdown(self.card_row("Storage", name="root"), "Edit mount point")
        self.dialog_wait_open()
        self.dialog_wait_val("mount_point", "/")
        self.dialog_set_val("mount_point", "/var")
        self.dialog_apply()
        self.dialog_wait_close()
        self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))

        # Mount/Unmount the filesystem
        self.click_card_row("Storage", location="/var")
        b.wait_in_text(self.card_desc("Stratis filesystem", "Mount point"), "/var")
        b.click(self.card_button("Stratis filesystem", "Mount"))
        self.dialog_wait_open()
        self.dialog_wait_val("mount_point", "/var")
        self.dialog_apply()
        self.dialog_wait_close()
        self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))

        b.click(self.card_button("Stratis filesystem", "Unmount"))
        self.dialog_wait_open()
        b.wait_text("#dialog .pf-v5-c-modal-box__title-text", "Unmount filesystem /var")
        self.dialog_apply()
        self.dialog_wait_close()
        self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))

        # Check exported mount point information.

        self.expectExportedDevice("/dev/stratis/pool0/root",
                                  {
                                      "type": "filesystem",
                                      "dir": "/var"
                                  })

        # Mount again, to check location in tear down information
        b.click(self.card_button("Stratis filesystem", "Mount"))
        self.confirm()
        self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot/var"))

        # Check and delete pool
        b.click(self.card_parent_link())
        b.wait_visible(self.card_row("Stratis pool", name=disk))
        self.click_card_dropdown("Stratis pool", "Delete")
        self.dialog_wait_open()
        b.wait_text("#dialog td[data-label='Location']", "/var")
        self.dialog_apply()
        self.dialog_wait_close()
        m.execute("! findmnt --fstab -n /sysroot/var")

    @testlib.skipImage('no btrfs support', 'rhel-*', 'centos-*')
    def testBtrfs(self):
        b = self.browser

        disk = self.add_ram_disk(200)

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk],
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        # Create a encrypted Btrfs filesystem

        b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
        self.click_dropdown(self.card_row("Storage", 1), "Format")
        self.dialog_wait_open()
        b.wait_not_present(self.dialog_field("at_boot"))
        b.wait_not_present(self.dialog_field("mount_options"))
        self.dialog_set_val("name", "butter")
        self.dialog_set_val("type", "btrfs")
        self.dialog_set_val("mount_point", "/mnt/butter")
        self.dialog_set_val("crypto", self.default_crypto_type)
        self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
        self.dialog_set_val("passphrase2", "vainu-reku-toma-rolle-kaja")
        self.dialog_apply()
        self.dialog_wait_close()

        # Mount so that we can create subvolumes
        b.wait_text(self.card_row_col("Storage", 1, 3), "btrfs filesystem (encrypted)")
        self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Mount")
        self.dialog({})

        # Create two subvolumes
        self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Create subvolume")
        self.dialog_wait_open()
        b.wait_not_present(self.dialog_field("at_boot"))
        b.wait_not_present(self.dialog_field("mount_options"))
        self.dialog_set_val("name", "root")
        self.dialog_set_val("mount_point", "/")
        b.wait(lambda: b.call_js_func('ph_count', "#dialog button.apply") == 1)
        self.dialog_apply()
        self.dialog_wait_close()

        self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Create subvolume")
        self.dialog_wait_open()
        self.dialog_set_val("name", "home")
        self.dialog_set_val("mount_point", "/home")
        self.dialog_apply()
        self.dialog_wait_close()

        # Unmount and lock, mount point exporting should still work

        self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Unmount")
        self.dialog({})
        self.click_dropdown(self.card_row("Storage", name=disk), "Lock")
        b.wait_text(self.card_row_col("Storage", 1, 3), "Locked data (encrypted)")

        self.expectExportedDevice(disk,
                                  {
                                      "type": "crypto",
                                      "content": {
                                          "type": "filesystem",
                                          "subvolumes": {
                                              "/": {
                                                  "dir": "/mnt/butter"
                                              },
                                              "root": {
                                                  "dir": "/"
                                              },
                                              "home": {
                                                  "dir": "/home"
                                              },
                                          }
                                      }
                                  })

    def testBiosboot(self):
        b = self.browser

        disk = self.add_ram_disk()

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk],
            "efi": False,
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        # Create a biosboot partition on GPT
        b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
        self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
        self.confirm()
        b.wait_text(self.card_row_col("Storage", 2, 2), "Free space")
        self.click_dropdown(self.card_row("Storage", 2), "Create partition")
        self.dialog({"type": "biosboot"})

        # Check the type and set it to something else
        b.wait_text(self.card_row_col("Storage", 2, 3), "Unformatted data (BIOS boot partition)")
        self.click_card_row("Storage", 2)
        b.wait_text(self.card_desc("Partition", "Type"), "BIOS boot partition")
        b.click(self.card_desc_action("Partition", "Type"))
        self.dialog({"type": "0fc63daf-8483-4772-8e79-3d69d8477de4"})
        b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data")

        # Correct it by reformatting as "biosboot"
        self.click_card_dropdown("Unformatted data", "Format")
        self.dialog({"type": "biosboot"})
        b.wait_text(self.card_desc("Partition", "Type"), "BIOS boot partition")

    def testEfiSystemPartition(self):
        b = self.browser

        disk = self.add_ram_disk()

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk],
            "efi": True,
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        # Create a EFI system partition on GPT
        b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
        self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
        self.confirm()
        b.wait_text(self.card_row_col("Storage", 2, 2), "Free space")
        self.click_dropdown(self.card_row("Storage", 2), "Create partition")
        self.dialog({"type": "efi"})

        # Check the type and set it to something else
        b.wait_text(self.card_row_col("Storage", 2, 3), "vfat filesystem (EFI system partition)")
        b.wait_text(self.card_row_col("Storage", 2, 4), "/boot/efi")
        self.click_card_row("Storage", 2)
        b.wait_visible(self.card("vfat filesystem"))
        b.wait_text(self.card_desc("Partition", "Type"), "EFI system partition")
        b.click(self.card_desc_action("Partition", "Type"))
        self.dialog({"type": "0fc63daf-8483-4772-8e79-3d69d8477de4"})
        b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data")

        # Correct it by reformatting as "efi"
        self.click_card_dropdown("vfat filesystem", "Format")
        self.dialog({"type": "efi"})
        b.wait_text(self.card_desc("Partition", "Type"), "EFI system partition")

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

        disk = self.add_ram_disk()

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk],
            "default_fsys_type": "vfat",
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
        self.click_dropdown(self.card_row("Storage", 1), "Create partition table")
        self.confirm()
        b.wait_text(self.card_row_col("Storage", 2, 2), "Free space")

        # Only one apply button in the Create Partition dialog,
        # default filesystem type should be "vfat".
        self.click_dropdown(self.card_row("Storage", 2), "Create partition")
        self.dialog_wait_open()
        self.dialog_wait_val("type", "vfat")
        self.dialog_set_val("type", "ext4")
        self.dialog_set_val("size", "30")
        b.wait(lambda: b.call_js_func('ph_count', "#dialog button.apply") == 1)
        b.wait_text("#dialog button.apply", "Create")
        self.dialog_apply()
        self.dialog_wait_close()

        # Page talks about assigned mount points instead of "not mounted".
        b.wait_text(self.card_row_col("Storage", 2, 4), "(no assigned mount point)")

        # Format it again and make it the root filesystem.  It should
        # keep ext4 as the type.
        self.click_dropdown(self.card_row("Storage", 2), "Format")
        self.dialog_wait_open()
        self.dialog_set_val("mount_point", "/")
        self.dialog_wait_val("type", "ext4")
        b.wait(lambda: b.call_js_func('ph_count', "#dialog button.apply") == 1)
        b.wait_text("#dialog button.apply", "Format")
        self.dialog_apply()
        self.dialog_wait_close()

        # Filesystem is not mounted but page doesn't mention "not mounted".
        m.execute(f"! findmnt {disk}")
        b.wait_text(self.card_row_col("Storage", 2, 4), "/")

        # Create another partition, it should inherit ext4
        # from the root
        b.wait_text(self.card_row_col("Storage", 3, 2), "Free space")
        self.click_dropdown(self.card_row("Storage", 3), "Create partition")
        self.dialog_wait_open()
        self.dialog_wait_val("type", "ext4")
        self.dialog_apply()
        self.dialog_wait_close()

    def testSwap(self):
        b = self.browser

        disk = self.add_ram_disk()

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk],
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        b.wait_text(self.card_row_col("Storage", 1, 3), "Unformatted data")
        self.click_dropdown(self.card_row("Storage", 1), "Format")
        self.dialog({"type": "swap"})

        self.expectExportedDevice(disk, {"type": "swap"})

    def testMDRaid(self):
        b = self.browser

        disk1 = self.add_loopback_disk(name="loop10")
        disk2 = self.add_loopback_disk(name="loop11")

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk1, disk2],
        }

        self.login_and_go("/storage")
        self.enterAnacondaMode(anaconda_config)

        # Create a mirrored MDRAID
        self.click_devices_dropdown("Create MDRAID device")
        self.dialog_wait_open()
        b.wait(lambda: b.call_js_func('ph_count', "#dialog .select-space-name") == 2)
        self.dialog_wait_val("name", "raid0")
        self.dialog_set_val("level", "raid1")
        self.dialog_set_val("disks", {disk1: True, disk2: True})
        self.dialog_apply()
        self.dialog_wait_close()

        # Create a partition with a filesystem on it
        self.click_dropdown(self.card_row("Storage", name="md/raid0"), "Create partition table")
        self.confirm()
        b.wait_text(self.card_row_col("Storage", 4, 2), "Free space")
        self.click_dropdown(self.card_row("Storage", 4), "Create partition")
        self.dialog({})

        # Stop the MDRAID, it should still be shown
        self.click_card_row("Storage", name="md/raid0")
        b.wait_visible(self.card("GPT partitions"))
        b.click(self.card_button("MDRAID device", "Stop"))
        self.confirm()
        b.wait_not_present(self.card("GPT partitions"))
        b.wait_visible(self.card("MDRAID device"))

        b.click(self.card_parent_link())
        b.wait_visible(self.card_row("Storage", name="raid0"))

        # Now remove disk2 from "available_devices". This should hide the mdraid.

        anaconda_config = {
            "mount_point_prefix": "/sysroot",
            "available_devices": [disk1],
        }

        self.enterAnacondaMode(anaconda_config)

        b.wait_visible(self.card("Storage"))
        b.wait_not_present(self.card_row("Storage", name="raid0"))
        b.wait_not_present(self.card_row("Storage", name="md/raid0"))

        # But it is still reachable via disk1.  This leads to a "Not
        # found" page.

        self.click_card_row("Storage", name=disk1)
        b.click(self.card_desc("MDRAID disk", "MDRAID device") + " button")
        b.wait_in_text("body", "Not found")


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