Skip to content

Commit 70f3eb3

Browse files
committed
storage: Confirmation before erasing actual data
But only in Anaconda mode.
1 parent 79f0f13 commit 70f3eb3

20 files changed

+299
-75
lines changed

pkg/storaged/block/format-dialog.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
dialog_open,
3333
TextInput, PassInput, CheckBoxes, SelectOne, SizeSlider,
3434
BlockingMessage, TeardownMessage,
35-
init_active_usage_processes
35+
init_teardown_usage
3636
} from "../dialog.jsx";
3737

3838
import { get_fstab_config, is_valid_mount_point } from "../filesystem/utils.jsx";
@@ -630,7 +630,7 @@ function format_dialog_internal(client, path, start, size, enable_dos_extended,
630630
}
631631
},
632632
Inits: [
633-
init_active_usage_processes(client, usage),
633+
init_teardown_usage(client, usage),
634634
unlock_before_format
635635
? init_existing_passphrase(block, true, type => { existing_passphrase_type = type })
636636
: null

pkg/storaged/block/resize.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
} from "../crypto/keyslots.jsx";
3232
import {
3333
dialog_open, SizeSlider, BlockingMessage, TeardownMessage, SelectSpaces,
34-
init_active_usage_processes
34+
init_teardown_usage
3535
} from "../dialog.jsx";
3636
import { std_reply } from "../stratis/utils.jsx";
3737
import { pvs_to_spaces } from "../lvm2/utils.jsx";
@@ -534,7 +534,7 @@ export function grow_dialog(client, lvol_or_part, info, to_fit) {
534534
}
535535
},
536536
Inits: [
537-
init_active_usage_processes(client, usage),
537+
init_teardown_usage(client, usage),
538538
passphrase_fields.length
539539
? init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
540540
: null
@@ -647,7 +647,7 @@ export function shrink_dialog(client, lvol_or_part, info, to_fit) {
647647
}
648648
},
649649
Inits: [
650-
init_active_usage_processes(client, usage),
650+
init_teardown_usage(client, usage),
651651
passphrase_fields.length
652652
? init_existing_passphrase(block, false, pp => { recovered_passphrase = pp })
653653
: null

pkg/storaged/btrfs/subvolume.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { btrfs_usage, validate_subvolume_name, parse_subvol_from_options } from
3636
import { at_boot_input, update_at_boot_input, mounting_dialog, mount_options } from "../filesystem/mounting-dialog.jsx";
3737
import {
3838
dialog_open, TextInput,
39-
TeardownMessage, init_active_usage_processes,
39+
TeardownMessage, init_teardown_usage,
4040
} from "../dialog.jsx";
4141
import { check_mismounted_fsys, MismountAlert } from "../filesystem/mismounting.jsx";
4242
import {
@@ -204,6 +204,7 @@ function subvolume_delete(volume, subvol, mount_point_in_parent, card) {
204204
const paths_to_delete = [];
205205
const usage = [];
206206

207+
usage.Teardown = true;
207208
for (const sv of all_subvols) {
208209
const [config, mount_point] = get_fstab_config_with_client(client, block, false, sv);
209210
const fs_is_mounted = is_mounted(client, block, sv);
@@ -241,7 +242,7 @@ function subvolume_delete(volume, subvol, mount_point_in_parent, card) {
241242
}
242243
},
243244
Inits: [
244-
init_active_usage_processes(client, usage)
245+
init_teardown_usage(client, usage)
245246
]
246247
});
247248
}

pkg/storaged/dialog.jsx

