* Copyright (C) 2023 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import React from "react";
import cockpit from "cockpit";
import client from "../client.js";
import {
block_name, get_active_usage, teardown_active_usage,
undo_temporary_teardown, is_mounted_synch, get_partitions
} from "../utils.js";
import {
existing_passphrase_fields, init_existing_passphrase,
request_passphrase_on_error_handler
} from "../crypto/keyslots.jsx";
import {
dialog_open, SizeSlider, BlockingMessage, TeardownMessage, SelectSpaces,
init_teardown_usage
} from "../dialog.jsx";
import { std_reply } from "../stratis/utils.jsx";
import { pvs_to_spaces } from "../lvm2/utils.jsx";
const _ = cockpit.gettext;
export function check_unused_space(path) {
const block = client.blocks[path];
const lvm2 = client.blocks_lvm2[path];
const lvol = lvm2 && client.lvols[lvm2.LogicalVolume];
const part = client.blocks_part[path];
let size;
let min_change;
if (lvol) {
size = lvol.Size;
min_change = client.vgroups[lvol.VolumeGroup].ExtentSize;
} else if (part) {
size = part.Size;
min_change = 1024 * 1024;
} else {
return null;
}
if (size !== block.Size) {
return null;
}
let content_path = null;
let crypto_overhead = 0;
const crypto = client.blocks_crypto[block.path];
const cleartext = client.blocks_cleartext[block.path];
if (crypto) {
if (crypto.MetadataSize !== undefined && cleartext) {
content_path = cleartext.path;
crypto_overhead = crypto.MetadataSize;
}
} else {
content_path = path;
}
const fsys = client.blocks_fsys[content_path];
const content_block = client.blocks[content_path];
const vdo = content_block ? client.legacy_vdo_overlay.find_by_backing_block(content_block) : null;
const stratis_bdev = client.blocks_stratis_blockdev[content_path];
if (fsys && fsys.Size && (size - fsys.Size - crypto_overhead) > min_change && fsys.Resize) {
return {
volume_size: size - crypto_overhead,
content_size: fsys.Size
};
}
if (vdo && (size - vdo.physical_size - crypto_overhead) > min_change) {
return {
volume_size: size - crypto_overhead,
content_size: vdo.physical_size
};
}
if (stratis_bdev && (size - Number(stratis_bdev.TotalPhysicalSize) - crypto_overhead) > min_change) {
return {
volume_size: size - crypto_overhead,
content_size: Number(stratis_bdev.TotalPhysicalSize)
};
}
return null;
}
function lvol_or_part_and_fsys_resize(client, lvol_or_part, size, offline, passphrase, pvs) {
let fsys;
let crypto_overhead;
let vdo;
let stratis_bdev;
let orig_size;
let block;
if (lvol_or_part.iface === "org.freedesktop.UDisks2.LogicalVolume") {
orig_size = lvol_or_part.Size;
block = client.lvols_block[lvol_or_part.path];
if (!block)
return lvol_or_part.Resize(size, { });
} else {
orig_size = lvol_or_part.Size;
block = client.blocks[lvol_or_part.path];
}
const crypto = client.blocks_crypto[block.path];
if (crypto) {
const cleartext = client.blocks_cleartext[block.path];
if (!cleartext)
return;
fsys = client.blocks_fsys[cleartext.path];
vdo = client.legacy_vdo_overlay.find_by_backing_block(cleartext);
stratis_bdev = client.blocks_stratis_blockdev[cleartext.path];
crypto_overhead = crypto.MetadataSize;
} else {
fsys = client.blocks_fsys[block.path];
vdo = client.legacy_vdo_overlay.find_by_backing_block(block);
stratis_bdev = client.blocks_stratis_blockdev[block.path];
crypto_overhead = 0;
}
function fsys_resize() {
if (fsys) {
const resize_size = (size > orig_size) ? 0 : size - crypto_overhead;
return (is_mounted_synch(client.blocks[fsys.path])
.then(is_mounted => {
if (!is_mounted) {
return (fsys.Repair({ })
.then(() => fsys.Resize(resize_size, { })));
} else {
return fsys.Resize(resize_size, { });
}
}));
} else if (vdo) {
if (size - crypto_overhead > vdo.physical_size)
return vdo.grow_physical();
else if (size - crypto_overhead < vdo.physical_size)
return Promise.reject(_("VDO backing devices can not be made smaller"));
else
return Promise.resolve();
} else if (stratis_bdev) {
const delta = size - crypto_overhead - Number(stratis_bdev.TotalPhysicalSize);
if (delta > 0) {
const pool = client.stratis_pools[stratis_bdev.Pool];
return pool.GrowPhysicalDevice(stratis_bdev.Uuid).then(std_reply);
} else if (delta < 0)
return Promise.reject(_("Stratis blockdevs can not be made smaller"));
else
return Promise.resolve();
} else if (!block.IdUsage) {
return Promise.resolve();
} else if (size < orig_size) {
return Promise.reject(_("Unrecognized data can not be made smaller here."));
} else {
return Promise.resolve();
}
}
function crypto_resize() {
const resize_size = (size > orig_size) ? 0 : size - crypto_overhead;
if (crypto) {
const opts = { };
if (passphrase)
opts.passphrase = { t: "s", v: passphrase };
return crypto.Resize(resize_size, opts);
} else {
return Promise.resolve();
}
}
function lvol_or_part_resize() {
if (size !== orig_size) {
return lvol_or_part.Resize(size, { pvs: pvs ? { t: 'ao', v: pvs } : undefined });
} else
return Promise.resolve();
}
if (size < orig_size) {
return fsys_resize().then(crypto_resize)
.then(lvol_or_part_resize);
} else if (size >= orig_size) {
return lvol_or_part_resize().then(crypto_resize)
.then(fsys_resize);
}
}
export function get_resize_info(client, block, to_fit) {
let info;
let shrink_excuse;
let grow_excuse;
if (block) {
if (block.IdUsage === 'crypto' && client.blocks_crypto[block.path]) {
const cleartext = client.blocks_cleartext[block.path];
if (!cleartext) {
info = { };
shrink_excuse = grow_excuse = _("Unlock before resizing");
} else {
return get_resize_info(client, cleartext, to_fit);
}
} else if (block.IdUsage === 'filesystem') {
info = client.fsys_info && client.fsys_info[block.IdType];
if (!info) {
info = { };
shrink_excuse = grow_excuse = cockpit.format(_("$0 can not be resized here"),
block.IdType);
} else {
if (!info.can_shrink && !info.can_grow) {
shrink_excuse = grow_excuse = cockpit.format(_("$0 can not be resized"),
block.IdType);
} else {
if (!info.can_shrink)
shrink_excuse = cockpit.format(_("$0 can not be made smaller"),
block.IdType);
if (!info.can_grow)
grow_excuse = cockpit.format(_("$0 can not be made larger"),
block.IdType);
}
}
} else if (client.blocks_stratis_blockdev[block.path]) {
info = {
can_shrink: false,
can_grow: true,
grow_needs_unmount: false
};
shrink_excuse = _("Stratis blockdevs can not be made smaller");
} else if (block.IdUsage === 'raid') {
info = { };
shrink_excuse = grow_excuse = _("Physical volumes can not be resized here");
} else if (client.legacy_vdo_overlay.find_by_backing_block(block)) {
info = {
can_shrink: false,
can_grow: true,
grow_needs_unmount: false
};
shrink_excuse = _("VDO backing devices can not be made smaller");
} else if (client.blocks_swap[block.path]) {
info = {
can_shrink: false,
can_grow: false,
};
shrink_excuse = grow_excuse = _("Swap can not be resized here");
} else if (!block.IdUsage) {
info = {
can_shrink: true,
can_grow: true,
shrink_needs_unmount: false,
grow_needs_unmount: false,
};
} else {
info = {
can_shrink: false,
can_grow: true,
grow_needs_unmount: true
};
shrink_excuse = _("Unrecognized data can not be made smaller here");
}
if (to_fit) {
shrink_excuse = null;
}
} else {
info = { };
shrink_excuse = grow_excuse = _("Activate before resizing");
}
return { info, shrink_excuse, grow_excuse };
}
export function free_space_after_part(client, part) {
const parts = get_partitions(client, client.blocks[part.Table]);
function find_it(parts) {
for (const p of parts) {
if (p.type === "free" && p.start === part.Offset + part.Size)
return p.size;
if (p.type === "container") {
const s = find_it(p.partitions);
if (s)
return s;
}
}
return false;
}
return find_it(parts) || 0;
}
export function grow_dialog(client, lvol_or_part, info, to_fit) {
let title;
let block;
let name;
let orig_size;
let max_size;
let allow_infinite;
let round_size;
let has_subvols;
let subvols;
let pvs_as_spaces;
let initial_pvs;
function compute_max_size(spaces) {
const layout = lvol_or_part.Layout;
const pvs = spaces.map(s => s.pvol);
const n_pvs = pvs.length;
const sum = pvs.reduce((sum, pv) => sum + pv.FreeSize, 0);
const min = Math.min.apply(null, pvs.map(pv => pv.FreeSize));
if (!has_subvols) {
return sum;
} else if (layout === "raid0") {
return n_pvs * min;
} else if (layout === "raid1") {
return min;
} else if (layout === "raid10") {
return (n_pvs / 2) * min;
} else if ((layout === "raid4" || layout === "raid5")) {
return (n_pvs - 1) * min;
} else if (layout === "raid6") {
return (n_pvs - 2) * min;
} else
return 0;
}
if (lvol_or_part.iface === "org.freedesktop.UDisks2.LogicalVolume") {
const vgroup = client.vgroups[lvol_or_part.VolumeGroup];
const pool = client.lvols[lvol_or_part.ThinPool];
pvs_as_spaces = pvs_to_spaces(client, client.vgroups_pvols[vgroup.path].filter(pvol => pvol.FreeSize > 0));
subvols = client.lvols_stripe_summary[lvol_or_part.path];
has_subvols = subvols && (lvol_or_part.Layout === "mirror" || lvol_or_part.Layout.indexOf("raid") === 0);
if (!has_subvols)
initial_pvs = pvs_as_spaces;
else {
initial_pvs = [];
for (const sv of subvols) {
let sel = null;
for (const p in sv) {
for (const spc of pvs_as_spaces)
if (spc.block.path === p && (!sel || sel.size < spc.size))
sel = spc;
}
if (sel)
initial_pvs.push(sel);
}
for (const pv of pvs_as_spaces) {
if (initial_pvs.indexOf(pv) === -1 && initial_pvs.length < subvols.length)
initial_pvs.push(pv);
}
}
title = _("Grow logical volume");
block = client.lvols_block[lvol_or_part.path];
name = lvol_or_part.Name;
orig_size = lvol_or_part.Size;
max_size = pool ? pool.Size * 3 : lvol_or_part.Size + compute_max_size(initial_pvs);
allow_infinite = !!pool;
round_size = vgroup.ExtentSize;
} else {
has_subvols = false;
title = _("Grow partition");
block = client.blocks[lvol_or_part.path];
name = block_name(block);
orig_size = lvol_or_part.Size;
max_size = lvol_or_part.Size + free_space_after_part(client, lvol_or_part);
allow_infinite = false;
round_size = 1024 * 1024;
}
const usage = get_active_usage(client,
block && info.grow_needs_unmount ? block.path : null,
_("grow"), null,
true);
if (usage.Blocking) {
dialog_open({
Title: cockpit.format(_("$0 is in use"), name),
Body: BlockingMessage(usage)
});
return;
}
let grow_size;
const size_fields = [];
if (!to_fit) {
if ((has_subvols || lvol_or_part.Layout === "linear") && pvs_as_spaces.length > 1)
size_fields.push(
SelectSpaces("pvs", _("Physical Volumes"),
{
spaces: pvs_as_spaces,
value: initial_pvs,
min_selected: subvols.length,
validate: val => {
if (has_subvols && subvols.length !== val.length)
return cockpit.format(_("Exactly $0 physical volumes must be selected"),
subvols.length);
}
}));
size_fields.push(
SizeSlider("size", _("Size"),
{
value: orig_size,
min: orig_size,
max: max_size,
allow_infinite,
round: round_size,
}));
} else {
grow_size = block.Size;
}
let recovered_passphrase;
let passphrase_fields = [];
if (block && block.IdType === "crypto_LUKS" && block.IdVersion === "2")
passphrase_fields = existing_passphrase_fields(_("Resizing an encrypted filesystem requires unlocking the disk. Please provide a current disk passphrase."));
function prepare_pvs(pvs) {
if (!pvs)
return pvs;
pvs = pvs.map(spc => spc.block.path);
if (!has_subvols)
return pvs;
const subvol_pvs = [];
subvols.forEach((sv, idx) => {
subvol_pvs[idx] = null;
for (const pv in sv) {
if (pvs.indexOf(pv) >= 0 && subvol_pvs.indexOf(pv) === -1) {
subvol_pvs[idx] = pv;
break;
}
}
});
subvols.forEach((sv, idx) => {
if (!subvol_pvs[idx]) {
for (const pv of pvs) {
if (subvol_pvs.indexOf(pv) === -1) {
subvol_pvs[idx] = pv;
break;
}
}
}
});
return subvol_pvs;
}
if (!usage.Teardown && size_fields.length + passphrase_fields.length === 0) {
return lvol_or_part_and_fsys_resize(client, lvol_or_part, grow_size, info.grow_needs_unmount,
null, prepare_pvs(initial_pvs));
}
const dlg = dialog_open({
Title: title,
Teardown: TeardownMessage(usage),
Body: has_subvols && <div><p>{cockpit.format(_("Exactly $0 physical volumes need to be selected, one for each stripe of the logical volume."), subvols.length)}</p><br /></div>,
Fields: size_fields.concat(passphrase_fields),
update: (dlg, vals, trigger) => {
if (vals.pvs) {
const max = lvol_or_part.Size + compute_max_size(vals.pvs);
if (vals.size > max)
dlg.set_values({ size: max });
dlg.set_options("size", { max });
}
},
Action: {
Title: _("Grow"),
disable_on_error: usage.Teardown,
action: function (vals) {
return teardown_active_usage(client, usage)
.then(function () {
return (lvol_or_part_and_fsys_resize(client, lvol_or_part,
to_fit ? grow_size : vals.size,
info.grow_needs_unmount,
vals.passphrase || recovered_passphrase,
prepare_pvs(vals.pvs))
.then(() => undo_temporary_teardown(client, usage))
.catch(request_passphrase_on_error_handler(dlg, vals, recovered_passphrase, block)));
});
}
},
Inits: [
init_teardown_usage(client, usage),
passphrase_fields.length
? init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
: null
]
});
}
export function shrink_dialog(client, lvol_or_part, info, to_fit) {
let title;
let block;
let name;
let orig_size;
let round_size;
if (lvol_or_part.iface === "org.freedesktop.UDisks2.LogicalVolume") {
const vgroup = client.vgroups[lvol_or_part.VolumeGroup];
title = _("Shrink logical volume");
block = client.lvols_block[lvol_or_part.path];
name = lvol_or_part.Name;
orig_size = lvol_or_part.Size;
round_size = vgroup.ExtentSize;
} else {
title = _("Shrink partition");
block = client.blocks[lvol_or_part.path];
name = block_name(block);
orig_size = lvol_or_part.Size;
round_size = 1024 * 1024;
}
const usage = get_active_usage(client,
block && !to_fit && info.shrink_needs_unmount ? block.path : null,
_("shrink"), null,
true);
if (usage.Blocking) {
dialog_open({
Title: cockpit.format(_("$0 is in use"), name),
Body: BlockingMessage(usage)
});
return;
}
let shrink_size;
let size_fields = [];
if (!to_fit) {
size_fields = [
SizeSlider("size", _("Size"),
{
value: orig_size,
max: orig_size,
round: round_size,
})
];
} else {
const crypto = client.blocks_crypto[block.path];
const cleartext = client.blocks_cleartext[block.path];
let content_path = null;
let crypto_overhead = 0;
if (crypto) {
if (crypto.MetadataSize !== undefined && cleartext) {
content_path = cleartext.path;
crypto_overhead = crypto.MetadataSize;
}
} else {
content_path = block.path;
}
const fsys = client.blocks_fsys[content_path];
if (fsys)
shrink_size = fsys.Size + crypto_overhead;
const vdo = client.legacy_vdo_overlay.find_by_backing_block(client.blocks[content_path]);
if (vdo)
shrink_size = vdo.physical_size + crypto_overhead;
const stratis_bdev = client.blocks_stratis_blockdev[content_path];
if (stratis_bdev)
shrink_size = Number(stratis_bdev.TotalPhysicalSize) + crypto_overhead;
if (shrink_size === undefined) {
console.warn("Couldn't determine size to shrink to.");
return;
}
}
let recovered_passphrase;
let passphrase_fields = [];
if (block && block.IdType === "crypto_LUKS" && block.IdVersion === "2")
passphrase_fields = existing_passphrase_fields(_("Resizing an encrypted filesystem requires unlocking the disk. Please provide a current disk passphrase."));
if (usage.length === 0 && size_fields.length + passphrase_fields.length === 0) {
return lvol_or_part_and_fsys_resize(client, lvol_or_part, shrink_size, false, null);
}
const dlg = dialog_open({
Title: title,
Teardown: TeardownMessage(usage),
Fields: size_fields.concat(passphrase_fields),
Action: {
Title: _("Shrink"),
disable_on_error: usage.Teardown,
action: function (vals) {
return teardown_active_usage(client, usage)
.then(function () {
return (lvol_or_part_and_fsys_resize(client, lvol_or_part,
to_fit ? shrink_size : vals.size,
to_fit ? false : info.shrink_needs_unmount,
vals.passphrase || recovered_passphrase)
.then(() => undo_temporary_teardown(client, usage))
.catch(request_passphrase_on_error_handler(dlg, vals, recovered_passphrase, block)));
});
}
},
Inits: [
init_teardown_usage(client, usage),
passphrase_fields.length
? init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
: null
]
});
}