-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
storage: Confirmation before erasing non-empty filesystems #20272
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -235,15 +235,21 @@ import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/comp | |
import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js"; | ||
import { ExclamationTriangleIcon, InfoIcon, HelpIcon, EyeIcon, EyeSlashIcon } from "@patternfly/react-icons"; | ||
import { InputGroup } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js"; | ||
import { Table, Tbody, Tr, Td } from '@patternfly/react-table'; | ||
|
||
import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx"; | ||
import { ListingTable } from "cockpit-components-table.jsx"; | ||
import { FormHelper } from "cockpit-components-form-helper"; | ||
|
||
import { fmt_size, block_name, format_size_and_text, format_delay, for_each_async, get_byte_units } from "./utils.js"; | ||
import { | ||
decode_filename, fmt_size, block_name, format_size_and_text, format_delay, for_each_async, get_byte_units, | ||
is_available_block | ||
} from "./utils.js"; | ||
import { fmt_to_fragments } from "utils.jsx"; | ||
import client from "./client.js"; | ||
|
||
import fsys_is_empty_sh from "./fsys-is-empty.sh"; | ||
|
||
const _ = cockpit.gettext; | ||
|
||
function make_rows(fields, values, errors, onChange) { | ||
|
@@ -328,6 +334,19 @@ const Body = ({ body, teardown, fields, values, errors, isFormHorizontal, onChan | |
); | ||
}; | ||
|
||
const ExtraConfirmation = ({ text, onChange }) => { | ||
const [confirmed, setConfirmed] = useState(false); | ||
|
||
return ( | ||
<Checkbox isChecked={confirmed} | ||
id="dialog-confirm" | ||
label={text} | ||
onChange={(_, val) => { | ||
setConfirmed(val); | ||
onChange(val); | ||
}} />); | ||
}; | ||
|
||
function flatten_fields(fields) { | ||
return fields.reduce( | ||
(acc, val) => acc.concat([val]).concat(val.options && val.options.nested_fields | ||
|
@@ -340,6 +359,8 @@ export const dialog_open = (def) => { | |
const nested_fields = def.Fields || []; | ||
const fields = flatten_fields(nested_fields); | ||
const values = { }; | ||
let confirmation = null; | ||
let confirmed = false; | ||
let errors = null; | ||
|
||
fields.forEach(f => { values[f.tag] = f.initial_value }); | ||
|
@@ -415,8 +436,10 @@ export const dialog_open = (def) => { | |
caption: variant.Title, | ||
style: actions.length == 0 ? "primary" : "secondary", | ||
danger: def.Action.Danger || def.Action.DangerButton, | ||
disabled: running_promise != null || (def.Action.disable_on_error && | ||
errors && errors.toString() != "[object Object]"), | ||
disabled: (running_promise != null || | ||
(def.Action.disable_on_error && | ||
errors && errors.toString() != "[object Object]") || | ||
(confirmation && !confirmed)), | ||
clicked: progress_callback => run_action(progress_callback, variant.tag), | ||
}); | ||
} | ||
|
@@ -436,13 +459,19 @@ export const dialog_open = (def) => { | |
} | ||
} | ||
|
||
const extra = ( | ||
<div> | ||
{ def.Action && def.Action.Danger | ||
? <HelperText><HelperTextItem variant="error">{def.Action.Danger} </HelperTextItem></HelperText> | ||
: null | ||
} | ||
</div>); | ||
let extra = null; | ||
if (confirmation) { | ||
extra = <ExtraConfirmation text={confirmation} | ||
onChange={val => { | ||
confirmed = val; | ||
update_footer(); | ||
}} />; | ||
} else if (def.Action && def.Action.Danger) { | ||
extra = ( | ||
<div> | ||
<HelperText><HelperTextItem variant="error">{def.Action.Danger} </HelperTextItem></HelperText> | ||
</div>); | ||
} | ||
|
||
return { | ||
idle_message: (running_promise | ||
|
@@ -537,6 +566,14 @@ export const dialog_open = (def) => { | |
update(); | ||
}, | ||
|
||
need_confirmation: (conf) => { | ||
confirmation = conf; | ||
confirmed = false; | ||
def.Action.Danger = null; | ||
def.Action.DangerButton = true; | ||
update_footer(); | ||
}, | ||
|
||
close: () => { | ||
dlg.footerProps.dialog_done(); | ||
} | ||
|
@@ -1204,13 +1241,16 @@ const teardown_block_name = use => { | |
name = block_name(client.blocks[use.block.CryptoBackingDevice] || use.block); | ||
} | ||
|
||
return name; | ||
return name.replace(/^\/dev\//, ""); | ||
}; | ||
|
||
export const TeardownMessage = (usage, expect_single_unmount) => { | ||
if (usage.length == 0) | ||
if (!usage.Teardown) | ||
return null; | ||
|
||
if (client.in_anaconda_mode() && !expect_single_unmount) | ||
return <AnacondaTeardownMessage usage={usage} />; | ||
|
||
if (is_expected_unmount(usage, expect_single_unmount)) | ||
return <StopProcessesMessage mount_point={expect_single_unmount} users={usage[0].users} />; | ||
|
||
|
@@ -1251,6 +1291,36 @@ export const TeardownMessage = (usage, expect_single_unmount) => { | |
</div>); | ||
}; | ||
|
||
const AnacondaTeardownMessage = ({ usage }) => { | ||
const rows = []; | ||
|
||
usage.forEach((use, index) => { | ||
if (use.data_warning) { | ||
const name = teardown_block_name(use); | ||
const location = client.strip_mount_point_prefix(use.location) || use.block.IdLabel || "-"; | ||
|
||
rows.push( | ||
<Tr key={index}> | ||
<Td className="pf-v5-u-font-weight-bold">{name}</Td> | ||
<Td>{location}</Td> | ||
<Td>{use.data_warning}</Td> | ||
</Tr>); | ||
} | ||
}); | ||
|
||
if (rows.length > 0) { | ||
return ( | ||
<div className="modal-footer-teardown"> | ||
<HelperText> | ||
<HelperTextItem variant="error"> | ||
{_("Important data might be deleted:")} | ||
</HelperTextItem> | ||
</HelperText> | ||
<Table variant="compact" borders={false}><Tbody>{rows}</Tbody></Table> | ||
</div>); | ||
} | ||
}; | ||
|
||
export function teardown_danger_message(usage, expect_single_unmount) { | ||
if (is_expected_unmount(usage, expect_single_unmount)) | ||
return stop_processes_danger_message(usage[0].users); | ||
|
@@ -1269,24 +1339,54 @@ export function teardown_danger_message(usage, expect_single_unmount) { | |
} | ||
} | ||
|
||
export function init_active_usage_processes(client, usage, expect_single_unmount) { | ||
export function init_teardown_usage(client, usage, expect_single_unmount) { | ||
return { | ||
title: _("Checking related processes"), | ||
func: dlg => { | ||
return for_each_async(usage, u => { | ||
title: _("Checking filesystem usage"), | ||
func: async function (dlg) { | ||
let have_data = false; | ||
for (const u of usage) { | ||
if (u.usage == "mounted") { | ||
return client.find_mount_users(u.location) | ||
.then(users => { | ||
u.users = users; | ||
}); | ||
} else | ||
return Promise.resolve(); | ||
}).then(() => { | ||
dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount)); | ||
u.users = await client.find_mount_users(u.location); | ||
} | ||
if (client.in_anaconda_mode() && !expect_single_unmount && u.block) { | ||
if (u.block.IdUsage == "filesystem" && | ||
["xfs", "ext2", "ext3", "ext4", "btrfs", "vfat", "ntfs"].indexOf(u.block.IdType) >= 0) { | ||
const empty = await cockpit.script(fsys_is_empty_sh, | ||
[decode_filename(u.block.PreferredDevice)], | ||
{ superuser: "require", err: "message" }); | ||
if (empty.trim() != "yes") { | ||
try { | ||
const info = JSON.parse(empty); | ||
u.data_warning = cockpit.format(_("$0 used, $1 total"), | ||
fmt_size((info.total - info.free) * info.unit), | ||
fmt_size(info.total * info.unit)); | ||
} catch { | ||
u.data_warning = _("Device contains unrecognized data"); | ||
Comment on lines
+1363
to
+1364
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These 2 added lines are not executed by any test. |
||
} | ||
} | ||
} else if (u.block.IdUsage == "crypto" && !client.blocks_cleartext[u.block.path]) { | ||
u.data_warning = _("Locked encrypted device might contain data"); | ||
} else if (!client.blocks_ptable[u.block.path] && | ||
u.block.IdUsage != "raid" && | ||
!is_available_block(client, u.block)) { | ||
u.data_warning = _("Device contains unrecognized data"); | ||
} | ||
if (u.data_warning) | ||
have_data = true; | ||
} | ||
} | ||
|
||
if (have_data) { | ||
usage.Teardown = true; | ||
dlg.need_confirmation(_("I confirm I want to lose this data forever")); | ||
mvollmer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else if (client.in_anaconda_mode() && !expect_single_unmount) { | ||
dlg.need_confirmation(null); | ||
} else { | ||
const msg = teardown_danger_message(usage, expect_single_unmount); | ||
if (msg) | ||
dlg.add_danger(msg); | ||
}); | ||
} | ||
dlg.set_attribute("Teardown", TeardownMessage(usage, expect_single_unmount)); | ||
} | ||
}; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#! /bin/bash | ||
|
||
set -eux | ||
|
||
dev=$1 | ||
|
||
need_unmount="" | ||
|
||
maybe_unmount () { | ||
if [ -n "$need_unmount" ]; then | ||
umount "$need_unmount" | ||
rmdir "$need_unmount" | ||
fi | ||
} | ||
|
||
trap maybe_unmount EXIT INT QUIT | ||
|
||
mp=$(findmnt -no TARGET "$dev" | cat) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That cat seems unnecessary:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It protects against the exit code of |
||
if [ -z "$mp" ]; then | ||
mp=$(mktemp -d) | ||
need_unmount=$mp | ||
mount "$dev" "$mp" -o ro | ||
fi | ||
|
||
# A filesystem is empty if it only has directories in it. | ||
|
||
first=$(find "$mp" -not -type d | head -1) | ||
info=$(stat -f -c '{ "unit": %S, "free": %f, "total": %b }' "$mp") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neither of these commands is allowed to fail as then we will exit and never unmount if needed. So maybe we should use a trap for that. (I can only get find to fail when not running as root for example but maybe it can fail on broken symlinks or pipes as root?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good point! |
||
|
||
if [ -z "$first" ]; then | ||
echo yes | ||
else | ||
echo "$info" | ||
fi |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,7 +27,7 @@ import { DescriptionList, DescriptionListDescription, DescriptionListGroup, Desc | |
|
||
import { block_short_name, get_active_usage, teardown_active_usage, fmt_size, decode_filename, reload_systemd } from "../utils.js"; | ||
import { | ||
dialog_open, SizeSlider, BlockingMessage, TeardownMessage, init_active_usage_processes | ||
dialog_open, SizeSlider, BlockingMessage, TeardownMessage, init_teardown_usage | ||
} from "../dialog.jsx"; | ||
import { StorageButton, StorageOnOff } from "../storage-controls.jsx"; | ||
|
||
|
@@ -68,7 +68,7 @@ export function make_legacy_vdo_page(parent, vdo, backing_block, next_card) { | |
} | ||
}, | ||
Inits: [ | ||
init_active_usage_processes(client, usage) | ||
init_teardown_usage(client, usage) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This added line is not executed by any test. |
||
] | ||
}); | ||
} else { | ||
|
@@ -130,7 +130,7 @@ export function make_legacy_vdo_page(parent, vdo, backing_block, next_card) { | |
} | ||
}, | ||
Inits: [ | ||
init_active_usage_processes(client, usage) | ||
init_teardown_usage(client, usage) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This added line is not executed by any test. |
||
] | ||
}); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these all the filesystems udisks supports? If so Cockpit should already have a list of them somewhere right? Or can't we get this from udisks? Like http://storaged.org/doc/udisks2-api/latest/gdbus-org.freedesktop.UDisks2.Manager.html#gdbus-property-org-freedesktop-UDisks2-Manager.SupportedFilesystems
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the idea is to only mount a small number of well-known filesystems, instead of just throwing everything that we find at the kernel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok! That sounds a bit weird:
This seems almost what we want imo.