Lines changed: 125 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,21 @@ import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/comp
235235
import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
236236
import { ExclamationTriangleIcon, InfoIcon, HelpIcon, EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
237237
import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
238+
import { Table, Tbody, Tr, Td } from '@patternfly/react-table';
238239

239240
import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
240241
import { ListingTable } from "cockpit-components-table.jsx";
241242
import { FormHelper } from "cockpit-components-form-helper";
242243

243-
import { fmt_size, block_name, format_size_and_text, format_delay, for_each_async, get_byte_units } from "./utils.js";
244+
import {
245+
decode_filename, fmt_size, block_name, format_size_and_text, format_delay, for_each_async, get_byte_units,
246+
is_available_block
247+
} from "./utils.js";
244248
import { fmt_to_fragments } from "utils.jsx";
245249
import client from "./client.js";
246250

251+
import fsys_is_empty_sh from "./fsys-is-empty.sh";
252+
247253
const _ = cockpit.gettext;
248254

249255
function make_rows(fields, values, errors, onChange) {
@@ -328,6 +334,19 @@ const Body = ({ body, teardown, fields, values, errors, isFormHorizontal, onChan
328334
);
329335
};
330336

337+
const ExtraConfirmation = ({ text, onChange }) => {
338+
const [confirmed, setConfirmed] = useState(false);
339+
340+
return (
341+
<Checkbox isChecked={confirmed}
342+
id="dialog-confirm"
343+
label={text}
344+
onChange={(_, val) => {
345+
setConfirmed(val);
346+
onChange(val);
347+
}} />);
348+
};
349+
331350
function flatten_fields(fields) {
332351
return fields.reduce(
333352
(acc, val) => acc.concat([val]).concat(val.options && val.options.nested_fields
@@ -340,6 +359,8 @@ export const dialog_open = (def) => {
340359
const nested_fields = def.Fields || [];
341360
const fields = flatten_fields(nested_fields);
342361
const values = { };
362+
let confirmation = null;
363+
let confirmed = false;
343364
let errors = null;
344365

345366
fields.forEach(f => { values[f.tag] = f.initial_value });
@@ -415,8 +436,10 @@ export const dialog_open = (def) => {
415436
caption: variant.Title,
416437
style: actions.length == 0 ? "primary" : "secondary",
417438
danger: def.Action.Danger || def.Action.DangerButton,
418-
disabled: running_promise != null || (def.Action.disable_on_error &&
419-
errors && errors.toString() != "[object Object]"),
439+
disabled: (running_promise != null ||
440+
(def.Action.disable_on_error &&
441+
errors && errors.toString() != "[object Object]") ||
442+
(confirmation && !confirmed)),
420443
clicked: progress_callback => run_action(progress_callback, variant.tag),
421444
});
422445
}
@@ -436,13 +459,19 @@ export const dialog_open = (def) => {
436459
}
437460
}
438461

439-
const extra = (
440-
<div>
441-
{ def.Action && def.Action.Danger
442-
? <HelperText><HelperTextItem variant="error">{def.Action.Danger} </HelperTextItem></HelperText>
443-
: null
444-
}
445-
</div>);
462+
let extra = null;
463+
if (confirmation) {
464+
extra = <ExtraConfirmation text={confirmation}
465+
onChange={val => {
466+
confirmed = val;
467+
update_footer();
468+
}} />;
469+
} else if (def.Action && def.Action.Danger) {
470+
extra = (
471+
<div>
472+
<HelperText><HelperTextItem variant="error">{def.Action.Danger} </HelperTextItem></HelperText>
473+
</div>);
474+
}
446475

447476
return {
448477
idle_message: (running_promise
@@ -537,6 +566,14 @@ export const dialog_open = (def) => {
537566
update();
538567
},
539568

569+
need_confirmation: (conf) => {
570+
confirmation = conf;
571+
confirmed = false;
572+
def.Action.Danger = null;
573+
def.Action.DangerButton = true;
574+
update_footer();
575+
},
576+
540577
close: () => {
541578
dlg.footerProps.dialog_done();
542579
}
@@ -1204,13 +1241,16 @@ const teardown_block_name = use => {
12041241
name = block_name(client.blocks[use.block.CryptoBackingDevice] || use.block);
12051242
}
12061243

1207-
return name;
1244+
return name.replace(/^\/dev\//, "");
12081245
};
12091246

12101247
export const TeardownMessage = (usage, expect_single_unmount) => {
1211-
if (usage.length == 0)
1248+
if (!usage.Teardown)
12121249
return null;
12131250

1251+
if (client.in_anaconda_mode() && !expect_single_unmount)
1252+
return <AnacondaTeardownMessage usage={usage} />;
1253+
12141254
if (is_expected_unmount(usage, expect_single_unmount))
12151255
return <StopProcessesMessage mount_point={expect_single_unmount} users={usage[0].users} />;
12161256

@@ -1251,6 +1291,36 @@ export const TeardownMessage = (usage, expect_single_unmount) => {
12511291
</div>);
12521292
};
12531293

1294+
const AnacondaTeardownMessage = ({ usage }) => {
1295+
const rows = [];
1296+
1297+
usage.forEach((use, index) => {
1298+
if (use.data_warning) {
1299+
const name = teardown_block_name(use);
1300+
const location = client.strip_mount_point_prefix(use.location) || use.block.IdLabel || "-";
1301+
1302+
rows.push(
1303+
<Tr key={index}>
1304+
<Td className="pf-v5-u-font-weight-bold">{name}</Td>
1305+
<Td>{location}</Td>
1306+
<Td>{use.data_warning}</Td>
1307+
</Tr>);
1308+
}
1309+
});
1310+
1311+
if (rows.length > 0) {
1312+
return (
1313+
<div className="modal-footer-teardown">
1314+
<HelperText>
1315+
<HelperTextItem variant="error">
1316+
{_("Important data might be deleted:")}
1317+
</HelperTextItem>
1318+
</HelperText>
1319+
<Table variant="compact" borders={false}><Tbody>{rows}</Tbody></Table>
1320+
</div>);
1321+
}
1322+
};
1323+
12541324
export function teardown_danger_message(usage, expect_single_unmount) {
12551325
if (is_expected_unmount(usage, expect_single_unmount))
12561326
return stop_processes_danger_message(usage[0].users);
@@ -1269,24 +1339,54 @@ export function teardown_danger_message(usage, expect_single_unmount) {
12691339
}
12701340
}
12711341

1272-
export function init_active_usage_processes(client, usage, expect_single_unmount) {
1342+
export function init_teardown_usage(client, usage, expect_single_unmount) {
12731343
return {
1274-
title: _("Checking related processes"),
1275-
func: dlg => {
1276-
return for_each_async(usage, u => {
1344+
title: _("Checking filesystem usage"),
1345+
func: async function (dlg) {
1346+
let have_data = false;
1347+
for (const u of usage) {
12771348
if (u.usage == "mounted") {
1278-
return client.find_mount_users(u.location)
1279-
.then(users => {
1280-
u.users = users;
1281-
});
1282-
} else
1283-
return Promise.resolve();
1284-
}).then(() => {
1285-
dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount));
1349+
u.users = await client.find_mount_users(u.location);
1350+
}
1351+
if (client.in_anaconda_mode() && !expect_single_unmount && u.block) {
1352+
if (u.block.IdUsage == "filesystem" &&
1353+
["xfs", "ext2", "ext3", "ext4", "btrfs", "vfat", "ntfs"].indexOf(u.block.IdType) >= 0) {
1354+
const empty = await cockpit.script(fsys_is_empty_sh,
1355+
[decode_filename(u.block.PreferredDevice)],
1356+
{ superuser: true, err: "message" });
1357+
if (empty.trim() != "yes") {
1358+
try {
1359+
const info = JSON.parse(empty);
1360+
u.data_warning = cockpit.format(_("$0 used, $1 total"),
1361+
fmt_size((info.total - info.free) * info.unit),
1362+
fmt_size(info.total * info.unit));
1363+
} catch {
1364+
u.data_warning = _("Device contains unrecognized data");
1365+
}
1366+
}
1367+
} else if (u.block.IdUsage == "crypto" && !client.blocks_cleartext[u.block.path]) {
1368+
u.data_warning = _("Locked encrypted device might contain data");
1369+
} else if (!client.blocks_ptable[u.block.path] &&
1370+
u.block.IdUsage != "raid" &&
1371+
!is_available_block(client, u.block)) {
1372+
u.data_warning = _("Device contains unrecognized data");
1373+
}
1374+
if (u.data_warning)
1375+
have_data = true;
1376+
}
1377+
}
1378+
1379+
if (have_data) {
1380+
usage.Teardown = true;
1381+
dlg.need_confirmation(_("I confirm I want to lose this data forever"));
1382+
} else if (client.in_anaconda_mode() && !expect_single_unmount) {
1383+
dlg.need_confirmation(null);
1384+
} else {
12861385
const msg = teardown_danger_message(usage, expect_single_unmount);
12871386
if (msg)
12881387
dlg.add_danger(msg);
1289-
});
1388+
}
1389+
dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount));
12901390
}
12911391
};
12921392
}

pkg/storaged/filesystem/mounting-dialog.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
dialog_open,
3737
TextInput, PassInput, CheckBoxes, SelectOne,
3838
TeardownMessage,
39-
init_active_usage_processes
39+
init_teardown_usage
4040
} from "../dialog.jsx";
4141
import { init_existing_passphrase, unlock_with_type } from "../crypto/keyslots.jsx";
4242
import { initial_tab_options } from "../block/format-dialog.jsx";
@@ -404,7 +404,7 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) {
404404
const dlg = dialog_open({
405405
Title: cockpit.format(mode_title[mode], old_dir_for_display),
406406
Fields: fields,
407-
Teardown: TeardownMessage(usage, old_dir),
407+
Teardown: TeardownMessage(usage, old_dir || true),
408408
update: function (dlg, vals, trigger) {
409409
update_at_boot_input(dlg, vals, trigger);
410410
if (trigger == "mount_options")
@@ -447,7 +447,7 @@ export function mounting_dialog(client, block, mode, forced_options, subvol) {
447447
}
448448
},
449449
Inits: [
450-
init_active_usage_processes(client, usage, old_dir),
450+
init_teardown_usage(client, usage, old_dir || true),
451451
init_existing_passphrase(block, true, type => {
452452
passphrase_type = type;
453453
update_explicit_passphrase(dlg.get_value("mount_options")?.ro ?? opt_ro);

pkg/storaged/fsys-is-empty.sh

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#! /bin/bash
2+
3+
set -eux
4+
5+
dev=$1
6+
7+
need_unmount=""
8+
9+
function maybe_unmount () {
10+
if [ -n "$need_unmount" ]; then
11+
umount "$need_unmount"
12+
rmdir "$need_unmount"
13+
fi
14+
}
15+
16+
trap maybe_unmount 0
17+
18+
mp=$(findmnt -no TARGET "$dev" | cat)
19+
if [ -z "$mp" ]; then
20+
mp=$(mktemp -d)
21+
need_unmount=$mp
22+
mount "$dev" "$mp" -o ro
23+
fi
24+
25+
# A filesystem is empty if it only has directories in it.
26+
27+
first=$(find "$mp" -not -type d | head -1)
28+
info=$(stat -f -c '{ "unit": %S, "free": %f, "total": %b }' "$mp")
29+
30+
if [ -z "$first" ]; then
31+
echo yes
32+
else
33+
echo "$info"
34+
fi

pkg/storaged/legacy-vdo/legacy-vdo.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { DescriptionList, DescriptionListDescription, DescriptionListGroup, Desc
2727

2828
import { block_short_name, get_active_usage, teardown_active_usage, fmt_size, decode_filename, reload_systemd } from "../utils.js";
2929
import {
30-
dialog_open, SizeSlider, BlockingMessage, TeardownMessage, init_active_usage_processes
30+
dialog_open, SizeSlider, BlockingMessage, TeardownMessage, init_teardown_usage
3131
} from "../dialog.jsx";
3232
import { StorageButton, StorageOnOff } from "../storage-controls.jsx";
3333

@@ -68,7 +68,7 @@ export function make_legacy_vdo_page(parent, vdo, backing_block, next_card) {
6868
}
6969
},
7070
Inits: [
71-
init_active_usage_processes(client, usage)
71+
init_teardown_usage(client, usage)
7272
]
7373
});
7474
} else {
@@ -130,7 +130,7 @@ export function make_legacy_vdo_page(parent, vdo, backing_block, next_card) {
130130
}
131131
},
132132
Inits: [
133-
init_active_usage_processes(client, usage)
133+
init_teardown_usage(client, usage)
134134
]
135135
});
136136
}

0 commit comments

Comments
 (0)