#!/usr/bin/python # -*- 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 . import parent from testlib import * from storagelib import * 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 = { }; $('#detail-sidebar tr').each(function (i, tr) { var cell = $(tr).find('td:nth-child(2)'); var name = $(cell).find('a').text(); var state = $(cell).find('span.state').text(); found[name] = state; }); for (s in states) { if (states[s] != found[s]) return false; } return true; })""", states) def raid_add_disk(self, name): self.browser.click('#detail-sidebar .panel-heading button') self.dialog_wait_open() self.dialog_select("disks", name, True) self.dialog_apply() self.dialog_wait_close() def raid_remove_disk(self, name): self.browser.click('#detail-sidebar tr:contains("%s") button' % name) def raid_action(self, action): self.browser.click_action_btn ('.panel-heading:contains("RAID Device") .btn-group', action) def raid_default_action(self, action): self.browser.wait_action_btn ('.panel-heading:contains("RAID Device") .btn-group', action) self.browser.click_action_btn ('.panel-heading:contains("RAID Device") .btn-group') 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 info_field(name): return '#detail-header tr:contains("%s") td:nth-child(2)' % name 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" % 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()