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 97b0d9f82ff..1486f17cc0e 100644
--- a/pkg/storaged/dialog.jsx
+++ b/pkg/storaged/dialog.jsx
@@ -415,7 +415,8 @@ export const dialog_open = (def) => {
caption: def.Action.Title,
style: "primary",
danger: def.Action.Danger || def.Action.DangerButton,
- disabled: running_promise != null || (def.Action.disable_on_error && errors),
+ disabled: running_promise != null || (def.Action.disable_on_error &&
+ errors && errors.toString() != "[object Object]"),
clicked: progress_callback => run_action(progress_callback, null),
}
];
@@ -1106,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 = [];
@@ -1196,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 3154b4bebea..a8f6e7c5278 100644
--- a/pkg/storaged/lvm2/volume-group.jsx
+++ b/pkg/storaged/lvm2/volume-group.jsx
@@ -38,7 +38,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 {
@@ -224,6 +224,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 12b2d24e5c8..4803cbdddc6 100644
--- a/pkg/storaged/mdraid/mdraid.jsx
+++ b/pkg/storaged/mdraid/mdraid.jsx
@@ -37,7 +37,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 {
@@ -195,6 +195,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 89502eb0ea6..ccd540798aa 100644
--- a/pkg/storaged/nfs/nfs.jsx
+++ b/pkg/storaged/nfs/nfs.jsx
@@ -284,6 +284,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 17a0a0dc6d0..af782a5e3e8 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;
@@ -822,6 +823,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 f70c470b164..13e63348e24 100644
--- a/pkg/storaged/stratis/pool.jsx
+++ b/pkg/storaged/stratis/pool.jsx
@@ -39,7 +39,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 {
@@ -91,7 +91,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"),
@@ -107,7 +110,7 @@ function create_fs(pool) {
}),
SelectOne("at_boot", _("At boot"),
{
- value: "nofail",
+ value: client.in_anaconda_mode() ? "local" : "nofail",
explanation: mount_explanation.nofail,
choices: [
{
@@ -285,6 +288,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 189abfdbb74..5787d75b0d4 100644
--- a/pkg/storaged/stratis/stopped-pool.jsx
+++ b/pkg/storaged/stratis/stopped-pool.jsx
@@ -91,6 +91,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/reference b/test/reference
index 034c1560ccf..23d27828e01 160000
--- a/test/reference
+++ b/test/reference
@@ -1 +1 @@
-Subproject commit 034c1560ccf51061e9eac7807056da3f574a7ce1
+Subproject commit 23d27828e014d9c20765b13d47d408ef5d6e1369
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()