* Copyright (C) 2023 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React from "react";
import client from "../client";
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
import { NetworkIcon } from "../icons/gnome-icons.jsx";
import {
dialog_open, TextInput, ComboBox, CheckBoxes,
StopProcessesMessage, stop_processes_danger_message
} from "../dialog.jsx";
import { StorageUsageBar } from "../storage-controls.jsx";
import {
StorageCard, StorageDescription,
new_page, new_card, PAGE_CATEGORY_NETWORK,
navigate_to_new_card_location, navigate_away_from_card
} from "../pages.jsx";
import { parse_options, unparse_options, extract_option } from "../utils.js";
const _ = cockpit.gettext;
function nfs_busy_dialog(dialog_title, entry, error, action_title, action) {
function show(users) {
if (users.length === 0) {
dialog_open({
Title: dialog_title,
Body: error.toString()
});
} else {
dialog_open({
Title: dialog_title,
Teardown: <StopProcessesMessage users={users} />,
Action: {
DangerButton: true,
Danger: stop_processes_danger_message(users),
Title: action_title,
action: function () {
return action(users);
}
}
});
}
}
client.nfs.entry_users(entry)
.then(function (users) {
show(users);
})
.catch(function () {
show([]);
});
}
function get_exported_directories(server) {
return cockpit.spawn(["showmount", "-e", "--no-headers", server], { err: "message" })
.then(function (output) {
const dirs = [];
output.split("\n").forEach(function (line) {
const d = line.split(" ")[0];
if (d)
dirs.push(d);
});
return dirs;
});
}
export function nfs_fstab_dialog(entry, card) {
const mount_options = entry ? entry.fields[3] : "defaults";
const split_options = parse_options(mount_options === "defaults" ? "" : mount_options);
const opt_auto = !extract_option(split_options, "noauto");
const opt_ro = extract_option(split_options, "ro");
const extra_options = unparse_options(split_options);
function mounting_options(vals) {
let opts = [];
if (!vals.mount_options.auto)
opts.push("noauto");
if (vals.mount_options.ro)
opts.push("ro");
if (vals.mount_options.extra !== false)
opts = opts.concat(parse_options(vals.mount_options.extra));
return unparse_options(opts);
}
function show(busy) {
let alert = null;
if (busy)
alert = <>
<Alert isInline variant="danger" title={_("This NFS mount is in use and only its options can be changed.")} />
<br />
</>;
let server_to_check = null;
let server_check_timeout = null;
function check_server(dlg, server, delay) {
if (server_check_timeout)
window.clearTimeout(server_check_timeout);
server_to_check = server;
server_check_timeout = window.setTimeout(() => {
server_check_timeout = null;
dlg.set_options("remote", { choices: [] });
get_exported_directories(server).then(choices => {
if (server === server_to_check)
dlg.set_options("remote", { choices });
});
}, delay);
}
const dlg = dialog_open({
Title: entry ? _("NFS mount") : _("New NFS mount"),
Body: alert,
Fields: [
TextInput("server", _("Server address"),
{
value: entry ? entry.fields[0].split(":")[0] : "",
validate: function (val) {
if (val === "")
return _("Server cannot be empty.");
},
disabled: busy
}),
ComboBox("remote", _("Path on server"),
{
value: entry ? entry.fields[0].split(":")[1] : "",
validate: function (val) {
if (val === "")
return _("Path on server cannot be empty.");
if (val[0] !== "/")
return _("Path on server must start with \"/\".");
},
disabled: busy,
choices: [],
}),
TextInput("dir", _("Local mount point"),
{
value: entry ? entry.fields[1] : "",
validate: function (val) {
if (val === "")
return _("Mount point cannot be empty.");
if (val[0] !== "/")
return _("Mount point must start with \"/\".");
},
disabled: busy
}),
CheckBoxes("mount_options", _("Mount options"),
{
fields: [
{ title: _("Mount at boot"), tag: "auto" },
{ title: _("Mount read only"), tag: "ro" },
{ title: _("Custom mount options"), tag: "extra", type: "checkboxWithInput" },
],
value: {
auto: opt_auto,
ro: opt_ro,
extra: extra_options === "" ? false : extra_options
}
},
),
],
update: (dlg, vals, trigger) => {
if (trigger === "server")
check_server(dlg, vals.server, 500);
},
Action: {
Title: entry ? _("Save") : _("Add"),
action: async function (vals) {
const fields = [
vals.server + ":" + vals.remote,
vals.dir,
entry ? entry.fields[2] : "nfs",
mounting_options(vals) || "defaults"
];
if (entry) {
await client.nfs.update_entry(entry, fields);
if (entry.fields[0] !== fields[0] || entry.fields[1] !== fields[1])
navigate_to_new_card_location(card, ["nfs", fields[0], fields[1]]);
} else
await client.nfs.add_entry(fields);
}
}
});
if (entry && !busy)
check_server(dlg, entry.fields[0].split(":")[0], 0);
}
if (entry) {
client.nfs.entry_users(entry)
.then(function (users) {
show(users.length > 0);
})
.catch(function () {
show(false);
});
} else
show(false);
}
function checked(error_title, promise) {
promise.catch(error => {
dialog_open({
Title: error_title,
Body: error.toString()
});
});
}
function mount(entry) {
checked("Could not mount the filesystem",
client.nfs.mount_entry(entry));
}
function unmount(entry, card) {
client.nfs.unmount_entry(entry)
.then(function () {
if (!entry.fstab)
navigate_away_from_card(card);
})
.catch(function (error) {
nfs_busy_dialog(_("Unable to unmount filesystem"),
entry, error,
_("Stop and unmount"),
function (users) {
return client.nfs.stop_and_unmount_entry(users, entry)
.then(function () {
if (!entry.fstab)
navigate_away_from_card(card);
});
});
});
}
function edit(entry, card) {
nfs_fstab_dialog(entry, card);
}
function remove(entry, card) {
client.nfs.remove_entry(entry)
.then(function () {
navigate_away_from_card(card);
})
.catch(function (error) {
nfs_busy_dialog(_("Unable to remove mount"),
entry, error,
_("Stop and remove"),
function (users) {
return client.nfs.stop_and_remove_entry(users, entry)
.then(function () {
navigate_away_from_card(card);
});
});
});
}
const NfsEntryUsageBar = ({ entry, not_mounted_text, short }) => {
if (entry.mounted)
return <StorageUsageBar stats={client.nfs.get_fsys_size(entry)} critical={0.95} block={entry.fields[1]}
short={short} />;
else
return not_mounted_text;
};
export function make_nfs_page(parent, entry) {
if (client.in_anaconda_mode())
return;
const remote = entry.fields[0];
const local = entry.fields[1];
let mount_point = local;
if (!entry.mounted)
mount_point += " " + _("(not mounted)");
const nfs_card = new_card({
title: _("NFS mount"),
location: mount_point,
next: null,
page_location: ["nfs", remote, local],
page_name: remote,
page_icon: NetworkIcon,
page_category: PAGE_CATEGORY_NETWORK,
page_size: <NfsEntryUsageBar key="size" entry={entry} not_mounted_text={null} short />,
component: NfsCard,
props: { entry },
actions: [
(entry.mounted
? { title: _("Unmount"), action: () => unmount(entry, nfs_card) }
: { title: _("Mount"), action: () => mount(entry) }),
(entry.fstab
? { title: _("Edit"), action: () => edit(entry, nfs_card) }
: null),
(entry.fstab
? { title: _("Remove"), action: () => remove(entry, nfs_card), danger: true }
: null),
]
});
new_page(parent, nfs_card);
}
const NfsCard = ({ card, entry }) => {
return (
<StorageCard card={card}>
<CardBody>
<DescriptionList className="pf-m-horizontal-on-sm">
<StorageDescription title={_("Server")} value={entry.fields[0]} />
<StorageDescription title={_("Mount point")} value={entry.fields[1]} />
<StorageDescription title={_("Size")}>
<NfsEntryUsageBar entry={entry} not_mounted_text="--" />
</StorageDescription>
</DescriptionList>
</CardBody>
</StorageCard>
);
};