diff --git a/doc/anaconda.md b/doc/anaconda.md
new file mode 100644
index 000000000000..a352d743d8d3
--- /dev/null
+++ b/doc/anaconda.md
@@ -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), 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 the storaged page 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
+------------------
+
+The storaged page 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.
+
+This is a simple map from block device to mount point, like
+
+```
+{
+ "/dev/vda1": "/boot",
+ "/dev/vda2": "/"
+}
+```
+
+The mount points do not include the mount point prefix.
diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js
index 2ce4cbdcc7ac..604a12e6b89c 100644
--- a/pkg/storaged/client.js
+++ b/pkg/storaged/client.js
@@ -479,6 +479,7 @@ function update_indices() {
client.update = () => {
update_indices();
client.path_warnings = find_warnings(client);
+ client.export_mount_point_mapping();
client.dispatchEvent("changed");
};
@@ -628,6 +629,15 @@ function init_model(callback) {
}
}
+ 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;
@@ -1371,4 +1381,60 @@ 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 = () => {
+ console.log("EXPORT");
+
+ 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) {
+ console.log("MPM", utils.decode_filename(b.PreferredDevice), dir);
+ mpm[utils.decode_filename(b.PreferredDevice)] = dir;
+ }
+ }
+ }
+ }
+
+ window.localStorage.setItem("cockpit_mount_points", JSON.stringify(mpm));
+};
+
export default client;
diff --git a/pkg/storaged/content-views.jsx b/pkg/storaged/content-views.jsx
index 23531db5c8e2..ec4b2015d3f1 100644
--- a/pkg/storaged/content-views.jsx
+++ b/pkg/storaged/content-views.jsx
@@ -467,7 +467,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];
@@ -559,6 +559,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 = ;
+ else
+ location = desc.used_for;
+
const cols = [
{
title: (
@@ -568,7 +580,7 @@ function append_row(client, rows, level, key, name, desc, tabs, job_object, opti
)
},
{ title: desc.type },
- { title: desc.link ? : desc.used_for },
+ { title: location },
{
title: desc.size.length
?
diff --git a/pkg/storaged/dialog.jsx b/pkg/storaged/dialog.jsx
index 3bc707515d1c..41f158e6321e 100644
--- a/pkg/storaged/dialog.jsx
+++ b/pkg/storaged/dialog.jsx
@@ -1069,7 +1069,8 @@ export const BlockingMessage = (usage) => {
pvol: _("physical volume of LVM2 volume group"),
mdraid: _("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"),
};
const rows = [];
@@ -1151,9 +1152,15 @@ export const TeardownMessage = (usage) => {
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/drives-panel.jsx b/pkg/storaged/drives-panel.jsx
index d60a887a739b..a529c211614d 100644
--- a/pkg/storaged/drives-panel.jsx
+++ b/pkg/storaged/drives-panel.jsx
@@ -105,7 +105,8 @@ export class DrivesPanel extends React.Component {
const drives = drive_rows(client);
return (
- {
- // "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;
}
@@ -160,10 +166,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);
}
}
@@ -260,6 +266,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");
@@ -298,7 +308,7 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended,
value: old_dir || "",
validate: (val, values, variant) => {
if (variant !== "nomount")
- return is_valid_mount_point(client, block, val);
+ return is_valid_mount_point(client, block, client.add_mount_point_prefix(val));
}
}),
SelectOne("type", _("Type"),
@@ -492,6 +502,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) },
diff --git a/pkg/storaged/fsys-panel.jsx b/pkg/storaged/fsys-panel.jsx
index fad062456c6c..56e3d821570e 100644
--- a/pkg/storaged/fsys-panel.jsx
+++ b/pkg/storaged/fsys-panel.jsx
@@ -49,6 +49,9 @@ export class FilesystemsPanel extends React.Component {
function is_mount(path) {
const block = client.blocks[path];
+ if (client.should_ignore_block(block))
+ return false;
+
// Stratis filesystems are handled separately
if (client.blocks_stratis_fsys[path])
return false;
@@ -72,7 +75,7 @@ export class FilesystemsPanel extends React.Component {
function make_mount(path) {
const block = client.blocks[path];
- const [, mount_point] = get_fstab_config(block, true);
+ let [, mount_point] = get_fstab_config(block, true);
const fsys_size = client.fsys_sizes.data[mount_point];
const backing_block = client.blocks[block.CryptoBackingDevice] || block;
const block_lvm2 = client.blocks_lvm2[backing_block.path];
@@ -80,6 +83,10 @@ export class FilesystemsPanel extends React.Component {
const vgroup = lvol && client.vgroups[lvol.VolumeGroup];
let name = null;
+ mount_point = client.strip_mount_point_prefix(mount_point);
+ if (mount_point === false)
+ return null;
+
if (vgroup)
name = vgroup.Name + "/" + lvol.Name;
@@ -106,7 +113,7 @@ export class FilesystemsPanel extends React.Component {
}
const mounts = Object.keys(client.blocks).filter(is_mount)
- .map(make_mount);
+ .map(make_mount).filter(m => m != null);
function has_filesystems(path) {
return client.stratis_pool_filesystems[path].length > 0;
@@ -132,8 +139,11 @@ export class FilesystemsPanel extends React.Component {
let mount = "-";
if (block) {
const [, mp] = get_fstab_config(block, true);
- if (mp)
- mount = mp;
+ if (mp) {
+ mount = client.strip_mount_point_prefix(mp);
+ if (mount === false)
+ return null;
+ }
}
return {
props: { path, client, key: fs.path },
@@ -152,11 +162,11 @@ export class FilesystemsPanel extends React.Component {
}
]
};
- });
+ }).filter(m => m != null);
}
const pools = Object.keys(client.stratis_pools).filter(has_filesystems)
- .map(make_pool);
+ .map(make_pool);
function onRowClick(event, row) {
if (!event || event.button !== 0)
@@ -177,6 +187,7 @@ export class FilesystemsPanel extends React.Component {
sortBy={{ index: 0, direction: SortByDirection.asc }}
aria-label={_("Filesystems")}
onRowClick={onRowClick}
+ emptyCaption={_("No filesystems")}
columns={[
{ title: _("Source"), sortable: true },
{ title: _("Type"), sortable: true },
diff --git a/pkg/storaged/fsys-tab.jsx b/pkg/storaged/fsys-tab.jsx
index bc247d98fb4d..08585d38f353 100644
--- a/pkg/storaged/fsys-tab.jsx
+++ b/pkg/storaged/fsys-tab.jsx
@@ -170,6 +170,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");
@@ -329,8 +333,8 @@ 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)
+ value: old_dir_for_display,
+ validate: val => is_valid_mount_point(client, block, client.add_mount_point_prefix(val))
}),
CheckBoxes("mount_options", _("Mount options"),
{
@@ -460,7 +464,8 @@ 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),
+ 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,
@@ -553,6 +558,7 @@ export class FilesystemTab extends React.Component {
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"));
@@ -564,11 +570,13 @@ export class FilesystemTab extends React.Component {
opt_texts.push(_("ignore failure"));
else
opt_texts.push(_("stop boot on failure"));
+ if (mount_point_text === false) {
+ mount_point_text = "";
+ opt_texzs.push(_("not part of target"));
+ }
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/nfs-panel.jsx b/pkg/storaged/nfs-panel.jsx
index ccce2e691a5e..780bbeb3d219 100644
--- a/pkg/storaged/nfs-panel.jsx
+++ b/pkg/storaged/nfs-panel.jsx
@@ -68,8 +68,8 @@ export class NFSPanel extends React.Component {
);
const nfs_feature = {
- is_enabled: () => client.features.nfs,
- package: client.get_config("nfs_client_package", false),
+ is_enabled: () => !client.in_anaconda_mode() && client.features.nfs,
+ package: !client.in_anaconda_mode() && client.get_config("nfs_client_package", false),
enable: () => {
client.features.nfs = true;
client.nfs.start();
diff --git a/pkg/storaged/side-panel.jsx b/pkg/storaged/side-panel.jsx
index 93357f26e0c9..558536ffa15a 100644
--- a/pkg/storaged/side-panel.jsx
+++ b/pkg/storaged/side-panel.jsx
@@ -41,7 +41,8 @@ export class SidePanel extends React.Component {
render() {
let show_all_button = null;
- let rows = this.props.rows.filter(row => !!row);
+ const client = this.props.client;
+ let rows = this.props.rows.filter(row => !!row && !(client && client.should_ignore_device(row.devname)));
// Find new items for animations
const current_keys = rows.map(row => row.key);
@@ -83,7 +84,7 @@ export class SidePanel extends React.Component {
feature={this.props.feature}
not_installed_text={this.props.not_installed_text}
install_title={this.props.install_title}>
- { this.props.rows.length > 0
+ { rows.length > 0
?
{ children }
diff --git a/pkg/storaged/stratis-details.jsx b/pkg/storaged/stratis-details.jsx
index 52acdf137241..2e470cadd2d5 100644
--- a/pkg/storaged/stratis-details.jsx
+++ b/pkg/storaged/stratis-details.jsx
@@ -120,6 +120,7 @@ function set_mount_options(client, 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",
@@ -277,7 +278,7 @@ export function stratis_content_rows(client, pool, options) {
};
}
- const [, mount_point] = get_fstab_config(block);
+ let [, mount_point] = get_fstab_config(block);
const fs_is_mounted = is_mounted(client, block);
function mount() {
@@ -345,7 +346,7 @@ export function stratis_content_rows(client, pool, options) {
}),
SelectOne("at_boot", _("At boot"),
{
- value: "nofail",
+ value: client.in_anaconda_mode()? "local" : "nofail",
explanation: mount_explanation.nofail,
choices: [
{
@@ -428,7 +429,7 @@ export function stratis_content_rows(client, pool, options) {
if (info)
info = <>{"\n"}{info}>;
- const tabs = [
+ let tabs = [
{
name,
renderer: FilesystemTab,
@@ -441,7 +442,7 @@ export function stratis_content_rows(client, pool, options) {
}
];
- const actions = [];
+ let actions = [];
const menuitems = [];
if (!fs_is_mounted) {
@@ -455,6 +456,13 @@ export function stratis_content_rows(client, pool, options) {
menuitems.push({_("Snapshot")});
menuitems.push({_("Delete")});
+ mount_point = client.strip_mount_point_prefix(mount_point);
+ if (mount_point === false) {
+ mount_point = _("(Not part of target)");
+ actions = null;
+ tabs = null;
+ }
+
const cols = [
{
title: (
@@ -484,7 +492,7 @@ export function stratis_content_rows(client, pool, options) {
return {
props: { key: fsys.Name },
columns: cols,
- expandedContent:
+ expandedContent: tabs ? : null
};
}
@@ -515,7 +523,7 @@ function create_fs(client, pool) {
{
validate: (val, values, variant) => {
if (variant !== "nomount")
- return is_valid_mount_point(client, null, val);
+ return is_valid_mount_point(client, null, client.add_mount_point_prefix(val));
}
}),
CheckBoxes("mount_options", _("Mount options"),
diff --git a/pkg/storaged/utils.js b/pkg/storaged/utils.js
index b94b0a38730d..6936e792791a 100644
--- a/pkg/storaged/utils.js
+++ b/pkg/storaged/utils.js
@@ -416,7 +416,9 @@ export function get_available_spaces(client) {
!is_vdo_backing_dev() &&
!is_swap() &&
!block_ptable &&
- !(block_part && block_part.IsContainer));
+ !(block_part && block_part.IsContainer) &&
+ !is_snap(client, block) &&
+ !client.should_ignore_device(decode_filename(block.PreferredDevice)));
}
function make(path) {
@@ -488,7 +490,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) &&
+ !client.should_ignore_block(block));
});
}
@@ -684,13 +687,14 @@ export function get_active_usage(client, path, top_action, child_action) {
if (fsys && fsys.MountPoints.length > 0) {
fsys.MountPoints.forEach(mp => {
+ const mpd = decode_filename(mp);
usage.push({
level,
usage: 'mounted',
block,
- location: decode_filename(mp),
+ location: mpd,
actions: get_actions(_("unmount")),
- blocking: false,
+ blocking: client.strip_mount_point_prefix(mpd) === false,
});
});
} else if (mdraid) {