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 { CardHeader, CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
11 : import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
12 : import { VolumeIcon } from "../icons/gnome-icons.jsx";
13 :
14 : import {
15 : new_card, new_page, PAGE_CATEGORY_VIRTUAL,
16 : get_crossrefs, ChildrenTable, PageTable, StorageCard, StorageDescription,
17 : navigate_away_from_card
18 : } from "../pages.jsx";
19 : import { StorageUsageBar, StorageLink } from "../storage-controls.jsx";
20 : import {
21 : fmt_size_long, validate_fsys_label, should_ignore,
22 : get_active_usage, teardown_active_usage,
23 : reload_systemd,
24 : } from "../utils.js";
25 : import { btrfs_usage } from "./utils.jsx";
26 : import {
27 : dialog_open, TextInput,
28 : BlockingMessage, TeardownMessage,
29 : init_teardown_usage
30 : } from "../dialog.jsx";
31 : import { make_btrfs_subvolume_pages } from "./subvolume.jsx";
32 : import { btrfs_device_actions } from "./device.jsx";
33 :
34 113 : const _ = cockpit.gettext;
35 :
36 : /*
37 : * Udisks is a disk/block library so it manages that, btrfs turns this a bit
38 : * around and has one "volume" which can have multiple blocks by a unique uuid.
39 : *
40 : * Cockpit shows Btrfs as following:
41 : *
42 : * -> btrfs subvolume
43 : * -> btrfs volume
44 : * -> btrfs device
45 : * -> block device
46 : */
47 :
48 1 : async function btrfs_delete(uuid, card) {
49 1 : const volume = client.uuids_btrfs_volume[uuid];
50 0 : const name = volume.data.label || uuid;
51 :
52 1 : const usage = get_active_usage(client, volume.path, _("delete"), undefined, false, undefined, true);
53 :
54 0 : if (usage.Blocking) {
55 0 : dialog_open({
56 0 : Title: cockpit.format(_("$0 is in use"), name),
57 0 : Body: BlockingMessage(usage)
58 0 : });
59 0 : return;
60 0 : }
61 :
62 1 : dialog_open({
63 1 : Title: cockpit.format(_("Permanently delete $0?"), name),
64 1 : Teardown: TeardownMessage(usage),
65 1 : Action: {
66 1 : Danger: _("Deleting erases all data on a btrfs volume."),
67 1 : Title: _("Delete"),
68 1 : disable_on_error: usage.Teardown,
69 1 : action: async function () {
70 1 : await teardown_active_usage(client, usage);
71 1 : const blocks = client.uuids_btrfs_blocks[uuid];
72 1 : for (let i = 0; i < blocks.length; i++) {
73 : // All block devices share the same Configuration
74 : // property, so we only tear down the first.
75 1 : await blocks[i].Format("empty", { 'tear-down': { t: 'b', v: i == 0 } });
76 1 : }
77 1 : await reload_systemd();
78 1 : navigate_away_from_card(card);
79 1 : }
80 1 : },
81 1 : Inits: [
82 1 : init_teardown_usage(client, usage)
83 1 : ]
84 1 : });
85 1 : }
86 :
87 112 : export function make_btrfs_volume_page(parent, uuid) {
88 112 : const block_devices = client.uuids_btrfs_blocks[uuid];
89 112 : const block_btrfs = client.blocks_fsys_btrfs[block_devices[0].path];
90 112 : const volume = client.uuids_btrfs_volume[uuid];
91 112 : const use = btrfs_usage(client, volume);
92 :
93 112 : if (block_devices.some(blk => should_ignore(client, blk.path)))
94 112 : return;
95 :
96 : // Single-device btrfs volumes are shown directly on the page of
97 : // their device; they don't get a standalone "btrfs volume" page.
98 104 : if (block_btrfs.data.num_devices === 1)
99 104 : return;
100 :
101 12 : const name = block_btrfs.data.label || uuid;
102 112 : const btrfs_volume_card = new_card({
103 112 : title: _("btrfs volume"),
104 112 : next: null,
105 112 : page_location: ["btrfs-volume", uuid],
106 112 : page_name: name,
107 112 : page_icon: VolumeIcon,
108 112 : page_category: PAGE_CATEGORY_VIRTUAL,
109 112 : page_size: use[1],
110 112 : component: BtrfsVolumeCard,
111 112 : props: { block_devices, uuid, use },
112 112 : actions: [
113 112 : {
114 112 : title: _("Delete filesystem"),
115 1 : action: () => btrfs_delete(uuid, btrfs_volume_card),
116 112 : danger: true,
117 112 : },
118 112 : ],
119 112 : });
120 :
121 112 : const subvolumes_card = make_btrfs_subvolumes_card(btrfs_volume_card, null, null);
122 :
123 112 : const subvolumes_page = new_page(parent, subvolumes_card);
124 112 : make_btrfs_subvolume_pages(subvolumes_page, volume);
125 112 : }
126 :
127 1 : function rename_dialog(block_btrfs, label, rw_mount_point) {
128 1 : dialog_open({
129 1 : Title: _("Change label"),
130 1 : Fields: [
131 1 : TextInput("name", _("Name"),
132 1 : {
133 1 : validate: name => validate_fsys_label(name, "btrfs"),
134 1 : value: label
135 1 : })
136 1 : ],
137 1 : Action: {
138 1 : Title: _("Save"),
139 1 : action: async function (vals) {
140 1 : if (rw_mount_point) {
141 1 : await cockpit.spawn(["btrfs", "filesystem", "label", rw_mount_point, vals.name],
142 1 : { superuser: true });
143 1 : const block = client.blocks[block_btrfs.path];
144 1 : await block.Rescan({});
145 1 : } else
146 1 : await block_btrfs.SetLabel(vals.name, {});
147 1 : }
148 1 : }
149 1 : });
150 1 : }
151 :
152 8 : export const BtrfsLabelDescription = ({ block_btrfs }) => {
153 0 : const label = block_btrfs.data.label || "-";
154 :
155 : // We can change the label when at least one filesystem subvolume
156 : // is mounted rw, or when nothing is mounted.
157 :
158 8 : let rw_mount_point = null;
159 8 : let is_mounted = false;
160 8 : const mount_points = client.btrfs_mounts[block_btrfs.data.uuid];
161 6 : for (const id in mount_points) {
162 6 : const mp = mount_points[id];
163 6 : if (mp.mount_points.length > 0)
164 6 : is_mounted = true;
165 6 : if (mp.rw_mount_points.length > 0 && !rw_mount_point)
166 6 : rw_mount_point = mp.rw_mount_points[0];
167 6 : }
168 :
169 6 : let excuse = null;
170 6 : if (is_mounted && !rw_mount_point)
171 1 : excuse = _("Filesystem is mounted read-only");
172 :
173 8 : return <StorageDescription title={_("Label")}
174 8 : value={label}
175 8 : action={
176 1 : <StorageLink onClick={() => rename_dialog(block_btrfs, label, rw_mount_point)}
177 8 : excuse={excuse}>
178 8 : {_("edit")}
179 8 : </StorageLink>}
180 8 : />;
181 8 : };
182 :
183 1 : const BtrfsVolumeCard = ({ card, block_devices, uuid, use }) => {
184 1 : const block_btrfs = client.blocks_fsys_btrfs[block_devices[0].path];
185 :
186 1 : return (
187 1 : <StorageCard card={card}>
188 1 : <CardBody>
189 1 : <DescriptionList className="pf-m-horizontal-on-sm">
190 1 : <BtrfsLabelDescription block_btrfs={block_btrfs} />
191 1 : <StorageDescription title={_("UUID")} value={uuid} />
192 1 : <StorageDescription title={_("Capacity")} value={fmt_size_long(use[1])} />
193 1 : <StorageDescription title={_("Usage")}>
194 1 : <StorageUsageBar key="s" stats={use} />
195 1 : </StorageDescription>
196 1 : </DescriptionList>
197 1 : </CardBody>
198 1 : <CardHeader><strong>{_("btrfs devices")}</strong></CardHeader>
199 1 : <PageTable
200 1 : emptyCaption={_("No devices found")}
201 1 : aria-label={_("btrfs device")}
202 1 : crossrefs={get_crossrefs(uuid)} />
203 1 : </StorageCard>
204 : );
205 1 : };
206 :
207 2 : export function make_btrfs_subvolumes_card(next, block, backing_block) {
208 2 : return new_card({
209 2 : title: _("btrfs subvolumes"),
210 2 : next,
211 2 : actions: btrfs_device_actions(block, backing_block),
212 2 : component: BtrfsSubVolumesCard,
213 2 : });
214 2 : }
215 :
216 1 : const BtrfsSubVolumesCard = ({ card }) => {
217 1 : return (
218 1 : <StorageCard card={card}>
219 1 : <ChildrenTable
220 1 : emptyCaption={_("No subvolumes")}
221 1 : aria-label={_("btrfs subvolumes")}
222 1 : page={card.page} />
223 1 : </StorageCard>
224 : );
225 1 : };
|