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-*")