LCOV - code coverage report
Current view: top level - pkg/storaged/filesystem - mounting-dialog.jsx Coverage Total Hit
Test: cockpit Lines: 93.4 % 363 339
Test Date: 2026-07-03 14:25:32

            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 : }
        

Generated by: LCOV version 2.0-1