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