Line data Source code
1 : /*
2 : * Copyright (C) 2023 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 113 : import cockpit from "cockpit";
7 : import client from "../client.js";
8 :
9 113 : import React from "react";
10 : import { FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
11 : import { HelperText, HelperTextItem, } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
12 : import { ExclamationTriangleIcon, InfoCircleIcon } from "@patternfly/react-icons";
13 :
14 : import {
15 : encode_filename,
16 : parse_options, unparse_options, extract_option, reload_systemd,
17 : set_crypto_options, is_mounted_synch,
18 : get_active_usage, teardown_active_usage,
19 : } from "../utils.js";
20 :
21 : import {
22 : dialog_open,
23 : TextInput, PassInput, CheckBoxes, SelectOne,
24 : TeardownMessage,
25 : init_teardown_usage
26 : } from "../dialog.jsx";
27 : import { init_existing_passphrase, unlock_with_type } from "../crypto/keyslots.jsx";
28 : import { initial_tab_options } from "../block/format-dialog.jsx";
29 :
30 : import {
31 : is_mounted, get_fstab_config,
32 : is_valid_mount_point
33 : } from "./utils.jsx";
34 :
35 113 : const _ = cockpit.gettext;
36 :
37 60 : export const mount_options = (opt_ro, extra_options, is_visible) => {
38 60 : return CheckBoxes("mount_options", _("Mount options"),
39 60 : {
40 39 : visible: vals => !client.in_anaconda_mode() && (!is_visible || is_visible(vals)),
41 60 : value: {
42 60 : ro: opt_ro,
43 60 : extra: extra_options || false
44 60 : },
45 60 : fields: [
46 60 : {
47 60 : title: _("Mount read only"),
48 60 : tag: "ro",
49 60 : },
50 60 : { title: _("Custom mount options"), tag: "extra", type: "checkboxWithInput" },
51 60 : ]
52 60 : });
53 60 : };
54 :
55 113 : export const mount_explanation = {
56 113 : local:
57 113 : <FormHelperText>
58 113 : <HelperText>
59 113 : <HelperTextItem>
60 113 : {_("Mounts before services start")}
61 113 : </HelperTextItem>
62 113 : <HelperTextItem>
63 113 : {_("Appropriate for critical mounts, such as /var")}
64 113 : </HelperTextItem>
65 113 : <HelperTextItem icon={<ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />}>
66 113 : {_("Boot fails if filesystem does not mount, preventing remote access")}
67 113 : </HelperTextItem>
68 113 : </HelperText>
69 113 : </FormHelperText>,
70 113 : nofail:
71 113 : <FormHelperText>
72 113 : <HelperText>
73 113 : <HelperTextItem>
74 113 : {_("Mounts in parallel with services")}
75 113 : </HelperTextItem>
76 113 : <HelperTextItem icon={<InfoCircleIcon className="ct-icon-info-circle" />}>
77 113 : {_("Boot still succeeds when filesystem does not mount")}
78 113 : </HelperTextItem>
79 113 : </HelperText>
80 113 : </FormHelperText>,
81 113 : netdev:
82 113 : <FormHelperText>
83 113 : <HelperText>
84 113 : <HelperTextItem>
85 113 : {_("Mounts in parallel with services, but after network is available")}
86 113 : </HelperTextItem>
87 113 : <HelperTextItem icon={<InfoCircleIcon className="ct-icon-info-circle" />}>
88 113 : {_("Boot still succeeds when filesystem does not mount")}
89 113 : </HelperTextItem>
90 113 : </HelperText>
91 113 : </FormHelperText>,
92 113 : never:
93 113 : <FormHelperText>
94 113 : <HelperText>
95 113 : <HelperTextItem>
96 113 : {_("Does not mount during boot")}
97 113 : </HelperTextItem>
98 113 : <HelperTextItem>
99 113 : {_("Useful for mounts that are optional or need interaction (such as passphrases)")}
100 113 : </HelperTextItem>
101 113 : </HelperText>
102 113 : </FormHelperText>,
103 113 : };
104 :
105 60 : export const at_boot_input = (at_boot, is_visible) => {
106 0 : const init = at_boot || (client.in_anaconda_mode() ? "local" : "nofail");
107 60 : return SelectOne("at_boot", _("At boot"),
108 60 : {
109 39 : visible: vals => !client.in_anaconda_mode() && (!is_visible || is_visible(vals)),
110 60 : value: init,
111 60 : explanation: mount_explanation[init],
112 60 : choices: [
113 60 : {
114 60 : value: "local",
115 60 : title: _("Mount before services start"),
116 60 : },
117 60 : {
118 60 : value: "nofail",
119 60 : title: _("Mount without waiting, ignore failure"),
120 60 : },
121 60 : {
122 60 : value: "netdev",
123 60 : title: _("Mount after network becomes available, ignore failure"),
124 60 : },
125 60 : {
126 60 : value: "never",
127 60 : title: _("Do not mount"),
128 60 : },
129 60 : ]
130 60 : });
131 60 : };
132 :
133 60 : export function update_at_boot_input(dlg, vals, trigger) {
134 60 : if (trigger == "at_boot")
135 9 : dlg.set_options("at_boot", { explanation: mount_explanation[vals.at_boot] });
136 60 : }
137 :
138 22 : export function mounting_dialog(client, block, mode, forced_options, subvol) {
139 22 : const block_fsys = client.blocks_fsys[block.path];
140 22 : const [old_config, old_dir, old_opts, old_parents] = get_fstab_config(block, true, subvol);
141 5 : const options = old_config ? old_opts : initial_tab_options(client, block, true);
142 :
143 22 : const old_dir_for_display = client.strip_mount_point_prefix(old_dir);
144 22 : if (old_dir_for_display === false)
145 0 : return Promise.reject(_("This device can not be used for the installation target."));
146 :
147 22 : const split_options = parse_options(options);
148 22 : extract_option(split_options, "noauto");
149 22 : const opt_never_auto = extract_option(split_options, "x-cockpit-never-auto");
150 22 : const opt_ro = extract_option(split_options, "ro");
151 22 : const opt_nofail = extract_option(split_options, "nofail");
152 22 : const opt_netdev = extract_option(split_options, "_netdev");
153 22 : if (forced_options)
154 11 : for (const opt of forced_options)
155 11 : extract_option(split_options, opt);
156 22 : const extra_options = unparse_options(split_options);
157 :
158 22 : const is_filesystem_mounted = is_mounted(client, block, subvol);
159 :
160 22 : function maybe_update_config(new_dir, new_opts, passphrase, passphrase_type, crypto_unlock_readonly) {
161 22 : let new_config = null;
162 22 : let all_new_opts;
163 :
164 22 : if (new_opts && old_parents)
165 1 : all_new_opts = new_opts + "," + old_parents;
166 14 : else if (new_opts)
167 0 : all_new_opts = new_opts;
168 : else
169 0 : all_new_opts = old_parents;
170 :
171 22 : if (new_dir != "") {
172 22 : if (new_dir[0] != "/")
173 0 : new_dir = "/" + new_dir;
174 22 : new_config = [
175 22 : "fstab", {
176 5 : fsname: old_config ? old_config[1].fsname : undefined,
177 22 : dir: { t: 'ay', v: encode_filename(new_dir) },
178 22 : type: { t: 'ay', v: encode_filename("auto") },
179 0 : opts: { t: 'ay', v: encode_filename(all_new_opts || "defaults") },
180 22 : freq: { t: 'i', v: 0 },
181 22 : passno: { t: 'i', v: 0 },
182 22 : "track-parents": { t: 'b', v: !old_config }
183 22 : }];
184 22 : }
185 :
186 1 : function undo() {
187 0 : if (!old_config && new_config)
188 0 : return block.RemoveConfigurationItem(new_config, {});
189 1 : else if (old_config && !new_config)
190 0 : return block.AddConfigurationItem(old_config, {});
191 1 : else if (old_config && new_config && (new_dir != old_dir || new_opts != old_opts)) {
192 1 : return block.UpdateConfigurationItem(new_config, old_config, {});
193 1 : }
194 1 : }
195 :
196 18 : function get_block_fsys() {
197 18 : if (block_fsys)
198 1 : return Promise.resolve(block_fsys);
199 : else
200 5 : return client.wait_for(() => (client.blocks_cleartext[block.path] &&
201 5 : client.blocks_fsys[client.blocks_cleartext[block.path].path]));
202 18 : }
203 :
204 22 : function maybe_mount() {
205 4 : if (mode == "mount" || (mode == "update" && is_filesystem_mounted)) {
206 18 : return (get_block_fsys()
207 18 : .then(block_fsys => {
208 18 : const block = client.blocks[block_fsys.path];
209 18 : return (client.mount_at(block, new_dir)
210 1 : .catch(error => {
211 : // systemd might have mounted the filesystem for us after
212 : // unlocking, because fstab told it to. Ignore any error
213 : // from mounting in that case. This only happens when this
214 : // code runs to fix a inconsistent mount.
215 1 : return (is_mounted_synch(block)
216 1 : .then(mounted_at => {
217 1 : if (mounted_at == new_dir)
218 1 : return;
219 1 : return (undo()
220 1 : .then(() => {
221 1 : if (is_filesystem_mounted)
222 1 : return client.mount_at(block, old_dir);
223 1 : })
224 0 : .catch(ignored_error => {
225 0 : console.warn("Error during undo:", ignored_error);
226 0 : })
227 1 : .then(() => Promise.reject(error)));
228 1 : }));
229 1 : }));
230 18 : }));
231 18 : } else
232 17 : return Promise.resolve();
233 22 : }
234 :
235 22 : async function maybe_unlock() {
236 4 : if (mode == "mount" || (mode == "update" && is_filesystem_mounted)) {
237 18 : let crypto = client.blocks_crypto[block.path];
238 18 : const backing = client.blocks[block.CryptoBackingDevice];
239 :
240 0 : if (backing && block.ReadOnly != crypto_unlock_readonly) {
241 : // We are working on a open crypto device, but it
242 : // has the wrong readonly-ness. Close it so that we can reopen it below.
243 0 : crypto = client.blocks_crypto[backing.path];
244 0 : await crypto.Lock({});
245 0 : }
246 :
247 5 : if (crypto) {
248 5 : try {
249 5 : await unlock_with_type(client, client.blocks[crypto.path],
250 5 : passphrase, passphrase_type, crypto_unlock_readonly);
251 5 : return await client.wait_for(() => client.blocks_cleartext[crypto.path]);
252 1 : } catch (error) {
253 1 : passphrase_type = null;
254 1 : dlg.set_values({ needs_explicit_passphrase: true });
255 1 : throw error;
256 1 : }
257 5 : }
258 18 : }
259 :
260 21 : return block;
261 22 : }
262 :
263 22 : function maybe_lock() {
264 9 : if (mode == "unmount" && !subvol && !client.in_anaconda_mode()) {
265 9 : const crypto_backing = client.blocks[block.CryptoBackingDevice];
266 5 : const crypto_backing_crypto = crypto_backing && client.blocks_crypto[crypto_backing.path];
267 5 : if (crypto_backing_crypto) {
268 5 : return crypto_backing_crypto.Lock({});
269 5 : } else
270 4 : return Promise.resolve();
271 9 : }
272 22 : }
273 :
274 : // We need to reload systemd twice: Once at the beginning so
275 : // that it is up to date with whatever is currently in fstab,
276 : // and once at the end to make it see our changes. Otherwise
277 : // systemd might do some uexpected mounts/unmounts behind our
278 : // backs.
279 :
280 22 : return (reload_systemd()
281 22 : .then(() => teardown_active_usage(client, usage))
282 22 : .then(maybe_unlock)
283 22 : .then(content_block => {
284 7 : if (!old_config && new_config)
285 7 : return (content_block.AddConfigurationItem(new_config, {})
286 5 : .then(maybe_mount));
287 20 : else if (old_config && !new_config)
288 1 : return content_block.RemoveConfigurationItem(old_config, {});
289 20 : else if (old_config && new_config)
290 20 : return (content_block.UpdateConfigurationItem(old_config, new_config, {})
291 0 : .then(maybe_mount));
292 0 : else if (new_config && !is_mounted(client, block))
293 0 : return maybe_mount();
294 22 : })
295 22 : .then(maybe_lock)
296 22 : .then(reload_systemd));
297 22 : }
298 :
299 22 : let at_boot;
300 22 : if (opt_never_auto)
301 2 : at_boot = "never";
302 21 : else if (opt_netdev)
303 1 : at_boot = "netdev";
304 20 : else if (opt_nofail)
305 1 : at_boot = "nofail";
306 : else
307 3 : at_boot = "local";
308 :
309 22 : let fields = null;
310 16 : if (mode == "mount" || mode == "update") {
311 20 : fields = [
312 20 : TextInput("mount_point", _("Mount point"),
313 20 : {
314 20 : value: old_dir_for_display,
315 20 : validate: val => is_valid_mount_point(client,
316 20 : block,
317 20 : client.add_mount_point_prefix(val),
318 6 : mode == "update" && !is_filesystem_mounted,
319 20 : mode == "update",
320 20 : subvol)
321 20 : }),
322 20 : mount_options(opt_ro, extra_options, null),
323 20 : at_boot_input(at_boot),
324 20 : ];
325 :
326 20 : fields = fields.concat([
327 20 : PassInput("passphrase", _("Passphrase"),
328 20 : {
329 20 : visible: vals => vals.needs_explicit_passphrase,
330 0 : validate: val => !val.length && _("Passphrase cannot be empty"),
331 20 : })
332 20 : ]);
333 20 : }
334 :
335 22 : const mode_title = {
336 22 : mount: _("Mount filesystem"),
337 22 : unmount: _("Unmount filesystem $0"),
338 22 : update: _("Mount configuration")
339 22 : };
340 :
341 22 : const mode_action = {
342 22 : mount: _("Mount"),
343 22 : unmount: _("Unmount"),
344 22 : update: _("Save")
345 22 : };
346 :
347 17 : function do_unmount() {
348 17 : let opts = [];
349 17 : opts.push("noauto");
350 17 : if (opt_ro)
351 1 : opts.push("ro");
352 17 : if (opt_never_auto)
353 2 : opts.push("x-cockpit-never-auto");
354 17 : if (opt_nofail)
355 14 : opts.push("nofail");
356 17 : if (opt_netdev)
357 1 : opts.push("_netdev");
358 17 : if (forced_options)
359 9 : opts = opts.concat(forced_options);
360 17 : if (extra_options)
361 0 : opts = opts.concat(extra_options);
362 17 : return (maybe_set_crypto_options(null, false, null, null)
363 17 : .then(() => maybe_update_config(old_dir, unparse_options(opts))));
364 17 : }
365 :
366 22 : let passphrase_type;
367 :
368 22 : function maybe_set_crypto_options(readonly, auto, nofail, netdev) {
369 5 : if (client.blocks_crypto[block.path]) {
370 5 : return set_crypto_options(block, readonly, auto, nofail, netdev);
371 4 : } else if (client.blocks_crypto[block.CryptoBackingDevice]) {
372 7 : return set_crypto_options(client.blocks[block.CryptoBackingDevice], readonly, auto, nofail, netdev);
373 7 : } else
374 14 : return Promise.resolve();
375 22 : }
376 :
377 22 : const usage = get_active_usage(client, block.path, null, null, false, subvol);
378 :
379 22 : function update_explicit_passphrase(vals_ro) {
380 22 : const backing = client.blocks[block.CryptoBackingDevice];
381 5 : let need_passphrase = (block.IdUsage == "crypto" && mode == "mount");
382 7 : if (backing) {
383 : // XXX - take subvols into account.
384 7 : if (block.ReadOnly != vals_ro)
385 0 : need_passphrase = true;
386 7 : }
387 5 : dlg.set_values({ needs_explicit_passphrase: need_passphrase && !passphrase_type });
388 22 : }
389 :
390 22 : const dlg = dialog_open({
391 22 : Title: cockpit.format(mode_title[mode], old_dir_for_display),
392 22 : Fields: fields,
393 7 : Teardown: TeardownMessage(usage, old_dir || true),
394 17 : update: function (dlg, vals, trigger) {
395 17 : update_at_boot_input(dlg, vals, trigger);
396 17 : if (trigger == "mount_options")
397 4 : update_explicit_passphrase(vals.mount_options.ro);
398 17 : },
399 22 : Action: {
400 22 : Title: mode_action[mode],
401 22 : disable_on_error: usage.Teardown,
402 22 : action: function (vals) {
403 17 : if (mode == "unmount") {
404 17 : return do_unmount();
405 5 : } else if (mode == "mount" || mode == "update") {
406 20 : let opts = [];
407 4 : if ((mode == "update" && !is_filesystem_mounted) || vals.at_boot == "never")
408 6 : opts.push("noauto");
409 18 : if (vals.mount_options?.ro)
410 0 : opts.push("ro");
411 20 : if (vals.at_boot == "never")
412 4 : opts.push("x-cockpit-never-auto");
413 20 : if (vals.at_boot == "nofail")
414 16 : opts.push("nofail");
415 20 : if (vals.at_boot == "netdev")
416 2 : opts.push("_netdev");
417 20 : if (forced_options)
418 11 : opts = opts.concat(forced_options);
419 18 : if (vals.mount_options?.extra)
420 3 : opts = opts.concat(parse_options(vals.mount_options.extra));
421 : // XXX - take subvols into account.
422 0 : const crypto_unlock_readonly = vals.mount_options?.ro ?? opt_ro;
423 20 : return (maybe_update_config(client.add_mount_point_prefix(vals.mount_point),
424 20 : unparse_options(opts),
425 20 : vals.passphrase,
426 20 : passphrase_type,
427 20 : crypto_unlock_readonly)
428 18 : .then(() => maybe_set_crypto_options(vals.mount_options?.ro,
429 20 : opts.indexOf("noauto") == -1,
430 20 : vals.at_boot == "nofail",
431 20 : vals.at_boot == "netdev")));
432 20 : }
433 22 : }
434 22 : },
435 22 : Inits: [
436 7 : init_teardown_usage(client, usage, old_dir || true),
437 22 : init_existing_passphrase(block, true, type => {
438 22 : passphrase_type = type;
439 15 : update_explicit_passphrase(dlg.get_value("mount_options")?.ro ?? opt_ro);
440 22 : }),
441 22 : ]
442 22 : });
443 22 : }
|