diff --git a/rust/agama-lib/share/storage.schema.json b/rust/agama-lib/share/storage.schema.json index 6ef176c028..18d3265729 100644 --- a/rust/agama-lib/share/storage.schema.json +++ b/rust/agama-lib/share/storage.schema.json @@ -10,7 +10,7 @@ "drives": { "description": "Drives (disks, BIOS RAIDs and multipath devices).", "type": "array", - "items": { "$ref": "#/$defs/drive" } + "items": { "$ref": "#/$defs/driveElement" } }, "volumeGroups": { "description": "LVM volume groups.", @@ -37,7 +37,7 @@ } } }, - "drive": { + "driveElement": { "anyOf": [ { "$ref": "#/$defs/formattedDrive" }, { "$ref": "#/$defs/partitionedDrive" } @@ -49,7 +49,7 @@ "additionalProperties": false, "required": ["filesystem"], "properties": { - "search": { "$ref": "#/$defs/search" }, + "search": { "$ref": "#/$defs/searchElement" }, "alias": { "$ref": "#/$defs/alias" }, "encryption": { "$ref": "#/$defs/encryption" }, "filesystem": { "$ref": "#/$defs/filesystem" } @@ -59,10 +59,105 @@ "type": "object", "additionalProperties": false, "properties": { - "search": { "$ref": "#/$defs/search" }, + "search": { "$ref": "#/$defs/searchElement" }, "alias": { "$ref": "#/$defs/alias" }, "ptableType": { "$ref": "#/$defs/ptableType" }, - "partitions": { "$ref": "#/$defs/partitions" } + "partitions": { + "type": "array", + "items": { "$ref": "#/$defs/partitionElement" } + } + } + }, + "ptableType": { + "description": "Partition table type.", + "$comment": "The partition table is created only if all the current partitions are deleted.", + "enum": ["gpt", "msdos", "dasd"] + }, + "partitionElement": { + "anyOf": [ + { "$ref": "#/$defs/simpleVolumesGenerator" }, + { "$ref": "#/$defs/advancedPartitionsGenerator" }, + { "$ref": "#/$defs/regularPartition" }, + { "$ref": "#/$defs/partitionToDelete" }, + { "$ref": "#/$defs/PartitionToDeleteIfNeeded" } + ] + }, + "simpleVolumesGenerator": { + "description": "Automatically creates the default or mandatory volumes configured by the selected product.", + "type": "object", + "additionalProperties": false, + "required": ["generate"], + "properties": { + "generate": { + "enum": ["default", "mandatory"] + } + } + }, + "advancedPartitionsGenerator": { + "description": "Creates the default or mandatory partitions configured by the selected product.", + "type": "object", + "additionalProperties": false, + "required": ["generate"], + "properties": { + "generate": { + "type": "object", + "additionalProperties": false, + "required": ["partitions"], + "properties": { + "partitions": { + "enum": ["default", "mandatory"] + }, + "encryption": { "$ref": "#/$defs/encryption" } + } + } + } + }, + "regularPartition": { + "type": "object", + "additionalProperties": false, + "properties": { + "search": { "$ref": "#/$defs/searchElement" }, + "alias": { "$ref": "#/$defs/alias" }, + "id": { + "title": "Partition id", + "enum": [ + "linux", + "swap", + "lvm", + "raid", + "esp", + "prep", + "bios_boot" + ] + }, + "size": { "$ref": "#/$defs/size" }, + "encryption": { "$ref": "#/$defs/encryption" }, + "filesystem": { "$ref": "#/$defs/filesystem" } + } + }, + "partitionToDelete": { + "type": "object", + "additionalProperties": false, + "required": ["delete", "search"], + "properties": { + "search": { "$ref": "#/$defs/searchElement" }, + "delete": { + "description": "Delete the partition.", + "const": true + } + } + }, + "PartitionToDeleteIfNeeded": { + "type": "object", + "additionalProperties": false, + "required": ["deleteIfNeeded", "search"], + "properties": { + "search": { "$ref": "#/$defs/searchElement" }, + "deleteIfNeeded": { + "description": "Delete the partition if needed to make space.", + "const": true + }, + "size": { "$ref": "#/$defs/size" } } }, "volumeGroup": { @@ -77,42 +172,22 @@ }, "extentSize": { "$ref": "#/$defs/sizeValue" }, "physicalVolumes": { - "title": "Physical volumes", "description": "Devices to use as physical volumes.", "type": "array", - "items": { - "anyOf": [ - { "$ref": "#/$defs/alias" }, - { "$ref": "#/$defs/simplePhysicalVolumesGenerator" }, - { "$ref": "#/$defs/advancedPhysicalVolumesGenerator" } - ] - } + "items": { "$ref": "#/$defs/physicalVolumeElement" } }, "logicalVolumes": { - "title": "Logical volumes", "type": "array", - "items": { - "anyOf": [ - { "$ref": "#/$defs/simpleVolumesGenerator" }, - { "$ref": "#/$defs/advancedLogicalVolumesGenerator" }, - { "$ref": "#/$defs/logicalVolume" }, - { "$ref": "#/$defs/thinPoolLogicalVolume" }, - { "$ref": "#/$defs/thinLogicalVolume" } - ] - } + "items": { "$ref": "#/$defs/logicalVolumeElement" } } } }, - "simpleVolumesGenerator": { - "description": "Automatically creates the default or mandatory volumes configured by the selected product.", - "type": "object", - "additionalProperties": false, - "required": ["generate"], - "properties": { - "generate": { - "enum": ["default", "mandatory"] - } - } + "physicalVolumeElement": { + "anyOf": [ + { "$ref": "#/$defs/alias" }, + { "$ref": "#/$defs/simplePhysicalVolumesGenerator" }, + { "$ref": "#/$defs/advancedPhysicalVolumesGenerator" } + ] }, "simplePhysicalVolumesGenerator": { "description": "Automatically creates the needed physical volumes in the indicated devices.", @@ -146,6 +221,15 @@ } } }, + "logicalVolumeElement": { + "anyOf": [ + { "$ref": "#/$defs/simpleVolumesGenerator" }, + { "$ref": "#/$defs/advancedLogicalVolumesGenerator" }, + { "$ref": "#/$defs/logicalVolume" }, + { "$ref": "#/$defs/thinPoolLogicalVolume" }, + { "$ref": "#/$defs/thinLogicalVolume" } + ] + }, "advancedLogicalVolumesGenerator": { "description": "Automatically creates the default or mandatory logical volumes configured by the selected product.", "type": "object", @@ -226,102 +310,18 @@ "minimum": 1, "maximum": 128 }, - "ptableType": { - "description": "Partition table type.", - "$comment": "The partition table is created only if all the current partitions are deleted.", - "enum": ["gpt", "msdos", "dasd"] - }, - "partitions": { - "type": "array", - "items": { - "anyOf": [ - { "$ref": "#/$defs/simpleVolumesGenerator" }, - { "$ref": "#/$defs/advancedPartitionsGenerator" }, - { "$ref": "#/$defs/partition" }, - { "$ref": "#/$defs/partitionToDelete" }, - { "$ref": "#/$defs/PartitionToDeleteIfNeeded" } - ] - } - }, - "advancedPartitionsGenerator": { - "description": "Creates the default or mandatory partitions configured by the selected product.", - "type": "object", - "additionalProperties": false, - "required": ["generate"], - "properties": { - "generate": { - "type": "object", - "additionalProperties": false, - "required": ["partitions"], - "properties": { - "partitions": { - "enum": ["default", "mandatory"] - }, - "encryption": { "$ref": "#/$defs/encryption" } - } - } - } - }, - "partition": { - "type": "object", - "additionalProperties": false, - "properties": { - "search": { "$ref": "#/$defs/search" }, - "alias": { "$ref": "#/$defs/alias" }, - "id": { - "title": "Partition id", - "enum": [ - "linux", - "swap", - "lvm", - "raid", - "esp", - "prep", - "bios_boot" - ] - }, - "size": { "$ref": "#/$defs/size" }, - "encryption": { "$ref": "#/$defs/encryption" }, - "filesystem": { "$ref": "#/$defs/filesystem" } - } - }, - "partitionToDelete": { - "type": "object", - "additionalProperties": false, - "required": ["delete", "search"], - "properties": { - "search": { "$ref": "#/$defs/search" }, - "delete": { - "description": "Delete the partition.", - "const": true - } - } - }, - "PartitionToDeleteIfNeeded": { - "type": "object", - "additionalProperties": false, - "required": ["deleteIfNeeded", "search"], - "properties": { - "search": { "$ref": "#/$defs/search" }, - "deleteIfNeeded": { - "description": "Delete the partition if needed to make space.", - "const": true - }, - "size": { "$ref": "#/$defs/size" } - } - }, - "search": { + "searchElement": { "anyOf": [ - { "$ref": "#/$defs/searchAll" }, - { "$ref": "#/$defs/searchByName" }, + { "$ref": "#/$defs/simpleSearchAll" }, + { "$ref": "#/$defs/simpleSearchByName" }, { "$ref": "#/$defs/advancedSearch" } ] }, - "searchAll": { + "simpleSearchAll": { "description": "Shortcut to match all devices if there is any (equivalent to specify no conditions and to skip the entry if no device is found).", "const": "*" }, - "searchByName": { + "simpleSearchByName": { "descrition": "Search by device name", "type": "string", "examples": ["/dev/vda", "/dev/disk/by-id/ata-WDC_WD3200AAKS-75L9"] @@ -337,7 +337,7 @@ "additionalProperties": false, "required": ["name"], "properties": { - "name": { "$ref": "#/$defs/searchByName" } + "name": { "$ref": "#/$defs/simpleSearchByName" } } }, "max": { @@ -398,6 +398,7 @@ "anyOf": [ { "$ref": "#/$defs/sizeValue" }, { + "title": "Size current", "description": "The current size of the device.", "const": "current" } diff --git a/web/src/api/storage.ts b/web/src/api/storage.ts index d8519e569b..2f3608427d 100644 --- a/web/src/api/storage.ts +++ b/web/src/api/storage.ts @@ -32,9 +32,11 @@ import { config } from "~/api/storage/types"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const probe = (): Promise => post("/api/storage/probe"); -const fetchConfig = (): Promise => get("/api/storage/config"); +const fetchConfig = (): Promise => + get("/api/storage/config").then((config) => config.storage); -const fetchSolvedConfig = (): Promise => get("/api/storage/solved_config"); +const fetchSolvedConfig = (): Promise => + get("/api/storage/solved_config").then((config) => config.storage); const setConfig = (config: config.Config) => put("/api/storage/config", config); diff --git a/web/src/api/storage/types/checks.ts b/web/src/api/storage/types/checks.ts new file mode 100644 index 0000000000..9dd7c1e4a0 --- /dev/null +++ b/web/src/api/storage/types/checks.ts @@ -0,0 +1,107 @@ +/* + * 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 * as config from "./config"; + +// Type guards. + +export function isFormattedDrive(drive: config.DriveElement): drive is config.FormattedDrive { + return "filesystem" in drive; +} + +export function isPartitionedDrive(drive: config.DriveElement): drive is config.PartitionedDrive { + return !("filesystem" in drive); +} + +export function isSimpleSearchAll(search: config.SearchElement): search is config.SimpleSearchAll { + return search === "*"; +} + +export function isSimpleSearchByName( + search: config.SearchElement, +): search is config.SimpleSearchByName { + return !isSimpleSearchAll(search) && typeof search === "string"; +} + +export function isAdvancedSearch(search: config.SearchElement): search is config.AdvancedSearch { + return !isSimpleSearchAll(search) && !isSimpleSearchByName(search); +} + +export function isPartitionToDelete( + partition: config.PartitionElement, +): partition is config.PartitionToDelete { + return "delete" in partition; +} + +export function isPartitionToDeleteIfNeeded( + partition: config.PartitionElement, +): partition is config.PartitionToDeleteIfNeeded { + return "deleteIfNeeded" in partition; +} + +export function isRegularPartition( + partition: config.PartitionElement, +): partition is config.RegularPartition { + if ("generate" in partition) return false; + + return !isPartitionToDelete(partition) && !isPartitionToDeleteIfNeeded(partition); +} + +export function isFilesystemTypeAny( + fstype: config.FilesystemType, +): fstype is config.FilesystemTypeAny { + return typeof fstype === "string"; +} + +export function isFilesystemTypeBtrfs( + fstype: config.FilesystemType, +): fstype is config.FilesystemTypeBtrfs { + return !isFilesystemTypeAny(fstype) && "btrfs" in fstype; +} + +export function isSizeCurrent(size: config.SizeValueWithCurrent): size is config.SizeCurrent { + return size === "current"; +} + +export function isSizeBytes( + size: config.Size | config.SizeValueWithCurrent, +): size is config.SizeBytes { + return typeof size === "number"; +} + +export function isSizeString( + size: config.Size | config.SizeValueWithCurrent, +): size is config.SizeString { + return typeof size === "string" && size !== "current"; +} + +export function isSizeValue(size: config.Size): size is config.SizeValue { + return isSizeBytes(size) || isSizeString(size); +} + +export function isSizeTuple(size: config.Size): size is config.SizeTuple { + return Array.isArray(size); +} + +export function isSizeRange(size: config.Size): size is config.SizeRange { + return !isSizeTuple(size) && typeof size === "object"; +} diff --git a/web/src/api/storage/types/config.ts b/web/src/api/storage/types/config.ts index c70c8c5a9c..6245c83964 100644 --- a/web/src/api/storage/types/config.ts +++ b/web/src/api/storage/types/config.ts @@ -5,13 +5,13 @@ * and run json-schema-to-typescript to regenerate this file. */ -export type Drive = FormattedDrive | PartitionedDrive; -export type Search = SearchAll | SearchByName | AdvancedSearch; +export type DriveElement = FormattedDrive | PartitionedDrive; +export type SearchElement = SimpleSearchAll | SimpleSearchByName | AdvancedSearch; /** * Shortcut to match all devices if there is any (equivalent to specify no conditions and to skip the entry if no device is found). */ -export type SearchAll = "*"; -export type SearchByName = string; +export type SimpleSearchAll = "*"; +export type SimpleSearchByName = string; /** * How to handle the section if the device is not found. */ @@ -64,6 +64,12 @@ export type MountBy = "device" | "id" | "label" | "path" | "uuid"; * Partition table type. */ export type PtableType = "gpt" | "msdos" | "dasd"; +export type PartitionElement = + | SimpleVolumesGenerator + | AdvancedPartitionsGenerator + | RegularPartition + | PartitionToDelete + | PartitionToDeleteIfNeeded; export type PartitionId = "linux" | "swap" | "lvm" | "raid" | "esp" | "prep" | "bios_boot"; export type Size = SizeValue | SizeTuple | SizeRange; export type SizeValue = SizeString | SizeBytes; @@ -82,29 +88,22 @@ export type SizeBytes = number; * @maxItems 2 */ export type SizeTuple = [SizeValueWithCurrent] | [SizeValueWithCurrent, SizeValueWithCurrent]; -export type SizeValueWithCurrent = SizeValue | "current"; -export type Partitions = ( - | SimpleVolumesGenerator - | AdvancedPartitionsGenerator - | Partition - | PartitionToDelete - | PartitionToDeleteIfNeeded -)[]; -/** - * Devices to use as physical volumes. - */ -export type PhysicalVolumes = (Alias | SimplePhysicalVolumesGenerator | AdvancedPhysicalVolumesGenerator)[]; +export type SizeValueWithCurrent = SizeValue | SizeCurrent; /** - * Number of stripes. + * The current size of the device. */ -export type LogicalVolumeStripes = number; -export type LogicalVolumes = ( +export type SizeCurrent = "current"; +export type PhysicalVolumeElement = Alias | SimplePhysicalVolumesGenerator | AdvancedPhysicalVolumesGenerator; +export type LogicalVolumeElement = | SimpleVolumesGenerator | AdvancedLogicalVolumesGenerator | LogicalVolume | ThinPoolLogicalVolume - | ThinLogicalVolume -)[]; + | ThinLogicalVolume; +/** + * Number of stripes. + */ +export type LogicalVolumeStripes = number; /** * Storage config. @@ -114,7 +113,7 @@ export interface Config { /** * Drives (disks, BIOS RAIDs and multipath devices). */ - drives?: Drive[]; + drives?: DriveElement[]; /** * LVM volume groups. */ @@ -137,7 +136,7 @@ export interface Boot { * Drive without a partition table (e.g., directly formatted). */ export interface FormattedDrive { - search?: Search; + search?: SearchElement; alias?: Alias; encryption?: Encryption; filesystem: Filesystem; @@ -154,7 +153,7 @@ export interface AdvancedSearch { ifNotFound?: SearchAction; } export interface SearchCondition { - name: SearchByName; + name: SimpleSearchByName; } /** * LUKS1 encryption. @@ -233,10 +232,10 @@ export interface FilesystemTypeBtrfs { }; } export interface PartitionedDrive { - search?: Search; + search?: SearchElement; alias?: Alias; ptableType?: PtableType; - partitions?: Partitions; + partitions?: PartitionElement[]; } /** * Automatically creates the default or mandatory volumes configured by the selected product. @@ -253,8 +252,8 @@ export interface AdvancedPartitionsGenerator { encryption?: Encryption; }; } -export interface Partition { - search?: Search; +export interface RegularPartition { + search?: SearchElement; alias?: Alias; id?: PartitionId; size?: Size; @@ -269,14 +268,14 @@ export interface SizeRange { max?: SizeValueWithCurrent; } export interface PartitionToDelete { - search: Search; + search: SearchElement; /** * Delete the partition. */ delete: true; } export interface PartitionToDeleteIfNeeded { - search: Search; + search: SearchElement; /** * Delete the partition if needed to make space. */ @@ -292,8 +291,11 @@ export interface VolumeGroup { */ name?: string; extentSize?: SizeValue; - physicalVolumes?: PhysicalVolumes; - logicalVolumes?: LogicalVolumes; + /** + * Devices to use as physical volumes. + */ + physicalVolumes?: PhysicalVolumeElement[]; + logicalVolumes?: LogicalVolumeElement[]; } /** * Automatically creates the needed physical volumes in the indicated devices. diff --git a/web/src/queries/storage.ts b/web/src/queries/storage.ts index d37c7035d8..a6a53b7a48 100644 --- a/web/src/queries/storage.ts +++ b/web/src/queries/storage.ts @@ -51,6 +51,7 @@ import { Volume, VolumeTarget, } from "~/types/storage"; +import * as ConfigModel from "~/storage/model/config"; import { QueryHookOptions } from "~/types/queries"; @@ -149,6 +150,18 @@ const useConfigMutation = () => { return useMutation(query); }; +/** + * Hook that returns the config devices. + */ +const useConfigDevices = (options?: QueryHookOptions): ConfigModel.Device[] => { + const config = useConfig(options); + const solvedConfig = useSolvedConfig(options); + + if (!config || !solvedConfig) return []; + + return ConfigModel.generate(config, solvedConfig); +}; + /** * Hook that returns the list of storage devices for the given scope. * @@ -334,6 +347,7 @@ export { useConfig, useSolvedConfig, useConfigMutation, + useConfigDevices, useDevices, useAvailableDevices, useProductParams, diff --git a/web/src/storage/model.ts b/web/src/storage/model.ts new file mode 100644 index 0000000000..ecbb67c203 --- /dev/null +++ b/web/src/storage/model.ts @@ -0,0 +1,25 @@ +/* + * 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 * as config from "~/storage/model/config"; + +export { config }; diff --git a/web/src/storage/model/config.test.ts b/web/src/storage/model/config.test.ts new file mode 100644 index 0000000000..eed3735cef --- /dev/null +++ b/web/src/storage/model/config.test.ts @@ -0,0 +1,90 @@ +/* + * 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 * as model from "~/storage/model/config"; + +describe("#generate", () => { + it("returns a list of devices", () => { + expect( + model.generate( + { + drives: [ + { partitions: [{ search: "*", delete: true }] }, + { partitions: [{ filesystem: { path: "/test" } }] }, + ], + }, + { + drives: [ + { + search: "/dev/vda", + partitions: [ + { search: "/dev/vda1", delete: true }, + { search: "/dev/vda2", delete: true }, + ], + }, + { + search: "/dev/vdb", + partitions: [ + { + filesystem: { type: "xfs", path: "/test" }, + size: { min: 1024, max: 2048 }, + }, + ], + }, + ], + }, + ), + ).toEqual([ + { + name: "/dev/vda", + alias: undefined, + spacePolicy: "delete", + partitions: [ + { + name: "/dev/vda1", + delete: true, + }, + { + name: "/dev/vda2", + delete: true, + }, + ], + }, + { + name: "/dev/vdb", + alias: undefined, + spacePolicy: "keep", + partitions: [ + { + name: undefined, + alias: undefined, + resizeIfNeeded: true, + filesystem: "xfs", + mountPath: "/test", + snapshots: undefined, + size: { min: 1024, max: 2048 }, + }, + ], + }, + ]); + }); +}); diff --git a/web/src/storage/model/config.ts b/web/src/storage/model/config.ts new file mode 100644 index 0000000000..fc3a121b88 --- /dev/null +++ b/web/src/storage/model/config.ts @@ -0,0 +1,56 @@ +/* + * 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 { config } from "~/api/storage/types"; +import { Drive, generate as generateDrive } from "~/storage/model/config/drive"; + +export type Device = Drive; + +class ConfigDevicesGenerator { + private config: config.Config; + private solvedConfig: config.Config; + + constructor(config: config.Config, solvedConfig: config.Config) { + this.config = config; + this.solvedConfig = solvedConfig; + } + + generate(): Device[] { + return this.generateDrives(); + } + + private generateDrives(): Drive[] { + const solvedDriveConfigs = this.solvedConfig.drives || []; + return solvedDriveConfigs.map((c, i) => this.generateDrive(c, i)); + } + + private generateDrive(solvedDriveConfig: config.DriveElement, id: number): Drive { + // TODO: Use an index to associate a drive config with an unsolved drive config. + const driveConfig = (this.config.drives || [])[id]; + return generateDrive(driveConfig, solvedDriveConfig); + } +} + +export function generate(config: config.Config, solvedConfig: config.Config): Device[] { + const generator = new ConfigDevicesGenerator(config, solvedConfig); + return generator.generate(); +} diff --git a/web/src/storage/model/config/common.test.ts b/web/src/storage/model/config/common.test.ts new file mode 100644 index 0000000000..a63d417b84 --- /dev/null +++ b/web/src/storage/model/config/common.test.ts @@ -0,0 +1,154 @@ +/* + * 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 * as model from "~/storage/model/config/common"; + +describe("#generateName", () => { + it("returns the device name from the search section", () => { + expect( + model.generateName({ + search: "/dev/vda", + }), + ).toEqual("/dev/vda"); + + expect( + model.generateName({ + search: { + condition: { + name: "/dev/vda", + }, + }, + }), + ).toEqual("/dev/vda"); + }); + + it("returns undefined if the search section has no name", () => { + expect( + model.generateName({ + search: "*", + }), + ).toBeUndefined; + + expect( + model.generateName({ + search: { + max: 5, + ifNotFound: "skip", + }, + }), + ).toBeUndefined; + }); + + it("returns undefined if there is no search section", () => { + expect(model.generateName({})).toBeUndefined; + }); +}); + +describe("#generateFilesystem", () => { + it("returns the file system type from the filesystem section", () => { + expect( + model.generateFilesystem({ + filesystem: { + type: "xfs", + }, + }), + ).toEqual("xfs"); + + expect( + model.generateFilesystem({ + filesystem: { + type: { + btrfs: { + snapshots: true, + }, + }, + }, + }), + ).toEqual("btrfs"); + }); + + it("returns undefined if the filesystem section has no type", () => { + expect( + model.generateFilesystem({ + filesystem: { + path: "/", + }, + }), + ).toBeUndefined; + }); + + it("returns undefined if there is no filesystem section", () => { + expect(model.generateFilesystem({})).toBeUndefined; + }); +}); + +describe("#generateSnapshots", () => { + it("returns the snapshots value from the filesystem section", () => { + expect( + model.generateSnapshots({ + filesystem: { + type: { + btrfs: { + snapshots: true, + }, + }, + }, + }), + ).toEqual(true); + + expect( + model.generateSnapshots({ + filesystem: { + type: { + btrfs: { + snapshots: false, + }, + }, + }, + }), + ).toEqual(false); + }); + + it("returns undefined if the filesystem section has no snapshots", () => { + expect( + model.generateSnapshots({ + filesystem: { + type: "btrfs", + }, + }), + ).toBeUndefined; + + expect( + model.generateSnapshots({ + filesystem: { + type: { + btrfs: {}, + }, + }, + }), + ).toBeUndefined; + }); + + it("returns undefined if there is no filesystem section", () => { + expect(model.generateSnapshots({})).toBeUndefined; + }); +}); diff --git a/web/src/storage/model/config/common.ts b/web/src/storage/model/config/common.ts new file mode 100644 index 0000000000..3dc8a9c37d --- /dev/null +++ b/web/src/storage/model/config/common.ts @@ -0,0 +1,57 @@ +/* + * 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 { config } from "~/api/storage/types"; +import * as checks from "~/api/storage/types/checks"; + +interface WithFilesystem { + filesystem?: config.Filesystem; +} + +interface WithSearch { + search?: config.SearchElement; +} + +export function generateName(config: Type): string | undefined { + const search = config.search; + + if (!search) return; + if (checks.isSimpleSearchAll(search)) return; + if (checks.isSimpleSearchByName(search)) return search; + if (checks.isAdvancedSearch(search)) return search.condition?.name; +} + +export function generateFilesystem(config: Type): string | undefined { + const fstype = config.filesystem?.type; + + if (!fstype) return; + if (checks.isFilesystemTypeBtrfs(fstype)) return "btrfs"; + if (checks.isFilesystemTypeAny(fstype)) return fstype; +} + +export function generateSnapshots(config: Type): boolean | undefined { + const fstype = config.filesystem?.type; + + if (!fstype) return; + if (checks.isFilesystemTypeAny(fstype)) return; + if (checks.isFilesystemTypeBtrfs(fstype)) return fstype.btrfs.snapshots; +} diff --git a/web/src/storage/model/config/drive.test.ts b/web/src/storage/model/config/drive.test.ts new file mode 100644 index 0000000000..82f1c9ed55 --- /dev/null +++ b/web/src/storage/model/config/drive.test.ts @@ -0,0 +1,100 @@ +/* + * 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 * as model from "~/storage/model/config/drive"; + +describe("#generate", () => { + it("returns a drive object from a drive section", () => { + expect( + model.generate(undefined, { + search: "/dev/vda", + alias: "test", + filesystem: { + type: "xfs", + path: "/test", + }, + }), + ).toEqual({ + name: "/dev/vda", + alias: "test", + filesystem: "xfs", + mountPath: "/test", + snapshots: undefined, + spacePolicy: undefined, + }); + + expect( + model.generate( + { + partitions: [{ search: "*", delete: true }], + }, + { + search: "/dev/vda", + alias: "test", + filesystem: { + type: { + btrfs: { snapshots: false }, + }, + path: "/test", + }, + }, + ), + ).toEqual({ + name: "/dev/vda", + alias: "test", + filesystem: "btrfs", + mountPath: "/test", + snapshots: false, + spacePolicy: "delete", + }); + + expect( + model.generate( + { + partitions: [{ search: "*", delete: true }, { generate: "default" }], + }, + { + search: "/dev/vda", + partitions: [{ search: "/dev/vda1", delete: true }, { filesystem: { path: "/" } }], + }, + ), + ).toEqual({ + name: "/dev/vda", + alias: undefined, + spacePolicy: "delete", + partitions: [ + { + name: "/dev/vda1", + delete: true, + }, + { + name: undefined, + alias: undefined, + filesystem: undefined, + mountPath: "/", + snapshots: undefined, + size: undefined, + }, + ], + }); + }); +}); diff --git a/web/src/storage/model/config/drive.ts b/web/src/storage/model/config/drive.ts new file mode 100644 index 0000000000..7d6aa54115 --- /dev/null +++ b/web/src/storage/model/config/drive.ts @@ -0,0 +1,98 @@ +/* + * 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 { config } from "~/api/storage/types"; +import * as checks from "~/api/storage/types/checks"; +import { + Partition, + isPartitionConfig, + generate as generatePartition, +} from "~/storage/model/config/partition"; +import { SpacePolicy, generate as generateSpacePolicy } from "~/storage/model/config/space-policy"; +import { generateName, generateFilesystem, generateSnapshots } from "~/storage/model/config/common"; + +export type Drive = { + name?: string; + alias?: string; + filesystem?: string; + mountPath?: string; + snapshots?: boolean; + spacePolicy?: SpacePolicy; + partitions?: Partition[]; +}; + +class DriveGenerator { + private driveConfig: config.DriveElement | undefined; + private solvedDriveConfig: config.DriveElement; + + constructor( + driveConfig: config.DriveElement | undefined, + solvedDriveConfig: config.DriveElement, + ) { + this.driveConfig = driveConfig; + this.solvedDriveConfig = solvedDriveConfig; + } + + generate(): Drive { + if (checks.isFormattedDrive(this.solvedDriveConfig)) { + return this.fromFormattedDrive(this.solvedDriveConfig); + } else if (checks.isPartitionedDrive(this.solvedDriveConfig)) { + return this.fromPartitionedDrive(this.solvedDriveConfig); + } + } + + private fromFormattedDrive(solvedDriveConfig: config.FormattedDrive): Drive { + return { + name: generateName(solvedDriveConfig), + alias: solvedDriveConfig.alias, + spacePolicy: this.generateSpacePolicy(), + filesystem: generateFilesystem(solvedDriveConfig), + mountPath: solvedDriveConfig.filesystem?.path, + snapshots: generateSnapshots(solvedDriveConfig), + }; + } + + private fromPartitionedDrive(solvedDriveConfig: config.PartitionedDrive): Drive { + return { + name: generateName(solvedDriveConfig), + alias: solvedDriveConfig.alias, + spacePolicy: this.generateSpacePolicy(), + partitions: this.generatePartitions(solvedDriveConfig), + }; + } + + private generateSpacePolicy(): SpacePolicy { + if (this.driveConfig === undefined) return; + + return generateSpacePolicy(this.driveConfig); + } + + private generatePartitions(solvedDriveConfig: config.PartitionedDrive): Partition[] { + const solvedPartitionConfigs = solvedDriveConfig.partitions || []; + return solvedPartitionConfigs.filter(isPartitionConfig).map(generatePartition); + } +} + +export function generate(config: config.DriveElement, solvedConfig: config.DriveElement): Drive { + const generator = new DriveGenerator(config, solvedConfig); + return generator.generate(); +} diff --git a/web/src/storage/model/config/partition.test.ts b/web/src/storage/model/config/partition.test.ts new file mode 100644 index 0000000000..310a101fb6 --- /dev/null +++ b/web/src/storage/model/config/partition.test.ts @@ -0,0 +1,88 @@ +/* + * 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 * as model from "~/storage/model/config/partition"; + +describe("#generate", () => { + it("returns a partition object from a partition section", () => { + expect( + model.generate({ + search: "/dev/vda1", + delete: true, + }), + ).toEqual({ + name: "/dev/vda1", + delete: true, + }); + + expect( + model.generate({ + search: "/dev/vda1", + deleteIfNeeded: true, + size: 1024, + }), + ).toEqual({ + name: "/dev/vda1", + deleteIfNeeded: true, + resizeIfNeeded: false, + size: { min: 1024, max: 1024 }, + }); + + expect( + model.generate({ + search: "/dev/vda1", + alias: "test", + filesystem: { + path: "/test", + type: { + btrfs: { snapshots: true }, + }, + }, + size: { min: 0, max: 2048 }, + }), + ).toEqual({ + name: "/dev/vda1", + alias: "test", + resizeIfNeeded: true, + filesystem: "btrfs", + mountPath: "/test", + snapshots: true, + size: { min: 0, max: 2048 }, + }); + + expect( + model.generate({ + filesystem: { + path: "/test", + }, + }), + ).toEqual({ + name: undefined, + alias: undefined, + resizeIfNeeded: undefined, + filesystem: undefined, + mountPath: "/test", + snapshots: undefined, + size: undefined, + }); + }); +}); diff --git a/web/src/storage/model/config/partition.ts b/web/src/storage/model/config/partition.ts new file mode 100644 index 0000000000..6f0025d143 --- /dev/null +++ b/web/src/storage/model/config/partition.ts @@ -0,0 +1,115 @@ +/* + * 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 { config } from "~/api/storage/types"; +import * as checks from "~/api/storage/types/checks"; +import { Size, WithSize, generate as generateSize } from "~/storage/model/config/size"; +import { generateName, generateFilesystem, generateSnapshots } from "~/storage/model/config/common"; + +export type Partition = { + name?: string; + alias?: string; + delete?: boolean; + deleteIfNeeded?: boolean; + resizeIfNeeded?: boolean; + filesystem?: string; + mountPath?: string; + snapshots?: boolean; + size?: Size; +}; + +export type PartitionConfig = + | config.RegularPartition + | config.PartitionToDelete + | config.PartitionToDeleteIfNeeded; + +export function isPartitionConfig( + partition: config.PartitionElement, +): partition is PartitionConfig { + return ( + checks.isRegularPartition(partition) || + checks.isPartitionToDelete(partition) || + checks.isPartitionToDeleteIfNeeded(partition) + ); +} + +class PartitionGenerator { + private partitionConfig: PartitionConfig; + + constructor(partitionConfig: PartitionConfig) { + this.partitionConfig = partitionConfig; + } + + generate(): Partition { + if (checks.isRegularPartition(this.partitionConfig)) { + return this.fromRegularPartition(this.partitionConfig); + } else if (checks.isPartitionToDelete(this.partitionConfig)) { + return this.fromPartitionToDelete(this.partitionConfig); + } else if (checks.isPartitionToDeleteIfNeeded(this.partitionConfig)) { + return this.fromPartitionToDeleteIfNeeded(this.partitionConfig); + } + } + + private fromRegularPartition(partitionConfig: config.RegularPartition): Partition { + return { + name: generateName(partitionConfig), + alias: partitionConfig.alias, + resizeIfNeeded: this.generateResizeIfNeeded(partitionConfig), + filesystem: generateFilesystem(partitionConfig), + mountPath: partitionConfig.filesystem?.path, + snapshots: generateSnapshots(partitionConfig), + size: generateSize(partitionConfig), + }; + } + + private fromPartitionToDelete(partitionConfig: config.PartitionToDelete): Partition { + return { + name: generateName(partitionConfig), + delete: true, + }; + } + + private fromPartitionToDeleteIfNeeded( + partitionConfig: config.PartitionToDeleteIfNeeded, + ): Partition { + return { + name: generateName(partitionConfig), + deleteIfNeeded: true, + resizeIfNeeded: this.generateResizeIfNeeded(partitionConfig), + size: generateSize(partitionConfig), + }; + } + + private generateResizeIfNeeded( + partitionConfig: TypeWithSize, + ): boolean | undefined { + if (!partitionConfig.size) return; + + const size = generateSize(partitionConfig); + return size.min !== undefined && size.min !== size.max; + } +} + +export function generate(config: PartitionConfig): Partition { + const generator = new PartitionGenerator(config); + return generator.generate(); +} diff --git a/web/src/storage/model/config/size.test.ts b/web/src/storage/model/config/size.test.ts new file mode 100644 index 0000000000..8c3c60f9a2 --- /dev/null +++ b/web/src/storage/model/config/size.test.ts @@ -0,0 +1,84 @@ +/* + * 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 * as model from "~/storage/model/config/size"; + +describe("#generate", () => { + it("returns the size in bytes from the size section", () => { + expect( + model.generate({ + size: 1024, + }), + ).toEqual({ min: 1024, max: 1024 }); + + // TODO: Generate bytes from string size. + // expect(model.generate( + // { + // size: "1 KiB" + // } + // )).toEqual({ min: 1024, max: 1024 }); + + expect( + model.generate({ + size: [1024], + }), + ).toEqual({ min: 1024 }); + + expect( + model.generate({ + size: [1024, 2048], + }), + ).toEqual({ min: 1024, max: 2048 }); + + expect( + model.generate({ + size: { + min: 1024, + }, + }), + ).toEqual({ min: 1024 }); + + expect( + model.generate({ + size: { + min: 1024, + max: 2048, + }, + }), + ).toEqual({ min: 1024, max: 2048 }); + }); + + it("returns undefined for 'custom' value", () => { + expect( + model.generate({ + size: { + min: "custom", + max: 2048, + }, + }), + ).toEqual({ min: undefined, max: 2048 }); + }); + + it("returns undefined if there is no size section", () => { + expect(model.generate({})).toBeUndefined; + }); +}); diff --git a/web/src/storage/model/config/size.ts b/web/src/storage/model/config/size.ts new file mode 100644 index 0000000000..8b25b315c6 --- /dev/null +++ b/web/src/storage/model/config/size.ts @@ -0,0 +1,81 @@ +/* + * 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 { config } from "~/api/storage/types"; +import * as checks from "~/api/storage/types/checks"; + +export type Size = { + min?: number; + max?: number; +}; + +export interface WithSize { + size?: config.Size; +} + +class SizeGenerator { + private config: TypeWithSize; + + constructor(config: TypeWithSize) { + this.config = config; + } + + // TODO: detect auto size by checking the unsolved config. + generate(): Size | undefined { + const size = this.config.size; + + if (!size) return; + if (checks.isSizeValue(size)) return this.fromSizeValue(size); + if (checks.isSizeTuple(size)) return this.fromSizeTuple(size); + if (checks.isSizeRange(size)) return this.fromSizeRange(size); + } + + private fromSizeValue(value: config.SizeValue): Size { + const bytes = this.bytes(value); + return { min: bytes, max: bytes }; + } + + private fromSizeTuple(sizeTuple: config.SizeTuple): Size { + const size: Size = { min: this.bytes(sizeTuple[0]) }; + if (sizeTuple.length === 2) size.max = this.bytes(sizeTuple[1]); + + return size; + } + + private fromSizeRange(sizeRange: config.SizeRange): Size { + const size: Size = { min: this.bytes(sizeRange.min) }; + if (sizeRange.max) size.max = this.bytes(sizeRange.max); + + return size; + } + + private bytes(value: config.SizeValueWithCurrent): number | undefined { + if (checks.isSizeCurrent(value)) return; + // TODO: bytes from string. + if (checks.isSizeString(value)) return; + if (checks.isSizeBytes(value)) return value; + } +} + +export function generate(config: TypeWithSize): Size | undefined { + return new SizeGenerator(config).generate(); +} diff --git a/web/src/storage/model/config/space-policy.test.ts b/web/src/storage/model/config/space-policy.test.ts new file mode 100644 index 0000000000..80a8ad66c3 --- /dev/null +++ b/web/src/storage/model/config/space-policy.test.ts @@ -0,0 +1,157 @@ +/* + * 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 * as model from "~/storage/model/config/space-policy"; + +describe("#generate", () => { + it("returns 'delete' if there is a file system", () => { + expect( + model.generate({ + filesystem: { type: "xfs" }, + }), + ).toEqual("delete"); + }); + + it("returns 'delete' if there is a 'delete all' partition", () => { + expect( + model.generate({ + partitions: [{ search: "*", delete: true }], + }), + ).toEqual("delete"); + + expect( + model.generate({ + partitions: [{ search: { ifNotFound: "skip" }, delete: true }], + }), + ).toEqual("delete"); + + expect( + model.generate({ + partitions: [{ search: "*", delete: true }, { filesystem: { path: "/" } }], + }), + ).toEqual("delete"); + }); + + it("returns 'resize' if there is a 'shrink all' partition", () => { + expect( + model.generate({ + partitions: [{ search: "*", size: { min: 0, max: "current" } }], + }), + ).toEqual("resize"); + + expect( + model.generate({ + partitions: [{ search: "*", size: [0, "current"] }], + }), + ).toEqual("resize"); + + expect( + model.generate({ + partitions: [{ search: { ifNotFound: "skip" }, size: { min: 0, max: "current" } }], + }), + ).toEqual("resize"); + + expect( + model.generate({ + partitions: [{ search: "*", size: { min: 0, max: "current" } }, { generate: "default" }], + }), + ).toEqual("resize"); + }); + + it("returns 'custom' if there is a 'delete' or 'resize' partition", () => { + expect( + model.generate({ + partitions: [{ search: "/dev/vda", delete: true }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: { max: 2, ifNotFound: "skip" }, delete: true }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: "*", deleteIfNeeded: true }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: "*", deleteIfNeeded: true, size: [0, "current"] }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: "*", size: { min: 0 } }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: "*", size: { min: 0, max: 1024 } }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: "/dev/vda", delete: true }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: "/dev/vda", size: { min: 0, max: "current" } }], + }), + ).toEqual("custom"); + + expect( + model.generate({ + partitions: [{ search: "/dev/vda", delete: true }, { filesystem: { path: "/" } }], + }), + ).toEqual("custom"); + }); + + it("returns 'keep' if there is neither 'delete' nor 'resize' partition", () => { + expect( + model.generate({ + partitions: [{ search: "*", filesystem: { type: "xfs" } }], + }), + ).toEqual("keep"); + + expect( + model.generate({ + partitions: [{ generate: "default" }, { filesystem: { path: "/home" } }], + }), + ).toEqual("keep"); + }); + + it("returns 'keep' if there are not partitions", () => { + expect( + model.generate({ + search: "/dev/vda", + }), + ).toEqual("keep"); + }); +}); diff --git a/web/src/storage/model/config/space-policy.ts b/web/src/storage/model/config/space-policy.ts new file mode 100644 index 0000000000..86e726b516 --- /dev/null +++ b/web/src/storage/model/config/space-policy.ts @@ -0,0 +1,152 @@ +/* + * 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 { config } from "~/api/storage/types"; +import * as checks from "~/api/storage/types/checks"; + +export type SpacePolicy = "keep" | "resize" | "delete" | "custom"; + +class SpacePolicyGenerator { + private driveConfig: config.DriveElement; + + constructor(driveConfig: config.DriveElement) { + this.driveConfig = driveConfig; + } + + generate(): SpacePolicy { + if (this.isDeletePolicy()) return "delete"; + if (this.isResizePolicy()) return "resize"; + if (this.isCustomPolicy()) return "custom"; + + return "keep"; + } + + private isDeletePolicy(): boolean { + return checks.isFormattedDrive(this.driveConfig) || this.hasDeleteAllPartition(); + } + + private isResizePolicy(): boolean { + return this.hasResizeAllPartition(); + } + + private isCustomPolicy(): boolean { + return this.hasDeletePartition() || this.hasResizePartition(); + } + + private hasDeleteAllPartition(): boolean { + const deleteAllPartition = this.partitionConfigs().find((c) => this.isDeleteAllPartition(c)); + return deleteAllPartition !== undefined; + } + + private hasDeletePartition(): boolean { + const deletePartition = this.partitionConfigs().find((c) => this.isDeletePartition(c)); + return deletePartition !== undefined; + } + + private hasResizeAllPartition(): boolean { + const resizeAllPartition = this.partitionConfigs().find((c) => this.isResizeAllPartition(c)); + return resizeAllPartition !== undefined; + } + + private hasResizePartition(): boolean { + const resizePartition = this.partitionConfigs().find((c) => this.isResizePartition(c)); + return resizePartition !== undefined; + } + + private isDeleteAllPartition(partitionConfig: config.PartitionElement): boolean { + return checks.isPartitionToDelete(partitionConfig) && this.isSearchAll(partitionConfig.search); + } + + private isDeletePartition(partitionConfig: config.PartitionElement): boolean { + const isDelete = + checks.isPartitionToDelete(partitionConfig) || + checks.isPartitionToDeleteIfNeeded(partitionConfig); + + return isDelete && !this.isDeleteAllPartition(partitionConfig); + } + + private isResizeAllPartition(partitionConfig: config.PartitionElement): boolean { + return ( + checks.isRegularPartition(partitionConfig) && + partitionConfig.search && + this.isSearchAll(partitionConfig.search) && + partitionConfig.size && + this.isShrinkAllSize(partitionConfig.size) + ); + } + + // TODO: if the size is not a range, then detect resize partitions by comparing with the size of + // the system device. + private isResizePartition(partitionConfig: config.PartitionElement): boolean { + if (!checks.isRegularPartition(partitionConfig)) return false; + + return ( + !this.isResizeAllPartition(partitionConfig) && + partitionConfig.search && + partitionConfig.size && + this.isResizeSize(partitionConfig.size) + ); + } + + private isSearchAll(searchConfig: config.SearchElement): boolean { + const isAdvancedSearchAll = + checks.isAdvancedSearch(searchConfig) && + !searchConfig.condition && + !searchConfig.max && + searchConfig.ifNotFound === "skip"; + + return isAdvancedSearchAll || checks.isSimpleSearchAll(searchConfig); + } + + private isShrinkAllSize(sizeConfig: config.Size): boolean { + if (!this.isResizeSize(sizeConfig)) return false; + + return ( + (checks.isSizeTuple(sizeConfig) && + sizeConfig[0] === 0 && + sizeConfig[1] && + checks.isSizeCurrent(sizeConfig[1])) || + (checks.isSizeRange(sizeConfig) && + sizeConfig.min === 0 && + sizeConfig.max && + checks.isSizeCurrent(sizeConfig.max)) + ); + } + + private isResizeSize(sizeConfig: config.Size): boolean { + if (checks.isSizeBytes(sizeConfig)) return false; + if (checks.isSizeString(sizeConfig)) return false; + if (checks.isSizeTuple(sizeConfig)) return sizeConfig[0] !== sizeConfig[1]; + if (checks.isSizeRange(sizeConfig)) return sizeConfig.min !== sizeConfig.max; + } + + private partitionConfigs(): config.PartitionElement[] { + if (checks.isFormattedDrive(this.driveConfig)) return []; + + return this.driveConfig.partitions || []; + } +} + +export function generate(config: config.DriveElement): SpacePolicy { + const generator = new SpacePolicyGenerator(config); + return generator.generate(); +}