diff --git a/pkg/storaged/pages/legacy-vdo.jsx b/pkg/storaged/pages/legacy-vdo.jsx new file mode 100644 index 000000000000..e1f5fb3350ac --- /dev/null +++ b/pkg/storaged/pages/legacy-vdo.jsx @@ -0,0 +1,387 @@ +/* + * 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 . + */ + +import cockpit from "cockpit"; +import React from "react"; +import client from "../client"; + +import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; +import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js'; +import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; +import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js"; + +import { block_name, get_active_usage, teardown_active_usage, fmt_size, decode_filename, reload_systemd } from "../utils.js"; +import { + dialog_open, SizeSlider, BlockingMessage, TeardownMessage, init_active_usage_processes +} from "../dialog.jsx"; +import { StorageButton, StorageOnOff, StorageBlockNavLink } from "../storage-controls.jsx"; + +import { SCard } from "../utils/card.jsx"; +import { PageChildrenCard, new_page, page_type, block_location } from "../pages.jsx"; +import { format_disk, erase_disk } from "../content-views.jsx"; // XXX +import { format_dialog } from "../format-dialog.jsx"; + +import { make_block_pages } from "../create-pages.jsx"; + +import inotify_py from "inotify.py"; +import vdo_monitor_py from "../vdo-monitor.py"; + +const _ = cockpit.gettext; + +export function make_legacy_vdo_page(parent, vdo) { + const block = client.slashdevs_block[vdo.dev]; + + const vdo_page = new_page({ + location: ["vdo", vdo.name], + parent, + name: vdo.name, + columns: [ + _("VDO device"), + block ? block_name(block) : "", + fmt_size(vdo.logical_size) + ], + component: VDODetails, + props: { client, vdo } + }); + + if (block) + make_block_pages(vdo_page, block); +} + +class VDODetails extends React.Component { + constructor() { + super(); + this.poll_path = null; + this.state = { stats: null }; + } + + ensure_polling(enable) { + const client = this.props.client; + const vdo = this.props.vdo; + const block = client.slashdevs_block[vdo.dev]; + const path = enable && block ? vdo.dev : null; + + let buf = ""; + + if (this.poll_path === path) + return; + + if (this.poll_path) { + this.poll_process.close(); + this.setState({ stats: null }); + } + + if (path) + this.poll_process = cockpit.spawn([client.legacy_vdo_overlay.python, "--", "-", path], { superuser: true }) + .input(inotify_py + vdo_monitor_py) + .stream((data) => { + buf += data; + const lines = buf.split("\n"); + buf = lines[lines.length - 1]; + if (lines.length >= 2) { + this.setState({ stats: JSON.parse(lines[lines.length - 2]) }); + } + }); + this.poll_path = path; + } + + componentDidMount() { + this.ensure_polling(true); + } + + componentDidUpdate() { + this.ensure_polling(true); + } + + componentWillUnmount() { + this.ensure_polling(false); + } + + render() { + const client = this.props.client; + const vdo = this.props.vdo; + const block = client.slashdevs_block[vdo.dev]; + const backing_block = client.slashdevs_block[vdo.backing_dev]; + + function force_delete() { + const location = cockpit.location; + return vdo.force_remove().then(function () { + location.go("/"); + }); + } + + if (vdo.broken) { + return ( + + + + + {_("Remove device")} + } /> + + + + ); + } + + let alert = null; + if (backing_block && backing_block.Size > vdo.physical_size) + alert = ( + + {_("Grow to take all space")}} + title={_("This VDO device does not use all of its backing device.")}> + { cockpit.format(_("Only $0 of $1 are used."), + fmt_size(vdo.physical_size), + fmt_size(backing_block.Size))} + + + ); + + function stop() { + const usage = get_active_usage(client, block ? block.path : "/", _("stop")); + + if (usage.Blocking) { + dialog_open({ + Title: cockpit.format(_("$0 is in use"), vdo.name), + Body: BlockingMessage(usage), + }); + return; + } + + if (usage.Teardown) { + dialog_open({ + Title: cockpit.format(_("Confirm stopping of $0"), + vdo.name), + Teardown: TeardownMessage(usage), + Action: { + Title: _("Stop"), + action: function () { + return teardown_active_usage(client, usage) + .then(function () { + return vdo.stop(); + }); + } + }, + Inits: [ + init_active_usage_processes(client, usage) + ] + }); + } else { + return vdo.stop(); + } + } + + function delete_() { + const usage = get_active_usage(client, block ? block.path : "/", _("delete")); + + if (usage.Blocking) { + dialog_open({ + Title: cockpit.format(_("$0 is in use"), vdo.name), + Body: BlockingMessage(usage), + }); + return; + } + + function wipe_with_teardown(block) { + return block.Format("empty", { 'tear-down': { t: 'b', v: true } }).then(reload_systemd); + } + + function teardown_configs() { + if (block) { + return wipe_with_teardown(block); + } else { + return vdo.start() + .then(function () { + return client.wait_for(() => client.slashdevs_block[vdo.dev]) + .then(function (block) { + return wipe_with_teardown(block) + .catch(error => { + // systemd might have mounted it, let's try unmounting + const block_fsys = client.blocks_fsys[block.path]; + if (block_fsys) { + return block_fsys.Unmount({}) + .then(() => wipe_with_teardown(block)); + } else { + return Promise.reject(error); + } + }); + }); + }); + } + } + + dialog_open({ + Title: cockpit.format(_("Permanently delete $0?"), vdo.name), + Body: TeardownMessage(usage), + Action: { + Title: _("Delete"), + Danger: _("Deleting erases all data on a VDO device."), + action: function () { + return (teardown_active_usage(client, usage) + .then(teardown_configs) + .then(function () { + const location = cockpit.location; + return vdo.remove().then(function () { + location.go("/"); + }); + })); + } + }, + Inits: [ + init_active_usage_processes(client, usage) + ] + }); + } + + function grow_logical() { + dialog_open({ + Title: cockpit.format(_("Grow logical size of $0"), vdo.name), + Fields: [ + SizeSlider("lsize", _("Logical size"), + { + max: 5 * vdo.logical_size, + min: vdo.logical_size, + round: 512, + value: vdo.logical_size, + allow_infinite: true + }) + ], + Action: { + Title: _("Grow"), + action: function (vals) { + if (vals.lsize > vdo.logical_size) + return vdo.grow_logical(vals.lsize).then(() => { + if (block && block.IdUsage == "filesystem") + return cockpit.spawn(["fsadm", "resize", + decode_filename(block.Device)], + { superuser: true, err: "message" }); + }); + } + } + }); + } + + function fmt_perc(num) { + if (num || num == 0) + return num + "%"; + else + return "--"; + } + + const stats = this.state.stats; + + const header = ( + + {alert} + + + { block + ? {_("Stop")} + : {_("Start")} + } + { "\n" } + {_("Delete")} + }> + + + + {_("Device file")} + {vdo.dev} + + + + {_("Backing device")} + + { backing_block + ? + : vdo.backing_dev + } + + + + + {_("Physical")} + + { stats + ? cockpit.format(_("$0 data + $1 overhead used of $2 ($3)"), + fmt_size(stats.dataBlocksUsed * stats.blockSize), + fmt_size(stats.overheadBlocksUsed * stats.blockSize), + fmt_size(vdo.physical_size), + fmt_perc(stats.usedPercent)) + : fmt_size(vdo.physical_size) + } + + + + + {_("Logical")} + + { stats + ? cockpit.format(_("$0 used of $1 ($2 saved)"), + fmt_size(stats.logicalBlocksUsed * stats.blockSize), + fmt_size(vdo.logical_size), + fmt_perc(stats.savingPercent)) + : fmt_size(vdo.logical_size) + } +   {_("Grow")} + + + + + {_("Index memory")} + {fmt_size(vdo.index_mem * 1024 * 1024 * 1024)} + + + + {_("Compression")} + + vdo.set_compression(!vdo.compression)} /> + + + + + {_("Deduplication")} + + vdo.set_deduplication(!vdo.deduplication)} /> + + + + + + + { block + ? ( + + ) + : null + } + + ); + + return header; + } +} diff --git a/pkg/storaged/pages/overview.jsx b/pkg/storaged/pages/overview.jsx index 0ba6ab074eb0..36eecb0e9965 100644 --- a/pkg/storaged/pages/overview.jsx +++ b/pkg/storaged/pages/overview.jsx @@ -46,6 +46,7 @@ import { make_stratis_stopped_pool_page } from "./stratis-stopped-pool.jsx"; import { make_nfs_page, nfs_fstab_dialog } from "./nfs.jsx"; import { make_iscsi_session_page } from "./iscsi-session.jsx"; import { make_other_page } from "./other.jsx"; +import { make_legacy_vdo_page } from "./legacy-vdo.jsx"; const _ = cockpit.gettext; @@ -67,6 +68,7 @@ export function make_overview_page() { Object.keys(client.stratis_manager.StoppedPools).map(uuid => make_stratis_stopped_pool_page(overview_page, uuid)); client.nfs.entries.forEach(e => make_nfs_page(overview_page, e)); get_other_devices(client).map(p => make_other_page(overview_page, client.blocks[p])); + client.legacy_vdo_overlay.volumes.forEach(vdo => make_legacy_vdo_page(overview_page, vdo)); } const OverviewPage = ({ page, plot_state }) => { diff --git a/test/verify/check-storage-vdo b/test/verify/check-storage-vdo index 122f7028df17..2147c50c791b 100755 --- a/test/verify/check-storage-vdo +++ b/test/verify/check-storage-vdo @@ -186,21 +186,18 @@ class TestStorageLegacyVDO(storagelib.StorageCase): self.login_and_go("/storage") - b.wait_visible("#devices") - # Make a logical volume for use as the backing device. m.add_disk(SIZE_10G, serial="DISK1") - b.wait_in_text("#drives", "DISK1") + b.wait_visible(self.card_row("Storage", location="/dev/sda")) m.execute("vgcreate vdo_vgroup /dev/sda; lvcreate -n lvol -L 5G vdo_vgroup") # Create VDO; this is not supported any more, thus no UI for it m.execute("vdo create --device /dev/vdo_vgroup/lvol --name vdo0 --vdoLogicalSize 5G", timeout=300) - b.wait_in_text("#devices", "vdo0") - b.click("#devices .sidepanel-row:contains(vdo0)") - b.wait_visible("#storage-detail") + b.click(self.card_row("Storage", name="vdo0")) def detail(index): - return f'#detail-header .pf-v5-c-description-list__group:nth-of-type({index}) > dd' + card = self.card("VDO device vdo0") + return f'{card} .pf-v5-c-description-list__group:nth-of-type({index}) > dd' b.wait_text(detail(1), "/dev/mapper/vdo0") b.wait_in_text(detail(2), "vdo_vgroup") @@ -212,17 +209,19 @@ class TestStorageLegacyVDO(storagelib.StorageCase): # Make a filesystem on it - self.content_row_wait_in_col(1, 2, "Unrecognized data") - self.content_row_action(1, "Format") + b.wait_text(self.card_row_col("Content", 1, 2), "Unformatted data") + b.clicks(self.dropdown_action(self.card_row("Content", 1), "Format")) self.dialog({"type": "xfs", "name": "FILESYSTEM", "mount_point": "/run/data"}) - self.content_row_wait_in_col(1, 2, "xfs filesystem") + b.wait_text(self.card_row_col("Content", 1, 2), "xfs filesystem") # _netdev etc should have been prefilled - self.content_tab_wait_in_info(1, 1, "Mount point", "after network") - self.content_tab_wait_in_info(1, 1, "Mount point", "x-systemd.device-timeout=0") - self.content_tab_wait_in_info(1, 1, "Mount point", "x-systemd.requires=vdo.service") - self.content_row_wait_in_col(1, 4, "/ 5.4 GB") + b.click(self.card_row("Content", 1)) + b.wait_in_text(self.card_desc("xfs filesystem", "Mount point"), "after network") + b.wait_in_text(self.card_desc("xfs filesystem", "Mount point"), "x-systemd.device-timeout=0") + b.wait_in_text(self.card_desc("xfs filesystem", "Mount point"), "x-systemd.requires=vdo.service") + b.click(self.card_desc("xfs filesystem", "Stored on") + " button") + b.wait_in_text(self.card_row_col("Content", 1, 4), "/ 5.4 GB") # Grow physical @@ -237,25 +236,25 @@ class TestStorageLegacyVDO(storagelib.StorageCase): b.click(detail(4) + " button:contains(Grow)") self.dialog({"lsize": 10000}) b.wait_in_text(detail(4), "used of 10.0 GB") - self.content_row_wait_in_col(1, 4, "/ 10 GB") + b.wait_in_text(self.card_row_col("Content", 1, 4), "/ 10 GB") # Stop - b.wait_visible('#detail-content table') + b.wait_visible(self.card("Content")) b.click('.pf-v5-c-card__header:contains("VDO") button:contains("Stop")') self.dialog_wait_open() b.wait_in_text("#dialog", "unmount, stop") self.dialog_apply() self.dialog_wait_close() - b.wait_not_present('#detail-content table') + b.wait_not_present(self.card("Content")) # Delete b.click('.pf-v5-c-card__header:contains("VDO") button:contains("Delete")') self.dialog_wait_open() self.dialog_apply_with_retry(expected_errors=["Device or resource busy"]) - b.wait_visible("#storage") - b.wait_not_in_text("#devices", "vdo0") + b.wait_visible(self.card("Storage")) + b.wait_not_present(self.card_row("Storage", name="vdo0")) def testBrokenVdo(self): m = self.machine @@ -263,10 +262,8 @@ class TestStorageLegacyVDO(storagelib.StorageCase): self.login_and_go("/storage") - b.wait_visible("#devices") - m.add_disk(SIZE_10G, serial="DISK1") - b.wait_in_text("#drives", "DISK1") + b.wait_visible(self.card_row("Storage", location="/dev/sda")) # Install a valid configuration file that describes a broken VDO m.write("/etc/vdoconf.yml", """ @@ -302,20 +299,19 @@ config: !Configuration version: 538380551 """) - b.wait_in_text("#devices", "vdo0") - b.click("#devices .sidepanel-row:contains(vdo0)") - - b.click("#storage-detail .pf-m-danger button:contains('Remove device')") - b.wait_visible("#storage") - b.wait_not_in_text("#devices", "vdo0") + b.click(self.card_row("Storage", name="vdo0")) + testlib.sit() + b.click(".pf-m-danger button:contains('Remove device')") + b.wait_visible(self.card("Storage")) + b.wait_not_present(self.card_row("Storage", name="vdo0")) def testBrokenVdoConfig(self): m = self.machine b = self.browser - self.login_and_go("/storage") + m.upload(["/home/mvo/work/cockpit/dist/storaged"], "/usr/share/cockpit") - b.wait_visible("#devices") + self.login_and_go("/storage") # Install a valid configuration file m.write("/etc/vdoconf.yml", """ @@ -351,7 +347,7 @@ config: !Configuration version: 538380551 """) - b.wait_in_text("#devices", "vdo0") + b.wait_visible(self.card_row("Storage", name="vdo0")) # Install a broken configuration file m.write("/etc/vdoconf.yml", """ @@ -361,7 +357,7 @@ config: !Configuration blah: 12 """) - b.wait_not_in_text("#devices", "vdo0") + b.wait_not_present(self.card_row("Storage", name="vdo0")) # Install a valid configuration file again m.write("/etc/vdoconf.yml", """ @@ -397,7 +393,7 @@ config: !Configuration version: 538380551 """) - b.wait_in_text("#devices", "vdo1") + b.wait_visible(self.card_row("Storage", name="vdo1")) @testlib.onlyImage("VDO API only supported on RHEL", "rhel-*", "centos-*")