From a1c0f6784c6d313e310bf43bd7663a6d557657dd Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Mon, 26 Jan 2026 15:17:14 +0000 Subject: [PATCH 1/4] web: Remove ProposalTransactionalInfo --- .../components/storage/PartitionPage.test.tsx | 1 - .../components/storage/ProposalPage.test.tsx | 1 - web/src/components/storage/ProposalPage.tsx | 2 - .../ProposalTransactionalInfo.test.tsx | 82 ------------------- .../storage/ProposalTransactionalInfo.tsx | 57 ------------- web/src/components/storage/index.ts | 1 - web/src/components/storage/utils.test.ts | 36 -------- web/src/components/storage/utils.ts | 22 +---- 8 files changed, 3 insertions(+), 199 deletions(-) delete mode 100644 web/src/components/storage/ProposalTransactionalInfo.test.tsx delete mode 100644 web/src/components/storage/ProposalTransactionalInfo.tsx diff --git a/web/src/components/storage/PartitionPage.test.tsx b/web/src/components/storage/PartitionPage.test.tsx index 8f9090094b..c17b6511a7 100644 --- a/web/src/components/storage/PartitionPage.test.tsx +++ b/web/src/components/storage/PartitionPage.test.tsx @@ -29,7 +29,6 @@ import type { Storage } from "~/model/system"; import { gib } from "./utils"; jest.mock("./ProposalResultSection", () => () =>
result section
); -jest.mock("./ProposalTransactionalInfo", () => () =>
transactional info
); const sda1: Storage.Device = { sid: 69, diff --git a/web/src/components/storage/ProposalPage.test.tsx b/web/src/components/storage/ProposalPage.test.tsx index e790fe458d..8ea6b5df9c 100644 --- a/web/src/components/storage/ProposalPage.test.tsx +++ b/web/src/components/storage/ProposalPage.test.tsx @@ -102,7 +102,6 @@ jest.mock("~/queries/storage/dasd", () => ({ useDASDSupported: () => mockUseDASDSupported(), })); -jest.mock("./ProposalTransactionalInfo", () => () =>
trasactional info
); jest.mock("./ProposalFailedInfo", () => () =>
proposal failed info
); jest.mock("./UnsupportedModelInfo", () => () =>
unsupported model info
); jest.mock("./FixableConfigInfo", () => () =>
fixable config info
); diff --git a/web/src/components/storage/ProposalPage.tsx b/web/src/components/storage/ProposalPage.tsx index e707d2cc6b..a5aab39257 100644 --- a/web/src/components/storage/ProposalPage.tsx +++ b/web/src/components/storage/ProposalPage.tsx @@ -49,7 +49,6 @@ import BootSection from "./BootSection"; import FixableConfigInfo from "./FixableConfigInfo"; import ProposalFailedInfo from "./ProposalFailedInfo"; import ProposalResultSection from "./ProposalResultSection"; -import ProposalTransactionalInfo from "./ProposalTransactionalInfo"; import UnsupportedModelInfo from "./UnsupportedModelInfo"; import { useAvailableDevices } from "~/hooks/model/system/storage"; import { useIssues } from "~/hooks/model/issue"; @@ -288,7 +287,6 @@ function ProposalPageContent(): React.ReactNode { return ( - {!configIssues.length && !proposal && } {!!configIssues.length && } {!model && } diff --git a/web/src/components/storage/ProposalTransactionalInfo.test.tsx b/web/src/components/storage/ProposalTransactionalInfo.test.tsx deleted file mode 100644 index 77005cef98..0000000000 --- a/web/src/components/storage/ProposalTransactionalInfo.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) [2024-2025] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { screen } from "@testing-library/react"; -import { plainRender } from "~/test-utils"; -import ProposalTransactionalInfo from "./ProposalTransactionalInfo"; -import type { Storage as System } from "~/model/system"; - -jest.mock("~/hooks/model/config/product", () => ({ - ...jest.requireActual("~/hooks/model/config/product"), - useProductInfo: () => ({ name: "Test" }), -})); - -const mockVolumeTemplates = jest.fn(); - -jest.mock("~/hooks/model/system/storage", () => ({ - ...jest.requireActual("~/hooks/model/system/storage"), - useVolumeTemplates: () => mockVolumeTemplates(), -})); - -const rootVolume: System.Volume = { - mountPath: "/", - mountOptions: [], - autoSize: false, - minSize: 1024, - maxSize: 2048, - fsType: "btrfs", - snapshots: false, - transactional: false, - outline: { - required: true, - fsTypes: ["btrfs", "ext4"], - supportAutoSize: true, - snapshotsConfigurable: true, - snapshotsAffectSizes: true, - sizeRelevantVolumes: [], - adjustByRam: false, - }, -}; - -describe("if the system is not transactional", () => { - beforeEach(() => { - mockVolumeTemplates.mockReturnValue([rootVolume]); - }); - - it("renders nothing", () => { - const { container } = plainRender(); - expect(container).toBeEmptyDOMElement(); - }); -}); - -describe("if the system is transactional", () => { - beforeEach(() => { - mockVolumeTemplates.mockReturnValue([{ ...rootVolume, transactional: true }]); - }); - - it("renders an explanation about the transactional system", () => { - plainRender(); - - screen.getByText("Transactional root file system"); - }); -}); diff --git a/web/src/components/storage/ProposalTransactionalInfo.tsx b/web/src/components/storage/ProposalTransactionalInfo.tsx deleted file mode 100644 index 78f0b53b56..0000000000 --- a/web/src/components/storage/ProposalTransactionalInfo.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) [2024] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License as published by the Free - * Software Foundation; either version 2 of the License, or (at your option) - * any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import React from "react"; -import { Alert } from "@patternfly/react-core"; -import { _ } from "~/i18n"; -import { sprintf } from "sprintf-js"; -import { useProductInfo } from "~/hooks/model/config/product"; -import { useVolumeTemplates } from "~/hooks/model/system/storage"; -import { isTransactionalSystem } from "~/components/storage/utils"; - -/** - * Information about the system being transactional, if needed - * @component - * - * @param props - */ -export default function ProposalTransactionalInfo() { - const selectedProduct = useProductInfo(); - const volumes = useVolumeTemplates(); - - if (!isTransactionalSystem(volumes)) return; - - const title = _("Transactional root file system"); - /* TRANSLATORS: %s is replaced by a product name (e.g., openSUSE Tumbleweed) */ - const description = sprintf( - _( - "%s is an immutable system with atomic updates. It uses a read-only Btrfs file system updated via snapshots.", - ), - selectedProduct.name, - ); - - return ( - - {description} - - ); -} diff --git a/web/src/components/storage/index.ts b/web/src/components/storage/index.ts index 1a83c1c55f..de8937a365 100644 --- a/web/src/components/storage/index.ts +++ b/web/src/components/storage/index.ts @@ -20,7 +20,6 @@ * find current contact information at www.suse.com. */ -export { default as ProposalTransactionalInfo } from "./ProposalTransactionalInfo"; export { default as ProposalResultSection } from "./ProposalResultSection"; export { default as DevicesFormSelect } from "./DevicesFormSelect"; export { default as SpaceActionsTable } from "./SpaceActionsTable"; diff --git a/web/src/components/storage/utils.test.ts b/web/src/components/storage/utils.test.ts index 068d45a410..c1a334a62e 100644 --- a/web/src/components/storage/utils.test.ts +++ b/web/src/components/storage/utils.test.ts @@ -29,8 +29,6 @@ import { splitSize, hasFS, hasSnapshots, - isTransactionalRoot, - isTransactionalSystem, } from "./utils"; import type { Storage } from "~/model/system"; import type { Volume } from "~/model/system/storage"; @@ -260,37 +258,3 @@ describe("hasSnapshots", () => { expect(hasSnapshots(volume({ fsType: "Btrfs", snapshots: true }))).toBe(true); }); }); - -describe("isTransactionalRoot", () => { - it("returns false if the volume is not root", () => { - expect(isTransactionalRoot(volume({ mountPath: "/home", transactional: true }))).toBe(false); - }); - - it("returns false if the volume has not transactional enabled", () => { - expect(isTransactionalRoot(volume({ mountPath: "/", transactional: false }))).toBe(false); - }); - - it("returns true if the volume is root and has transactional enabled", () => { - expect(isTransactionalRoot(volume({ mountPath: "/", transactional: true }))).toBe(true); - }); -}); - -describe("isTransactionalSystem", () => { - it("returns false if volumes does not include a transactional root", () => { - expect(isTransactionalSystem([])).toBe(false); - - const volumes = [ - volume({ mountPath: "/" }), - volume({ mountPath: "/home", transactional: true }), - ]; - expect(isTransactionalSystem(volumes)).toBe(false); - }); - - it("returns true if volumes includes a transactional root", () => { - const volumes = [ - volume({ mountPath: "EXT4" }), - volume({ mountPath: "/", transactional: true }), - ]; - expect(isTransactionalSystem(volumes)).toBe(true); - }); -}); diff --git a/web/src/components/storage/utils.ts b/web/src/components/storage/utils.ts index 4c6ff3d2a6..374103eef1 100644 --- a/web/src/components/storage/utils.ts +++ b/web/src/components/storage/utils.ts @@ -22,9 +22,9 @@ /** * @fixme This file implements utils for the storage components and it also offers several functions - * to get information from a Volume (e.g., #hasSnapshots, #isTransactionalRoot, etc). It would be - * better to use another approach to encapsulate the volume information. For example, by creating - * a Volume class or by providing a kind of interface for volumes. + * to get information from a Volume (e.g., #hasSnapshots, etc). It would be better to use another + * approach to encapsulate the volume information. For example, by creating a Volume class or by + * providing a kind of interface for volumes. */ import xbytes from "xbytes"; @@ -283,20 +283,6 @@ const hasSnapshots = (volume: System.Volume): boolean => { return hasFS(volume, "btrfs") && volume.snapshots; }; -/** - * Checks whether the given volume defines a transactional root. - */ -const isTransactionalRoot = (volume: System.Volume): boolean => { - return volume.mountPath === "/" && volume.transactional; -}; - -/** - * Checks whether the given volumes defines a transactional system. - */ -const isTransactionalSystem = (volumes: System.Volume[] = []): boolean => { - return volumes.find((v) => isTransactionalRoot(v)) !== undefined; -}; - /** * Generates a label for the given volume. */ @@ -403,8 +389,6 @@ export { sizeDescription, hasFS, hasSnapshots, - isTransactionalRoot, - isTransactionalSystem, volumeLabel, createPartitionableLocation, findPartitionableDevice, From d51f5ef4a6ee52a5f3c43b8312ef6643f7c2143e Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Mon, 26 Jan 2026 15:38:36 +0000 Subject: [PATCH 2/4] service: Incorporate Btrfs information into the fsType --- .../agama-lib/share/storage.model.schema.json | 3 +- rust/agama-lib/src/storage/model.rs | 7 - rust/share/device.storage.schema.json | 2 + rust/share/system.storage.schema.json | 3 - .../from_model_conversions/filesystem_type.rb | 6 +- .../storage/config_conversions/to_model.rb | 9 +- .../to_model_conversions/config.rb | 29 ++- .../to_model_conversions/drive.rb | 7 +- .../to_model_conversions/filesystem.rb | 37 +++- .../to_model_conversions/logical_volume.rb | 7 +- .../to_model_conversions/md_raid.rb | 7 +- .../to_model_conversions/partition.rb | 6 +- .../to_model_conversions/volume_group.rb | 9 +- .../to_model_conversions/with_filesystem.rb | 2 +- .../to_model_conversions/with_partitions.rb | 2 +- service/lib/agama/storage/proposal.rb | 4 +- .../storage/volume_conversions/to_json.rb | 55 ++++-- .../config_conversions/from_model_test.rb | 103 ++++++++-- .../to_model_conversions/config_test.rb | 20 +- .../to_model_conversions/drive_test.rb | 5 +- .../to_model_conversions/examples.rb | 80 +++++++- .../logical_volume_test.rb | 5 +- .../to_model_conversions/md_raid_test.rb | 5 +- .../to_model_conversions/partition_test.rb | 5 +- .../to_model_conversions/volume_group_test.rb | 8 +- .../config_conversions/to_model_test.rb | 7 +- .../volume_conversions/to_json_test.rb | 187 +++++++++++++----- 27 files changed, 479 insertions(+), 141 deletions(-) diff --git a/rust/agama-lib/share/storage.model.schema.json b/rust/agama-lib/share/storage.model.schema.json index 973b8f473a..fb598c9f44 100644 --- a/rust/agama-lib/share/storage.model.schema.json +++ b/rust/agama-lib/share/storage.model.schema.json @@ -135,7 +135,6 @@ "reuse": { "type": "boolean" }, "default": { "type": "boolean" }, "type": { "$ref": "#/$defs/filesystemType" }, - "snapshots": { "type": "boolean" }, "label": { "type": "string" } } }, @@ -143,6 +142,8 @@ "enum": [ "bcachefs", "btrfs", + "btrfsImmutable", + "btrfsSnapshots", "exfat", "ext2", "ext3", diff --git a/rust/agama-lib/src/storage/model.rs b/rust/agama-lib/src/storage/model.rs index ff456f1e5e..385216d8c4 100644 --- a/rust/agama-lib/src/storage/model.rs +++ b/rust/agama-lib/src/storage/model.rs @@ -399,7 +399,6 @@ pub struct VolumeOutline { fs_types: Vec, support_auto_size: bool, adjust_by_ram: bool, - snapshots_configurable: bool, snapshots_affect_sizes: bool, size_relevant_volumes: Vec, } @@ -414,7 +413,6 @@ impl TryFrom> for VolumeOutline { fs_types: get_property(&mvalue, "FsTypes")?, support_auto_size: get_property(&mvalue, "SupportAutoSize")?, adjust_by_ram: get_property(&mvalue, "AdjustByRam")?, - snapshots_configurable: get_property(&mvalue, "SnapshotsConfigurable")?, snapshots_affect_sizes: get_property(&mvalue, "SnapshotsAffectSizes")?, size_relevant_volumes: get_property(&mvalue, "SizeRelevantVolumes")?, }; @@ -435,8 +433,6 @@ pub struct Volume { min_size: Option, max_size: Option, auto_size: bool, - snapshots: bool, - transactional: Option, outline: Option, } @@ -448,7 +444,6 @@ impl From for zbus::zvariant::Value<'_> { ("Target", val.target.into()), ("FsType", Value::new(val.fs_type)), ("AutoSize", Value::new(val.auto_size)), - ("Snapshots", Value::new(val.snapshots)), ]); if let Some(dev) = val.target_device { result.insert("TargetDevice", Value::new(dev)); @@ -487,8 +482,6 @@ impl TryFrom> for Volume { min_size: get_optional_property(&volume_hash, "MinSize")?, max_size: get_optional_property(&volume_hash, "MaxSize")?, auto_size: get_property(&volume_hash, "AutoSize")?, - snapshots: get_property(&volume_hash, "Snapshots")?, - transactional: get_optional_property(&volume_hash, "Transactional")?, outline: get_optional_property(&volume_hash, "Outline")?, }; diff --git a/rust/share/device.storage.schema.json b/rust/share/device.storage.schema.json index e84b7a51b7..860f828417 100644 --- a/rust/share/device.storage.schema.json +++ b/rust/share/device.storage.schema.json @@ -115,6 +115,8 @@ "enum": [ "bcachefs", "btrfs", + "btrfsSnapshots", + "btrfsImmutable", "exfat", "ext2", "ext3", diff --git a/rust/share/system.storage.schema.json b/rust/share/system.storage.schema.json index 17d1c9cbd8..73b9883353 100644 --- a/rust/share/system.storage.schema.json +++ b/rust/share/system.storage.schema.json @@ -72,8 +72,6 @@ "autoSize": { "type": "boolean" }, "minSize": { "type": "integer" }, "maxSize": { "type": "integer" }, - "snapshots": { "type": "boolean" }, - "transactional": { "type": "boolean" }, "outline": { "$ref": "#/$defs/volumeOutline" } } }, @@ -89,7 +87,6 @@ "items": { "$ref": "device.storage.schema.json#/$defs/filesystemType" } }, "adjustByRam": { "type": "boolean" }, - "snapshotsConfigurable": { "type": "boolean" }, "snapshotsAffectSizes": { "type": "boolean" }, "sizeRelevantVolumes": { "type": "array", diff --git a/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem_type.rb b/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem_type.rb index 04a7565985..637eb669f1 100644 --- a/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem_type.rb +++ b/service/lib/agama/storage/config_conversions/from_model_conversions/filesystem_type.rb @@ -55,14 +55,16 @@ def convert_type value = filesystem_model[:type] return unless value + value = "btrfs" if value.start_with?("btrfs") Y2Storage::Filesystems::Type.find(value.to_sym) end # @return [Configs::Btrfs, nil] def convert_btrfs - return unless filesystem_model[:type] == "btrfs" + type = filesystem_model[:type] + return unless type&.start_with?("btrfs") - Configs::Btrfs.new.tap { |c| c.snapshots = filesystem_model[:snapshots] } + Configs::Btrfs.new.tap { |c| c.snapshots = type != "btrfs" } end end end diff --git a/service/lib/agama/storage/config_conversions/to_model.rb b/service/lib/agama/storage/config_conversions/to_model.rb index c59ad9f12d..125862436d 100644 --- a/service/lib/agama/storage/config_conversions/to_model.rb +++ b/service/lib/agama/storage/config_conversions/to_model.rb @@ -27,21 +27,26 @@ module ConfigConversions # Config conversion to model according to the JSON schema. class ToModel # @param config [Storage::Config] - def initialize(config) + # @param product_config [Agama::Config, nil] Agama config + def initialize(config, product_config = nil) @config = config + @product_config = product_config end # Performs the conversion to config model according to the JSON schema. # # @return [Hash] def convert - ToModelConversions::Config.new(config).convert + ToModelConversions::Config.new(config, product_config).convert end private # @return [Storage::Config] attr_reader :config + + # @return [Agama::Config, nil] + attr_reader :product_config end end end diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb index 1d406db6c9..2a90ecc4f4 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/config.rb @@ -33,13 +33,18 @@ module ToModelConversions # Config conversion to model according to the JSON schema. class Config < Base # @param config [Storage::Config] - def initialize(config) + # @param product_config [Agama::Config, nil] + def initialize(config, product_config) super() @config = config + @product_config = product_config end private + # @return [Agama::Config, nil] + attr_reader :product_config + # @see Base#conversions def conversions { @@ -66,17 +71,33 @@ def convert_encryption # @return [Array] def convert_drives - config.valid_drives.map { |d| ToModelConversions::Drive.new(d).convert } + config.valid_drives.map do |drive| + ToModelConversions::Drive.new(drive, volumes).convert + end end # @return [Array] def convert_md_raids - config.valid_md_raids.map { |r| ToModelConversions::MdRaid.new(r).convert } + config.valid_md_raids.map do |raid| + ToModelConversions::MdRaid.new(raid, volumes).convert + end end # @return [Array] def convert_volume_groups - config.volume_groups.map { |v| ToModelConversions::VolumeGroup.new(v, config).convert } + config.volume_groups.map do |vol| + ToModelConversions::VolumeGroup.new(vol, config, volumes).convert + end + end + + # @return [VolumeTemplatesBuilder] + def volumes + @volumes ||= + if product_config + VolumeTemplatesBuilder.new_from_config(product_config) + else + VolumeTemplatesBuilder.new([]) + end end end end diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb index 78dbc52887..60a29e59da 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/drive.rb @@ -35,13 +35,18 @@ class Drive < Base include WithSpacePolicy # @param config [Configs::Drive] - def initialize(config) + # @param volumes [VolumeTemplatesBuilder] + def initialize(config, volumes) super() @config = config + @volumes = volumes end private + # @return [VolumeTemplatesBuilder] + attr_reader :volumes + # @see Base#conversions def conversions { diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb index 2652d885b1..2f3d7b13ec 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/filesystem.rb @@ -28,21 +28,25 @@ module ToModelConversions # Drive conversion to model according to the JSON schema. class Filesystem < Base # @param config [Configs::Filesystem] - def initialize(config) + # @param volumes [VolumeTemplatesBuilder] + def initialize(config, volumes) super() @config = config + @volumes = volumes end private + # @return [VolumeTemplatesBuilder] + attr_reader :volumes + # @see Base#conversions def conversions { - reuse: config.reuse?, - default: convert_default, - type: convert_type, - snapshots: convert_snapshots, - label: config.label + reuse: config.reuse?, + default: convert_default, + type: convert_type, + label: config.label } end @@ -57,14 +61,27 @@ def convert_default def convert_type return unless config.type&.fs_type + if config.type.fs_type.is?(:btrfs) + return "btrfsImmutable" if immutable? + return "btrfsSnapshots" if snapshots? + end + config.type.fs_type.to_s end - # @return [Boolean, nil] - def convert_snapshots - return unless config.type&.fs_type&.is?(:btrfs) + # @return [Boolean] + def snapshots? + !!config.type.btrfs&.snapshots? + end + + # @return [Boolean] + def immutable? + return false unless config.path + + volume = volumes.for(config.path) + return false unless volume - config.type.btrfs&.snapshots? + !!volume.btrfs&.read_only? end end end diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/logical_volume.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/logical_volume.rb index 769630b023..81a8524541 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/logical_volume.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/logical_volume.rb @@ -33,13 +33,18 @@ class LogicalVolume < Base include WithSize # @param config [Configs::LogicalVolume] - def initialize(config) + # @param volumes [VolumeTemplatesBuilder] + def initialize(config, volumes) super() @config = config + @volumes = volumes end private + # @return [VolumeTemplatesBuilder] + attr_reader :volumes + # @see Base#conversions def conversions { diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/md_raid.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/md_raid.rb index 168be1a450..5fd27482b1 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/md_raid.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/md_raid.rb @@ -35,13 +35,18 @@ class MdRaid < Base include WithSpacePolicy # @param config [Configs::MdRaid] - def initialize(config) + # @param volumes [VolumeTemplatesBuilder] + def initialize(config, volumes) super() @config = config + @volumes = volumes end private + # @return [VolumeTemplatesBuilder] + attr_reader :volumes + # @see Base#conversions def conversions { diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb index 8911a0a463..d6f0ac0865 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/partition.rb @@ -33,13 +33,17 @@ class Partition < Base include WithSize # @param config [Configs::Partition] - def initialize(config) + def initialize(config, volumes) super() @config = config + @volumes = volumes end private + # @return [VolumeTemplatesBuilder] + attr_reader :volumes + # @see Base#conversions def conversions { diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/volume_group.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/volume_group.rb index 9b851e9cb5..4ad39bd1f5 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/volume_group.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/volume_group.rb @@ -32,10 +32,12 @@ class VolumeGroup < Base # @param config [Configs::VolumeGroup] # @param storage_config [Storage::Config] - def initialize(config, storage_config) + # @param volumes [VolumeTemplatesBuilder] + def initialize(config, storage_config, volumes) super() @config = config @storage_config = storage_config + @volumes = volumes end private @@ -43,6 +45,9 @@ def initialize(config, storage_config) # @return [Storage::Config] attr_reader :storage_config + # @return [VolumeTemplatesBuilder] + attr_reader :volumes + # @see Base#conversions def conversions { @@ -65,7 +70,7 @@ def convert_target_devices # @return [Array] def convert_logical_volumes config.logical_volumes.map do |logical_volume| - ToModelConversions::LogicalVolume.new(logical_volume).convert + ToModelConversions::LogicalVolume.new(logical_volume, volumes).convert end end end diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/with_filesystem.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/with_filesystem.rb index 903d6609d5..0468738ba4 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/with_filesystem.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/with_filesystem.rb @@ -32,7 +32,7 @@ def convert_filesystem filesystem = config.filesystem return unless filesystem - ToModelConversions::Filesystem.new(filesystem).convert + ToModelConversions::Filesystem.new(filesystem, volumes).convert end end end diff --git a/service/lib/agama/storage/config_conversions/to_model_conversions/with_partitions.rb b/service/lib/agama/storage/config_conversions/to_model_conversions/with_partitions.rb index 592ad48008..2fa0dc55c7 100644 --- a/service/lib/agama/storage/config_conversions/to_model_conversions/with_partitions.rb +++ b/service/lib/agama/storage/config_conversions/to_model_conversions/with_partitions.rb @@ -31,7 +31,7 @@ module WithPartitions def convert_partitions config.partitions .reject(&:skipped?) - .map { |p| ToModelConversions::Partition.new(p).convert } + .map { |p| ToModelConversions::Partition.new(p, volumes).convert } .compact end end diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb index bd010f276e..1c1231f57c 100644 --- a/service/lib/agama/storage/proposal.rb +++ b/service/lib/agama/storage/proposal.rb @@ -100,7 +100,7 @@ def model_json config = config(solved: true) return unless config && model_supported?(config) - ConfigConversions::ToModel.new(config).convert + ConfigConversions::ToModel.new(config, product_config).convert end # Solves a given model. @@ -115,7 +115,7 @@ def solve_model(model_json) .convert ConfigSolver.new(product_config, storage_system).solve(config) - ConfigConversions::ToModel.new(config).convert + ConfigConversions::ToModel.new(config, product_config).convert end # Calculates a new proposal using the given JSON. diff --git a/service/lib/agama/storage/volume_conversions/to_json.rb b/service/lib/agama/storage/volume_conversions/to_json.rb index 09b270d627..fe391d6430 100644 --- a/service/lib/agama/storage/volume_conversions/to_json.rb +++ b/service/lib/agama/storage/volume_conversions/to_json.rb @@ -47,14 +47,12 @@ def initialize(volume) # @return [Hash] def convert { - mountPath: volume.mount_path.to_s, - mountOptions: volume.mount_options, - fsType: volume.fs_type&.to_s || "", - minSize: min_size_conversion, - autoSize: volume.auto_size?, - snapshots: volume.btrfs.snapshots?, - transactional: volume.btrfs.read_only?, - outline: outline_conversion + mountPath: volume.mount_path.to_s, + mountOptions: volume.mount_options, + fsType: fs_type_conversion, + minSize: min_size_conversion, + autoSize: volume.auto_size?, + outline: outline_conversion }.tap do |volume_json| # Some volumes could not have "MaxSize". max_size_conversion(volume_json) @@ -96,15 +94,42 @@ def outline_conversion outline = volume.outline { - required: outline.required?, - fsTypes: outline.filesystems.map(&:to_s), - supportAutoSize: outline.adaptive_sizes?, - adjustByRam: outline.adjust_by_ram?, - snapshotsConfigurable: outline.snapshots_configurable?, - snapshotsAffectSizes: outline.snapshots_affect_sizes?, - sizeRelevantVolumes: outline.size_relevant_volumes + required: outline.required?, + fsTypes: fs_types_conversion(outline), + supportAutoSize: outline.adaptive_sizes?, + adjustByRam: outline.adjust_by_ram?, + snapshotsAffectSizes: outline.snapshots_affect_sizes?, + sizeRelevantVolumes: outline.size_relevant_volumes } end + + # @see #convert + def fs_type_conversion + type = volume.fs_type&.to_s || "" + if type == "btrfs" + return "btrfsImmutable" if volume.btrfs.read_only? + return "btrfsSnapshots" if volume.btrfs.snapshots? + end + type + end + + # @see #outline_conversion + def fs_types_conversion(outline) + types = outline.filesystems.map(&:to_s) + if types.include?("btrfs") + idx = types.index("btrfs") + + if volume.btrfs.read_only? + types[idx] = "btrfsImmutable" + elsif outline.snapshots_configurable? + types = types[0..idx] + ["btrfsSnapshots"] + types[idx + 1..-1] + elsif volume.btrfs.snapshots? + types[idx] = "btrfsSnapshots" + end + end + + types + end end end end diff --git a/service/test/agama/storage/config_conversions/from_model_test.rb b/service/test/agama/storage/config_conversions/from_model_test.rb index 0ecf202183..239a51d331 100644 --- a/service/test/agama/storage/config_conversions/from_model_test.rb +++ b/service/test/agama/storage/config_conversions/from_model_test.rb @@ -160,11 +160,10 @@ shared_examples "with filesystem" do |config_proc| let(:filesystem) do { - reuse: reuse, - default: default, - type: type, - snapshots: true, - label: label + reuse: reuse, + default: default, + type: type, + label: label } end @@ -176,10 +175,8 @@ context "if the filesystem is default" do let(:default) { true } - context "and the type is 'btrfs'" do - let(:type) { "btrfs" } - - it "sets #filesystem to the expected value" do + RSpec.shared_examples "#filesystem set to default btrfs" do + it "sets #filesystem to the expected btrfs-related values" do config = config_proc.call(subject.convert) filesystem = config.filesystem expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem) @@ -187,7 +184,6 @@ expect(filesystem.type.default?).to eq(true) expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS) expect(filesystem.type.btrfs).to be_a(Agama::Storage::Configs::Btrfs) - expect(filesystem.type.btrfs.snapshots?).to eq(true) expect(filesystem.label).to eq("test") expect(filesystem.path).to be_nil expect(filesystem.mount_by).to be_nil @@ -196,6 +192,42 @@ end end + context "and the type is 'btrfs'" do + let(:type) { "btrfs" } + + include_examples "#filesystem set to default btrfs" + + it "sets Btrfs snapshots to false" do + config = config_proc.call(subject.convert) + filesystem = config.filesystem + expect(filesystem.type.btrfs.snapshots?).to eq(false) + end + end + + context "and the type is 'btrfsSnapshots'" do + let(:type) { "btrfsSnapshots" } + + include_examples "#filesystem set to default btrfs" + + it "sets Btrfs snapshots to true" do + config = config_proc.call(subject.convert) + filesystem = config.filesystem + expect(filesystem.type.btrfs.snapshots?).to eq(true) + end + end + + context "and the type is 'btrfsImmutable'" do + let(:type) { "btrfsSnapshots" } + + include_examples "#filesystem set to default btrfs" + + it "sets Btrfs snapshots to true" do + config = config_proc.call(subject.convert) + filesystem = config.filesystem + expect(filesystem.type.btrfs.snapshots?).to eq(true) + end + end + context "and the type is not 'btrfs'" do let(:type) { "xfs" } @@ -219,10 +251,8 @@ context "if the filesystem is not default" do let(:default) { false } - context "and the type is 'btrfs'" do - let(:type) { "btrfs" } - - it "sets #filesystem to the expected value" do + RSpec.shared_examples "#filesystem set to non-default btrfs" do + it "sets #filesystem to the expected btrfs-related values" do config = config_proc.call(subject.convert) filesystem = config.filesystem expect(filesystem).to be_a(Agama::Storage::Configs::Filesystem) @@ -230,7 +260,6 @@ expect(filesystem.type.default?).to eq(false) expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS) expect(filesystem.type.btrfs).to be_a(Agama::Storage::Configs::Btrfs) - expect(filesystem.type.btrfs.snapshots?).to eq(true) expect(filesystem.label).to eq("test") expect(filesystem.path).to be_nil expect(filesystem.mount_by).to be_nil @@ -239,6 +268,42 @@ end end + context "and the type is 'btrfs'" do + let(:type) { "btrfs" } + + include_examples "#filesystem set to non-default btrfs" + + it "sets Btrfs snapshots to false" do + config = config_proc.call(subject.convert) + filesystem = config.filesystem + expect(filesystem.type.btrfs.snapshots?).to eq(false) + end + end + + context "and the type is 'btrfsSnapshots'" do + let(:type) { "btrfsSnapshots" } + + include_examples "#filesystem set to non-default btrfs" + + it "sets Btrfs snapshots to true" do + config = config_proc.call(subject.convert) + filesystem = config.filesystem + expect(filesystem.type.btrfs.snapshots?).to eq(true) + end + end + + context "and the type is 'btrfsImmutable'" do + let(:type) { "btrfsImmutable" } + + include_examples "#filesystem set to non-default btrfs" + + it "sets Btrfs snapshots to true" do + config = config_proc.call(subject.convert) + filesystem = config.filesystem + expect(filesystem.type.btrfs.snapshots?).to eq(true) + end + end + context "and the type is not 'btrfs'" do let(:type) { "xfs" } @@ -322,10 +387,9 @@ let(:filesystem) do { - default: false, - type: "btrfs", - snapshots: true, - label: "test" + default: false, + type: "btrfs", + label: "test" } end @@ -337,7 +401,6 @@ expect(filesystem.type.default?).to eq(false) expect(filesystem.type.fs_type).to eq(Y2Storage::Filesystems::Type::BTRFS) expect(filesystem.type.btrfs).to be_a(Agama::Storage::Configs::Btrfs) - expect(filesystem.type.btrfs.snapshots?).to eq(true) expect(filesystem.label).to eq("test") expect(filesystem.path).to eq("/test") expect(filesystem.mount_by).to be_nil diff --git a/service/test/agama/storage/config_conversions/to_model_conversions/config_test.rb b/service/test/agama/storage/config_conversions/to_model_conversions/config_test.rb index 6602ffa0d4..2a2df340ed 100644 --- a/service/test/agama/storage/config_conversions/to_model_conversions/config_test.rb +++ b/service/test/agama/storage/config_conversions/to_model_conversions/config_test.rb @@ -25,7 +25,7 @@ describe Agama::Storage::ConfigConversions::ToModelConversions::Config do include_context "config" - subject { described_class.new(config) } + subject { described_class.new(config, product_config) } let(:config_json) do { @@ -97,10 +97,9 @@ resize: false, resizeIfNeeded: false, filesystem: { - reuse: false, - default: true, - type: "btrfs", - snapshots: false + reuse: false, + default: true, + type: "btrfs" }, mountPath: "/", size: { @@ -154,10 +153,9 @@ resize: false, resizeIfNeeded: false, filesystem: { - reuse: false, - default: true, - type: "btrfs", - snapshots: false + reuse: false, + default: true, + type: "btrfs" }, mountPath: "/", size: { @@ -195,7 +193,9 @@ targetDevices: [], logicalVolumes: [ { - filesystem: { reuse: false }, + filesystem: { + reuse: false + }, mountPath: "/", size: { default: true, diff --git a/service/test/agama/storage/config_conversions/to_model_conversions/drive_test.rb b/service/test/agama/storage/config_conversions/to_model_conversions/drive_test.rb index 34518d5807..856511a3d4 100644 --- a/service/test/agama/storage/config_conversions/to_model_conversions/drive_test.rb +++ b/service/test/agama/storage/config_conversions/to_model_conversions/drive_test.rb @@ -23,9 +23,10 @@ require_relative "./examples" require "agama/storage/config_conversions/from_json_conversions/drive" require "agama/storage/config_conversions/to_model_conversions/drive" +require "agama/storage/volume_templates_builder" describe Agama::Storage::ConfigConversions::ToModelConversions::Drive do - subject { described_class.new(config) } + subject { described_class.new(config, volumes) } let(:config) do Agama::Storage::ConfigConversions::FromJSONConversions::Drive @@ -42,6 +43,8 @@ } end + let(:volumes) { Agama::Storage::VolumeTemplatesBuilder.new([]) } + let(:search) { nil } let(:filesystem) { nil } let(:ptable_type) { nil } diff --git a/service/test/agama/storage/config_conversions/to_model_conversions/examples.rb b/service/test/agama/storage/config_conversions/to_model_conversions/examples.rb index 567f3c2987..b35556e29e 100644 --- a/service/test/agama/storage/config_conversions/to_model_conversions/examples.rb +++ b/service/test/agama/storage/config_conversions/to_model_conversions/examples.rb @@ -21,6 +21,7 @@ require "y2storage/blk_device" require "y2storage/refinements" +require "agama/config" using Y2Storage::Refinements::SizeCasts @@ -102,6 +103,81 @@ ) end end + + context "if btrfs with snapshots is configured" do + let(:filesystem) do + { + type: { + btrfs: { + snapshots: true + } + }, + label: "test", + path: "/" + } + end + + it "generates the expected JSON" do + model_json = subject.convert + expect(model_json[:mountPath]).to eq("/") + expect(model_json[:filesystem]).to eq( + { + reuse: false, + default: false, + type: "btrfsSnapshots", + label: "test" + } + ) + end + end + + context "if btrfs with no snapshots is configured" do + let(:filesystem) do + { + type: "btrfs", + label: "test", + path: "/" + } + end + + it "generates the expected JSON" do + model_json = subject.convert + expect(model_json[:mountPath]).to eq("/") + expect(model_json[:filesystem]).to eq( + { + reuse: false, + default: false, + type: "btrfs", + label: "test" + } + ) + end + + context "if the root product volume is configured as read-only" do + let(:volumes) { Agama::Storage::VolumeTemplatesBuilder.new_from_config(product_config) } + let(:product_config) { Agama::Config.new(product_config_data) } + let(:product_config_data) { { "storage" => { "volume_templates" => [root_template] } } } + let(:root_template) do + { + "mount_path" => "/", "filesystem" => "btrfs", + "btrfs" => { "read_only" => true, "snapshots" => true } + } + end + + it "generates the expected JSON" do + model_json = subject.convert + expect(model_json[:mountPath]).to eq("/") + expect(model_json[:filesystem]).to eq( + { + reuse: false, + default: false, + type: "btrfsImmutable", + label: "test" + } + ) + end + end + end end shared_examples "with size" do @@ -161,7 +237,9 @@ deleteIfNeeded: false, resize: false, resizeIfNeeded: false, - filesystem: { reuse: false }, + filesystem: { + reuse: false + }, mountPath: "/", size: { default: true, diff --git a/service/test/agama/storage/config_conversions/to_model_conversions/logical_volume_test.rb b/service/test/agama/storage/config_conversions/to_model_conversions/logical_volume_test.rb index 09fde89289..5ddd7eef56 100644 --- a/service/test/agama/storage/config_conversions/to_model_conversions/logical_volume_test.rb +++ b/service/test/agama/storage/config_conversions/to_model_conversions/logical_volume_test.rb @@ -23,12 +23,13 @@ require_relative "./examples" require "agama/storage/config_conversions/from_json_conversions/logical_volume" require "agama/storage/config_conversions/to_model_conversions/logical_volume" +require "agama/storage/volume_templates_builder" require "y2storage/refinements" using Y2Storage::Refinements::SizeCasts describe Agama::Storage::ConfigConversions::ToModelConversions::LogicalVolume do - subject { described_class.new(config) } + subject { described_class.new(config, volumes) } let(:config) do Agama::Storage::ConfigConversions::FromJSONConversions::LogicalVolume @@ -46,6 +47,8 @@ } end + let(:volumes) { Agama::Storage::VolumeTemplatesBuilder.new([]) } + let(:filesystem) { nil } let(:size) { nil } let(:name) { nil } diff --git a/service/test/agama/storage/config_conversions/to_model_conversions/md_raid_test.rb b/service/test/agama/storage/config_conversions/to_model_conversions/md_raid_test.rb index 695542861a..fd3c936088 100644 --- a/service/test/agama/storage/config_conversions/to_model_conversions/md_raid_test.rb +++ b/service/test/agama/storage/config_conversions/to_model_conversions/md_raid_test.rb @@ -23,9 +23,10 @@ require_relative "./examples" require "agama/storage/config_conversions/from_json_conversions/md_raid" require "agama/storage/config_conversions/to_model_conversions/md_raid" +require "agama/storage/volume_templates_builder" describe Agama::Storage::ConfigConversions::ToModelConversions::MdRaid do - subject { described_class.new(config) } + subject { described_class.new(config, volumes) } let(:config) do Agama::Storage::ConfigConversions::FromJSONConversions::MdRaid @@ -42,6 +43,8 @@ } end + let(:volumes) { Agama::Storage::VolumeTemplatesBuilder.new([]) } + let(:search) { nil } let(:filesystem) { nil } let(:ptable_type) { nil } diff --git a/service/test/agama/storage/config_conversions/to_model_conversions/partition_test.rb b/service/test/agama/storage/config_conversions/to_model_conversions/partition_test.rb index ef5829af0d..7622b8ce40 100644 --- a/service/test/agama/storage/config_conversions/to_model_conversions/partition_test.rb +++ b/service/test/agama/storage/config_conversions/to_model_conversions/partition_test.rb @@ -23,9 +23,10 @@ require_relative "./examples" require "agama/storage/config_conversions/from_json_conversions/partition" require "agama/storage/config_conversions/to_model_conversions/partition" +require "agama/storage/volume_templates_builder" describe Agama::Storage::ConfigConversions::ToModelConversions::Partition do - subject { described_class.new(config) } + subject { described_class.new(config, volumes) } let(:config) do Agama::Storage::ConfigConversions::FromJSONConversions::Partition @@ -44,6 +45,8 @@ } end + let(:volumes) { Agama::Storage::VolumeTemplatesBuilder.new([]) } + let(:search) { nil } let(:filesystem) { nil } let(:size) { nil } diff --git a/service/test/agama/storage/config_conversions/to_model_conversions/volume_group_test.rb b/service/test/agama/storage/config_conversions/to_model_conversions/volume_group_test.rb index 885cf865d4..e35cd7d8e6 100644 --- a/service/test/agama/storage/config_conversions/to_model_conversions/volume_group_test.rb +++ b/service/test/agama/storage/config_conversions/to_model_conversions/volume_group_test.rb @@ -27,7 +27,7 @@ using Y2Storage::Refinements::SizeCasts describe Agama::Storage::ConfigConversions::ToModelConversions::VolumeGroup do - subject { described_class.new(volume_group_config, config) } + subject { described_class.new(volume_group_config, config, volumes) } let(:config) do Agama::Storage::ConfigConversions::FromJSON @@ -51,6 +51,8 @@ } end + let(:volumes) { Agama::Storage::VolumeTemplatesBuilder.new([]) } + let(:drives) { nil } let(:name) { nil } let(:extent_size) { nil } @@ -148,7 +150,9 @@ } }, { - filesystem: { reuse: false }, + filesystem: { + reuse: false + }, mountPath: "/", size: { default: true, diff --git a/service/test/agama/storage/config_conversions/to_model_test.rb b/service/test/agama/storage/config_conversions/to_model_test.rb index a679ece7d5..0ccda4499d 100644 --- a/service/test/agama/storage/config_conversions/to_model_test.rb +++ b/service/test/agama/storage/config_conversions/to_model_test.rb @@ -117,10 +117,9 @@ logicalVolumes: [ { filesystem: { - reuse: false, - default: true, - type: "btrfs", - snapshots: false + reuse: false, + default: true, + type: "btrfs" }, mountPath: "/", size: { diff --git a/service/test/agama/storage/volume_conversions/to_json_test.rb b/service/test/agama/storage/volume_conversions/to_json_test.rb index 81b52c904f..a423bd8810 100644 --- a/service/test/agama/storage/volume_conversions/to_json_test.rb +++ b/service/test/agama/storage/volume_conversions/to_json_test.rb @@ -52,62 +52,157 @@ # @todo Check whether the result matches the JSON schema. expect(described_class.new(default_volume).convert).to eq( - mountPath: "/test", - mountOptions: [], - fsType: "", - minSize: 0, - autoSize: false, - snapshots: false, - transactional: false, - outline: { - required: false, - fsTypes: [], - supportAutoSize: false, - adjustByRam: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, - sizeRelevantVolumes: [] + mountPath: "/test", + mountOptions: [], + fsType: "", + minSize: 0, + autoSize: false, + outline: { + required: false, + fsTypes: [], + supportAutoSize: false, + adjustByRam: false, + snapshotsAffectSizes: false, + sizeRelevantVolumes: [] } ) expect(described_class.new(custom_volume1).convert).to eq( - mountPath: "/test", - mountOptions: [], - fsType: "xfs", - minSize: 0, - autoSize: true, - snapshots: false, - transactional: false, - outline: { - required: false, - fsTypes: [], - supportAutoSize: false, - adjustByRam: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, - sizeRelevantVolumes: [] + mountPath: "/test", + mountOptions: [], + fsType: "xfs", + minSize: 0, + autoSize: true, + outline: { + required: false, + fsTypes: [], + supportAutoSize: false, + adjustByRam: false, + snapshotsAffectSizes: false, + sizeRelevantVolumes: [] } ) expect(described_class.new(custom_volume2).convert).to eq( - mountPath: "/test", - mountOptions: ["rw", "default"], - fsType: "btrfs", - minSize: 1024, - maxSize: 2048, - autoSize: false, - snapshots: true, - transactional: false, - outline: { - required: false, - fsTypes: [], - supportAutoSize: false, - adjustByRam: false, - snapshotsConfigurable: false, - snapshotsAffectSizes: false, - sizeRelevantVolumes: [] + mountPath: "/test", + mountOptions: ["rw", "default"], + fsType: "btrfsSnapshots", + minSize: 1024, + maxSize: 2048, + autoSize: false, + outline: { + required: false, + fsTypes: [], + supportAutoSize: false, + adjustByRam: false, + snapshotsAffectSizes: false, + sizeRelevantVolumes: [] } ) end + + context "if btrfs snapshots can be configured" do + let(:volume) do + Agama::Storage::Volume.new("/").tap do |volume| + volume.fs_type = Y2Storage::Filesystems::Type::BTRFS + volume.auto_size = true + volume.btrfs.snapshots = true + volume.outline = Agama::Storage::VolumeOutline.new.tap do |outline| + outline.filesystems = [ + Y2Storage::Filesystems::Type::XFS, Y2Storage::Filesystems::Type::BTRFS + ] + outline.snapshots_configurable = true + end + end + end + + it "includes all the expected filesystem types" do + converted = described_class.new(volume).convert + expect(converted[:outline][:fsTypes]).to contain_exactly("xfs", "btrfs", "btrfsSnapshots") + end + + it "sets the correct default filesystem type" do + converted = described_class.new(volume).convert + expect(converted[:fsType]).to eq "btrfsSnapshots" + end + end + + context "if btrfs snapshots are mandatory" do + let(:volume) do + Agama::Storage::Volume.new("/").tap do |volume| + volume.fs_type = Y2Storage::Filesystems::Type::BTRFS + volume.auto_size = true + volume.btrfs.snapshots = true + volume.outline = Agama::Storage::VolumeOutline.new.tap do |outline| + outline.filesystems = [ + Y2Storage::Filesystems::Type::XFS, Y2Storage::Filesystems::Type::BTRFS + ] + outline.snapshots_configurable = false + end + end + end + + it "includes all the expected filesystem types" do + converted = described_class.new(volume).convert + expect(converted[:outline][:fsTypes]).to contain_exactly("xfs", "btrfsSnapshots") + end + + it "sets the correct default filesystem type" do + converted = described_class.new(volume).convert + expect(converted[:fsType]).to eq "btrfsSnapshots" + end + end + + context "if btrfs snapshots cannot be enabled" do + let(:volume) do + Agama::Storage::Volume.new("/").tap do |volume| + volume.fs_type = Y2Storage::Filesystems::Type::BTRFS + volume.auto_size = true + volume.btrfs.snapshots = false + volume.outline = Agama::Storage::VolumeOutline.new.tap do |outline| + outline.filesystems = [ + Y2Storage::Filesystems::Type::XFS, Y2Storage::Filesystems::Type::BTRFS + ] + outline.snapshots_configurable = false + end + end + end + + it "includes all the expected filesystem types" do + converted = described_class.new(volume).convert + expect(converted[:outline][:fsTypes]).to contain_exactly("xfs", "btrfs") + end + + it "sets the correct default filesystem type" do + converted = described_class.new(volume).convert + expect(converted[:fsType]).to eq "btrfs" + end + end + + context "if the system is configured as immutable" do + let(:volume) do + Agama::Storage::Volume.new("/").tap do |volume| + volume.fs_type = Y2Storage::Filesystems::Type::BTRFS + volume.auto_size = true + volume.btrfs.read_only = true + volume.btrfs.snapshots = true + volume.outline = Agama::Storage::VolumeOutline.new.tap do |outline| + outline.filesystems = [ + Y2Storage::Filesystems::Type::XFS, Y2Storage::Filesystems::Type::BTRFS + ] + end + end + end + + it "includes all the expected filesystem types" do + converted = described_class.new(volume).convert + expect(converted[:outline][:fsTypes]).to contain_exactly("xfs", "btrfsImmutable") + end + + it "sets the correct default filesystem type" do + converted = described_class.new(volume).convert + expect(converted[:fsType]).to eq "btrfsImmutable" + end + end end end From d36cef2e821ed208ab6e968875e0e06a31c48054 Mon Sep 17 00:00:00 2001 From: Ancor Gonzalez Sosa Date: Mon, 26 Jan 2026 16:21:03 +0000 Subject: [PATCH 3/4] web: Use new btrfs-driven fsTypes for volumes and model --- .../storage/FilesystemMenu.test.tsx | 4 +-- .../storage/FormattableDevicePage.test.tsx | 4 --- .../storage/FormattableDevicePage.tsx | 33 +++++------------ .../components/storage/LogicalVolumePage.tsx | 33 +++++------------ .../components/storage/PartitionPage.test.tsx | 2 -- web/src/components/storage/PartitionPage.tsx | 35 +++++-------------- .../storage/ProposalFailedInfo.test.tsx | 3 +- web/src/components/storage/utils.test.ts | 9 ++--- web/src/components/storage/utils.ts | 6 ++-- web/src/openapi/proposal/storage.ts | 2 ++ web/src/openapi/storage/config-model.ts | 3 +- web/src/openapi/system/storage.ts | 5 ++- 12 files changed, 40 insertions(+), 99 deletions(-) diff --git a/web/src/components/storage/FilesystemMenu.test.tsx b/web/src/components/storage/FilesystemMenu.test.tsx index 908e2cfdaa..5851e9c0d8 100644 --- a/web/src/components/storage/FilesystemMenu.test.tsx +++ b/web/src/components/storage/FilesystemMenu.test.tsx @@ -45,7 +45,7 @@ describe("FilesystemMenu", () => { const deviceModel: ConfigModel.Drive = { name: "/dev/vda", mountPath: "/home", - filesystem: { type: "btrfs", default: false, snapshots: true }, + filesystem: { type: "btrfsSnapshots", default: false }, }; mockPartitionable.mockReturnValue(deviceModel); @@ -67,7 +67,7 @@ describe("FilesystemMenu", () => { const deviceModel: ConfigModel.Drive = { name: "/dev/vda", mountPath: "/home", - filesystem: { type: "btrfs", default: false, snapshots: true }, + filesystem: { type: "btrfsSnapshots", default: false }, }; mockPartitionable.mockReturnValue(deviceModel); diff --git a/web/src/components/storage/FormattableDevicePage.test.tsx b/web/src/components/storage/FormattableDevicePage.test.tsx index caa48f6516..84b1038862 100644 --- a/web/src/components/storage/FormattableDevicePage.test.tsx +++ b/web/src/components/storage/FormattableDevicePage.test.tsx @@ -53,13 +53,10 @@ const homeVolume: Storage.Volume = { minSize: gib(1), maxSize: gib(5), autoSize: false, - snapshots: false, - transactional: false, outline: { required: false, fsTypes: ["btrfs", "xfs"], supportAutoSize: false, - snapshotsConfigurable: false, snapshotsAffectSizes: false, sizeRelevantVolumes: [], adjustByRam: false, @@ -198,7 +195,6 @@ describe("FormattableDevicePage", () => { mountPath: "/home", filesystem: { type: "xfs", - snapshots: false, label: "TEST", }, }); diff --git a/web/src/components/storage/FormattableDevicePage.tsx b/web/src/components/storage/FormattableDevicePage.tsx index 5783ea81f9..d7fb308c4e 100644 --- a/web/src/components/storage/FormattableDevicePage.tsx +++ b/web/src/components/storage/FormattableDevicePage.tsx @@ -69,7 +69,6 @@ import type { ConfigModel, Data, Partitionable } from "~/model/storage/config-mo import type { Storage as System } from "~/model/system"; const NO_VALUE = ""; -const BTRFS_SNAPSHOTS = "btrfsSnapshots"; const REUSE_FILESYSTEM = "reuse"; type DeviceModel = ConfigModel.Drive | ConfigModel.MdRaid; @@ -92,7 +91,6 @@ type ErrorsHandler = { function toData(value: FormValue): Data.Formattable { const filesystemType = (): ConfigModel.FilesystemType | undefined => { if (value.filesystem === NO_VALUE) return undefined; - if (value.filesystem === BTRFS_SNAPSHOTS) return "btrfs"; /** * @note This type cast is needed because the list of filesystems coming from a volume is not @@ -112,7 +110,6 @@ function toData(value: FormValue): Data.Formattable { return { type, - snapshots: value.filesystem === BTRFS_SNAPSHOTS, label: value.filesystemLabel, }; }; @@ -131,7 +128,6 @@ function toFormValue(deviceModel: DeviceModel): FormValue { if (!fsConfig) return NO_VALUE; if (fsConfig.reuse) return REUSE_FILESYSTEM; if (!fsConfig.type) return NO_VALUE; - if (fsConfig.type === "btrfs" && fsConfig.snapshots) return BTRFS_SNAPSHOTS; return fsConfig.type; }; @@ -165,7 +161,7 @@ function useCurrentFilesystem(): string | null { function useDefaultFilesystem(mountPoint: string): string { const volume = useVolumeTemplate(mountPoint); - return volume.mountPath === "/" && volume.snapshots ? BTRFS_SNAPSHOTS : volume.fsType; + return volume.fsType; } function useInitialFormValue(): FormValue | null { @@ -186,21 +182,7 @@ function useUsableFilesystems(mountPoint: string): string[] { const usableFilesystems = React.useMemo(() => { const volumeFilesystems = (): string[] => { - const allValues = volume.outline.fsTypes; - - if (volume.mountPath !== "/") return allValues; - - // Btrfs without snapshots is not an option. - if (!volume.outline.snapshotsConfigurable && volume.snapshots) { - return [BTRFS_SNAPSHOTS, ...allValues].filter((v) => v !== "btrfs"); - } - - // Btrfs with snapshots is not an option - if (!volume.outline.snapshotsConfigurable && !volume.snapshots) { - return allValues; - } - - return [BTRFS_SNAPSHOTS, ...allValues]; + return volume.outline.fsTypes; }; return unique([defaultFilesystem, ...volumeFilesystems()]); @@ -281,6 +263,7 @@ function mountPointSelectOptions(mountPoints: string[]): SelectOptionProps[] { type FilesystemOptionLabelProps = { value: string; + volume: System.Volume; }; function FilesystemOptionLabel({ value }: FilesystemOptionLabelProps): React.ReactNode { @@ -289,7 +272,6 @@ function FilesystemOptionLabel({ value }: FilesystemOptionLabelProps): React.Rea // TRANSLATORS: %s is a filesystem type, like Btrfs if (value === REUSE_FILESYSTEM && filesystem) return sprintf(_("Current %s"), filesystemLabel(filesystem)); - if (value === BTRFS_SNAPSHOTS) return _("Btrfs with snapshots"); return filesystemLabel(value); } @@ -317,7 +299,7 @@ function FilesystemOptions({ mountPoint }: FilesystemOptionsProps): React.ReactN {mountPoint === NO_VALUE && ( - + )} {mountPoint !== NO_VALUE && canReuse && ( @@ -328,7 +310,7 @@ function FilesystemOptions({ mountPoint }: FilesystemOptionsProps): React.ReactN sprintf(_("Do not format %s and keep the data"), deviceBaseName(device, true)) } > - + )} {mountPoint !== NO_VALUE && canReuse && usableFilesystems.length && } @@ -340,7 +322,7 @@ function FilesystemOptions({ mountPoint }: FilesystemOptionsProps): React.ReactN value={fsType} description={fsType === defaultFilesystem && defaultOptText} > - + ))} @@ -362,13 +344,14 @@ function FilesystemSelect({ mountPoint, onChange, }: FilesystemSelectProps): React.ReactNode { + const volume = useVolumeTemplate(mountPoint); const usedValue = mountPoint === NO_VALUE ? NO_VALUE : value; return ( } + label={} onChange={onChange} isDisabled={mountPoint === NO_VALUE} > diff --git a/web/src/components/storage/PartitionPage.test.tsx b/web/src/components/storage/PartitionPage.test.tsx index c17b6511a7..e40712f6d8 100644 --- a/web/src/components/storage/PartitionPage.test.tsx +++ b/web/src/components/storage/PartitionPage.test.tsx @@ -111,12 +111,10 @@ const homeVolume: Storage.Volume = { fsType: "btrfs", minSize: 1024, maxSize: 1024, - snapshots: false, autoSize: false, outline: { required: false, fsTypes: ["btrfs"], - snapshotsConfigurable: false, snapshotsAffectSizes: false, supportAutoSize: false, sizeRelevantVolumes: [], diff --git a/web/src/components/storage/PartitionPage.tsx b/web/src/components/storage/PartitionPage.tsx index 36db1f9197..948f5e2376 100644 --- a/web/src/components/storage/PartitionPage.tsx +++ b/web/src/components/storage/PartitionPage.tsx @@ -77,7 +77,6 @@ import type { Storage as System } from "~/model/system"; const NO_VALUE = ""; const NEW_PARTITION = "new"; -const BTRFS_SNAPSHOTS = "btrfsSnapshots"; const REUSE_FILESYSTEM = "reuse"; type SizeOptionValue = "" | SizeMode; @@ -111,7 +110,6 @@ function toPartitionConfig(value: FormValue): ConfigModel.Partition { const filesystemType = (): ConfigModel.FilesystemType | undefined => { if (value.filesystem === NO_VALUE) return undefined; - if (value.filesystem === BTRFS_SNAPSHOTS) return "btrfs"; /** * @note This type cast is needed because the list of filesystems coming from a volume is not @@ -132,7 +130,6 @@ function toPartitionConfig(value: FormValue): ConfigModel.Partition { return { default: false, type, - snapshots: value.filesystem === BTRFS_SNAPSHOTS, label: value.filesystemLabel, }; }; @@ -165,7 +162,6 @@ function toFormValue(partitionConfig: ConfigModel.Partition): FormValue { const fsConfig = partitionConfig.filesystem; if (fsConfig.reuse) return REUSE_FILESYSTEM; if (!fsConfig.type) return NO_VALUE; - if (fsConfig.type === "btrfs" && fsConfig.snapshots) return BTRFS_SNAPSHOTS; return fsConfig.type; }; @@ -224,8 +220,7 @@ function usePartitionFilesystem(target: string): string | null { function useDefaultFilesystem(mountPoint: string): string { const volume = useVolumeTemplate(mountPoint); - - return volume.mountPath === "/" && volume.snapshots ? BTRFS_SNAPSHOTS : volume.fsType; + return volume.fsType; } function useInitialPartitionConfig(): ConfigModel.Partition | null { @@ -272,21 +267,7 @@ function useUsableFilesystems(mountPoint: string): string[] { const usableFilesystems = React.useMemo(() => { const volumeFilesystems = (): string[] => { - const allValues = volume.outline.fsTypes; - - if (volume.mountPath !== "/") return allValues; - - // Btrfs without snapshots is not an option. - if (!volume.outline.snapshotsConfigurable && volume.snapshots) { - return [BTRFS_SNAPSHOTS, ...allValues].filter((v) => v !== "btrfs"); - } - - // Btrfs with snapshots is not an option - if (!volume.outline.snapshotsConfigurable && !volume.snapshots) { - return allValues; - } - - return [BTRFS_SNAPSHOTS, ...allValues]; + return volume.outline.fsTypes; }; return unique([defaultFilesystem, ...volumeFilesystems()]); @@ -567,16 +548,17 @@ function TargetOptions(): React.ReactNode { type FilesystemOptionLabelProps = { value: string; target: string; + volume: System.Volume; }; function FilesystemOptionLabel({ value, target }: FilesystemOptionLabelProps): React.ReactNode { const partition = usePartition(target); const filesystem = partition?.filesystem?.type; + if (value === NO_VALUE) return _("Waiting for a mount point"); // TRANSLATORS: %s is a filesystem type, like Btrfs if (value === REUSE_FILESYSTEM && filesystem) return sprintf(_("Current %s"), filesystemLabel(filesystem)); - if (value === BTRFS_SNAPSHOTS) return _("Btrfs with snapshots"); return filesystemLabel(value); } @@ -604,7 +586,7 @@ function FilesystemOptions({ mountPoint, target }: FilesystemOptionsProps): Reac {mountPoint === NO_VALUE && ( - + )} {mountPoint !== NO_VALUE && canReuse && ( @@ -613,7 +595,7 @@ function FilesystemOptions({ mountPoint, target }: FilesystemOptionsProps): Reac // TRANSLATORS: %s is the name of a partition, like /dev/vda2 description={sprintf(_("Do not format %s and keep the data"), target)} > - + )} {mountPoint !== NO_VALUE && canReuse && usableFilesystems.length && } @@ -625,7 +607,7 @@ function FilesystemOptions({ mountPoint, target }: FilesystemOptionsProps): Reac value={fsType} description={fsType === defaultFilesystem && defaultOptText} > - + ))} @@ -649,13 +631,14 @@ function FilesystemSelect({ target, onChange, }: FilesystemSelectProps): React.ReactNode { + const volume = useVolumeTemplate(mountPoint); const usedValue = mountPoint === NO_VALUE ? NO_VALUE : value; return (