Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

storage: Anaconda mode #19352

Merged
merged 2 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions doc/anaconda.md
Original file line number Diff line number Diff line change
@@ -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
mvollmer marked this conversation as resolved.
Show resolved Hide resolved
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.
28 changes: 28 additions & 0 deletions pkg/storaged/anaconda.jsx
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import client from "./client.js";

export const AnacondaAdvice = () => {
if (!client.in_anaconda_mode())
return null;

// Nothing yet.
return null;
mvollmer marked this conversation as resolved.
Show resolved Hide resolved
};
3 changes: 2 additions & 1 deletion pkg/storaged/block/create-pages.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,6 @@ export function make_block_page(parent, block, card) {
}
}

new_page(parent, card);
if (card)
new_page(parent, card);
}
31 changes: 22 additions & 9 deletions pkg/storaged/block/format-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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."));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details


const split_options = parse_options(old_opts);
extract_option(split_options, "noauto");
const opt_ro = extract_option(split_options, "ro");
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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) },
Expand Down
6 changes: 5 additions & 1 deletion pkg/storaged/block/other.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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,
Expand Down
45 changes: 45 additions & 0 deletions pkg/storaged/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,15 @@ function init_model(callback) {
).then(() => info);
}

try {
client.anaconda = JSON.parse(window.localStorage.getItem("cockpit_anaconda"));
Fixed Show fixed Hide fixed
} catch {
console.warn("Can't parse cockpit_anaconda configuration as JSON");
client.anaconda = null;
Comment on lines +746 to +748
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 added lines are not executed by any test. Details

}

console.log("ANACONDA", client.anaconda);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this runs unconditionally... it's better to remove it I guess.


pull_time().then(() => {
read_os_release().then(os_release => {
client.os_release = os_release;
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details


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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

else
dir = mpp + dir;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

}
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;
14 changes: 11 additions & 3 deletions pkg/storaged/dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
];
Expand Down Expand Up @@ -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"),
Comment on lines +1110 to +1111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test. Details

};

const rows = [];
Expand Down Expand Up @@ -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)");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

}
rows.push({
columns: [name,
use.location || "-",
location || "-",
use.actions.length ? use.actions.join(", ") : "-",
{
title: <UsersPopover users={use.users || []} />,
Expand Down
5 changes: 4 additions & 1 deletion pkg/storaged/drive/drive.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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";
Expand Down
12 changes: 7 additions & 5 deletions pkg/storaged/filesystem/filesystem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

if (!mounted)
mp_text = mp_text + " " + _("(not mounted)");
} else
mp_text = _("(not mounted)");

return new_card({
Expand Down
20 changes: 15 additions & 5 deletions pkg/storaged/filesystem/mounting-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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."));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details


const split_options = parse_options(options);
extract_option(split_options, "noauto");
const opt_never_auto = extract_option(split_options, "x-cockpit-never-auto");
Expand Down Expand Up @@ -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"),
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading