diff --git a/pkg/storaged/block/format-dialog.jsx b/pkg/storaged/block/format-dialog.jsx index b30edfbe3dcf..f6abb5f638f6 100644 --- a/pkg/storaged/block/format-dialog.jsx +++ b/pkg/storaged/block/format-dialog.jsx @@ -171,7 +171,7 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, title = cockpit.format(_("Format $0"), block_name(block)); function is_filesystem(vals) { - return vals.type != "empty" && vals.type != "dos-extended" && vals.type != "biosboot"; + return vals.type != "empty" && vals.type != "dos-extended" && vals.type != "biosboot" && vals.type != "swap"; } function add_fsys(storaged_name, entry) { @@ -186,6 +186,7 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, add_fsys("ext4", { value: "ext4", title: "EXT4" }); add_fsys("vfat", { value: "vfat", title: "VFAT" }); add_fsys("ntfs", { value: "ntfs", title: "NTFS" }); + add_fsys("swap", { value: "swap", title: "Swap" }); if (client.in_anaconda_mode()) { if (block_ptable && block_ptable.Type == "gpt" && !client.anaconda.efi) add_fsys(true, { value: "biosboot", title: "BIOS boot partition" }); @@ -288,8 +289,13 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, { tag: null, Title: create_partition ? _("Create") : _("Format") } ]; + let action_variants_for_swap = [ + { tag: null, Title: create_partition ? _("Create and start") : _("Format and start") }, + { tag: "nomount", Title: create_partition ? _("Create only") : _("Format only") } + ]; + if (client.in_anaconda_mode()) { - action_variants = [ + action_variants = action_variants_for_swap = [ { tag: "nomount", Title: create_partition ? _("Create") : _("Format") } ]; } @@ -423,6 +429,8 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, else if (trigger == "type") { if (dlg.get_value("type") == "empty") { dlg.update_actions({ Variants: action_variants_for_empty }); + } else if (dlg.get_value("type") == "swap") { + dlg.update_actions({ Variants: action_variants_for_swap }); } else { dlg.update_actions({ Variants: action_variants }); } @@ -450,6 +458,10 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, partition_type = "21686148-6449-6e6f-744e-656564454649"; } + if (type == "swap") { + partition_type = block_ptable.Type == "dos" ? "0x82" : "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f"; + } + const options = { 'tear-down': { t: 'b', v: true } }; @@ -532,6 +544,17 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, } } + if (type == "swap") { + config_items.push(["fstab", { + dir: { t: 'ay', v: encode_filename("none") }, + type: { t: 'ay', v: encode_filename("swap") }, + opts: { t: 'ay', v: encode_filename(mount_now ? "defaults" : "noauto") }, + freq: { t: 'i', v: 0 }, + passno: { t: 'i', v: 0 }, + "track-parents": { t: 'b', v: true } + }]); + } + if (config_items.length > 0) options["config-items"] = { t: 'a(sa{sv})', v: config_items }; @@ -587,6 +610,17 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, return client.blocks_fsys[path]; } + function block_swap_for_block(path) { + if (keep_keys) { + const content_block = client.blocks_cleartext[path]; + return client.blocks_swap[content_block.path]; + } else if (is_encrypted(vals)) + return (client.blocks_cleartext[path] && + client.blocks_swap[client.blocks_cleartext[path].path]); + else + return client.blocks_swap[path]; + } + function block_crypto_for_block(path) { return client.blocks_crypto[path]; } @@ -597,7 +631,10 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended, return (client.wait_for(() => block_fsys_for_block(path)) .then(block_fsys => client.mount_at(client.blocks[block_fsys.path], mount_point))); - if (is_encrypted(vals) && is_filesystem(vals) && !mount_now) + if (type == "swap" && mount_now) + return (client.wait_for(() => block_swap_for_block(path)) + .then(block_swap => block_swap.Start({}))); + if (is_encrypted(vals) && (is_filesystem(vals) || type == "swap") && !mount_now) return (client.wait_for(() => block_crypto_for_block(path)) .then(block_crypto => block_crypto.Lock({ }))); } diff --git a/pkg/storaged/swap/swap.jsx b/pkg/storaged/swap/swap.jsx index e1cbbd74c04a..de588dfc2667 100644 --- a/pkg/storaged/swap/swap.jsx +++ b/pkg/storaged/swap/swap.jsx @@ -27,14 +27,63 @@ import { useEvent } from "hooks"; import { StorageCard, StorageDescription, new_card } from "../pages.jsx"; import { format_dialog } from "../block/format-dialog.jsx"; -import { fmt_size, decode_filename } from "../utils.js"; +import { + fmt_size, decode_filename, encode_filename, + parse_options, unparse_options, extract_option, +} from "../utils.js"; import { std_lock_action } from "../crypto/actions.jsx"; const _ = cockpit.gettext; +async function set_swap_noauto(block, noauto) { + for (const conf of block.Configuration) { + if (conf[0] == "fstab") { + const options = parse_options(decode_filename(conf[1].opts.v)); + extract_option(options, "defaults"); + extract_option(options, "noauto"); + if (noauto) + options.push("noauto"); + if (options.length == 0) + options.push("defaults"); + const new_conf = [ + "fstab", + Object.assign({ }, conf[1], + { + opts: { + t: 'ay', + v: encode_filename(unparse_options(options)) + } + }) + ]; + await block.UpdateConfigurationItem(conf, new_conf, { }); + return; + } + } + + await block.AddConfigurationItem( + ["fstab", { + dir: { t: 'ay', v: encode_filename("none") }, + type: { t: 'ay', v: encode_filename("swap") }, + opts: { t: 'ay', v: encode_filename(noauto ? "noauto" : "defaults") }, + freq: { t: 'i', v: 0 }, + passno: { t: 'i', v: 0 }, + "track-parents": { t: 'b', v: true } + }], { }); +} + export function make_swap_card(next, backing_block, content_block) { const block_swap = client.blocks_swap[content_block.path]; + async function start() { + await block_swap.Start({}); + await set_swap_noauto(content_block, false); + } + + async function stop() { + await block_swap.Stop({}); + await set_swap_noauto(content_block, true); + } + return new_card({ title: _("Swap"), next, @@ -43,10 +92,10 @@ export function make_swap_card(next, backing_block, content_block) { actions: [ std_lock_action(backing_block, content_block), (block_swap && block_swap.Active - ? { title: _("Stop"), action: () => block_swap.Stop({}) } + ? { title: _("Stop"), action: stop } : null), (block_swap && !block_swap.Active - ? { title: _("Start"), action: () => block_swap.Start({}) } + ? { title: _("Start"), action: start } : null), { title: _("Format"), action: () => format_dialog(client, backing_block.path), danger: true }, ] diff --git a/pkg/storaged/utils.js b/pkg/storaged/utils.js index cfa95d2e55bb..8491614695c4 100644 --- a/pkg/storaged/utils.js +++ b/pkg/storaged/utils.js @@ -842,6 +842,7 @@ export function get_active_usage(client, path, top_action, child_action, is_temp function get_usage(usage, path, level) { const block = client.blocks[path]; const fsys = client.blocks_fsys[path]; + const swap = client.blocks_swap[path]; const mdraid = block && client.mdraids[block.MDRaidMember]; const pvol = client.blocks_pvol[path]; const vgroup = pvol && client.vgroups[pvol.VolumeGroup]; @@ -910,6 +911,15 @@ export function get_active_usage(client, path, top_action, child_action, is_temp enter_unmount(children[c], c, false); enter_unmount(block, mp, true); }); + } else if (swap) { + if (swap.Active) { + usage.push({ + level, + usage: 'swap', + block, + actions: get_actions(_("stop")), + }); + } } else if (mdraid) { const active_state = mdraid.ActiveDevices.find(as => as[0] == block.path); usage.push({ @@ -1024,6 +1034,12 @@ export function teardown_active_usage(client, usage) { } } + async function stop_swap(swaps) { + for (const s of swaps) { + await client.blocks_swap[s.block.path].Stop({}); + } + } + function mdraid_remove(members) { return Promise.all(members.map(m => m.mdraid.RemoveDevice(m.block.path, { wipe: { t: 'b', v: true } }))); } @@ -1053,6 +1069,7 @@ export function teardown_active_usage(client, usage) { return Promise.all(Array.prototype.concat( unmount(usage.filter(function(use) { return use.usage == "mounted" })), + stop_swap(usage.filter(function(use) { return use.usage == "swap" })), mdraid_remove(usage.filter(function(use) { return use.usage == "mdraid-member" })), pvol_remove(usage.filter(function(use) { return use.usage == "pvol" })) )); diff --git a/test/verify/check-storage-swap b/test/verify/check-storage-swap index b592ae4b44cf..587ce21a9904 100755 --- a/test/verify/check-storage-swap +++ b/test/verify/check-storage-swap @@ -24,27 +24,97 @@ import testlib @testlib.nondestructive class TestStorageswap(storagelib.StorageCase): - def test(self): - m = self.machine + def testBasic(self): b = self.browser + m = self.machine + + disk = self.add_ram_disk() self.login_and_go("/storage") - disk = self.add_ram_disk() + # Create a swap partition on GPT self.click_card_row("Storage", name=disk) + self.click_card_dropdown("Solid State Drive", "Create partition table") + self.confirm() + b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space") + self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition") + self.dialog({"type": "swap"}) + b.wait_text(self.card_row_col("GPT partitions", 1, 2), "Swap") - b.wait_visible(self.card("Unformatted data")) - m.execute(f"mkswap {disk}") - b.wait_visible(self.card("Swap")) + # It should have been started and have a fstab entry + self.click_card_row("GPT partitions", 1) + b.wait_text(self.card_desc("Swap", "Used"), "0") + self.assertIn("defaults", m.execute(f"findmnt --fstab -n -o OPTIONS {disk}1")) + + # Stopping should set it to noauto + b.click(self.card_button("Swap", "Stop")) b.wait_text(self.card_desc("Swap", "Used"), "-") + self.assertIn("noauto", m.execute(f"findmnt --fstab -n -o OPTIONS {disk}1")) + # Start it again to test teardown below b.click(self.card_button("Swap", "Start")) b.wait_text(self.card_desc("Swap", "Used"), "0") + self.assertIn("defaults", m.execute(f"findmnt --fstab -n -o OPTIONS {disk}1")) - self.assertEqual(m.execute(f"lsblk -n -o MOUNTPOINT {disk}").strip(), "[SWAP]") + # It should have the right partition type + b.wait_visible(self.card("Swap")) + b.wait_text(self.card_desc("Partition", "Type"), "Linux swap space") - b.click(self.card_button("Swap", "Stop")) - b.wait_text(self.card_desc("Swap", "Used"), "-") + # Set it to something else + b.click(self.card_desc_action("Partition", "Type")) + self.dialog({"type": "0fc63daf-8483-4772-8e79-3d69d8477de4"}) + b.wait_text(self.card_desc("Partition", "Type"), "Linux filesystem data") + + # Correct it by reformatting + self.click_card_dropdown("Swap", "Format") + self.dialog_wait_open() + self.dialog_set_val("type", "swap") + b.wait_in_text("#dialog .modal-footer-teardown", f"{disk}1") + b.wait_in_text("#dialog .modal-footer-teardown", "stop, format") + self.dialog_apply_secondary() + self.dialog_wait_close() + b.wait_text(self.card_desc("Partition", "Type"), "Linux swap space") + + # Delete the partition, the fstab entry should disappear + self.click_card_dropdown("Partition", "Delete") + self.confirm() + b.wait_visible(self.card("Solid State Drive")) + m.execute(f"! findmnt --fstab -n -o OPTIONS {disk}1") + + # Format as swap on the command line, starting it should add + # fstab entry + m.execute(f"mkswap -f {disk}") + b.click(self.card_button("Swap", "Start")) + b.wait_text(self.card_desc("Swap", "Used"), "0") + self.assertIn("defaults", m.execute(f"findmnt --fstab -n -o OPTIONS {disk}")) + + def testEncrypted(self): + b = self.browser + m = self.machine + + disk = self.add_ram_disk() + + self.login_and_go("/storage") + + # Create a encrypted swap partition on GPT + self.click_card_row("Storage", name=disk) + self.click_card_dropdown("Solid State Drive", "Create partition table") + self.confirm() + b.wait_text(self.card_row_col("GPT partitions", 1, 1), "Free space") + self.click_dropdown(self.card_row("GPT partitions", 1), "Create partition") + self.dialog({ + "type": "swap", + "crypto": "luks2", + "passphrase": "foobar", + "passphrase2": "foobar", + }) + b.wait_text(self.card_row_col("GPT partitions", 1, 2), "Swap (encrypted)") + + # It should have been started and have a fstab entry + self.click_card_row("GPT partitions", 1) + b.wait_text(self.card_desc("Swap", "Used"), "0") + dev = b.text(self.card_desc("Encryption", "Cleartext device")) + self.assertIn("defaults", m.execute(f"findmnt --fstab -n -o OPTIONS {dev}")) if __name__ == '__main__':