diff --git a/pkg/storaged/block/create-pages.jsx b/pkg/storaged/block/create-pages.jsx index 3054c5ef01a3..96d05d9a20e8 100644 --- a/pkg/storaged/block/create-pages.jsx +++ b/pkg/storaged/block/create-pages.jsx @@ -35,7 +35,8 @@ import { make_swap_card } from "../swap/swap.jsx"; import { make_encryption_card } from "../crypto/encryption.jsx"; import { make_btrfs_device_card } from "../btrfs/device.jsx"; import { make_btrfs_filesystem_card } from "../btrfs/filesystem.jsx"; -import { make_btrfs_subvolume_pages } from "../btrfs/subvolume.jsx"; +import { make_btrfs_subvolume_pages, make_btrfs_subvolume_pages_from_child_config } from "../btrfs/subvolume.jsx"; +import { is_multi_device_btrfs_volume } from "../btrfs/utils.jsx"; import { new_page } from "../pages.jsx"; @@ -59,7 +60,7 @@ export function make_block_page(parent, block, card) { (fstab_config[2].indexOf("subvol=") >= 0 || fstab_config[2].indexOf("subvolid=") >= 0)); const block_btrfs_blockdev = content_block && client.blocks_fsys_btrfs[content_block.path]; - const single_device_volume = block_btrfs_blockdev && block_btrfs_blockdev.data.num_devices === 1; + const single_device_volume = !is_multi_device_btrfs_volume(content_block || block); if (client.blocks_ptable[block.path]) { make_partition_table_page(parent, block, card); @@ -85,8 +86,14 @@ export function make_block_page(parent, block, card) { // can not happen unless there is a bug in the code above. console.error("Assertion failure: is_crypto == false"); } - if (fstab_config.length > 0 && !is_btrfs) { - card = make_filesystem_card(card, block, null, fstab_config); + if (fstab_config.length > 0) { + if (is_btrfs) { + if (single_device_volume) + card = make_btrfs_filesystem_card(card, block, null); + else + card = make_locked_encrypted_data_card(card, block); + } else + card = make_filesystem_card(card, block, null, fstab_config); } else { card = make_locked_encrypted_data_card(card, block); } @@ -124,6 +131,8 @@ export function make_block_page(parent, block, card) { const page = new_page(parent, card); if (block_btrfs_blockdev && single_device_volume) make_btrfs_subvolume_pages(page, block_btrfs_blockdev); + else if (!content_block && is_btrfs && single_device_volume) + make_btrfs_subvolume_pages_from_child_config(page, block); return page; } } diff --git a/pkg/storaged/btrfs/filesystem.jsx b/pkg/storaged/btrfs/filesystem.jsx index d062d7a38152..3e5844aa8bf8 100644 --- a/pkg/storaged/btrfs/filesystem.jsx +++ b/pkg/storaged/btrfs/filesystem.jsx @@ -27,9 +27,9 @@ import { DescriptionList } from "@patternfly/react-core/dist/esm/components/Desc import { new_card, ChildrenTable, StorageCard, StorageDescription } from "../pages.jsx"; +import { format_dialog } from "../block/format-dialog.jsx"; import { StorageUsageBar, StorageLink } from "../storage-controls.jsx"; import { btrfs_device_usage, btrfs_is_volume_mounted } from "./utils.jsx"; -import { btrfs_device_actions } from "./device.jsx"; import { rename_dialog } from "./volume.jsx"; const _ = cockpit.gettext; @@ -43,17 +43,18 @@ export function make_btrfs_filesystem_card(next, backing_block, content_block) { return new_card({ title: _("btrfs filesystem"), next, - actions: btrfs_device_actions(backing_block, content_block), + actions: [ + { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true }, + ], component: BtrfsFilesystemCard, props: { backing_block, content_block }, }); } const BtrfsFilesystemCard = ({ card, backing_block, content_block }) => { - const block_btrfs = client.blocks_fsys_btrfs[content_block.path]; + const block_btrfs = content_block && client.blocks_fsys_btrfs[content_block.path]; const uuid = block_btrfs && block_btrfs.data.uuid; const label = block_btrfs && block_btrfs.data.label; - const use = btrfs_device_usage(client, uuid, block_btrfs.path); // Changing the label is only supported when the device is not mounted // otherwise we will get btrfs filesystem error ERROR: device /dev/vda5 is @@ -66,18 +67,22 @@ const BtrfsFilesystemCard = ({ card, backing_block, content_block }) => { + { block_btrfs && rename_dialog(block_btrfs, label)} - excuse={is_mounted ? _("Btrfs volume is mounted") : null}> - {_("edit")} - } + value={label} + action={ + rename_dialog(block_btrfs, label)} + excuse={is_mounted ? _("Btrfs volume is mounted") : null}> + {_("edit")} + } /> + } + { content_block && + } { block_btrfs && - + } diff --git a/pkg/storaged/btrfs/subvolume.jsx b/pkg/storaged/btrfs/subvolume.jsx index b64d43f45b54..1b0c35e5c8b5 100644 --- a/pkg/storaged/btrfs/subvolume.jsx +++ b/pkg/storaged/btrfs/subvolume.jsx @@ -29,10 +29,13 @@ import { import { StorageUsageBar } from "../storage-controls.jsx"; import { encode_filename, decode_filename, - get_fstab_config_with_client, reload_systemd, extract_option, parse_options, + reload_systemd, extract_option, parse_options, flatten, teardown_active_usage, + maybe_update_crypto_options, } from "../utils.js"; -import { btrfs_usage, validate_subvolume_name, parse_subvol_from_options } from "./utils.jsx"; +import { + btrfs_usage, validate_subvolume_name, parse_subvol_from_options, is_multi_device_btrfs_volume +} from "./utils.jsx"; import { at_boot_input, mounting_dialog, mount_options } from "../filesystem/mounting-dialog.jsx"; import { dialog_open, TextInput, @@ -40,19 +43,18 @@ import { } from "../dialog.jsx"; import { check_mismounted_fsys, MismountAlert } from "../filesystem/mismounting.jsx"; import { - is_mounted, is_valid_mount_point, mount_point_text, MountPoint, edit_mount_point + get_fstab_config, is_mounted, is_valid_mount_point, mount_point_text, MountPoint, edit_mount_point } from "../filesystem/utils.jsx"; + import client, { btrfs_poll } from "../client.js"; const _ = cockpit.gettext; -function subvolume_unmount(volume, subvol, forced_options) { - const block = client.blocks[volume.path]; +function subvolume_unmount(block, subvol, forced_options) { mounting_dialog(client, block, "unmount", forced_options, subvol); } -function subvolume_mount(volume, subvol, forced_options) { - const block = client.blocks[volume.path]; +function subvolume_mount(block, subvol, forced_options) { mounting_dialog(client, block, "mount", forced_options, subvol); } @@ -67,7 +69,7 @@ function get_mount_point_in_parent(volume, subvol) { (subvol.pathname.substring(0, p.pathname.length) == p.pathname && subvol.pathname[p.pathname.length] == "/"); if (has_parent_subvol && is_mounted(client, block, p)) { - const [, pmp, opts] = get_fstab_config_with_client(client, block, false, p); + const [, pmp, opts] = get_fstab_config(block, false, p); const opt_ro = extract_option(parse_options(opts), "ro"); if (!opt_ro) { if (p.pathname == "/") @@ -80,7 +82,7 @@ function get_mount_point_in_parent(volume, subvol) { return null; } -function set_mount_options(subvol, block, vals) { +async function set_mount_options(subvol, block, vals) { const mount_options = []; const mount_now = vals.variant != "nomount"; @@ -118,14 +120,12 @@ function set_mount_options(subvol, block, vals) { } ]; - return block.AddConfigurationItem(config, {}) - .then(reload_systemd) - .then(() => { - if (mount_now) { - return client.mount_at(block, mount_point); - } else - return Promise.resolve(); - }); + await block.AddConfigurationItem(config, {}); + await reload_systemd(); + if (mount_now) + await client.mount_at(block, mount_point); + if (!is_multi_device_btrfs_volume(block)) + await maybe_update_crypto_options(client, block); } function subvolume_create(volume, subvol, parent_dir) { @@ -204,7 +204,7 @@ function subvolume_delete(volume, subvol, mount_point_in_parent, card) { const usage = []; for (const sv of all_subvols) { - const [config, mount_point] = get_fstab_config_with_client(client, block, false, sv); + const [config, mount_point] = get_fstab_config(block, false, sv); const fs_is_mounted = is_mounted(client, block, sv); usage.push({ @@ -253,93 +253,107 @@ function dirname(path) { return path.substr(0, i); } -export function make_btrfs_subvolume_pages(parent, volume) { - let subvols = client.uuids_btrfs_subvols[volume.data.uuid]; - if (!subvols) { - const block = client.blocks[volume.path]; - /* - * Try to show subvolumes based on fstab entries, this is a bit tricky - * as mounts where subvolid cannot be shown userfriendly. - * - * The real subvolume data structure has "id" fields and - * "parent" fields that refer to the ids to form a tree. We - * want to do the same here, and we give fake ids to our fake - * subvolumes for this reason. We don't store these fake ids - * in the "id" field since we don't want them to be taken - * seriously by the rest of the code. - */ - let fake_id = 5; - subvols = [{ pathname: "/", id: 5, fake_id: fake_id++ }]; - const subvols_by_pathname = { }; - for (const config of block.Configuration) { - if (config[0] == "fstab") { - const opts = config[1].opts; - if (!opts) - continue; - - const fstab_subvol = parse_subvol_from_options(decode_filename(opts.v)); - - if (fstab_subvol && fstab_subvol.pathname && fstab_subvol.pathname !== "/") { - fstab_subvol.fake_id = fake_id++; - subvols_by_pathname[fstab_subvol.pathname] = fstab_subvol; - subvols.push(fstab_subvol); - } +function get_subvols_from_fstab(configuration) { + /* + * Try to show subvolumes based on fstab entries, this is a bit tricky + * as mounts where subvolid cannot be shown userfriendly. + * + * The real subvolume data structure has "id" fields and + * "parent" fields that refer to the ids to form a tree. We + * want to do the same here, and we give fake ids to our fake + * subvolumes for this reason. We don't store these fake ids + * in the "id" field since we don't want them to be taken + * seriously by the rest of the code. + */ + let fake_id = 5; + const subvols = [{ pathname: "/", id: 5, fake_id: fake_id++ }]; + const subvols_by_pathname = { }; + for (const config of configuration) { + if (config[0] == "fstab") { + const opts = config[1].opts; + if (!opts) + continue; + + const fstab_subvol = parse_subvol_from_options(decode_filename(opts.v)); + + if (fstab_subvol && fstab_subvol.pathname && fstab_subvol.pathname !== "/") { + fstab_subvol.fake_id = fake_id++; + subvols_by_pathname[fstab_subvol.pathname] = fstab_subvol; + subvols.push(fstab_subvol); } } + } - // Find parents - for (const pn in subvols_by_pathname) { - let dn = pn; - while (true) { - dn = dirname(dn); - if (!dn) { - subvols_by_pathname[pn].parent = 5; - break; - } else if (subvols_by_pathname[dn]) { - subvols_by_pathname[pn].parent = subvols_by_pathname[dn].fake_id; - break; - } + // Find parents + for (const pn in subvols_by_pathname) { + let dn = pn; + while (true) { + dn = dirname(dn); + if (!dn) { + subvols_by_pathname[pn].parent = 5; + break; + } else if (subvols_by_pathname[dn]) { + subvols_by_pathname[pn].parent = subvols_by_pathname[dn].fake_id; + break; } } } + return subvols; +} + +export function make_btrfs_subvolume_pages(parent, volume) { + let subvols = client.uuids_btrfs_subvols[volume.data.uuid]; + if (!subvols) { + const block = client.blocks[volume.path]; + subvols = get_subvols_from_fstab(block.Configuration); + } + const root = subvols.find(s => s.id == 5); if (root) - make_btrfs_subvolume_page(parent, volume, root, "", subvols); + make_btrfs_subvolume_page(parent, client.blocks[volume.path], volume, root, "", subvols); } -function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) { +export function make_btrfs_subvolume_pages_from_child_config(parent, backing_block) { + const block_crypto = client.blocks_crypto[backing_block.path]; + const subvols = get_subvols_from_fstab(block_crypto.ChildConfiguration); + + const root = subvols.find(s => s.id == 5); + if (root) + make_btrfs_subvolume_page(parent, backing_block, null, root, "", subvols); +} + +function make_btrfs_subvolume_page(parent, backing_block, volume, subvol, path_prefix, subvols) { const actions = []; - const use = btrfs_usage(client, volume); - const block = client.blocks[volume.path]; - const fstab_config = get_fstab_config_with_client(client, block, false, subvol); + const content_block = volume && client.blocks[volume.path]; + const fstab_config = get_fstab_config(backing_block, true, subvol); const [, mount_point, opts] = fstab_config; const opt_ro = extract_option(parse_options(opts), "ro"); - const mismount_warning = check_mismounted_fsys(block, block, fstab_config, subvol); - const mounted = is_mounted(client, block, subvol); + const mismount_warning = check_mismounted_fsys(backing_block, content_block, fstab_config, subvol); + const mounted = content_block && is_mounted(client, content_block, subvol); const mp_text = mount_point_text(mount_point, mounted); if (mp_text == null) return null; const forced_options = [`subvol=${subvol.pathname}`]; - const mount_point_in_parent = get_mount_point_in_parent(volume, subvol); + const mount_point_in_parent = volume && get_mount_point_in_parent(volume, subvol); if (client.in_anaconda_mode()) { actions.push({ title: _("Edit mount point"), - action: () => edit_mount_point(block, forced_options, subvol), + action: () => edit_mount_point(content_block || backing_block, forced_options, subvol), }); } if (mounted) { actions.push({ title: _("Unmount"), - action: () => subvolume_unmount(volume, subvol, forced_options), + action: () => subvolume_unmount(content_block, subvol, forced_options), }); } else { actions.push({ title: _("Mount"), - action: () => subvolume_mount(volume, subvol, forced_options), + action: () => subvolume_mount(content_block || backing_block, subvol, forced_options), }); } @@ -383,36 +397,36 @@ function make_btrfs_subvolume_page(parent, volume, subvol, path_prefix, subvols) const card = new_card({ title: _("btrfs subvolume"), next: null, - page_location: ["btrfs", volume.data.uuid, subvol.pathname], + page_location: ["btrfs", volume ? volume.data.uuid : "XXX", subvol.pathname], page_name: strip_prefix(subvol.pathname, path_prefix), - page_size: is_mounted && , + page_size: mounted && , location: mp_text, component: BtrfsSubvolumeCard, has_warning: !!mismount_warning, - props: { subvol, mount_point, mismount_warning, block, fstab_config, forced_options }, + props: { subvol, mount_point, mismount_warning, backing_block, content_block, fstab_config, forced_options }, actions, }); const page = new_page(parent, card); for (const sv of subvols) { if (sv.parent && (sv.parent === subvol.id || sv.parent === subvol.fake_id)) { - make_btrfs_subvolume_page(page, volume, sv, subvol.pathname + "/", subvols); + make_btrfs_subvolume_page(page, backing_block, volume, sv, subvol.pathname + "/", subvols); } } } -const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, block, fstab_config, forced_options }) => { +const BtrfsSubvolumeCard = ({ card, subvol, mismount_warning, content_block, backing_block, fstab_config, forced_options }) => { return ( }> + backing_block={backing_block} content_block={content_block} subvol={subvol} />}> diff --git a/pkg/storaged/btrfs/utils.jsx b/pkg/storaged/btrfs/utils.jsx index 402ded0fef9d..9e4dbca2a513 100644 --- a/pkg/storaged/btrfs/utils.jsx +++ b/pkg/storaged/btrfs/utils.jsx @@ -17,6 +17,7 @@ * along with Cockpit; If not, see . */ import cockpit from "cockpit"; +import client from "../client.js"; import { decode_filename } from "../utils.js"; @@ -88,3 +89,36 @@ export function validate_subvolume_name(name) { if (name.includes('/')) return cockpit.format(_("Name cannot contain the character '/'.")); } + +// Determine whether BLOCK is a multi-device btrfs volume. This works +// also when the volume is stored on LUKS devices, and some of them +// are locked. + +export function is_multi_device_btrfs_volume(block) { + // Check the direct case. + const block_btrfs = client.blocks_fsys_btrfs[block.path]; + if (block_btrfs) { + return block_btrfs.data.num_devices > 1; + } + + // Might be a locked LUKS device. Try to figure out the uuid from + // its child fstab entries. + const block_crypto = client.blocks_crypto[block.path]; + if (!block_crypto || client.blocks_cleartext[block.path]) { + return false; + } + + for (const c of block_crypto.ChildConfiguration) { + if (c[0] == "fstab") { + const fsname = decode_filename(c[1].fsname.v); + const uuid_match = fsname.match(/^UUID=(?[A-Fa-f0-9-]+)/); + if (uuid_match) { + const btrfs = client.uuids_btrfs_volume[uuid_match.groups.uuid]; + if (btrfs && btrfs.data.num_devices > 1) + return true; + } + } + } + + return false; +} diff --git a/pkg/storaged/crypto/actions.jsx b/pkg/storaged/crypto/actions.jsx index d6139c8283f9..e6e9cba7068f 100644 --- a/pkg/storaged/crypto/actions.jsx +++ b/pkg/storaged/crypto/actions.jsx @@ -21,9 +21,9 @@ import cockpit from "cockpit"; import client from "../client"; import { get_existing_passphrase, unlock_with_type } from "./keyslots.jsx"; -import { set_crypto_auto_option } from "../utils.js"; import { dialog_open, PassInput } from "../dialog.jsx"; import { remember_passphrase } from "../anaconda.jsx"; +import { set_crypto_auto_option } from "../utils.js"; const _ = cockpit.gettext; diff --git a/pkg/storaged/filesystem/mismounting.jsx b/pkg/storaged/filesystem/mismounting.jsx index 0fb5fb7756cf..511eb4dd9a3e 100644 --- a/pkg/storaged/filesystem/mismounting.jsx +++ b/pkg/storaged/filesystem/mismounting.jsx @@ -26,7 +26,7 @@ import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js import { encode_filename, parse_options, unparse_options, extract_option, reload_systemd, - set_crypto_auto_option, get_mount_points, + get_mount_points, set_crypto_auto_option, } from "../utils.js"; import { StorageButton } from "../storage-controls.jsx"; diff --git a/pkg/storaged/filesystem/mounting-dialog.jsx b/pkg/storaged/filesystem/mounting-dialog.jsx index 1360fef93dd4..035b5251d2ef 100644 --- a/pkg/storaged/filesystem/mounting-dialog.jsx +++ b/pkg/storaged/filesystem/mounting-dialog.jsx @@ -18,13 +18,16 @@ */ import cockpit from "cockpit"; +import React from "react"; import client from "../client.js"; import { encode_filename, parse_options, unparse_options, extract_option, reload_systemd, - set_crypto_options, is_mounted_synch, + is_mounted_synch, get_active_usage, teardown_active_usage, + set_crypto_auto_option, + maybe_update_crypto_options, } from "../utils.js"; import { @@ -35,6 +38,7 @@ import { } from "../dialog.jsx"; import { init_existing_passphrase, unlock_with_type } from "../crypto/keyslots.jsx"; import { initial_tab_options, mount_explanation } from "../block/format-dialog.jsx"; +import { is_multi_device_btrfs_volume } from "../btrfs/utils.jsx"; import { is_mounted, get_fstab_config, @@ -182,26 +186,41 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) { return Promise.resolve(); } - function maybe_unlock() { + async function maybe_unlock() { const crypto = client.blocks_crypto[block.path]; if (mode == "mount" && crypto) { - return (unlock_with_type(client, block, passphrase, passphrase_type) - .catch(error => { - dlg.set_values({ needs_explicit_passphrase: true }); - return Promise.reject(error); - })); - } else - return Promise.resolve(); - } - - function maybe_lock() { - if (mode == "unmount" && !subvol && !client.in_anaconda_mode()) { - const crypto_backing = client.blocks[block.CryptoBackingDevice]; - const crypto_backing_crypto = crypto_backing && client.blocks_crypto[crypto_backing.path]; - if (crypto_backing_crypto) { - return crypto_backing_crypto.Lock({}); - } else - return Promise.resolve(); + try { + await unlock_with_type(client, block, passphrase, passphrase_type); + // Check whether we have just opened a + // multi-device btrfs volume. If so, we need to + // give up. + // + // Ideally, Cockpit would always know when + // something is part of a multi-device btrfs + // volume, even for locked LUKS devices, and would + // never let people mount subvolumes of such a + // volume unless all devices are available. + // + // But that knowledge is based on the "x-parent" + // options in /etc/fstab, and those are + // unreliable. + // + // Thus, while we should try to not let the user + // run into this situation here as much as + // possible, we will probably not be able to rule + // it out completely. + const cleartext = await client.wait_for(() => client.blocks_cleartext[crypto.path]); + if (cleartext.IdType == "btrfs" && client.features.btrfs) { + const btrfs = await client.wait_for(() => client.blocks_fsys_btrfs[cleartext.path]); + if (btrfs.data.num_devices > 1) { + await set_crypto_auto_option(block, true); + throw new Error("unexpected-multi-device-btrfs"); + } + } + } catch (error) { + dlg.set_values({ needs_explicit_passphrase: true }); + throw error; + } } } @@ -226,7 +245,6 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) { else if (new_config && !is_mounted(client, block)) return maybe_mount(); }) - .then(maybe_lock) .then(reload_systemd)); } @@ -279,7 +297,18 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) { update: _("Save") }; - function do_unmount() { + function maybe_lock() { + const block_fsys = client.blocks_fsys[block.path]; + const crypto_backing = client.blocks[block.CryptoBackingDevice]; + const crypto_backing_crypto = crypto_backing && client.blocks_crypto[crypto_backing.path]; + console.log("LOCK?", block_fsys?.MountPoints.length == 0); + if (crypto_backing_crypto && block_fsys && block_fsys.MountPoints.length == 0 && !client.in_anaconda_mode()) { + return crypto_backing_crypto.Lock({}); + } else + return Promise.resolve(); + } + + async function do_unmount() { let opts = []; opts.push("noauto"); if (opt_ro) @@ -294,21 +323,16 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) { opts = opts.concat(forced_options); if (extra_options) opts = opts.concat(extra_options); - return (maybe_set_crypto_options(null, false, null, null) - .then(() => maybe_update_config(old_dir, unparse_options(opts)))); + + await maybe_update_config(old_dir, unparse_options(opts)); + if (!is_multi_device_btrfs_volume(block)) { + await maybe_update_crypto_options(client, block); + await maybe_lock(); + } } let passphrase_type; - function maybe_set_crypto_options(readonly, auto, nofail, netdev) { - if (client.blocks_crypto[block.path]) { - return set_crypto_options(block, readonly, auto, nofail, netdev); - } else if (client.blocks_crypto[block.CryptoBackingDevice]) { - return set_crypto_options(client.blocks[block.CryptoBackingDevice], readonly, auto, nofail, netdev); - } else - return Promise.resolve(); - } - const usage = get_active_usage(client, block.path, null, null, false, subvol); const dlg = dialog_open({ @@ -322,9 +346,9 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) { Action: { Title: mode_action[mode], disable_on_error: usage.Teardown, - action: function (vals) { + action: async function (vals) { if (mode == "unmount") { - return do_unmount(); + await do_unmount(); } else if (mode == "mount" || mode == "update") { let opts = []; if ((mode == "update" && !is_filesystem_mounted) || vals.at_boot == "never") @@ -341,14 +365,22 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) { opts = opts.concat(forced_options); if (vals.mount_options?.extra) opts = opts.concat(parse_options(vals.mount_options.extra)); - 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", - vals.at_boot == "netdev"))); + try { + await maybe_update_config(client.add_mount_point_prefix(vals.mount_point), + unparse_options(opts), + vals.passphrase, + passphrase_type); + if (!is_multi_device_btrfs_volume(block)) + await maybe_update_crypto_options(client, block); + } catch (error) { + if (error.message == "unexpected-multi-device-btrfs") { + dialog_open({ + Title: _("Multi-device btrfs volume detected"), + Body:

{_("This device is only part of a btrfs volume. Please make all devices available in order to mount it.")}

+ }); + } else + throw error; + } } } }, diff --git a/pkg/storaged/utils.js b/pkg/storaged/utils.js index 23e050e1cb8a..a9155857551c 100644 --- a/pkg/storaged/utils.js +++ b/pkg/storaged/utils.js @@ -97,7 +97,7 @@ export function edit_crypto_config(block, modify) { }); } -export function set_crypto_options(block, readonly, auto, nofail, netdev) { +function set_crypto_options(block, readonly, auto, nofail, netdev) { return edit_crypto_config(block, (config, commit) => { const opts = config.options ? parse_options(decode_filename(config.options.v)) : []; if (readonly !== null) { @@ -129,6 +129,44 @@ export function set_crypto_auto_option(block, flag) { return set_crypto_options(block, null, flag, null, null); } +async function update_crypto_options_for_children(client, block) { + // This sets the readonly, noauto, nofail, and _netdev options as + // required by the content of this block device. + + const block_crypto = client.blocks_crypto[block.path]; + console.log("UPDATE", block_crypto.path); + + let readonly = true; + let noauto = true; + let nofail = true; + let netdev = true; + + for (const c of block_crypto.ChildConfiguration) { + if (c[0] == "fstab") { + const opts = parse_options(decode_filename(c[1].opts.v)); + console.log(" ", opts); + if (opts.indexOf("ro") < 0) + readonly = false; + if (opts.indexOf("noauto") < 0) + noauto = false; + if (opts.indexOf("nofail") < 0) + nofail = false; + if (opts.indexOf("_netdev") < 0) + netdev = false; + } + } + + console.log("=", { readonly, noauto, nofail, netdev }); + await set_crypto_options(block, readonly, !noauto, nofail, netdev); +} + +export async function maybe_update_crypto_options(client, block) { + if (client.blocks_crypto[block.path]) + await update_crypto_options_for_children(client, block); + else if (client.blocks_crypto[block.CryptoBackingDevice]) + await update_crypto_options_for_children(client, client.blocks[block.CryptoBackingDevice]); +} + export let hostnamed = cockpit.dbus("org.freedesktop.hostname1").proxy(); // for unit tests @@ -807,8 +845,8 @@ export function get_fstab_config_with_client(client, block, also_child_config, s // btrfs mounted without subvol argument. const btrfs_volume = client.blocks_fsys_btrfs[block.path]; - const default_subvolid = client.uuids_btrfs_default_subvol[btrfs_volume.data.uuid]; - if (default_subvolid === subvol.id && !opts.find(o => o.indexOf("subvol=") >= 0 || o.indexOf("subvolid=") >= 0)) + const default_subvolid = btrfs_volume && client.uuids_btrfs_default_subvol[btrfs_volume.data.uuid]; + if (default_subvolid && default_subvolid === subvol.id && !opts.find(o => o.indexOf("subvol=") >= 0 || o.indexOf("subvolid=") >= 0)) return true; return false; @@ -819,7 +857,7 @@ export function get_fstab_config_with_client(client, block, also_child_config, s let config = block.Configuration.find(match); if (!config && also_child_config && client.blocks_crypto[block.path]) - config = client.blocks_crypto[block.path]?.ChildConfiguration.find(c => c[0] == "fstab"); + config = client.blocks_crypto[block.path]?.ChildConfiguration.find(match); if (config && decode_filename(config[1].type.v) != "swap") { const mnt_opts = get_block_mntopts(config[1]).split(","); diff --git a/test/verify/check-storage-anaconda b/test/verify/check-storage-anaconda index bd63558b1448..ab7338c0ca3f 100755 --- a/test/verify/check-storage-anaconda +++ b/test/verify/check-storage-anaconda @@ -311,16 +311,17 @@ class TestStorageAnaconda(storagelib.StorageCase): self.dialog_apply() self.dialog_wait_close() - # Unmount and lock, mount point exporting should still work - + # Unmount, mount point exporting should still work. self.click_dropdown(self.card_row("Storage", location="/mnt/butter"), "Unmount") - self.dialog({}) - self.click_dropdown(self.card_row("Storage", name=disk), "Lock") - b.wait_text(self.card_row_col("Storage", 1, 3), "Locked data (encrypted)") + self.confirm() + + self.click_card_row("Storage", name=disk) + cleartext = b.text(self.card_desc("Encryption", "Cleartext device")) self.expectExportedDevice(disk, { "type": "crypto", + "cleartext_device": cleartext, "content": { "type": "filesystem", "subvolumes": { diff --git a/test/verify/check-storage-btrfs b/test/verify/check-storage-btrfs index c5ca64b94f1d..65a142e9ff82 100755 --- a/test/verify/check-storage-btrfs +++ b/test/verify/check-storage-btrfs @@ -522,32 +522,199 @@ class TestStorageBtrfs(storagelib.StorageCase): mount_point = "/run/butter" passphrase = "einszweidrei" - m.execute(f""" - echo {passphrase} | cryptsetup luksFormat --pbkdf-memory 32768 {disk} - echo {passphrase} | cryptsetup luksOpen {disk} btrfs-test - mkfs.btrfs -L {label} /dev/mapper/btrfs-test - """) + self.login_and_go("/storage") + + self.addCleanup(self.machine.execute, f"umount {mount_point} || true") + self.addCleanup(self.machine.execute, f"umount /run/cake || true") + + self.click_dropdown(self.card_row("Storage", name=disk), "Format") + self.dialog_wait_open() + self.dialog_set_val("name", label) + self.dialog_set_val("type", "btrfs") + self.dialog_set_val("mount_point", mount_point) + self.dialog_set_val("crypto", "luks1") + self.dialog_set_val("passphrase", passphrase) + self.dialog_set_val("passphrase2", passphrase) + self.dialog_apply() + # creation of btrfs partition can take a while on TF. + with b.wait_timeout(30): + self.dialog_wait_close() + + b.wait_in_text(self.card_row("Storage", name=disk), "btrfs filesystem (encrypted)") + self.click_card_row("Storage", name=disk) + + # LUKS should be open, "nofail", but not "noauto" or "readonly" + b.wait_text_not(self.card_desc("Encryption", "Cleartext device"), "") + self.assert_in_configuration(disk, "crypttab", "options", "nofail") + self.assert_not_in_configuration(disk, "crypttab", "options", "noauto") + self.assert_not_in_configuration(disk, "crypttab", "options", "readonly") + + # Make a second subvolume that is readonly and not nofail + self.click_dropdown(self.card_row("btrfs filesystem", name="/"), "Create subvolume") + self.dialog_wait_open() + self.dialog_set_val("name", "cake") + self.dialog_set_val("mount_point", "/run/cake") + self.dialog_set_val("mount_options.ro", True) + self.dialog_set_val("at_boot", "local") + self.dialog_apply() + self.dialog_wait_close() + + # LUKS should be open, and none of "nofail", "noauto", or "readonly" + b.wait_text_not(self.card_desc("Encryption", "Cleartext device"), "-") + self.assert_not_in_configuration(disk, "crypttab", "options", "nofail") + self.assert_not_in_configuration(disk, "crypttab", "options", "noauto") + self.assert_not_in_configuration(disk, "crypttab", "options", "readonly") + + # Unmount first subvol + self.click_dropdown(self.card_row("btrfs filesystem", name="/"), "Unmount") + self.confirm() + + # LUKS should still be open, with the same options + b.wait_text_not(self.card_desc("Encryption", "Cleartext device"), "-") + self.assert_not_in_configuration(disk, "crypttab", "options", "nofail") + self.assert_not_in_configuration(disk, "crypttab", "options", "noauto") + self.assert_not_in_configuration(disk, "crypttab", "options", "readonly") + + # Unmount second subvol + self.click_dropdown(self.card_row("btrfs filesystem", name="cake"), "Unmount") + self.confirm() + + # Only now should LUKS be closed and have "noauto" option. + b.wait_text(self.card_desc("Encryption", "Cleartext device"), "-") + self.assert_not_in_configuration(disk, "crypttab", "options", "nofail") + self.assert_in_configuration(disk, "crypttab", "options", "noauto") + self.assert_not_in_configuration(disk, "crypttab", "options", "readonly") + + # Mount first subvol readonly, this unlocks LUKS and requires + # the passphrase + self.click_dropdown(self.card_row("btrfs filesystem", name="/"), "Mount") + self.dialog_wait_open() + self.dialog_set_val("mount_options.ro", True) + self.dialog_set_val("passphrase", passphrase) + self.dialog_apply() + self.dialog_wait_close() + + # Now LUKS is open again, and "readonly" + b.wait_text_not(self.card_desc("Encryption", "Cleartext device"), "-") + self.assert_not_in_configuration(disk, "crypttab", "options", "nofail") + self.assert_not_in_configuration(disk, "crypttab", "options", "noauto") + self.assert_in_configuration(disk, "crypttab", "options", "readonly") + + # Mount second subvol as "nofail", this makes LUKS "nofail" as well. + self.click_dropdown(self.card_row("btrfs filesystem", name="cake"), "Mount") + self.dialog_wait_open() + self.dialog_set_val("at_boot", "nofail") + self.dialog_apply() + self.dialog_wait_close() + + b.wait_text_not(self.card_desc("Encryption", "Cleartext device"), "-") + self.assert_in_configuration(disk, "crypttab", "options", "nofail") + self.assert_not_in_configuration(disk, "crypttab", "options", "noauto") + self.assert_in_configuration(disk, "crypttab", "options", "readonly") + + def testMultiDeviceLuks(self): + m = self.machine + b = self.browser + + label = "butter" + mount_point = "/run/butter" + passphrase = "einszweidrei" + + disk1 = self.add_ram_disk(size=140) + disk2 = self.add_loopback_disk(size=140) self.login_and_go("/storage") + + # Create single-device encrypted btrfs first + self.click_dropdown(self.card_row("Storage", name=disk1), "Format") + self.dialog_wait_open() + self.dialog_set_val("name", label) + self.dialog_set_val("type", "btrfs") + self.dialog_set_val("mount_point", mount_point) + self.dialog_set_val("crypto", "luks1") + self.dialog_set_val("passphrase", passphrase) + self.dialog_set_val("passphrase2", passphrase) + self.dialog_apply() # creation of btrfs partition can take a while on TF. with b.wait_timeout(30): - b.wait_visible(self.card_row("Storage", name="sda")) - b.wait_in_text(self.card_row("Storage", name="sda"), "btrfs filesystem (encrypted)") - self.click_dropdown(self.card_row("Storage", name="sda") + " + tr", "Mount") - self.dialog({"mount_point": mount_point}) + self.dialog_wait_close() - m.execute(f""" - umount {mount_point} - cryptsetup luksClose /dev/mapper/btrfs-test - """) - b.wait_in_text(self.card_row("Storage", name="sda"), "Locked data (encrypted)") - self.click_dropdown(self.card_row("Storage", name="sda"), "Unlock") - self.dialog({"passphrase": "einszweidrei"}) - b.wait_in_text(self.card_row("Storage", name="sda"), "btrfs filesystem (encrypted)") + # LUKS should not be "noauto". + self.assert_not_in_configuration(disk1, "crypttab", "options", "noauto") + + # Add a second encrypted device + self.click_dropdown(self.card_row("Storage", name=disk2), "Format") + self.dialog_wait_open() + self.dialog_set_val("type", "empty") + self.dialog_set_val("crypto", "luks1") + self.dialog_set_val("passphrase", passphrase) + self.dialog_set_val("passphrase2", passphrase) + self.dialog_apply() + # creation of luks can take a while. + with b.wait_timeout(30): + self.dialog_wait_close() + + self.assert_not_in_configuration(disk2, "crypttab", "options", "noauto") - self.click_dropdown(self.card_row("Storage", name="sda") + " + tr", "Mount") + self.click_card_row("Storage", name=disk2) + cleartext = b.text(self.card_desc("Encryption", "Cleartext device")) + + m.execute(f"btrfs device add {cleartext} {mount_point}; udevadm trigger") + + # Navigate to volume + b.click(self.card_desc("btrfs device", "btrfs volume") + " button") + + # Unmount + self.click_dropdown(self.card_row("btrfs subvolumes", name="/"), "Unmount") self.confirm() - b.wait_in_text(self.card_row("Storage", location=mount_point), "btrfs subvolume") + + # Both disks are still "auto" + self.assert_not_in_configuration(disk1, "crypttab", "options", "noauto") + self.assert_not_in_configuration(disk2, "crypttab", "options", "noauto") + + # Lock both LUKS + b.click(self.card_parent_link()) + self.click_dropdown(self.card_row("Storage", name=disk1), "Lock") + self.click_dropdown(self.card_row("Storage", name=disk2), "Lock") + b.wait_text(self.card_row_col("Storage", row_name=disk1, col_index=3), "btrfs filesystem (encrypted)") + b.wait_text(self.card_row_col("Storage", row_name=disk2, col_index=3), "Locked data (encrypted)") + + # Check both are now "noauto" + self.assert_in_configuration(disk1, "crypttab", "options", "noauto") + self.assert_in_configuration(disk2, "crypttab", "options", "noauto") + + # Try to mount via first disk. Cockpit thinks that this might + # be a single-device btrfs so it let's us mount it, but will + # figure it all out once the device is open. + self.click_dropdown(self.card_row("Storage", location=mount_point + " (not mounted)"), "Mount") + self.dialog_wait_open() + self.dialog_set_val("passphrase", passphrase) + self.dialog_apply() + b.wait_in_text("#dialog", "This device is only part of a btrfs volume.") + self.dialog_cancel() + self.dialog_wait_close() + + # Cockpit now knows it's a multi-device volume + b.wait_text(self.card_row_col("Storage", row_name=label, col_index=3), "btrfs subvolumes") + + # LUKS for disk1 is now "auto" + self.assert_not_in_configuration(disk1, "crypttab", "options", "noauto") + + # Unlock second disk + self.click_dropdown(self.card_row("Storage", name=disk2), "Unlock") + self.dialog_wait_open() + self.dialog_set_val("passphrase", passphrase) + self.dialog_apply() + self.dialog_wait_close() + + # LUKS for disk2 is now "auto" as well + self.assert_not_in_configuration(disk2, "crypttab", "options", "noauto") + + # The btrfs volume is now functional, so we can finally mount it + self.click_dropdown(self.card_row("Storage", location=mount_point + " (not mounted)"), "Mount") + self.confirm() + + testlib.sit() def testNoSubvolMount(self): m = self.machine