Skip to content

Commit

Permalink
WIP - anaconda mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mvollmer committed Oct 19, 2023
1 parent ca6006e commit 0928a0c
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 51 deletions.
84 changes: 84 additions & 0 deletions doc/anaconda.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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",
"ignore_devices": [ "/dev/sr0", "/dev/loop0" ]
}));
window.open("/cockpit/@localhost/storage/index.html", "storage-tab");
```

Ignoring storage devices
------------------------

Anaconda needs to tell Cockpit which devices can not be used to
install the OS on. This is done with the "ignore_devices" entry, which
is a array of strings.

```
{
"ignore_devices": [ "/dev/sda" ]
}
```

Entries in that array can refer to block devices, LVM2 volume groups
(/dev/vgroup-name/), and Stratis pools (/dev/stratis/pool-name/).

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. 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).

In addition to that, Cockpit will also store the mount points in the
`"cockpit_mount_points"` item in `window.localStorage`, as a JSON
encoded object, for the benefit of Anaconda.

This is a simple map from mount point to block device, like

```
{
"/boot": "/dev/vda1",
"/": "/dev/vda2"
}
```

The mount points do not include the mount point prefix.
35 changes: 35 additions & 0 deletions pkg/storaged/anaconda.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 cockpit from "cockpit";
import React from "react";
import { Alert, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import AlertActionLink.
import { Page, PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused imports Page, PageSection.

const _ = cockpit.gettext;

export const AnacondaAdvice = ({ client }) => {
return (
<Alert isInline
variant='info'
title={_("What you need to do")}>
<p>Anaconda will tell us here what is wrong with the current config.</p>
</Alert>
);
};
62 changes: 62 additions & 0 deletions pkg/storaged/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ client.update = (first_time) => {
update_indices();
client.path_warnings = find_warnings(client);
create_pages();
client.export_mount_point_mapping();
client.dispatchEvent("changed");
}
};
Expand Down Expand Up @@ -721,6 +722,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 @@ -1464,4 +1474,56 @@ 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) {
if (dir == "/")
dir = mpp;
else
dir = mpp + dir;
}
return dir;
};

client.should_ignore_device = (devname) => {
return client.anaconda?.ignore_devices && client.anaconda.ignore_devices.indexOf(devname) != -1;
};

client.should_ignore_block = (block) => {
return client.should_ignore_device(utils.decode_filename(block.PreferredDevice));
};

client.export_mount_point_mapping = () => {
const mpm = { };
for (const p in client.blocks) {
const b = client.blocks[p];
for (const c of b.Configuration) {
if (c[0] == "fstab") {
const dir = client.strip_mount_point_prefix(utils.decode_filename(c[1].dir.v));
if (dir)
mpm[dir] = utils.decode_filename(b.PreferredDevice);
}
}
}

window.localStorage.setItem("cockpit_mount_points", JSON.stringify(mpm));
};

export default client;
16 changes: 14 additions & 2 deletions pkg/storaged/content-views.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ function block_description(client, block, options) {
type = cockpit.format(C_("storage-id-desc", "$0 filesystem"), block.IdType);
if (client.fsys_sizes.data[mount_point])
size = client.fsys_sizes.data[mount_point];
used_for = mount_point;
used_for = client.strip_mount_point_prefix(mount_point);
} else if (block.IdUsage == "raid") {
if (block_pvol && client.vgroups[block_pvol.VolumeGroup]) {
const vgroup = client.vgroups[block_pvol.VolumeGroup];
Expand Down Expand Up @@ -601,6 +601,18 @@ function append_row(client, rows, level, key, name, desc, tabs, job_object, opti
if (info)
info = <>{"\n"}{info}</>;

let location;
if (desc.used_for === false) {
// XXX - urks
location = _("(Not part of target)");
menu = null;
tabs.actions = null;
tabs.renderers = [];
} else if (desc.link)
location = <Button isInline variant="link" onClick={() => cockpit.location.go(desc.link)}>{desc.used_for}</Button>;
else
location = desc.used_for;

const cols = [
{
title: (
Expand All @@ -610,7 +622,7 @@ function append_row(client, rows, level, key, name, desc, tabs, job_object, opti
</span>)
},
{ title: desc.type },
{ title: desc.link ? <Button isInline variant="link" onClick={() => cockpit.location.go(desc.link)}>{desc.used_for}</Button> : desc.used_for },
{ title: location },
{
title: desc.size.length
? <StorageUsageBar stats={desc.size} critical={desc.critical_size || 0.95} block={name} />
Expand Down
9 changes: 9 additions & 0 deletions pkg/storaged/details.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,21 @@ import { VDODetails } from "./vdo-details.jsx";
import { NFSDetails } from "./nfs-details.jsx";
import { StratisPoolDetails, StratisStoppedPoolDetails } from "./stratis-details.jsx";
import { JobsPanel } from "./jobs-panel.jsx";
import { AnacondaAdvice } from "./anaconda.jsx";

const _ = cockpit.gettext;

export const StdDetailsLayout = ({ client, alerts, header, content, sidebar }) => {
const top = <>
{ (alerts || []).filter(a => !!a).map((a, i) => <StackItem key={i}><Card>{a}</Card></StackItem>) }
{ client.in_anaconda_mode()
? <StackItem id="detail-header">
<Card>
<AnacondaAdvice client={client} />
</Card>
</StackItem>
: null
}
<StackItem id="detail-header">
{ header }
</StackItem>
Expand Down
11 changes: 9 additions & 2 deletions pkg/storaged/dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,8 @@ export const BlockingMessage = (usage) => {
pvol: _("physical volume of LVM2 volume group"),
"mdraid-member": _("member of RAID 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"),
};

console.log("U", usage);
Expand Down Expand Up @@ -1173,9 +1174,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
3 changes: 2 additions & 1 deletion pkg/storaged/drives-panel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export class DrivesPanel extends React.Component {
const drives = drive_rows(client);

return (
<SidePanel id="drives"
<SidePanel client={client}
id="drives"
className="storage-drives-list"
title={_("Drives")}
empty_text={_("No drives attached")}
Expand Down
28 changes: 19 additions & 9 deletions pkg/storaged/format-dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,19 @@ const _ = cockpit.gettext;
export function initial_tab_options(client, block, for_fstab) {
const options = { };

utils.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;

utils.get_parent_blocks(client, block.path).forEach(p => {
if (utils.is_netdev(client, p)) {
options._netdev = true;
}
Expand Down Expand Up @@ -137,10 +142,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 @@ -237,6 +242,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 @@ -274,7 +283,7 @@ 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 @@ -468,6 +477,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: utils.encode_filename(mount_point) },
Expand Down
Loading

0 comments on commit 0928a0c

Please sign in to comment.