Skip to content

Commit bcecc43

Browse files
committed
storage: Confirmation before erasing actual data
But only in Anaconda mode.
1 parent 5bfe3ce commit bcecc43

21 files changed

+282
-76
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: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,22 @@ 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 } from "./utils.js";
244+
import {
245+
decode_filename, fmt_size, block_name, format_size_and_text, format_delay, for_each_async,
246+
is_available_block
247+
} from "./utils.js";
244248
import { fmt_to_fragments } from "utils.jsx";
249+
245250
import client from "./client.js";
246251

252+
import fsys_is_empty_sh from "./fsys-is-empty.sh";
253+
247254
const _ = cockpit.gettext;
248255

249256
function make_rows(fields, values, errors, onChange) {
@@ -328,6 +335,19 @@ const Body = ({ body, teardown, fields, values, errors, isFormHorizontal, onChan
328335
);
329336
};
330337

338+
const ExtraConfirmation = ({ text, onChange }) => {
339+
const [confirmed, setConfirmed] = useState(false);
340+
341+
return (
342+
<Checkbox isChecked={confirmed}
343+
id="dialog-confirm"
344+
label={text}
345+
onChange={(_, val) => {
346+
setConfirmed(val);
347+
onChange(val);
348+
}} />);
349+
};
350+
331351
function flatten_fields(fields) {
332352
return fields.reduce(
333353
(acc, val) => acc.concat([val]).concat(val.options && val.options.nested_fields
@@ -340,6 +360,8 @@ export const dialog_open = (def) => {
340360
const nested_fields = def.Fields || [];
341361
const fields = flatten_fields(nested_fields);
342362
const values = { };
363+
let confirmation = null;
364+
let confirmed = false;
343365
let errors = null;
344366

345367
fields.forEach(f => { values[f.tag] = f.initial_value });
@@ -415,8 +437,10 @@ export const dialog_open = (def) => {
415437
caption: variant.Title,
416438
style: actions.length == 0 ? "primary" : "secondary",
417439
danger: def.Action.Danger || def.Action.DangerButton,
418-
disabled: running_promise != null || (def.Action.disable_on_error &&
419-
errors && errors.toString() != "[object Object]"),
440+
disabled: (running_promise != null ||
441+
(def.Action.disable_on_error &&
442+
errors && errors.toString() != "[object Object]") ||
443+
(confirmation && !confirmed)),
420444
clicked: progress_callback => run_action(progress_callback, variant.tag),
421445
});
422446
}
@@ -436,13 +460,19 @@ export const dialog_open = (def) => {
436460
}
437461
}
438462

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>);
463+
let extra = null;
464+
if (confirmation) {
465+
extra = <ExtraConfirmation text={confirmation}
466+
onChange={val => {
467+
confirmed = val;
468+
update_footer();
469+
}} />;
470+
} else if (def.Action && def.Action.Danger) {
471+
extra = (
472+
<div>
473+
<HelperText><HelperTextItem variant="error">{def.Action.Danger} </HelperTextItem></HelperText>
474+
</div>);
475+
}
446476

447477
return {
448478
idle_message: (running_promise
@@ -537,6 +567,14 @@ export const dialog_open = (def) => {
537567
update();
538568
},
539569

570+
need_confirmation: (conf) => {
571+
confirmation = conf;
572+
confirmed = false;
573+
def.Action.Danger = null;
574+
def.Action.DangerButton = true;
575+
update_footer();
576+
},
577+
540578
close: () => {
541579
dlg.footerProps.dialog_done();
542580
}
@@ -1203,13 +1241,16 @@ const teardown_block_name = use => {
12031241
name = block_name(client.blocks[use.block.CryptoBackingDevice] || use.block);
12041242
}
12051243

1206-
return name;
1244+
return name.replace(/^\/dev\//, "");
12071245
};
12081246

12091247
export const TeardownMessage = (usage, expect_single_unmount) => {
1210-
if (usage.length == 0)
1248+
if (!usage.Teardown)
12111249
return null;
12121250

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

@@ -1250,6 +1291,36 @@ export const TeardownMessage = (usage, expect_single_unmount) => {
12501291
</div>);
12511292
};
12521293

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+
12531324
export function teardown_danger_message(usage, expect_single_unmount) {
12541325
if (is_expected_unmount(usage, expect_single_unmount))
12551326
return stop_processes_danger_message(usage[0].users);
@@ -1268,24 +1339,51 @@ export function teardown_danger_message(usage, expect_single_unmount) {
12681339
}
12691340
}
12701341

1271-
export function init_active_usage_processes(client, usage, expect_single_unmount) {
1342+
export function init_teardown_usage(client, usage, expect_single_unmount) {
12721343
return {
1273-
title: _("Checking related processes"),
1274-
func: dlg => {
1275-
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) {
12761348
if (u.usage == "mounted") {
1277-
return client.find_mount_users(u.location)
1278-
.then(users => {
1279-
u.users = users;
1280-
});
1281-
} else
1282-
return Promise.resolve();
1283-
}).then(() => {
1284-
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+
console.log("INFO", empty);
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+
}
1364+
} else if (u.block.IdUsage == "crypto" && !client.blocks_cleartext[u.block.path]) {
1365+
u.data_warning = _("Locked encrypted device might contain data");
1366+
} else if (!client.blocks_ptable[u.block.path] &&
1367+
u.block.IdUsage != "raid" &&
1368+
!is_available_block(client, u.block)) {
1369+
u.data_warning = _("Device contains unrecognized data");
1370+
}
1371+
if (u.data_warning)
1372+
have_data = true;
1373+
}
1374+
}
1375+
1376+
if (have_data) {
1377+
usage.Teardown = true;
1378+
dlg.need_confirmation(_("I confirm I want to lose this data forever"));
1379+
} else if (client.in_anaconda_mode() && !expect_single_unmount) {
1380+
dlg.need_confirmation(null);
1381+
} else {
12851382
const msg = teardown_danger_message(usage, expect_single_unmount);
12861383
if (msg)
12871384
dlg.add_danger(msg);
1288-
});
1385+
}
1386+
dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount));
12891387
}
12901388
};
12911389
}

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