From 5f933e4c54889d3779b25c51118e51187df79ee1 Mon Sep 17 00:00:00 2001 From: Marius Vollmer Date: Wed, 13 Dec 2023 13:21:48 +0200 Subject: [PATCH] storage: Anaconda mode --- doc/anaconda.md | 74 +++++++ pkg/storaged/anaconda.jsx | 28 +++ pkg/storaged/block/create-pages.jsx | 3 +- pkg/storaged/block/format-dialog.jsx | 31 ++- pkg/storaged/block/other.jsx | 6 +- pkg/storaged/client.js | 45 +++++ pkg/storaged/dialog.jsx | 11 +- pkg/storaged/drive/drive.jsx | 5 +- pkg/storaged/filesystem/filesystem.jsx | 12 +- pkg/storaged/filesystem/mounting-dialog.jsx | 20 +- pkg/storaged/filesystem/utils.jsx | 10 +- pkg/storaged/lvm2/volume-group.jsx | 5 +- pkg/storaged/mdraid/mdraid.jsx | 8 +- pkg/storaged/nfs/nfs.jsx | 3 + pkg/storaged/overview/overview.jsx | 10 +- pkg/storaged/pages.jsx | 2 + pkg/storaged/stratis/filesystem.jsx | 17 +- pkg/storaged/stratis/pool.jsx | 12 +- pkg/storaged/stratis/stopped-pool.jsx | 3 + pkg/storaged/stratis/utils.jsx | 1 + pkg/storaged/utils.js | 29 ++- test/verify/check-storage-anaconda | 208 ++++++++++++++++++++ 22 files changed, 496 insertions(+), 47 deletions(-) create mode 100644 doc/anaconda.md create mode 100644 pkg/storaged/anaconda.jsx create mode 100755 test/verify/check-storage-anaconda diff --git a/doc/anaconda.md b/doc/anaconda.md new file mode 100644 index 00000000000..cff580c38cc --- /dev/null +++ b/doc/anaconda.md @@ -0,0 +1,74 @@ +Cockpit Storage in Anaconda Mode +================================ + +Anaconda (the OS Installer) can open the Cockpit "storaged" page for +advanced setup of the target storage devices. When this is done, +storaged is in a special "Anaconda mode" and behaves significantly +different. + +In essence, the storaged page restricts itself to working with the +target environment. It will hide the real root filesystem (on the USB +stick that the Live environment was booted from, say), but let the +user create a "fake" root filesystem on some block device. + +Entering Anaconda mode +---------------------- + +The "storaged" page is put into Anaconda mode by storing a +"cockpit_anaconda" item in its `window.localStorage`. The value +should be a JSON encoded object, the details of which are explained +below. + +Since both Anaconda and the storaged page are served from the same +origin, Anaconda can just execute something like this: + +``` + window.localStorage.setItem("cockpit_anaconda", + JSON.stringify({ + "mount_point_prefix": "/sysroot", + "available_devices": [ "/dev/sda" ] + })); + window.open("/cockpit/@localhost/storage/index.html", "storage-tab"); +``` + +Ignoring storage devices +------------------------ + +Anaconda needs to tell Cockpit which devices can be used to install +the OS on. This is done with the "available_devices" entry, which is +an array of strings. + +``` +{ + "available_devices": [ "/dev/sda" ] +} +``` + +This list should only contain entries for top-level block devices. It +should not contain things like partitions, device mapper devices, or +mdraid devices. + +Mount point prefix +------------------ + +Cockpit can be put into a kind of "chroot" environment by giving it a +mount point prefix like so: + +``` +{ + "mount_point_prefix": "/sysroot" +} +``` + +This works at the UI level: filesystems that have mount points outside +of "/sysroot" are hidden from the user, and when letting the user work +with mount points below "/sysroot", the "/sysroot" prefix is omitted +in the UI. So when the user says to create a filesystem on "/var", +they are actually creating one on "/sysroot/var". + +However, Cockpit (via UDisks2) will still write the new mount point +configuration into the real /etc/fstab (_not_ +/sysroot/etc/fstab). This is done for the convenience of Cockpit, and +Anaconda is not expected to read it. + +If and how Cockpit communicates back to Anaconda is still open. diff --git a/pkg/storaged/anaconda.jsx b/pkg/storaged/anaconda.jsx new file mode 100644 index 00000000000..19800072a87 --- /dev/null +++ b/pkg/storaged/anaconda.jsx @@ -0,0 +1,28 @@ +/* + * 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 client from "./client.js"; + +export const AnacondaAdvice = () => { + if (!client.in_anaconda_mode()) + return null; + + // Nothing yet. + return null; +}; diff --git a/pkg/storaged/block/create-pages.jsx b/pkg/storaged/block/create-pages.jsx index 2ab82396eca..21257c0a0f5 100644 --- a/pkg/storaged/block/create-pages.jsx +++ b/pkg/storaged/block/create-pages.jsx @@ -106,5 +106,6 @@ export function make_block_page(parent, block, card) { } } - new_page(parent, card); + if (card) + new_page(parent, card); } diff --git a/pkg/storaged/block/format-dialog.jsx b/pkg/storaged/block/format-dialog.jsx index 4b21aba617b..e94f26f7f51 100644 --- a/pkg/storaged/block/format-dialog.jsx +++ b/pkg/storaged/block/format-dialog.jsx @@ -47,14 +47,19 @@ const _ = cockpit.gettext; export function initial_tab_options(client, block, for_fstab) { const options = { }; - get_parent_blocks(client, block.path).forEach(p => { - // "nofail" is the default for new filesystems with Cockpit so - // that a failure to mount one of them will not prevent - // Cockpit from starting. This allows people to debug and fix - // these failures with Cockpit itself. - // + // "nofail" is the default for new filesystems with Cockpit so + // that a failure to mount one of them will not prevent + // Cockpit from starting. This allows people to debug and fix + // these failures with Cockpit itself. + // + // In Anaconda mode however, we don't make "nofail" the + // default since people will be creating the core filesystems + // like "/", "/var", etc. + + if (!client.in_anaconda_mode()) options.nofail = true; + get_parent_blocks(client, block.path).forEach(p => { if (is_netdev(client, p)) { options._netdev = true; } @@ -142,10 +147,10 @@ export function format_dialog(client, path, start, size, enable_dos_extended) { return false; }) .then(version => { - format_dialog_internal(client, path, start, size, enable_dos_extended, version); + return format_dialog_internal(client, path, start, size, enable_dos_extended, version); }); } else { - format_dialog_internal(client, path, start, size, enable_dos_extended); + return format_dialog_internal(client, path, start, size, enable_dos_extended); } } @@ -242,6 +247,10 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, if (old_opts == undefined) old_opts = initial_mount_options(client, block); + old_dir = client.strip_mount_point_prefix(old_dir); + if (old_dir === false) + return Promise.reject(_("This device can not be used for the installation target.")); + const split_options = parse_options(old_opts); extract_option(split_options, "noauto"); const opt_ro = extract_option(split_options, "ro"); @@ -279,7 +288,10 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, visible: is_filesystem, value: old_dir || "", validate: (val, values, variant) => { - return is_valid_mount_point(client, block, val, variant == "nomount"); + return is_valid_mount_point(client, + block, + client.add_mount_point_prefix(val), + variant == "nomount"); } }), SelectOne("type", _("Type"), @@ -474,6 +486,7 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, if (mount_point != "") { if (mount_point[0] != "/") mount_point = "/" + mount_point; + mount_point = client.add_mount_point_prefix(mount_point); config_items.push(["fstab", { dir: { t: 'ay', v: encode_filename(mount_point) }, diff --git a/pkg/storaged/block/other.jsx b/pkg/storaged/block/other.jsx index e739e8c8544..a564a3f9ad7 100644 --- a/pkg/storaged/block/other.jsx +++ b/pkg/storaged/block/other.jsx @@ -19,12 +19,13 @@ import cockpit from "cockpit"; import React from "react"; +import client from "../client.js"; import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js"; import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js"; import { StorageCard, StorageDescription, new_card } from "../pages.jsx"; -import { block_name } from "../utils.js"; +import { block_name, should_ignore } from "../utils.js"; import { partitionable_block_actions } from "../partitions/actions.jsx"; import { OtherIcon } from "../icons/gnome-icons.jsx"; @@ -33,6 +34,9 @@ import { make_block_page } from "../block/create-pages.jsx"; const _ = cockpit.gettext; export function make_other_page(parent, block) { + if (should_ignore(client, block.path)) + return; + const other_card = new_card({ title: _("Block device"), next: null, diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js index 9772851f7fe..25be444eed8 100644 --- a/pkg/storaged/client.js +++ b/pkg/storaged/client.js @@ -741,6 +741,15 @@ function init_model(callback) { ).then(() => info); } + try { + client.anaconda = JSON.parse(window.localStorage.getItem("cockpit_anaconda")); + } catch { + console.warn("Can't parse cockpit_anaconda configuration as JSON"); + client.anaconda = null; + } + + console.log("ANACONDA", client.anaconda); + pull_time().then(() => { read_os_release().then(os_release => { client.os_release = os_release; @@ -1484,4 +1493,40 @@ client.get_config = (name, def) => { } }; +client.in_anaconda_mode = () => !!client.anaconda; + +client.strip_mount_point_prefix = (dir) => { + const mpp = client.anaconda?.mount_point_prefix; + + if (dir && mpp) { + if (dir.indexOf(mpp) != 0) + return false; + + dir = dir.substr(mpp.length); + if (dir == "") + dir = "/"; + } + + return dir; +}; + +client.add_mount_point_prefix = (dir) => { + const mpp = client.anaconda?.mount_point_prefix; + if (mpp && dir != "") { + if (dir == "/") + dir = mpp; + else + dir = mpp + dir; + } + return dir; +}; + +client.should_ignore_device = (devname) => { + return client.anaconda?.available_devices && client.anaconda.available_devices.indexOf(devname) == -1; +}; + +client.should_ignore_block = (block) => { + return client.should_ignore_device(utils.decode_filename(block.PreferredDevice)); +}; + export default client; diff --git a/pkg/storaged/dialog.jsx b/pkg/storaged/dialog.jsx index e54e7e932c3..1486f17cc0e 100644 --- a/pkg/storaged/dialog.jsx +++ b/pkg/storaged/dialog.jsx @@ -1107,7 +1107,8 @@ export const BlockingMessage = (usage) => { pvol: _("physical volume of LVM2 volume group"), "mdraid-member": _("member of MDRAID device"), vdo: _("backing device for VDO device"), - "stratis-pool-member": _("member of Stratis pool") + "stratis-pool-member": _("member of Stratis pool"), + mounted: _("Filesystem outside the target"), }; const rows = []; @@ -1197,9 +1198,15 @@ export const TeardownMessage = (usage, expect_single_unmount) => { const name = (fsys ? fsys.Devnode : block_name(client.blocks[use.block.CryptoBackingDevice] || use.block)); + let location = use.location; + if (use.usage == "mounted") { + location = client.strip_mount_point_prefix(location); + if (location === false) + location = _("(Not part of target)"); + } rows.push({ columns: [name, - use.location || "-", + location || "-", use.actions.length ? use.actions.join(", ") : "-", { title: , diff --git a/pkg/storaged/drive/drive.jsx b/pkg/storaged/drive/drive.jsx index 60320fb15b7..c6fdd96874f 100644 --- a/pkg/storaged/drive/drive.jsx +++ b/pkg/storaged/drive/drive.jsx @@ -27,7 +27,7 @@ import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { HDDIcon, SSDIcon, MediaDriveIcon } from "../icons/gnome-icons.jsx"; import { StorageCard, StorageDescription, new_card, new_page } from "../pages.jsx"; -import { block_name, drive_name, format_temperature, fmt_size_long } from "../utils.js"; +import { block_name, drive_name, format_temperature, fmt_size_long, should_ignore } from "../utils.js"; import { make_block_page } from "../block/create-pages.jsx"; import { partitionable_block_actions } from "../partitions/actions.jsx"; @@ -47,6 +47,9 @@ export function make_drive_page(parent, drive) { if (!block) return; + if (should_ignore(client, block.path)) + return; + let cls; if (client.drives_iscsi_session[drive.path]) cls = "iscsi"; diff --git a/pkg/storaged/filesystem/filesystem.jsx b/pkg/storaged/filesystem/filesystem.jsx index fd119e3f95a..dfedb95236a 100644 --- a/pkg/storaged/filesystem/filesystem.jsx +++ b/pkg/storaged/filesystem/filesystem.jsx @@ -84,11 +84,13 @@ export function make_filesystem_card(next, backing_block, content_block, fstab_c const mounted = content_block && is_mounted(client, content_block); let mp_text; - if (mount_point && mounted) - mp_text = mount_point; - else if (mount_point && !mounted) - mp_text = mount_point + " " + _("(not mounted)"); - else + if (mount_point) { + mp_text = client.strip_mount_point_prefix(mount_point); + if (mp_text == false) + return null; + if (!mounted) + mp_text = mp_text + " " + _("(not mounted)"); + } else mp_text = _("(not mounted)"); return new_card({ diff --git a/pkg/storaged/filesystem/mounting-dialog.jsx b/pkg/storaged/filesystem/mounting-dialog.jsx index 6b01fb56180..b4663b27151 100644 --- a/pkg/storaged/filesystem/mounting-dialog.jsx +++ b/pkg/storaged/filesystem/mounting-dialog.jsx @@ -47,6 +47,10 @@ export function mounting_dialog(client, block, mode, forced_options) { const [old_config, old_dir, old_opts, old_parents] = get_fstab_config(block, true); const options = old_config ? old_opts : initial_tab_options(client, block, true); + const old_dir_for_display = client.strip_mount_point_prefix(old_dir); + if (old_dir_for_display === false) + return Promise.reject(_("This device can not be used for the installation target.")); + const split_options = parse_options(options); extract_option(split_options, "noauto"); const opt_never_auto = extract_option(split_options, "x-cockpit-never-auto"); @@ -198,8 +202,12 @@ export function mounting_dialog(client, block, mode, forced_options) { fields = [ TextInput("mount_point", _("Mount point"), { - value: old_dir, - validate: val => is_valid_mount_point(client, block, val, mode == "update" && !is_filesystem_mounted, true) + value: old_dir_for_display, + validate: val => is_valid_mount_point(client, + block, + client.add_mount_point_prefix(val), + mode == "update" && !is_filesystem_mounted, + true) }), CheckBoxes("mount_options", _("Mount options"), { @@ -292,7 +300,7 @@ export function mounting_dialog(client, block, mode, forced_options) { const usage = get_active_usage(client, block.path); const dlg = dialog_open({ - Title: cockpit.format(mode_title[mode], old_dir), + Title: cockpit.format(mode_title[mode], old_dir_for_display), Fields: fields, Teardown: TeardownMessage(usage, old_dir), update: function (dlg, vals, trigger) { @@ -321,8 +329,10 @@ export function mounting_dialog(client, block, mode, forced_options) { opts = opts.concat(forced_options); if (vals.mount_options.extra !== false) opts = opts.concat(parse_options(vals.mount_options.extra)); - return (maybe_update_config(vals.mount_point, unparse_options(opts), - vals.passphrase, passphrase_type) + return (maybe_update_config(client.add_mount_point_prefix(vals.mount_point), + unparse_options(opts), + vals.passphrase, + passphrase_type) .then(() => maybe_set_crypto_options(vals.mount_options.ro, opts.indexOf("noauto") == -1, vals.at_boot == "nofail", diff --git a/pkg/storaged/filesystem/utils.jsx b/pkg/storaged/filesystem/utils.jsx index 65fabfb75b9..fe1765525ee 100644 --- a/pkg/storaged/filesystem/utils.jsx +++ b/pkg/storaged/filesystem/utils.jsx @@ -90,7 +90,10 @@ export function is_valid_mount_point(client, block, val, format_only, for_fstab) if (Object.keys(children).length > 0) return <> {_("Filesystems are already mounted below this mountpoint.")} - {Object.keys(children).map(m =>
{cockpit.format("• $0 on $1", nice_block_name(children[m]), m)}
)} + {Object.keys(children).map(m =>
+ {cockpit.format("• $0 on $1", nice_block_name(children[m]), + client.strip_mount_point_prefix(m))} +
)} {_("Please unmount them first.")} ; } @@ -125,6 +128,7 @@ export const MountPoint = ({ fstab_config, forced_options, backing_block, conten let mount_point_text = null; if (old_dir) { + mount_point_text = client.strip_mount_point_prefix(old_dir); let opt_texts = []; if (opt_ro) opt_texts.push(_("read only")); @@ -138,9 +142,7 @@ export const MountPoint = ({ fstab_config, forced_options, backing_block, conten opt_texts.push(_("stop boot on failure")); opt_texts = opt_texts.concat(split_options); if (opt_texts.length) { - mount_point_text = cockpit.format("$0 ($1)", old_dir, opt_texts.join(", ")); - } else { - mount_point_text = old_dir; + mount_point_text = cockpit.format("$0 ($1)", mount_point_text, opt_texts.join(", ")); } } diff --git a/pkg/storaged/lvm2/volume-group.jsx b/pkg/storaged/lvm2/volume-group.jsx index 3de6bc550e5..df72475bf93 100644 --- a/pkg/storaged/lvm2/volume-group.jsx +++ b/pkg/storaged/lvm2/volume-group.jsx @@ -37,7 +37,7 @@ import { fmt_size_long, get_active_usage, teardown_active_usage, for_each_async, validate_lvm2_name, get_available_spaces, prepare_available_spaces, - reload_systemd, + reload_systemd, should_ignore, } from "../utils.js"; import { @@ -223,6 +223,9 @@ export function make_lvm2_volume_group_page(parent, vgroup) { else if (vgroup.FreeSize == 0) lvol_excuse = _("No free space"); + if (should_ignore(client, vgroup.path)) + return; + const vgroup_card = new_card({ title: _("LVM2 volume group"), next: null, diff --git a/pkg/storaged/mdraid/mdraid.jsx b/pkg/storaged/mdraid/mdraid.jsx index 6b09e2b9a44..179be5c946f 100644 --- a/pkg/storaged/mdraid/mdraid.jsx +++ b/pkg/storaged/mdraid/mdraid.jsx @@ -33,7 +33,7 @@ import { block_short_name, mdraid_name, encode_filename, decode_filename, fmt_size, fmt_size_long, get_active_usage, teardown_active_usage, get_available_spaces, prepare_available_spaces, - reload_systemd, + reload_systemd, should_ignore, } from "../utils.js"; import { @@ -191,6 +191,12 @@ function missing_bitmap(mdraid) { export function make_mdraid_page(parent, mdraid) { const block = client.mdraids_block[mdraid.path]; + if (block && should_ignore(client, block.path)) + return; + + if (!block && client.in_anaconda_mode()) + return; + let add_excuse = false; if (!block) add_excuse = _("MDRAID device must be running"); diff --git a/pkg/storaged/nfs/nfs.jsx b/pkg/storaged/nfs/nfs.jsx index f5552f5c8a6..1aa32e6395c 100644 --- a/pkg/storaged/nfs/nfs.jsx +++ b/pkg/storaged/nfs/nfs.jsx @@ -280,6 +280,9 @@ const NfsEntryUsageBar = ({ entry, not_mounted_text, short }) => { }; export function make_nfs_page(parent, entry) { + if (client.in_anaconda_mode()) + return; + const remote = entry.fields[0]; const local = entry.fields[1]; let mount_point = local; diff --git a/pkg/storaged/overview/overview.jsx b/pkg/storaged/overview/overview.jsx index aa3add892b9..e4d59ffb8c3 100644 --- a/pkg/storaged/overview/overview.jsx +++ b/pkg/storaged/overview/overview.jsx @@ -143,13 +143,13 @@ const OverviewCard = ({ card, plot_state }) => { menu_item(null, _("Create MDRAID device"), () => create_mdraid()), menu_item(lvm2_feature, _("Create LVM2 volume group"), () => create_vgroup()), menu_item(stratis_feature, _("Create Stratis pool"), () => create_stratis_pool()), - ].filter(item => item !== null); + ].filter(item => !!item); const net_menu_items = [ - menu_item(nfs_feature, _("New NFS mount"), () => nfs_fstab_dialog(null, null)), + !client.in_anaconda_mode() && menu_item(nfs_feature, _("New NFS mount"), () => nfs_fstab_dialog(null, null)), menu_item(iscsi_feature, _("Change iSCSI initiater name"), () => iscsi_change_name()), menu_item(iscsi_feature, _("Add iSCSI portal"), () => iscsi_discover()), - ].filter(item => item !== null); + ].filter(item => !!item); const groups = []; @@ -173,6 +173,7 @@ const OverviewCard = ({ card, plot_state }) => { return ( + { !client.in_anaconda_mode() && @@ -180,6 +181,7 @@ const OverviewCard = ({ card, plot_state }) => { + } @@ -190,8 +192,10 @@ const OverviewCard = ({ card, plot_state }) => { + { !client.in_anaconda_mode() && + } ); }; diff --git a/pkg/storaged/pages.jsx b/pkg/storaged/pages.jsx index 170970bf2de..85c844d5dcc 100644 --- a/pkg/storaged/pages.jsx +++ b/pkg/storaged/pages.jsx @@ -41,6 +41,7 @@ import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/ind import { decode_filename, block_short_name, fmt_size } from "./utils.js"; import { StorageButton, StorageBarMenu, StorageMenuItem, StorageSize } from "./storage-controls.jsx"; import { MultipathAlert } from "./multipath.jsx"; +import { AnacondaAdvice } from "./anaconda.jsx"; import { JobsPanel } from "./jobs-panel.jsx"; const _ = cockpit.gettext; @@ -801,6 +802,7 @@ export const StoragePage = ({ location, plot_state }) => { + diff --git a/pkg/storaged/stratis/filesystem.jsx b/pkg/storaged/stratis/filesystem.jsx index e8c414853a2..49dafebf38d 100644 --- a/pkg/storaged/stratis/filesystem.jsx +++ b/pkg/storaged/stratis/filesystem.jsx @@ -91,7 +91,10 @@ export function make_stratis_filesystem_page(parent, pool, fsys, TextInput("mount_point", _("Mount point"), { validate: (val, values, variant) => { - return is_valid_mount_point(client, null, val, variant == "nomount"); + return is_valid_mount_point(client, + null, + client.add_mount_point_prefix(val), + variant == "nomount"); } }), CheckBoxes("mount_options", _("Mount options"), @@ -181,11 +184,13 @@ export function make_stratis_filesystem_page(parent, pool, fsys, } let mp_text; - if (mount_point && fs_is_mounted) - mp_text = mount_point; - else if (mount_point && !fs_is_mounted) - mp_text = mount_point + " " + _("(not mounted)"); - else + if (mount_point) { + mp_text = client.strip_mount_point_prefix(mount_point); + if (mp_text == false) + return; + if (!fs_is_mounted) + mp_text = mp_text + " " + _("(not mounted)"); + } else mp_text = _("(not mounted)"); const fsys_card = new_card({ diff --git a/pkg/storaged/stratis/pool.jsx b/pkg/storaged/stratis/pool.jsx index 5ca95fc1f81..af20e5389b6 100644 --- a/pkg/storaged/stratis/pool.jsx +++ b/pkg/storaged/stratis/pool.jsx @@ -38,7 +38,7 @@ import { import { get_active_usage, teardown_active_usage, for_each_async, get_available_spaces, prepare_available_spaces, - decode_filename, + decode_filename, should_ignore, } from "../utils.js"; import { @@ -90,7 +90,10 @@ function create_fs(pool) { TextInput("mount_point", _("Mount point"), { validate: (val, values, variant) => { - return is_valid_mount_point(client, null, val, variant == "nomount"); + return is_valid_mount_point(client, + null, + client.add_mount_point_prefix(val), + variant == "nomount"); } }), CheckBoxes("mount_options", _("Mount options"), @@ -106,7 +109,7 @@ function create_fs(pool) { }), SelectOne("at_boot", _("At boot"), { - value: "nofail", + value: client.in_anaconda_mode() ? "local" : "nofail", explanation: mount_explanation.nofail, choices: [ { @@ -284,6 +287,9 @@ export function make_stratis_pool_page(parent, pool) { const use = pool.TotalPhysicalUsed[0] && [Number(pool.TotalPhysicalUsed[1]), Number(pool.TotalPhysicalSize)]; + if (should_ignore(client, pool.path)) + return; + const pool_card = new_card({ title: pool.Encrypted ? _("Encrypted Stratis pool") : _("Stratis pool"), next: null, diff --git a/pkg/storaged/stratis/stopped-pool.jsx b/pkg/storaged/stratis/stopped-pool.jsx index 48a7e1b69ba..fae896a04cf 100644 --- a/pkg/storaged/stratis/stopped-pool.jsx +++ b/pkg/storaged/stratis/stopped-pool.jsx @@ -86,6 +86,9 @@ function start_pool(uuid, show_devs) { } export function make_stratis_stopped_pool_page(parent, uuid) { + if (client.in_anaconda_mode()) + return; + const pool_card = new_card({ title: _("Stratis pool"), type_extra: _("stopped"), diff --git a/pkg/storaged/stratis/utils.jsx b/pkg/storaged/stratis/utils.jsx index 7fd76b0ebe7..f19e0742e2c 100644 --- a/pkg/storaged/stratis/utils.jsx +++ b/pkg/storaged/stratis/utils.jsx @@ -148,6 +148,7 @@ export function set_mount_options(path, vals, forced_options) { return Promise.resolve(); if (mount_point[0] != "/") mount_point = "/" + mount_point; + mount_point = client.add_mount_point_prefix(mount_point); const config = ["fstab", diff --git a/pkg/storaged/utils.js b/pkg/storaged/utils.js index 114c1ae0287..3df3f78b45a 100644 --- a/pkg/storaged/utils.js +++ b/pkg/storaged/utils.js @@ -501,7 +501,8 @@ export function is_available_block(client, block, honor_ignore_hint) { !is_vdo_backing_dev() && !is_swap() && !block_ptable && - !(block_part && block_part.IsContainer)); + !(block_part && block_part.IsContainer) && + !should_ignore(client, block.path)); } export function get_available_spaces(client) { @@ -574,7 +575,8 @@ export function get_other_devices(client) { block.Size > 0 && !client.legacy_vdo_overlay.find_by_block(block) && !client.blocks_stratis_fsys[block.path] && - !is_snap(client, block)); + !is_snap(client, block) && + !should_ignore(client, block.path)); }); } @@ -608,7 +610,7 @@ export function get_multipathd_service () { return multipathd_service; } -export function get_parent(client, path) { +function get_parent(client, path) { if (client.blocks_part[path] && client.blocks[client.blocks_part[path].Table]) return client.blocks_part[path].Table; if (client.blocks[path] && client.blocks[client.blocks[path].CryptoBackingDevice]) @@ -623,9 +625,13 @@ export function get_parent(client, path) { return client.lvols[path].VolumeGroup; if (client.blocks_stratis_fsys[path]) return client.blocks_stratis_fsys[path].Pool; + if (client.vgroups[path]) + return path; + if (client.stratis_pools[path]) + return path; } -export function get_direct_parent_blocks(client, path) { +function get_direct_parent_blocks(client, path) { let parent = get_parent(client, path); if (!parent) return []; @@ -660,6 +666,19 @@ export function is_netdev(client, path) { return false; } +export function should_ignore(client, path) { + if (!client.in_anaconda_mode()) + return false; + + const parents = get_direct_parent_blocks(client, path); + if (parents.length == 0) { + const b = client.blocks[path]; + return b && client.should_ignore_block(b); + } else { + return parents.some(p => should_ignore(client, p)); + } +} + /* GET_CHILDREN gets the direct children of the storage object at PATH, like the partitions of a partitioned block device, or the volume group of a physical volume. By calling GET_CHILDREN @@ -839,7 +858,7 @@ export function get_active_usage(client, path, top_action, child_action, is_temp has_fstab_entry, set_noauto: !is_top && !is_temporary, actions: (is_top ? get_actions(_("unmount")) : [_("unmount")]).concat(has_fstab_entry ? [_("mount")] : []), - blocking: false + blocking: client.strip_mount_point_prefix(location) === false, }); } diff --git a/test/verify/check-storage-anaconda b/test/verify/check-storage-anaconda new file mode 100755 index 00000000000..03cc99748ee --- /dev/null +++ b/test/verify/check-storage-anaconda @@ -0,0 +1,208 @@ +#!/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 . + +import json + +import storagelib +import testlib + + +@testlib.nondestructive +class TestStorageAnaconda(storagelib.StorageCase): + + 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") + b.call_js_func("window.localStorage.setItem", "cockpit_anaconda", json.dumps(anaconda_config)) + b.reload() + b.enter_page("/storage") + + # 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() + self.dialog_wait_val("at_boot", "local") + self.dialog_set_val("type", "ext4") + 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") + # Empty mount point should be failure + self.dialog_set_val("mount_point", "") + self.dialog_apply() + self.dialog_wait_error("mount_point", "Mount point cannot be empty") + self.dialog_set_val("mount_point", "/") + self.dialog_apply() + self.dialog_wait_close() + + self.assertNotIn("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]") + b.assert_pixels("body", "page") + + # Unount/mount the filesystem, edit mount options + self.click_card_row("Storage", location="/") + b.wait_visible(self.card("Encryption")) + b.wait_in_text(self.card_desc("ext4 filesystem", "Mount point"), "/ (stop boot on failure)") + b.click(self.card_desc("ext4 filesystem", "Mount point") + " button") + self.dialog_wait_open() + self.dialog_wait_val("mount_point", "/") + self.dialog_set_val("mount_options.ro", val=True) + # Empty mount point should be failure + self.dialog_set_val("mount_point", "") + self.dialog_apply() + self.dialog_wait_error("mount_point", "Mount point cannot be empty") + self.dialog_set_val("mount_point", "/") + self.dialog_apply() + self.dialog_wait_close() + self.assertIn("ro", m.execute("findmnt --fstab -n -o OPTIONS /sysroot")) + 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 /") + self.dialog_apply() + self.dialog_wait_close() + self.wait_not_mounted("Filesystem") + self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot")) + b.click(self.card_button("Filesystem", "Mount")) + self.dialog_wait_open() + self.dialog_wait_val("mount_point", "/") + self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja") + self.dialog_apply() + self.dialog_wait_close() + self.wait_mounted("ext4 filesystem") + self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot")) + + # 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']", "/") + self.dialog_apply() + self.dialog_wait_close() + m.execute("! findmnt --fstab -n /sysroot") + + # 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="/")) + + @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") + b.call_js_func("window.localStorage.setItem", "cockpit_anaconda", json.dumps(anaconda_config)) + b.reload() + b.enter_page("/storage") + + # 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() + self.dialog_wait_val("at_boot", "local") + self.dialog_set_val("name", "root") + self.dialog_set_val("mount_point", "/") + self.dialog_apply() + self.dialog_wait_close() + + self.assertNotIn("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]") + + # Unount/mount the filesystem, edit mount options + self.click_card_row("Storage", location="/") + b.wait_in_text(self.card_desc("Stratis filesystem", "Mount point"), "/ (stop boot on failure)") + b.click(self.card_desc("Stratis filesystem", "Mount point") + " button") + self.dialog_wait_open() + self.dialog_wait_val("mount_point", "/") + self.dialog_set_val("mount_options.ro", val=True) + self.dialog_apply() + self.dialog_wait_close() + self.assertIn("ro", m.execute("findmnt --fstab -n -o OPTIONS /sysroot")) + 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 /") + self.dialog_apply() + self.dialog_wait_close() + self.wait_not_mounted("Stratis filesystem") + self.assertIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot")) + b.click(self.card_button("Stratis filesystem", "Mount")) + self.dialog_wait_open() + self.dialog_wait_val("mount_point", "/") + self.dialog_apply() + self.dialog_wait_close() + self.wait_mounted("Stratis filesystem") + self.assertNotIn("noauto", m.execute("findmnt --fstab -n -o OPTIONS /sysroot")) + + # 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']", "/") + self.dialog_apply() + self.dialog_wait_close() + m.execute("! findmnt --fstab -n /sysroot") + + +if __name__ == '__main__': + testlib.test_main()