diff --git a/doc/anaconda.md b/doc/anaconda.md new file mode 100644 index 000000000000..c5eb782ab4eb --- /dev/null +++ b/doc/anaconda.md @@ -0,0 +1,73 @@ +Cockpit Storage in Anaconda Mode +================================ + +Anaconda (the OS Installer) can open the Cockpit "storaged" 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 changing its +manifest, probably via the /etc/cockpit/storaged.override.json file. + +Anaconda mode is activated when the manifest contains a "anaconda" +entry. All further configuration is done via the value of that entry. +For example, you could put this into /etc/cockpit/storaged.override.json: + +``` +{ + "anaconda": { + } +} +``` + +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. + +``` +{ + "anaconda": { + "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: + +``` +{ + "anaconda": { + "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). + +Anaconda will read the real /etc/fstab and reverse engineer the +storage configuration from it. diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js index 2ce4cbdcc7ac..b58fcf39766b 100644 --- a/pkg/storaged/client.js +++ b/pkg/storaged/client.js @@ -628,6 +628,8 @@ function init_model(callback) { } } + client.anaconda = cockpit.manifests?.storage.anaconda; + pull_time().then(() => { read_os_release().then(os_release => { client.os_release = os_release; @@ -1371,4 +1373,36 @@ 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 && client.anaconda.ignore_devices.indexOf(devname) != -1; +} + 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..769fdc936915 100644 --- a/pkg/storaged/fsys-panel.jsx +++ b/pkg/storaged/fsys-panel.jsx @@ -72,7 +72,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 +80,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 +110,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 +136,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 +159,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 +184,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..6c9e3874e3f0 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) { @@ -684,13 +686,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) {