LCOV - code coverage report
Current view: top level - pkg/storaged/nfs - nfs.jsx Coverage Total Hit
Test: cockpit Lines: 90.1 % 274 247
Test Date: 2026-06-25 11:17:56

            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          113 : import React from "react";
       8              : import client from "../client";
       9              : 
      10              : import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
      11              : import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
      12              : import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
      13              : 
      14              : import { NetworkIcon } from "../icons/gnome-icons.jsx";
      15              : import {
      16              :     dialog_open, TextInput, ComboBox, CheckBoxes,
      17              :     StopProcessesMessage, stop_processes_danger_message
      18              : } from "../dialog.jsx";
      19              : 
      20              : import { StorageUsageBar } from "../storage-controls.jsx";
      21              : import {
      22              :     StorageCard, StorageDescription,
      23              :     new_page, new_card, PAGE_CATEGORY_NETWORK,
      24              :     navigate_to_new_card_location, navigate_away_from_card
      25              : } from "../pages.jsx";
      26              : import { parse_options, unparse_options, extract_option } from "../utils.js";
      27              : 
      28          113 : const _ = cockpit.gettext;
      29              : 
      30            1 : function nfs_busy_dialog(dialog_title, entry, error, action_title, action) {
      31            1 :     function show(users) {
      32            0 :         if (users.length === 0) {
      33            0 :             dialog_open({
      34            0 :                 Title: dialog_title,
      35            0 :                 Body: error.toString()
      36            0 :             });
      37            0 :         } else {
      38            1 :             dialog_open({
      39            1 :                 Title: dialog_title,
      40            1 :                 Teardown: <StopProcessesMessage users={users} />,
      41            1 :                 Action: {
      42            1 :                     DangerButton: true,
      43            1 :                     Danger: stop_processes_danger_message(users),
      44            1 :                     Title: action_title,
      45            1 :                     action: function () {
      46            1 :                         return action(users);
      47            1 :                     }
      48            1 :                 }
      49            1 :             });
      50            1 :         }
      51            1 :     }
      52              : 
      53            1 :     client.nfs.entry_users(entry)
      54            1 :             .then(function (users) {
      55            1 :                 show(users);
      56            1 :             })
      57            0 :             .catch(function () {
      58            0 :                 show([]);
      59            0 :             });
      60            1 : }
      61              : 
      62            5 : function get_exported_directories(server) {
      63            5 :     return cockpit.spawn(["showmount", "-e", "--no-headers", server], { err: "message" })
      64            4 :             .then(function (output) {
      65            4 :                 const dirs = [];
      66            4 :                 output.split("\n").forEach(function (line) {
      67            4 :                     const d = line.split(" ")[0];
      68            4 :                     if (d)
      69            3 :                         dirs.push(d);
      70            4 :                 });
      71            4 :                 return dirs;
      72            4 :             });
      73            5 : }
      74              : 
      75            5 : export function nfs_fstab_dialog(entry, card) {
      76            2 :     const mount_options = entry ? entry.fields[3] : "defaults";
      77            0 :     const split_options = parse_options(mount_options == "defaults" ? "" : mount_options);
      78            5 :     const opt_auto = !extract_option(split_options, "noauto");
      79            5 :     const opt_ro = extract_option(split_options, "ro");
      80            5 :     const extra_options = unparse_options(split_options);
      81              : 
      82            3 :     function mounting_options(vals) {
      83            3 :         let opts = [];
      84            3 :         if (!vals.mount_options.auto)
      85            1 :             opts.push("noauto");
      86            3 :         if (vals.mount_options.ro)
      87            1 :             opts.push("ro");
      88            3 :         if (vals.mount_options.extra !== false)
      89            1 :             opts = opts.concat(parse_options(vals.mount_options.extra));
      90            3 :         return unparse_options(opts);
      91            3 :     }
      92              : 
      93            5 :     function show(busy) {
      94            5 :         let alert = null;
      95            5 :         if (busy)
      96            1 :             alert = <>
      97            1 :                 <Alert isInline variant="danger" title={_("This NFS mount is in use and only its options can be changed.")} />
      98            1 :                 <br />
      99            1 :             </>;
     100              : 
     101            5 :         let server_to_check = null;
     102            5 :         let server_check_timeout = null;
     103              : 
     104            5 :         function check_server(dlg, server, delay) {
     105            5 :             if (server_check_timeout)
     106            5 :                 window.clearTimeout(server_check_timeout);
     107            5 :             server_to_check = server;
     108            5 :             server_check_timeout = window.setTimeout(() => {
     109            5 :                 server_check_timeout = null;
     110            5 :                 dlg.set_options("remote", { choices: [] });
     111            4 :                 get_exported_directories(server).then(choices => {
     112            4 :                     if (server == server_to_check)
     113            4 :                         dlg.set_options("remote", { choices });
     114            4 :                 });
     115            5 :             }, delay);
     116            5 :         }
     117              : 
     118            5 :         const dlg = dialog_open({
     119            2 :             Title: entry ? _("NFS mount") : _("New NFS mount"),
     120            5 :             Body: alert,
     121            5 :             Fields: [
     122            5 :                 TextInput("server", _("Server address"),
     123            5 :                           {
     124            2 :                               value: entry ? entry.fields[0].split(":")[0] : "",
     125            3 :                               validate: function (val) {
     126            3 :                                   if (val === "")
     127            0 :                                       return _("Server cannot be empty.");
     128            3 :                               },
     129            5 :                               disabled: busy
     130            5 :                           }),
     131            5 :                 ComboBox("remote", _("Path on server"),
     132            5 :                          {
     133            2 :                              value: entry ? entry.fields[0].split(":")[1] : "",
     134            3 :                              validate: function (val) {
     135            3 :                                  if (val === "")
     136            0 :                                      return _("Path on server cannot be empty.");
     137            3 :                                  if (val[0] !== "/")
     138            0 :                                      return _("Path on server must start with \"/\".");
     139            3 :                              },
     140            5 :                              disabled: busy,
     141            5 :                              choices: [],
     142            5 :                          }),
     143            5 :                 TextInput("dir", _("Local mount point"),
     144            5 :                           {
     145            2 :                               value: entry ? entry.fields[1] : "",
     146            3 :                               validate: function (val) {
     147            3 :                                   if (val === "")
     148            0 :                                       return _("Mount point cannot be empty.");
     149            3 :                                   if (val[0] !== "/")
     150            0 :                                       return _("Mount point must start with \"/\".");
     151            3 :                               },
     152            5 :                               disabled: busy
     153            5 :                           }),
     154            5 :                 CheckBoxes("mount_options", _("Mount options"),
     155            5 :                            {
     156            5 :                                fields: [
     157            5 :                                    { title: _("Mount at boot"), tag: "auto" },
     158            5 :                                    { title: _("Mount read only"), tag: "ro" },
     159            5 :                                    { title: _("Custom mount options"), tag: "extra", type: "checkboxWithInput" },
     160            5 :                                ],
     161            5 :                                value: {
     162            5 :                                    auto: opt_auto,
     163            5 :                                    ro: opt_ro,
     164            0 :                                    extra: extra_options === "" ? false : extra_options
     165            5 :                                }
     166            5 :                            },
     167            5 :                 ),
     168            5 :             ],
     169            5 :             update: (dlg, vals, trigger) => {
     170            5 :                 if (trigger === "server")
     171            5 :                     check_server(dlg, vals.server, 500);
     172            5 :             },
     173            5 :             Action: {
     174            2 :                 Title: entry ? _("Save") : _("Add"),
     175            3 :                 action: async function (vals) {
     176            3 :                     const fields = [
     177            3 :                         vals.server + ":" + vals.remote,
     178            3 :                         vals.dir,
     179            1 :                         entry ? entry.fields[2] : "nfs",
     180            3 :                         mounting_options(vals) || "defaults"
     181            3 :                     ];
     182            1 :                     if (entry) {
     183            1 :                         await client.nfs.update_entry(entry, fields);
     184            1 :                         if (entry.fields[0] != fields[0] || entry.fields[1] != fields[1])
     185            1 :                             navigate_to_new_card_location(card, ["nfs", fields[0], fields[1]]);
     186            1 :                     } else
     187            3 :                         await client.nfs.add_entry(fields);
     188            3 :                 }
     189            5 :             }
     190            5 :         });
     191              : 
     192            2 :         if (entry && !busy)
     193            1 :             check_server(dlg, entry.fields[0].split(":")[0], 0);
     194            5 :     }
     195              : 
     196            2 :     if (entry) {
     197            2 :         client.nfs.entry_users(entry)
     198            2 :                 .then(function (users) {
     199            2 :                     show(users.length > 0);
     200            2 :                 })
     201            0 :                 .catch(function () {
     202            0 :                     show(false);
     203            0 :                 });
     204            2 :     } else
     205            5 :         show(false);
     206            5 : }
     207              : 
     208            2 : function checked(error_title, promise) {
     209            0 :     promise.catch(error => {
     210            0 :         dialog_open({
     211            0 :             Title: error_title,
     212            0 :             Body: error.toString()
     213            0 :         });
     214            0 :     });
     215            2 : }
     216              : 
     217            2 : function mount(entry) {
     218            2 :     checked("Could not mount the filesystem",
     219            2 :             client.nfs.mount_entry(entry));
     220            2 : }
     221              : 
     222            2 : function unmount(entry, card) {
     223            2 :     client.nfs.unmount_entry(entry)
     224            1 :             .then(function () {
     225            1 :                 if (!entry.fstab)
     226            0 :                     navigate_away_from_card(card);
     227            1 :             })
     228            1 :             .catch(function (error) {
     229            1 :                 nfs_busy_dialog(_("Unable to unmount filesystem"),
     230            1 :                                 entry, error,
     231            1 :                                 _("Stop and unmount"),
     232            1 :                                 function (users) {
     233            1 :                                     return client.nfs.stop_and_unmount_entry(users, entry)
     234            1 :                                             .then(function () {
     235            1 :                                                 if (!entry.fstab)
     236            0 :                                                     navigate_away_from_card(card);
     237            1 :                                             });
     238            1 :                                 });
     239            1 :             });
     240            2 : }
     241              : 
     242            2 : function edit(entry, card) {
     243            2 :     nfs_fstab_dialog(entry, card);
     244            2 : }
     245              : 
     246            2 : function remove(entry, card) {
     247            2 :     client.nfs.remove_entry(entry)
     248            1 :             .then(function () {
     249            1 :                 navigate_away_from_card(card);
     250            1 :             })
     251            1 :             .catch(function (error) {
     252            1 :                 nfs_busy_dialog(_("Unable to remove mount"),
     253            1 :                                 entry, error,
     254            1 :                                 _("Stop and remove"),
     255            1 :                                 function (users) {
     256            1 :                                     return client.nfs.stop_and_remove_entry(users, entry)
     257            1 :                                             .then(function () {
     258            1 :                                                 navigate_away_from_card(card);
     259            1 :                                             });
     260            1 :                                 });
     261            1 :             });
     262            2 : }
     263              : 
     264            3 : const NfsEntryUsageBar = ({ entry, not_mounted_text, short }) => {
     265            3 :     if (entry.mounted)
     266            3 :         return <StorageUsageBar stats={client.nfs.get_fsys_size(entry)} critical={0.95} block={entry.fields[1]}
     267            2 :                                 short={short} />;
     268              :     else
     269            2 :         return not_mounted_text;
     270            3 : };
     271              : 
     272            3 : export function make_nfs_page(parent, entry) {
     273            3 :     if (client.in_anaconda_mode())
     274            3 :         return;
     275              : 
     276            3 :     const remote = entry.fields[0];
     277            3 :     const local = entry.fields[1];
     278            3 :     let mount_point = local;
     279            3 :     if (!entry.mounted)
     280            2 :         mount_point += " " + _("(not mounted)");
     281              : 
     282            3 :     const nfs_card = new_card({
     283            3 :         title: _("NFS mount"),
     284            3 :         location: mount_point,
     285            3 :         next: null,
     286            3 :         page_location: ["nfs", remote, local],
     287            3 :         page_name: remote,
     288            3 :         page_icon: NetworkIcon,
     289            3 :         page_category: PAGE_CATEGORY_NETWORK,
     290            3 :         page_size: <NfsEntryUsageBar key="size" entry={entry} not_mounted_text={null} short />,
     291            3 :         component: NfsCard,
     292            3 :         props: { entry },
     293            3 :         actions: [
     294            3 :             (entry.mounted
     295            2 :                 ? { title: _("Unmount"), action: () => unmount(entry, nfs_card) }
     296            2 :                 : { title: _("Mount"), action: () => mount(entry) }),
     297            3 :             (entry.fstab
     298            2 :                 ? { title: _("Edit"), action: () => edit(entry, nfs_card) }
     299            3 :                 : null),
     300            3 :             (entry.fstab
     301            2 :                 ? { title: _("Remove"), action: () => remove(entry, nfs_card), danger: true }
     302            3 :                 : null),
     303            3 :         ]
     304            3 :     });
     305              : 
     306            3 :     new_page(parent, nfs_card);
     307            3 : }
     308              : 
     309            2 : const NfsCard = ({ card, entry }) => {
     310            2 :     return (
     311            2 :         <StorageCard card={card}>
     312            2 :             <CardBody>
     313            2 :                 <DescriptionList className="pf-m-horizontal-on-sm">
     314            2 :                     <StorageDescription title={_("Server")} value={entry.fields[0]} />
     315            2 :                     <StorageDescription title={_("Mount point")} value={entry.fields[1]} />
     316            2 :                     <StorageDescription title={_("Size")}>
     317            2 :                         <NfsEntryUsageBar entry={entry} not_mounted_text="--" />
     318            2 :                     </StorageDescription>
     319            2 :                 </DescriptionList>
     320            2 :             </CardBody>
     321            2 :         </StorageCard>
     322              :     );
     323            2 : };
        

Generated by: LCOV version 2.0-1