Skip to content

Commit

Permalink
storage: Anaconda mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Dec 13, 2023
1 parent 4195ba3 commit 8eb1cc7
Show file tree
Hide file tree
Showing 23 changed files with 497 additions and 48 deletions.
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
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;
};
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."));

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"));
} 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;
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;

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;
11 changes: 9 additions & 2 deletions pkg/storaged/dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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: <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;
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."));

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

0 comments on commit 8eb1cc7

Please sign in to comment.