#!/usr/bin/python2
# -*- coding: utf-8 -*-

# 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 parent
from testlib import *
from storagelib import *


def info_field(name):
    return '#detail-header tr:contains("%s") td:nth-child(2)' % name


class TestStorage(StorageCase):
    def tearDown(self):
        # HACK - for debugging https://github.com/cockpit-project/cockpit/issues/2924
        print(self.machine.execute("parted -s /dev/md127 print || true"))
        print(self.machine.execute("lsblk || true"))
        MachineCase.tearDown(self)

    def wait_states(self, states):
        self.browser.wait_js_func("""(function(states) {
          var found = { };
          var trs = ph_select('#detail-sidebar tr');
          for (i = 0; i < trs.length; ++i) {
            var cell = trs[i].querySelector('td:nth-child(2)');
            var name = cell.querySelector('a').textContent;
            var state = cell.querySelector('span.state').textContent;
            found[name] = state;
          };
          for (s in states) {
            if (states[s] != found[s])
              return false;
          }
          return true;
        })""", states)

    def raid_add_disk(self, name):
        self.dialog_with_retry(trigger = lambda: self.browser.click('#detail-sidebar .panel-heading button'),
                               expect = lambda: self.dialog_is_present('disks', name),
                               values = { "disks": { name: True }})

    def raid_remove_disk(self, name):
        self.browser.click('#detail-sidebar tr:contains("%s") button' % name)

    def raid_action(self, action):
        self.browser.click('.panel-heading:contains("RAID Device") button:contains(%s)' % action)

    def raid_default_action(self, action):
        def do_click():
            self.browser.wait_present('.panel-heading:contains("RAID Device") button:contains(%s)' % action)
            self.browser.click('.panel-heading:contains("RAID Device") button:contains(%s)' % action)

        do_click()

        if action == "Stop":
            # right after assembling an array the device might be busy from
            # udev rules probing or the mdadm monitor; retry a few times
            for retry in range(3):
                try:
                    self.browser.wait_in_text(info_field("State"), "Not running")
                    break
                except Error as ex:
                    if not ex.msg.startswith('timeout'):
                        raise
                    self.browser.wait_present(".modal-dialog")
                    self.browser.wait_visible(".modal-dialog")
                    self.browser.wait_in_text(".modal-dialog", "Error stopping RAID array /dev/md127")
                    self.browser.click(".modal-dialog div.modal-footer button.btn-default")
                    self.browser.wait_not_visible(".modal-dialog")
                    do_click()
            else:
                self.fail("Timed out waiting for array to get stopped")

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

        self.login_and_go("/storage")
        b.wait_in_text("#drives", "VirtIO")

        # Add four disks and make a RAID out of three of them
        m.add_disk("50M", serial="DISK1")
        m.add_disk("50M", serial="DISK2")
        m.add_disk("50M", serial="DISK3")
        m.add_disk("50M", serial="DISK4")
        b.wait_in_text("#drives", "DISK1")
        b.wait_in_text("#drives", "DISK2")
        b.wait_in_text("#drives", "DISK3")
        b.wait_in_text("#drives", "DISK4")

        b.click("#create-mdraid")
        self.dialog_wait_open()
        self.dialog_wait_val("level", "raid5")
        self.dialog_apply()
        self.dialog_wait_error("disks", "At least 2 disks are needed")
        self.dialog_select("disks", "DISK1", True)
        self.dialog_apply()
        self.dialog_wait_error("disks", "At least 2 disks are needed")
        self.dialog_select("disks", "DISK2", True)
        self.dialog_select("disks", "DISK3", True)
        self.dialog_set_val("level", "raid6")
        self.dialog_apply()
        self.dialog_wait_error("disks", "At least 4 disks are needed")
        self.dialog_set_val("level", "raid5")
        self.dialog_set_val("name", "ARR")
        self.dialog_apply()
        self.dialog_wait_close()
        b.wait_in_text("#mdraids", "ARR")

        b.click('#mdraids tr:contains("ARR")')
        b.wait_visible('#storage-detail')

        self.wait_states({ "QEMU QEMU HARDDISK (DISK1)": "In Sync",
                           "QEMU QEMU HARDDISK (DISK2)": "In Sync",
                           "QEMU QEMU HARDDISK (DISK3)": "In Sync" })

        def wait_degraded_state(is_degraded):
            degraded_selector = ".alert span:contains('The RAID Array is in a degraded state')"
            if is_degraded:
                b.wait_present(degraded_selector)
                b.wait_visible(degraded_selector)
            else:
                b.wait_not_present(degraded_selector)

        # The preferred device should be /dev/md/ARR, but a freshly
        # created array seems to have no symlink in /dev/md/.
        #
        # See https://bugzilla.redhat.com/show_bug.cgi?id=1397320
        #
        dev = b.text(info_field("Device"))

        # Degrade and repair it

        m.execute("mdadm --quiet %s --fail /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK1 && udevadm trigger" % dev)
        wait_degraded_state(True)

        self.wait_states({ "QEMU QEMU HARDDISK (DISK1)": "FAILED" })
        self.raid_remove_disk("DISK1")
        b.wait_not_in_text('#detail-sidebar', "DISK1")

        self.raid_add_disk("DISK1")
        self.wait_states({ "QEMU QEMU HARDDISK (DISK1)": "In Sync",
                           "QEMU QEMU HARDDISK (DISK2)": "In Sync",
                           "QEMU QEMU HARDDISK (DISK3)": "In Sync" })

        wait_degraded_state(False)

        # Turn it off and on again
        self.raid_default_action("Stop")
        b.wait_in_text(info_field("State"), "Not running")
        self.raid_default_action("Start")
        b.wait_in_text(info_field("State"), "Running")

        # Stopping and starting the array will create the expected
        # symlink in /dev/md/.
        #
        b.wait_text(info_field("Device"), "/dev/md/ARR")

        # Partitions also get symlinks in /dev/md/, so they should
        # have names like "/dev/md/ARR1".
        if self.storaged_version >= [ 2, 6, 3]:
            part_prefix = "/dev/md/ARR"
        else:
            # Older storaged versions don't pick them up as the preferred
            # device, however, so we will see "/dev/md127p1", for example.
            # (https://github.com/storaged-project/storaged/issues/98)
            part_prefix = dev + "p"

        # Create partition table
        b.click('button:contains(Create partition table)')
        self.dialog({ "type": "gpt" })
        self.content_row_wait_in_col(1, 1, "Free Space")

        # Create first partition
        self.content_row_action(1, "Create Partition")
        self.dialog({ "size": 20,
                      "type": "ext4",
                      "name": "One" })
        self.content_row_wait_in_col(1, 1, "ext4 File System")
        self.content_row_wait_in_col(1, 2, part_prefix + "1")

        # Create second partition
        self.content_row_action(2, "Create Partition")
        self.dialog({ "type": "ext4",
                      "name": "Two" })

        self.content_row_wait_in_col(2, 1, "ext4 File System")
        self.content_row_wait_in_col(2, 2, part_prefix + "2")
        b.wait_not_in_text("#detail-content", "Free Space")

        # Replace a disk by adding a spare and then removing a "In Sync" disk

        # Add a spare
        self.raid_add_disk("DISK4")
        self.wait_states({ "QEMU QEMU HARDDISK (DISK4)": "Spare" })

        # Remove DISK1.  The spare takes over.
        self.raid_remove_disk("DISK1")
        b.wait_not_in_text('#detail-sidebar', "DISK1")
        self.wait_states({ "QEMU QEMU HARDDISK (DISK4)": "In Sync" })

        # Stop the array, destroy a disk, and start the array
        self.raid_default_action("Stop")
        b.wait_in_text(info_field("State"), "Not running")
        m.execute("wipefs -a /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK2")
        b.wait_not_in_text('#detail-sidebar', "DISK2")
        self.raid_default_action("Start")
        b.wait_in_text(info_field("State"), "Running")
        wait_degraded_state(True)

        # Add DISK1.  The array recovers.
        self.raid_add_disk("DISK1")
        b.wait_in_text('#detail-sidebar', "DISK1")
        wait_degraded_state(False)

        # Delete the array.  We are back on the storage page.
        self.raid_action("Delete")
        self.confirm()
        with b.wait_timeout(120):
            b.wait_visible("#storage")
            b.wait_not_in_text ("#mdraids", "ARR")

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

        def info_field(name):
            return '#detail-header tr:contains("%s") td:nth-child(2)' % name

        self.login_and_go("/storage")
        b.wait_in_text("#drives", "VirtIO")

        m.add_disk("50M", serial="DISK1")
        m.add_disk("50M", serial="DISK2")
        m.add_disk("50M", serial="DISK3")
        b.wait_in_text("#drives", "DISK1")
        b.wait_in_text("#drives", "DISK2")
        b.wait_in_text("#drives", "DISK3")

        b.click("#create-mdraid")
        self.dialog_wait_open()
        self.dialog_set_val("level", "raid5")
        self.dialog_select("disks", "DISK1", True)
        self.dialog_select("disks", "DISK2", True)
        self.dialog_set_val("name", "ARR")
        self.dialog_apply()
        self.dialog_wait_close()
        b.wait_in_text("#mdraids", "ARR")

        b.click('#mdraids tr:contains("ARR")')
        b.wait_visible('#storage-detail')

        self.wait_states({ "QEMU QEMU HARDDISK (DISK1)": "In Sync",
                           "QEMU QEMU HARDDISK (DISK2)": "In Sync" })

        # All buttons should be disabled when the array is stopped

        self.raid_default_action("Stop")
        b.wait_in_text(info_field("State"), "Not running")

        b.wait_present('#detail-sidebar .panel-heading button.disabled')
        b.wait_present('#detail-sidebar tr:contains(DISK1) button.disabled')
        b.wait_present('#detail-sidebar tr:contains(DISK2) button.disabled')

        self.raid_default_action("Start")
        b.wait_in_text(info_field("State"), "Running")

        # With a running array, we can add spares, but not remove "in-sync" disks
        b.wait_not_present('#detail-sidebar .panel-heading button.disabled')
        b.wait_present('#detail-sidebar tr:contains(DISK1) button.disabled')
        b.wait_present('#detail-sidebar tr:contains(DISK2) button.disabled')

        # Adding a spare will allow removal of the "in-sync" disks.
        self.raid_add_disk("DISK3")
        self.wait_states({ "QEMU QEMU HARDDISK (DISK3)": "Spare" })
        b.wait_not_present('#detail-sidebar tr:contains(DISK1) button.disabled')
        b.wait_not_present('#detail-sidebar tr:contains(DISK2) button.disabled')

        # Removing the spare will make them un-removable again
        self.raid_remove_disk("DISK3")
        b.wait_not_in_text('#detail-sidebar', "DISK3")
        b.wait_present('#detail-sidebar tr:contains(DISK1) button.disabled')
        b.wait_present('#detail-sidebar tr:contains(DISK2) button.disabled')

        # A failed disk can be removed
        dev = b.text(info_field("Device"))
        m.execute("mdadm --quiet %s --fail /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_DISK1" % dev)
        self.wait_states({ "QEMU QEMU HARDDISK (DISK1)": "FAILED" })
        b.wait_not_present('#detail-sidebar tr:contains(DISK1) button.disabled')
        self.raid_remove_disk("DISK1")
        b.wait_not_in_text('#detail-sidebar', "DISK1")

        # The last disk can not be removed
        b.wait_present('#detail-sidebar tr:contains(DISK2) button.disabled')

if __name__ == '__main__':
    test_main()
