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 { Card, CardBody } from '@patternfly/react-core/dist/esm/components/Card/index.js';
12 : import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
13 :
14 : import { block_short_name, get_active_usage, teardown_active_usage, fmt_size, decode_filename, reload_systemd } from "../utils.js";
15 : import {
16 : dialog_open, SizeSlider, BlockingMessage, TeardownMessage, init_teardown_usage
17 : } from "../dialog.jsx";
18 : import { StorageButton, StorageOnOff } from "../storage-controls.jsx";
19 :
20 : import { StorageCard, new_page, new_card } from "../pages.jsx";
21 : import { make_block_page } from "../block/create-pages.jsx";
22 :
23 : import inotify_py from "inotify.py";
24 : import vdo_monitor_py from "./vdo-monitor.py";
25 :
26 113 : const _ = cockpit.gettext;
27 :
28 0 : export function make_legacy_vdo_page(parent, vdo, backing_block, next_card) {
29 0 : const block = client.slashdevs_block[vdo.dev];
30 :
31 0 : function stop() {
32 0 : const usage = get_active_usage(client, block ? block.path : "/", _("stop"));
33 :
34 0 : if (usage.Blocking) {
35 0 : dialog_open({
36 0 : Title: cockpit.format(_("$0 is in use"), vdo.name),
37 0 : Body: BlockingMessage(usage),
38 0 : });
39 0 : return;
40 0 : }
41 :
42 0 : if (usage.Teardown) {
43 0 : dialog_open({
44 0 : Title: cockpit.format(_("Confirm stopping of $0"),
45 0 : vdo.name),
46 0 : Teardown: TeardownMessage(usage),
47 0 : Action: {
48 0 : Title: _("Stop"),
49 0 : action: function () {
50 0 : return teardown_active_usage(client, usage)
51 0 : .then(function () {
52 0 : return vdo.stop();
53 0 : });
54 0 : }
55 0 : },
56 0 : Inits: [
57 0 : init_teardown_usage(client, usage)
58 0 : ]
59 0 : });
60 0 : } else {
61 0 : return vdo.stop();
62 0 : }
63 0 : }
64 :
65 0 : function delete_() {
66 0 : const usage = get_active_usage(client, block ? block.path : "/", _("delete"));
67 :
68 0 : if (usage.Blocking) {
69 0 : dialog_open({
70 0 : Title: cockpit.format(_("$0 is in use"), vdo.name),
71 0 : Body: BlockingMessage(usage),
72 0 : });
73 0 : return;
74 0 : }
75 :
76 0 : function wipe_with_teardown(block) {
77 0 : return block.Format("empty", { 'tear-down': { t: 'b', v: true } }).then(reload_systemd);
78 0 : }
79 :
80 0 : function teardown_configs() {
81 0 : if (block) {
82 0 : return wipe_with_teardown(block);
83 0 : } else {
84 0 : return vdo.start()
85 0 : .then(function () {
86 0 : return client.wait_for(() => client.slashdevs_block[vdo.dev])
87 0 : .then(function (block) {
88 0 : return wipe_with_teardown(block)
89 0 : .catch(error => {
90 : // systemd might have mounted it, let's try unmounting
91 0 : const block_fsys = client.blocks_fsys[block.path];
92 0 : if (block_fsys) {
93 0 : return block_fsys.Unmount({})
94 0 : .then(() => wipe_with_teardown(block));
95 0 : } else {
96 0 : return Promise.reject(error);
97 0 : }
98 0 : });
99 0 : });
100 0 : });
101 0 : }
102 0 : }
103 :
104 0 : dialog_open({
105 0 : Title: cockpit.format(_("Permanently delete $0?"), vdo.name),
106 0 : Body: TeardownMessage(usage),
107 0 : Action: {
108 0 : Title: _("Delete"),
109 0 : Danger: _("Deleting erases all data on a VDO device."),
110 0 : action: function () {
111 0 : return (teardown_active_usage(client, usage)
112 0 : .then(teardown_configs)
113 0 : .then(function () {
114 0 : return vdo.remove();
115 0 : }));
116 0 : }
117 0 : },
118 0 : Inits: [
119 0 : init_teardown_usage(client, usage)
120 0 : ]
121 0 : });
122 0 : }
123 :
124 0 : const vdo_card = new_card({
125 0 : title: cockpit.format(_("VDO device $0"), vdo.name),
126 0 : next: next_card,
127 0 : page_location: ["vdo", vdo.name],
128 0 : page_name: block_short_name(backing_block),
129 0 : page_size: vdo.logical_size,
130 0 : job_path: backing_block.path,
131 0 : component: VDODetails,
132 0 : props: { client, vdo },
133 0 : actions: [
134 0 : (block
135 0 : ? { title: _("Stop"), action: stop }
136 0 : : { title: _("Start"), action: () => vdo.start() }
137 : ),
138 0 : { title: _("Delete"), action: delete_, danger: true }
139 0 : ],
140 0 : });
141 :
142 0 : if (block) {
143 0 : make_block_page(parent, block, vdo_card);
144 0 : } else {
145 0 : new_page(parent, vdo_card);
146 0 : }
147 0 : }
148 :
149 113 : class VDODetails extends React.Component {
150 0 : constructor() {
151 0 : super();
152 0 : this.poll_path = null;
153 0 : this.state = { stats: null };
154 0 : }
155 :
156 0 : ensure_polling(enable) {
157 0 : const client = this.props.client;
158 0 : const vdo = this.props.vdo;
159 0 : const block = client.slashdevs_block[vdo.dev];
160 0 : const path = enable && block ? vdo.dev : null;
161 :
162 0 : let buf = "";
163 :
164 0 : if (this.poll_path === path)
165 0 : return;
166 :
167 0 : if (this.poll_path) {
168 0 : this.poll_process.close();
169 0 : this.setState({ stats: null });
170 0 : }
171 :
172 0 : if (path)
173 0 : this.poll_process = cockpit.spawn([client.legacy_vdo_overlay.python, "--", "-", path], { superuser: "require" })
174 0 : .input(inotify_py + vdo_monitor_py)
175 0 : .stream((data) => {
176 0 : buf += data;
177 0 : const lines = buf.split("\n");
178 0 : buf = lines[lines.length - 1];
179 0 : if (lines.length >= 2) {
180 0 : this.setState({ stats: JSON.parse(lines[lines.length - 2]) });
181 0 : }
182 0 : });
183 0 : this.poll_path = path;
184 0 : }
185 :
186 0 : componentDidMount() {
187 0 : this.ensure_polling(true);
188 0 : }
189 :
190 0 : componentDidUpdate() {
191 0 : this.ensure_polling(true);
192 0 : }
193 :
194 0 : componentWillUnmount() {
195 0 : this.ensure_polling(false);
196 0 : }
197 :
198 0 : render() {
199 0 : const client = this.props.client;
200 0 : const vdo = this.props.vdo;
201 0 : const block = client.slashdevs_block[vdo.dev];
202 0 : const backing_block = client.slashdevs_block[vdo.backing_dev];
203 :
204 0 : function force_delete() {
205 0 : return vdo.force_remove();
206 0 : }
207 :
208 0 : if (vdo.broken) {
209 0 : return (
210 0 : <Card isPlain>
211 0 : <Alert variant='danger' isInline
212 0 : title={_("The creation of this VDO device did not finish and the device can't be used.")}
213 0 : actionClose={<StorageButton onClick={force_delete}>
214 0 : {_("Remove device")}
215 0 : </StorageButton>} />
216 0 : </Card>
217 : );
218 0 : }
219 :
220 0 : const alerts = [];
221 0 : if (backing_block && backing_block.Size > vdo.physical_size)
222 0 : alerts.push(
223 0 : <Alert variant='warning' isInline key="unused"
224 0 : actionClose={<StorageButton onClick={vdo.grow_physical}>{_("Grow to take all space")}</StorageButton>}
225 0 : title={_("This VDO device does not use all of its backing device.")}>
226 0 : { cockpit.format(_("Only $0 of $1 are used."),
227 0 : fmt_size(vdo.physical_size),
228 0 : fmt_size(backing_block.Size))}
229 0 : </Alert>);
230 :
231 0 : function grow_logical() {
232 0 : dialog_open({
233 0 : Title: cockpit.format(_("Grow logical size of $0"), vdo.name),
234 0 : Fields: [
235 0 : SizeSlider("lsize", _("Logical size"),
236 0 : {
237 0 : max: 5 * vdo.logical_size,
238 0 : min: vdo.logical_size,
239 0 : round: 512,
240 0 : value: vdo.logical_size,
241 0 : allow_infinite: true
242 0 : })
243 0 : ],
244 0 : Action: {
245 0 : Title: _("Grow"),
246 0 : action: function (vals) {
247 0 : if (vals.lsize > vdo.logical_size)
248 0 : return vdo.grow_logical(vals.lsize).then(() => {
249 0 : if (block && block.IdUsage == "filesystem")
250 0 : return cockpit.spawn(["fsadm", "resize",
251 0 : decode_filename(block.Device)],
252 0 : { superuser: "require", err: "message" });
253 0 : });
254 0 : }
255 0 : }
256 0 : });
257 0 : }
258 :
259 0 : function fmt_perc(num) {
260 0 : if (num || num == 0)
261 0 : return num + "%";
262 : else
263 0 : return "--";
264 0 : }
265 :
266 0 : const stats = this.state.stats;
267 :
268 0 : const header = (
269 0 : <StorageCard card={this.props.card} alerts={alerts}>
270 0 : <CardBody>
271 0 : <DescriptionList className="pf-m-horizontal-on-sm">
272 0 : <DescriptionListGroup>
273 0 : <DescriptionListTerm>{_("Device file")}</DescriptionListTerm>
274 0 : <DescriptionListDescription>{vdo.dev}</DescriptionListDescription>
275 0 : </DescriptionListGroup>
276 :
277 0 : <DescriptionListGroup>
278 0 : <DescriptionListTerm>{_("Physical")}</DescriptionListTerm>
279 0 : <DescriptionListDescription>
280 0 : { stats
281 0 : ? cockpit.format(_("$0 data + $1 overhead used of $2 ($3)"),
282 0 : fmt_size(stats.dataBlocksUsed * stats.blockSize),
283 0 : fmt_size(stats.overheadBlocksUsed * stats.blockSize),
284 0 : fmt_size(vdo.physical_size),
285 0 : fmt_perc(stats.usedPercent))
286 0 : : fmt_size(vdo.physical_size)
287 : }
288 0 : </DescriptionListDescription>
289 0 : </DescriptionListGroup>
290 :
291 0 : <DescriptionListGroup>
292 0 : <DescriptionListTerm>{_("Logical")}</DescriptionListTerm>
293 0 : <DescriptionListDescription>
294 0 : { stats
295 0 : ? cockpit.format(_("$0 used of $1 ($2 saved)"),
296 0 : fmt_size(stats.logicalBlocksUsed * stats.blockSize),
297 0 : fmt_size(vdo.logical_size),
298 0 : fmt_perc(stats.savingPercent))
299 0 : : fmt_size(vdo.logical_size)
300 0 : }
301 0 : <StorageButton onClick={grow_logical}>{_("Grow")}</StorageButton>
302 0 : </DescriptionListDescription>
303 0 : </DescriptionListGroup>
304 :
305 0 : <DescriptionListGroup>
306 0 : <DescriptionListTerm>{_("Index memory")}</DescriptionListTerm>
307 0 : <DescriptionListDescription>{fmt_size(vdo.index_mem * 1024 * 1024 * 1024)}</DescriptionListDescription>
308 0 : </DescriptionListGroup>
309 :
310 0 : <DescriptionListGroup>
311 0 : <DescriptionListTerm>{_("Compression")}</DescriptionListTerm>
312 0 : <DescriptionListDescription>
313 0 : <StorageOnOff state={vdo.compression}
314 0 : aria-label={_("Use compression")}
315 0 : onChange={() => vdo.set_compression(!vdo.compression)} />
316 0 : </DescriptionListDescription>
317 0 : </DescriptionListGroup>
318 :
319 0 : <DescriptionListGroup>
320 0 : <DescriptionListTerm>{_("Deduplication")}</DescriptionListTerm>
321 0 : <DescriptionListDescription>
322 0 : <StorageOnOff state={vdo.deduplication}
323 0 : aria-label={_("Use deduplication")}
324 0 : onChange={() => vdo.set_deduplication(!vdo.deduplication)} />
325 0 : </DescriptionListDescription>
326 0 : </DescriptionListGroup>
327 0 : </DescriptionList>
328 0 : </CardBody>
329 0 : </StorageCard>
330 : );
331 :
332 0 : return header;
333 0 : }
334 113 : }
|