diff --git a/service/lib/agama/dbus/storage/device.rb b/service/lib/agama/dbus/storage/device.rb index 5001dd25e0..4e1b07a206 100644 --- a/service/lib/agama/dbus/storage/device.rb +++ b/service/lib/agama/dbus/storage/device.rb @@ -21,12 +21,7 @@ require "dbus" require "agama/dbus/base_object" -require "agama/dbus/storage/interfaces/drive" -require "agama/dbus/storage/interfaces/raid" -require "agama/dbus/storage/interfaces/multipath" -require "agama/dbus/storage/interfaces/md" -require "agama/dbus/storage/interfaces/block" -require "agama/dbus/storage/interfaces/partition_table" +require "agama/dbus/storage/interfaces/device" module Agama module DBus @@ -76,40 +71,14 @@ def storage_device=(value) # @return [DevicesTree] attr_reader :tree - # Adds the required interfaces according to the storage object - def add_interfaces # rubocop:disable Metrics/CyclomaticComplexity - interfaces = [] - interfaces << Interfaces::Drive if drive? - interfaces << Interfaces::Raid if storage_device.is?(:dm_raid) - interfaces << Interfaces::Md if storage_device.is?(:md) - interfaces << Interfaces::Multipath if storage_device.is?(:multipath) - interfaces << Interfaces::Block if storage_device.is?(:blk_device) - interfaces << Interfaces::PartitionTable if partition_table? + # Adds the required interfaces according to the storage object. + def add_interfaces + interfaces = Interfaces::Device.constants + .map { |c| Interfaces::Device.const_get(c) } + .select { |c| c.is_a?(Module) && c.respond_to?(:apply?) && c.apply?(storage_device) } interfaces.each { |i| singleton_class.include(i) } end - - # Whether the storage device is a drive - # - # Drive and disk device are very close concepts, but there are subtle differences. For - # example, a MD RAID is never considered as a drive. - # - # TODO: Revisit the defintion of drive. Maybe some MD devices could implement the drive - # interface if hwinfo provides useful information for them. - # - # @return [Boolean] - def drive? - storage_device.is?(:disk, :dm_raid, :multipath, :dasd) && storage_device.is?(:disk_device) - end - - # Whether the storage device has a partition table - # - # @return [Boolean] - def partition_table? - storage_device.is?(:blk_device) && - storage_device.respond_to?(:partition_table?) && - storage_device.partition_table? - end end end end diff --git a/service/lib/agama/dbus/storage/interfaces.rb b/service/lib/agama/dbus/storage/interfaces.rb index 7ac6c81c0f..1c7b917c16 100644 --- a/service/lib/agama/dbus/storage/interfaces.rb +++ b/service/lib/agama/dbus/storage/interfaces.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -22,17 +22,13 @@ module Agama module DBus module Storage - # Module for storage specific D-Bus interfaces + # Module for D-Bus interfaces of storage. module Interfaces end end end end -require "agama/dbus/storage/interfaces/drive" -require "agama/dbus/storage/interfaces/raid" -require "agama/dbus/storage/interfaces/multipath" -require "agama/dbus/storage/interfaces/md" -require "agama/dbus/storage/interfaces/block" -require "agama/dbus/storage/interfaces/partition_table" require "agama/dbus/storage/interfaces/dasd_manager" +require "agama/dbus/storage/interfaces/device" +require "agama/dbus/storage/interfaces/zfcp_manager" diff --git a/service/lib/agama/dbus/storage/interfaces/block.rb b/service/lib/agama/dbus/storage/interfaces/block.rb deleted file mode 100644 index 84648443f0..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/block.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for block devices - # - # @note This interface is intended to be included by {Device} if needed. - module Block - BLOCK_INTERFACE = "org.opensuse.Agama.Storage1.Block" - private_constant :BLOCK_INTERFACE - - # Name of the block device - # - # @return [String] e.g., "/dev/sda" - def block_name - storage_device.name - end - - # Whether the block device is currently active - # - # @return [Boolean] - def block_active - storage_device.active? - end - - # Name of the udev by-id links - # - # @return [Array] - def block_udev_ids - storage_device.udev_ids - end - - # Name of the udev by-path links - # - # @return [Array] - def block_udev_paths - storage_device.udev_paths - end - - # Size of the block device in bytes - # - # @return [Integer] - def block_size - storage_device.size.to_i - end - - # Size of the space that could be theoretically reclaimed by shrinking the device. - # - # @return [Integer] - def block_recoverable_size - storage_device.recoverable_size.to_i - end - - # Name of the currently installed systems - # - # @return [Array] - def block_systems - return @systems if @systems - - filesystems = storage_device.descendants.select { |d| d.is?(:filesystem) } - @systems = filesystems.map(&:system_name).compact - end - - def self.included(base) - base.class_eval do - dbus_interface BLOCK_INTERFACE do - dbus_reader :block_name, "s", dbus_name: "Name" - dbus_reader :block_active, "b", dbus_name: "Active" - dbus_reader :block_udev_ids, "as", dbus_name: "UdevIds" - dbus_reader :block_udev_paths, "as", dbus_name: "UdevPaths" - dbus_reader :block_size, "t", dbus_name: "Size" - dbus_reader :block_recoverable_size, "t", dbus_name: "RecoverableSize" - dbus_reader :block_systems, "as", dbus_name: "Systems" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/raid.rb b/service/lib/agama/dbus/storage/interfaces/device.rb similarity index 52% rename from service/lib/agama/dbus/storage/interfaces/raid.rb rename to service/lib/agama/dbus/storage/interfaces/device.rb index 5767759e33..98f09690c2 100644 --- a/service/lib/agama/dbus/storage/interfaces/raid.rb +++ b/service/lib/agama/dbus/storage/interfaces/device.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2024] SUSE LLC # # All Rights Reserved. # @@ -19,37 +19,23 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require "dbus" - module Agama module DBus module Storage module Interfaces - # Interface for DM RAID devices - # - # @note This interface is intended to be included by {Device} if needed. - module Raid - RAID_INTERFACE = "org.opensuse.Agama.Storage1.RAID" - private_constant :RAID_INTERFACE - - # Devices used by the DM RAID - # - # TODO: return object paths - # - # @return [Array] - def raid_devices - storage_device.parents.map(&:name) - end - - def self.included(base) - base.class_eval do - dbus_interface RAID_INTERFACE do - dbus_reader :raid_devices, "as", dbus_name: "Devices" - end - end - end + # Module for D-Bus interfaces of a device. + module Device end end end end end + +require "agama/dbus/storage/interfaces/device/block" +require "agama/dbus/storage/interfaces/device/component" +require "agama/dbus/storage/interfaces/device/drive" +require "agama/dbus/storage/interfaces/device/filesystem" +require "agama/dbus/storage/interfaces/device/md" +require "agama/dbus/storage/interfaces/device/multipath" +require "agama/dbus/storage/interfaces/device/partition_table" +require "agama/dbus/storage/interfaces/device/raid" diff --git a/service/lib/agama/dbus/storage/interfaces/device/block.rb b/service/lib/agama/dbus/storage/interfaces/device/block.rb new file mode 100644 index 0000000000..b5c0e00017 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/block.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for block devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Block + # Whether this interface should be implemented for the given device. + # + # @note Block devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) + end + + BLOCK_INTERFACE = "org.opensuse.Agama.Storage1.Block" + private_constant :BLOCK_INTERFACE + + # Name of the block device + # + # @return [String] e.g., "/dev/sda" + def block_name + storage_device.name + end + + # Whether the block device is currently active + # + # @return [Boolean] + def block_active + storage_device.active? + end + + # Name of the udev by-id links + # + # @return [Array] + def block_udev_ids + storage_device.udev_ids + end + + # Name of the udev by-path links + # + # @return [Array] + def block_udev_paths + storage_device.udev_paths + end + + # Size of the block device in bytes + # + # @return [Integer] + def block_size + storage_device.size.to_i + end + + # Size of the space that could be theoretically reclaimed by shrinking the device. + # + # @return [Integer] + def block_recoverable_size + storage_device.recoverable_size.to_i + end + + # Name of the currently installed systems + # + # @return [Array] + def block_systems + return @systems if @systems + + filesystems = storage_device.descendants.select { |d| d.is?(:filesystem) } + @systems = filesystems.map(&:system_name).compact + end + + def self.included(base) + base.class_eval do + dbus_interface BLOCK_INTERFACE do + dbus_reader :block_name, "s", dbus_name: "Name" + dbus_reader :block_active, "b", dbus_name: "Active" + dbus_reader :block_udev_ids, "as", dbus_name: "UdevIds" + dbus_reader :block_udev_paths, "as", dbus_name: "UdevPaths" + dbus_reader :block_size, "t", dbus_name: "Size" + dbus_reader :block_recoverable_size, "t", dbus_name: "RecoverableSize" + dbus_reader :block_systems, "as", dbus_name: "Systems" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/component.rb b/service/lib/agama/dbus/storage/interfaces/device/component.rb new file mode 100644 index 0000000000..c66422b2b4 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/component.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for devices that are used as component of other device (e.g., physical volume, + # MD RAID device, etc). + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Component + # Whether this interface should be implemented for the given device. + # + # @note Components of other devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) && storage_device.component_of.any? + end + + COMPONENT_INTERFACE = "org.opensuse.Agama.Storage1.Component" + private_constant :COMPONENT_INTERFACE + + # Type of component. + # + # @return ["physical_volume", "md_device", "raid_device", "multipath_wire", + # "bcache_device", "bcache_cset_device", "md_btrfs_device", ""] Empty if type is + # unknown. + def component_type + types = { + lvm_vg: "physical_volume", + md: "md_device", + dm_raid: "raid_device", + multipath: "multipath_wire", + bcache: "bcache_device", + bcache_cset: "bcache_cset_device", + btrfs: "md_btrfs_device" + } + + device = storage_device.component_of.first + + types.find { |k, _v| device.is?(k) }&.last || "" + end + + # Name of the devices for which this device is component of. + # + # @return [Array] + def component_device_names + storage_device.component_of.map(&:display_name).compact + end + + # Paths of the D-Bus objects representing the devices. + # + # @return [Array<::DBus::ObjectPath>] + def component_devices + storage_device.component_of.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface COMPONENT_INTERFACE do + dbus_reader :component_type, "s", dbus_name: "Type" + dbus_reader :component_device_names, "as", dbus_name: "DeviceNames" + dbus_reader :component_devices, "ao", dbus_name: "Devices" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/drive.rb b/service/lib/agama/dbus/storage/interfaces/device/drive.rb new file mode 100644 index 0000000000..0466ad7984 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/drive.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for drive devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Drive + # Whether this interface should be implemented for the given device. + # + # @note Drive devices implement this interface. + # Drive and disk device are very close concepts, but there are subtle differences. For + # example, a MD RAID is never considered as a drive. + # + # TODO: Revisit the defintion of drive. Maybe some MD devices could implement the drive + # interface if hwinfo provides useful information for them. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:disk, :dm_raid, :multipath, :dasd) && + storage_device.is?(:disk_device) + end + + DRIVE_INTERFACE = "org.opensuse.Agama.Storage1.Drive" + private_constant :DRIVE_INTERFACE + + # Drive type + # + # @return ["disk", "raid", "multipath", "dasd", ""] Empty if type is unknown. + def drive_type + if storage_device.is?(:disk) + "disk" + elsif storage_device.is?(:dm_raid) + "raid" + elsif storage_device.is?(:multipath) + "multipath" + elsif storage_device.is?(:dasd) + "dasd" + else + "" + end + end + + # Vendor name + # + # @return [String] + def drive_vendor + storage_device.vendor || "" + end + + # Model name + # + # @return [String] + def drive_model + storage_device.model || "" + end + + # Bus name + # + # @return [String] + def drive_bus + storage_device.bus || "" + end + + # Bus Id for DASD + # + # @return [String] + def drive_bus_id + return "" unless storage_device.respond_to?(:bus_id) + + storage_device.bus_id + end + + # Kernel drivers used by the device + # + # @return [Array] + def drive_driver + storage_device.driver + end + + # Data transport layer, if any + # + # @return [String] + def drive_transport + return "" unless storage_device.respond_to?(:transport) + + storage_device.transport.to_s + end + + # More info about the device + # + # @return [Hash] + def drive_info + { + "SDCard" => storage_device.sd_card?, + "DellBOSS" => storage_device.boss? + } + end + + def self.included(base) + base.class_eval do + dbus_interface DRIVE_INTERFACE do + dbus_reader :drive_type, "s", dbus_name: "Type" + dbus_reader :drive_vendor, "s", dbus_name: "Vendor" + dbus_reader :drive_model, "s", dbus_name: "Model" + dbus_reader :drive_bus, "s", dbus_name: "Bus" + dbus_reader :drive_bus_id, "s", dbus_name: "BusId" + dbus_reader :drive_driver, "as", dbus_name: "Driver" + dbus_reader :drive_transport, "s", dbus_name: "Transport" + dbus_reader :drive_info, "a{sv}", dbus_name: "Info" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb new file mode 100644 index 0000000000..36ee17c2ea --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/filesystem.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for file systems. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Filesystem + # Whether this interface should be implemented for the given device. + # + # @note Formatted devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) && !storage_device.filesystem.nil? + end + + FILESYSTEM_INTERFACE = "org.opensuse.Agama.Storage1.Filesystem" + private_constant :FILESYSTEM_INTERFACE + + # File system type. + # + # @return [String] e.g., "ext4" + def filesystem_type + storage_device.filesystem.type.to_s + end + + # Whether the filesystem contains the directory layout of an ESP partition. + # + # @return [Boolean] + def filesystem_efi? + storage_device.filesystem.efi? + end + + def self.included(base) + base.class_eval do + dbus_interface FILESYSTEM_INTERFACE do + dbus_reader :filesystem_type, "s", dbus_name: "Type" + dbus_reader :filesystem_efi?, "b", dbus_name: "EFI" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/md.rb b/service/lib/agama/dbus/storage/interfaces/device/md.rb new file mode 100644 index 0000000000..6cf144489d --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/md.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for MD RAID devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Md + # Whether this interface should be implemented for the given device. + # + # @note MD RAIDs implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:md) + end + + MD_INTERFACE = "org.opensuse.Agama.Storage1.MD" + private_constant :MD_INTERFACE + + # UUID of the MD RAID + # + # @return [String] + def md_uuid + storage_device.uuid + end + + # RAID level + # + # @return [String] + def md_level + storage_device.md_level.to_s + end + + # Paths of the D-Bus objects representing the devices of the MD RAID. + # + # @return [Array] + def md_devices + storage_device.plain_devices.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface MD_INTERFACE do + dbus_reader :md_uuid, "s", dbus_name: "UUID" + dbus_reader :md_level, "s", dbus_name: "Level" + dbus_reader :md_devices, "ao", dbus_name: "Devices" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/multipath.rb b/service/lib/agama/dbus/storage/interfaces/device/multipath.rb new file mode 100644 index 0000000000..3293ed376d --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/multipath.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for Multipath devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Multipath + # Whether this interface should be implemented for the given device. + # + # @note Multipath devices implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:multipath) + end + + MULTIPATH_INTERFACE = "org.opensuse.Agama.Storage1.Multipath" + private_constant :MULTIPATH_INTERFACE + + # Paths of the D-Bus objects representing the multipath wires. + # + # @return [Array] + def multipath_wires + storage_device.parents.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface MULTIPATH_INTERFACE do + dbus_reader :multipath_wires, "ao", dbus_name: "Wires" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb b/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb new file mode 100644 index 0000000000..546ddfd934 --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/partition_table.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for devices that contain a partition table. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module PartitionTable + # Whether this interface should be implemented for the given device. + # + # @note Devices containing a partition table implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:blk_device) && + storage_device.respond_to?(:partition_table?) && + storage_device.partition_table? + end + + PARTITION_TABLE_INTERFACE = "org.opensuse.Agama.Storage1.PartitionTable" + private_constant :PARTITION_TABLE_INTERFACE + + # Type of the partition table + # + # @return [String] + def partition_table_type + storage_device.partition_table.type.to_s + end + + # Paths of the D-Bus objects representing the partitions. + # + # @return [Array<::DBus::ObjectPath>] + def partition_table_partitions + storage_device.partition_table.partitions.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface PARTITION_TABLE_INTERFACE do + dbus_reader :partition_table_type, "s", dbus_name: "Type" + dbus_reader :partition_table_partitions, "ao", dbus_name: "Partitions" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/device/raid.rb b/service/lib/agama/dbus/storage/interfaces/device/raid.rb new file mode 100644 index 0000000000..228e389c4b --- /dev/null +++ b/service/lib/agama/dbus/storage/interfaces/device/raid.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +# Copyright (c) [2023-2024] SUSE LLC +# +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require "dbus" + +module Agama + module DBus + module Storage + module Interfaces + module Device + # Interface for DM RAID devices. + # + # @note This interface is intended to be included by {Agama::DBus::Storage::Device} if + # needed. + module Raid + # Whether this interface should be implemented for the given device. + # + # @note DM RAIDs implement this interface. + # + # @param storage_device [Y2Storage::Device] + # @return [Boolean] + def self.apply?(storage_device) + storage_device.is?(:dm_raid) + end + + RAID_INTERFACE = "org.opensuse.Agama.Storage1.RAID" + private_constant :RAID_INTERFACE + + # Paths of the D-Bus objects representing the devices used by the DM RAID. + # + # @return [Array] + def raid_devices + storage_device.parents.map { |p| tree.path_for(p) } + end + + def self.included(base) + base.class_eval do + dbus_interface RAID_INTERFACE do + dbus_reader :raid_devices, "ao", dbus_name: "Devices" + end + end + end + end + end + end + end + end +end diff --git a/service/lib/agama/dbus/storage/interfaces/drive.rb b/service/lib/agama/dbus/storage/interfaces/drive.rb deleted file mode 100644 index 6d87ef33a4..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/drive.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for drive devices - # - # @note This interface is intended to be included by {Device} if needed. - module Drive - DRIVE_INTERFACE = "org.opensuse.Agama.Storage1.Drive" - private_constant :DRIVE_INTERFACE - - # Drive type - # - # @return ["disk", "raid", "multipath", "dasd"] - def drive_type - if storage_device.is?(:disk) - "disk" - elsif storage_device.is?(:dm_raid) - "raid" - elsif storage_device.is?(:multipath) - "multipath" - elsif storage_device.is?(:dasd) - "dasd" - else - "" - end - end - - # Vendor name - # - # @return [String] - def drive_vendor - storage_device.vendor || "" - end - - # Model name - # - # @return [String] - def drive_model - storage_device.model || "" - end - - # Bus name - # - # @return [String] - def drive_bus - storage_device.bus || "" - end - - # Bus Id for DASD - # - # @return [String] - def drive_bus_id - return "" unless storage_device.respond_to?(:bus_id) - - storage_device.bus_id - end - - # Kernel drivers used by the device - # - # @return [Array] - def drive_driver - storage_device.driver - end - - # Data transport layer, if any - # - # @return [String] - def drive_transport - return "" unless storage_device.respond_to?(:transport) - - storage_device.transport.to_s - end - - # More info about the device - # - # @return [Hash] - def drive_info - { - "SDCard" => storage_device.sd_card?, - "DellBOSS" => storage_device.boss? - } - end - - def self.included(base) - base.class_eval do - dbus_interface DRIVE_INTERFACE do - dbus_reader :drive_type, "s", dbus_name: "Type" - dbus_reader :drive_vendor, "s", dbus_name: "Vendor" - dbus_reader :drive_model, "s", dbus_name: "Model" - dbus_reader :drive_bus, "s", dbus_name: "Bus" - dbus_reader :drive_bus_id, "s", dbus_name: "BusId" - dbus_reader :drive_driver, "as", dbus_name: "Driver" - dbus_reader :drive_transport, "s", dbus_name: "Transport" - dbus_reader :drive_info, "a{sv}", dbus_name: "Info" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/md.rb b/service/lib/agama/dbus/storage/interfaces/md.rb deleted file mode 100644 index ed94477fec..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/md.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for MD RAID devices - # - # @note This interface is intended to be included by {Device} if needed. - module Md - MD_INTERFACE = "org.opensuse.Agama.Storage1.MD" - private_constant :MD_INTERFACE - - # UUID of the MD RAID - # - # @return [String] - def md_uuid - storage_device.uuid - end - - # RAID level - # - # @return [String] - def md_level - storage_device.md_level.to_s - end - - # Member devices of the MD RAID - # - # TODO: return object paths once all possible members (e.g., partitions) are exported - # on D-Bus. - # - # @return [Array] - def md_members - storage_device.plain_devices.map(&:name) - end - - def self.included(base) - base.class_eval do - dbus_interface MD_INTERFACE do - dbus_reader :md_uuid, "s", dbus_name: "UUID" - dbus_reader :md_level, "s", dbus_name: "Level" - dbus_reader :md_members, "as", dbus_name: "Members" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/multipath.rb b/service/lib/agama/dbus/storage/interfaces/multipath.rb deleted file mode 100644 index 29fc9279d1..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/multipath.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for Multipath devices - # - # @note This interface is intended to be included by {Device} if needed. - module Multipath - MULTIPATH_INTERFACE = "org.opensuse.Agama.Storage1.Multipath" - private_constant :MULTIPATH_INTERFACE - - # Multipath wires - # - # TODO: return object paths - # - # @return [Array] - def multipath_wires - storage_device.parents.map(&:name) - end - - def self.included(base) - base.class_eval do - dbus_interface MULTIPATH_INTERFACE do - dbus_reader :multipath_wires, "as", dbus_name: "Wires" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/interfaces/partition_table.rb b/service/lib/agama/dbus/storage/interfaces/partition_table.rb deleted file mode 100644 index 635a331984..0000000000 --- a/service/lib/agama/dbus/storage/interfaces/partition_table.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) [2023-2024] SUSE LLC -# -# All Rights Reserved. -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of version 2 of the GNU General Public License as published -# by the Free Software Foundation. -# -# 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. - -require "dbus" - -module Agama - module DBus - module Storage - module Interfaces - # Interface for devices that contain a partition table - # - # @note This interface is intended to be included by {Device} if needed. - module PartitionTable - PARTITION_TABLE_INTERFACE = "org.opensuse.Agama.Storage1.PartitionTable" - private_constant :PARTITION_TABLE_INTERFACE - - # Type of the partition table - # - # @return [String] - def partition_table_type - storage_device.partition_table.type.to_s - end - - # Paths of the D-Bus objects representing the partitions. - # - # @return [Array<::DBus::ObjectPath>] - def partition_table_partitions - storage_device.partition_table.partitions.map { |p| tree.path_for(p) } - end - - def self.included(base) - base.class_eval do - dbus_interface PARTITION_TABLE_INTERFACE do - dbus_reader :partition_table_type, "s", dbus_name: "Type" - dbus_reader :partition_table_partitions, "ao", dbus_name: "Partitions" - end - end - end - end - end - end - end -end diff --git a/service/lib/agama/dbus/storage/proposal.rb b/service/lib/agama/dbus/storage/proposal.rb index fe1df2f179..2d55331f35 100644 --- a/service/lib/agama/dbus/storage/proposal.rb +++ b/service/lib/agama/dbus/storage/proposal.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -58,6 +58,8 @@ def initialize(backend, logger) dbus_reader :space_policy, "s" + dbus_reader :space_actions, "aa{sv}" + dbus_reader :volumes, "aa{sv}" dbus_reader :actions, "aa{sv}" @@ -109,6 +111,13 @@ def space_policy dbus_settings.fetch("SpacePolicy", "") end + # Space actions + # + # @return [Array] + def space_actions + dbus_settings.fetch("SpaceActions", []) + end + # Volumes used to calculate the storage proposal # # @return [Array] diff --git a/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb b/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb index 4e7d7b1356..412607d72c 100644 --- a/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb +++ b/service/lib/agama/dbus/storage/proposal_settings_conversion/from_dbus.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -130,9 +130,11 @@ def space_policy_conversion(target, value) end # @param target [Agama::Storage::ProposalSettings] - # @param value [Hash] + # @param value [Array] def space_actions_conversion(target, value) - target.space.actions = value + target.space.actions = value.each_with_object({}) do |v, result| + result[v["Device"]] = v["Action"].to_sym + end end # @param target [Agama::Storage::ProposalSettings] diff --git a/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb b/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb index 8d641f0b1b..d4820e847f 100644 --- a/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb +++ b/service/lib/agama/dbus/storage/proposal_settings_conversion/to_dbus.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -35,7 +35,7 @@ def initialize(settings) # Performs the conversion to D-Bus format. # # @return [Hash] - def convert # rubocop:disable Metrics/AbcSize + def convert { "BootDevice" => settings.boot_device.to_s, "LVM" => settings.lvm.enabled?, @@ -44,7 +44,7 @@ def convert # rubocop:disable Metrics/AbcSize "EncryptionMethod" => settings.encryption.method.id.to_s, "EncryptionPBKDFunction" => settings.encryption.pbkd_function&.value || "", "SpacePolicy" => settings.space.policy.to_s, - "SpaceActions" => settings.space.actions, + "SpaceActions" => space_actions_conversion, "Volumes" => settings.volumes.map { |v| VolumeConversion.to_dbus(v) } } end @@ -53,6 +53,13 @@ def convert # rubocop:disable Metrics/AbcSize # @return [Agama::Storage::ProposalSettings] attr_reader :settings + + # @return [Array] + def space_actions_conversion + settings.space.actions.each_with_object([]) do |(device, action), actions| + actions << { "Device" => device, "Action" => action.to_s } + end + end end end end diff --git a/service/lib/agama/storage/proposal.rb b/service/lib/agama/storage/proposal.rb index 7232bc71a6..0b3d546dc3 100644 --- a/service/lib/agama/storage/proposal.rb +++ b/service/lib/agama/storage/proposal.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -85,10 +85,9 @@ def settings proposal.settings, config: config ).tap do |settings| - # FIXME: The conversion from Y2Storage cannot infer the space policy. Copying space - # settings from the original settings. + # The conversion from Y2Storage cannot infer the space policy. Copying space policy from + # the original settings. settings.space.policy = original_settings.space.policy - settings.space.actions = original_settings.space.actions # FIXME: The conversion from Y2Storage cannot reliably infer the system VG devices in all # cases. Copying system VG devices from the original settings. settings.lvm.system_vg_devices = original_settings.lvm.system_vg_devices diff --git a/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb b/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb index db13a73bc8..a60a0c59a3 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb +++ b/service/lib/agama/storage/proposal_settings_conversion/from_y2storage.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -82,9 +82,10 @@ def encryption_conversion(target) target.encryption.pbkd_function = settings.encryption_pbkdf end + # @note The space policy cannot be inferred from Y2Storage settings. # @param target [Agama::Storage::ProposalSettings] def space_settings_conversion(target) - # FIXME: No way to infer space settings from Y2Storage. + target.space.actions = settings.space_settings.actions end # @param target [Agama::Storage::ProposalSettings] diff --git a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb b/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb index e583db872b..e7ca55e481 100644 --- a/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb +++ b/service/lib/agama/storage/proposal_settings_conversion/to_y2storage.rb @@ -162,7 +162,7 @@ def find_max_size_fallback(mount_path) # All block devices affected by the space policy. # - # This includes the partitions from the root device, the candidate devices and from the + # This includes the partitions from the boot device, the candidate devices and from the # devices directly assigned to a volume as target device. If a device is not partitioned, # then the device itself is included. # diff --git a/service/package/rubygem-agama.changes b/service/package/rubygem-agama.changes index 4b2c8a6496..8c07ea0b36 100644 --- a/service/package/rubygem-agama.changes +++ b/service/package/rubygem-agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Feb 20 13:15:15 UTC 2024 - José Iván López González + +- Add Filesystem and Component D-Bus interfaces + (gh#openSUSE/agama#1028). + ------------------------------------------------------------------- Wed Feb 7 11:49:02 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/test/agama/dbus/storage/device_test.rb b/service/test/agama/dbus/storage/device_test.rb index 6fa03e64d4..649131a83f 100644 --- a/service/test/agama/dbus/storage/device_test.rb +++ b/service/test/agama/dbus/storage/device_test.rb @@ -19,17 +19,19 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../test_helper" -require_relative "../../storage/storage_helpers" -require_relative "./interfaces/drive_examples" -require_relative "./interfaces/raid_examples" -require_relative "./interfaces/multipath_examples" -require_relative "./interfaces/block_examples" -require_relative "./interfaces/md_examples" -require_relative "./interfaces/partition_table_examples" require "agama/dbus/storage/device" require "agama/dbus/storage/devices_tree" require "dbus" +require_relative "../../../test_helper" +require_relative "../../storage/storage_helpers" +require_relative "./interfaces/device/block_examples" +require_relative "./interfaces/device/component_examples" +require_relative "./interfaces/device/drive_examples" +require_relative "./interfaces/device/filesystem_examples" +require_relative "./interfaces/device/md_examples" +require_relative "./interfaces/device/multipath_examples" +require_relative "./interfaces/device/partition_table_examples" +require_relative "./interfaces/device/raid_examples" describe Agama::DBus::Storage::Device do include Agama::RSpec::StorageHelpers @@ -99,7 +101,7 @@ expect(subject).to_not include_dbus_interface("org.opensuse.Agama.Storage1.Drive") end - it "defines the RAID interface" do + it "defines the MD interface" do expect(subject).to include_dbus_interface("org.opensuse.Agama.Storage1.MD") end @@ -142,6 +144,10 @@ include_examples "PartitionTable interface" + include_examples "Filesystem interface" + + include_examples "Component interface" + describe "#storage_device=" do before do allow(subject).to receive(:dbus_properties_changed) diff --git a/service/test/agama/dbus/storage/devices_tree_test.rb b/service/test/agama/dbus/storage/devices_tree_test.rb index 2f38bd69fe..42e0024a33 100644 --- a/service/test/agama/dbus/storage/devices_tree_test.rb +++ b/service/test/agama/dbus/storage/devices_tree_test.rb @@ -149,7 +149,7 @@ context "if an exported D-Bus object does not represent any of the current devices" do let(:dbus_objects) { [dbus_object1] } let(:dbus_object1) { Agama::DBus::Storage::Device.new(sdd, subject.path_for(sdd), subject) } - let(:sdd) { instance_double(Y2Storage::Disk, sid: 1, is?: false) } + let(:sdd) { instance_double(Y2Storage::Disk, sid: 1, is?: false, filesystem: false) } it "unexports the D-Bus object" do expect(service).to unexport_object("#{root_path}/1") diff --git a/service/test/agama/dbus/storage/interfaces/block_examples.rb b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb similarity index 97% rename from service/test/agama/dbus/storage/interfaces/block_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/block_examples.rb index 5922038a3f..11590a3390 100644 --- a/service/test/agama/dbus/storage/interfaces/block_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/block_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "Block interface" do describe "Block D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/device/component_examples.rb b/service/test/agama/dbus/storage/interfaces/device/component_examples.rb new file mode 100644 index 0000000000..c7ea69f495 --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/device/component_examples.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../../../../test_helper" + +shared_examples "Component interface" do + describe "Component D-Bus interface" do + let(:scenario) { "empty-dm_raids.xml" } + + let(:device) { devicegraph.find_by_name("/dev/sdb") } + + describe "#component_type" do + it "returns the type of component" do + expect(subject.component_type).to eq("raid_device") + end + end + + describe "#component_devices" do + it "returns the D-Bus path of the devices for which the device is component" do + raid1 = devicegraph.find_by_name("/dev/mapper/isw_ddgdcbibhd_test1") + raid2 = devicegraph.find_by_name("/dev/mapper/isw_ddgdcbibhd_test2") + + expect(subject.component_devices) + .to contain_exactly(tree.path_for(raid1), tree.path_for(raid2)) + end + end + end +end diff --git a/service/test/agama/dbus/storage/interfaces/drive_examples.rb b/service/test/agama/dbus/storage/interfaces/device/drive_examples.rb similarity index 98% rename from service/test/agama/dbus/storage/interfaces/drive_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/drive_examples.rb index f27e243012..62595298c4 100644 --- a/service/test/agama/dbus/storage/interfaces/drive_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/drive_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" require "y2storage/disk_size" shared_examples "Drive interface" do diff --git a/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb new file mode 100644 index 0000000000..c581bd7668 --- /dev/null +++ b/service/test/agama/dbus/storage/interfaces/device/filesystem_examples.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# 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 version 2 of the GNU General Public License as published +# by the Free Software Foundation. +# +# 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. + +require_relative "../../../../../test_helper" + +shared_examples "Filesystem interface" do + describe "Filesystem D-Bus interface" do + let(:scenario) { "multipath-formatted.xml" } + + let(:device) { devicegraph.find_by_name("/dev/mapper/0QEMU_QEMU_HARDDISK_mpath1") } + + describe "#filesystem_type" do + it "returns the file system type" do + expect(subject.filesystem_type).to eq("ext4") + end + end + + describe "#filesystem_efi?" do + it "returns whether the file system is an EFI" do + expect(subject.filesystem_efi?).to eq(false) + end + end + end +end diff --git a/service/test/agama/dbus/storage/interfaces/md_examples.rb b/service/test/agama/dbus/storage/interfaces/device/md_examples.rb similarity index 77% rename from service/test/agama/dbus/storage/interfaces/md_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/md_examples.rb index 2816200979..a8fd1d1711 100644 --- a/service/test/agama/dbus/storage/interfaces/md_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/md_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "MD interface" do describe "MD D-Bus interface" do @@ -45,9 +45,13 @@ end end - describe "#md_members" do - it "returns the name of the MD members" do - expect(subject.md_members).to contain_exactly("/dev/sda1", "/dev/sda2") + describe "#md_devices" do + it "returns the D-Bus path of the MD components" do + sda1 = devicegraph.find_by_name("/dev/sda1") + sda2 = devicegraph.find_by_name("/dev/sda2") + + expect(subject.md_devices) + .to contain_exactly(tree.path_for(sda1), tree.path_for(sda2)) end end end diff --git a/service/test/agama/dbus/storage/interfaces/multipath_examples.rb b/service/test/agama/dbus/storage/interfaces/device/multipath_examples.rb similarity index 73% rename from service/test/agama/dbus/storage/interfaces/multipath_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/multipath_examples.rb index f46209a9c3..ddf59a7d73 100644 --- a/service/test/agama/dbus/storage/interfaces/multipath_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/multipath_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "Multipath interface" do describe "Multipath D-Bus interface" do @@ -28,8 +28,12 @@ let(:device) { devicegraph.multipaths.first } describe "#multipath_wires" do - it "returns the name of the Multipath wires" do - expect(subject.multipath_wires).to contain_exactly("/dev/sda", "/dev/sdb") + it "returns the D-Bus path of the Multipath wires" do + sda = devicegraph.find_by_name("/dev/sda") + sdb = devicegraph.find_by_name("/dev/sdb") + + expect(subject.multipath_wires) + .to contain_exactly(tree.path_for(sda), tree.path_for(sdb)) end end end diff --git a/service/test/agama/dbus/storage/interfaces/partition_table_examples.rb b/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb similarity index 96% rename from service/test/agama/dbus/storage/interfaces/partition_table_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb index a902e3dbda..1af30111ab 100644 --- a/service/test/agama/dbus/storage/interfaces/partition_table_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/partition_table_examples.rb @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "PartitionTable interface" do describe "PartitionTable D-Bus interface" do diff --git a/service/test/agama/dbus/storage/interfaces/raid_examples.rb b/service/test/agama/dbus/storage/interfaces/device/raid_examples.rb similarity index 73% rename from service/test/agama/dbus/storage/interfaces/raid_examples.rb rename to service/test/agama/dbus/storage/interfaces/device/raid_examples.rb index c8ac7bd28a..7b53df1978 100644 --- a/service/test/agama/dbus/storage/interfaces/raid_examples.rb +++ b/service/test/agama/dbus/storage/interfaces/device/raid_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -19,7 +19,7 @@ # To contact SUSE LLC about this file by physical or electronic mail, you may # find current contact information at www.suse.com. -require_relative "../../../../test_helper" +require_relative "../../../../../test_helper" shared_examples "RAID interface" do describe "RAID D-Bus interface" do @@ -28,8 +28,12 @@ let(:device) { devicegraph.dm_raids.first } describe "#raid_devices" do - it "returns the name of the RAID devices" do - expect(subject.raid_devices).to contain_exactly("/dev/sdb", "/dev/sdc") + it "returns the D-Bus path of the RAID devices" do + sdb = devicegraph.find_by_name("/dev/sdb") + sdc = devicegraph.find_by_name("/dev/sdc") + + expect(subject.raid_devices) + .to contain_exactly(tree.path_for(sdb), tree.path_for(sdc)) end end end diff --git a/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb b/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb index bd43e1362c..3114f1a9e6 100644 --- a/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb +++ b/service/test/agama/dbus/storage/proposal_settings_conversion/from_dbus_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -69,7 +69,16 @@ "EncryptionMethod" => "luks1", "EncryptionPBKDFunction" => "pbkdf2", "SpacePolicy" => "custom", - "SpaceActions" => { "/dev/sda" => "force_delete" }, + "SpaceActions" => [ + { + "Device" => "/dev/sda", + "Action" => "force_delete" + }, + { + "Device" => "/dev/sdb1", + "Action" => "resize" + } + ], "Volumes" => [ { "MountPath" => "/" }, { "MountPath" => "/test" } @@ -87,7 +96,9 @@ expect(settings.encryption.method).to eq(Y2Storage::EncryptionMethod::LUKS1) expect(settings.encryption.pbkd_function).to eq(Y2Storage::PbkdFunction::PBKDF2) expect(settings.space.policy).to eq(:custom) - expect(settings.space.actions).to eq({ "/dev/sda" => "force_delete" }) + expect(settings.space.actions).to eq({ + "/dev/sda" => :force_delete, "/dev/sdb1" => :resize + }) expect(settings.volumes.map(&:mount_path)).to contain_exactly("/", "/test") end diff --git a/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb b/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb index 168ab4852e..8cc085e5ff 100644 --- a/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb +++ b/service/test/agama/dbus/storage/proposal_settings_conversion/to_dbus_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2024] SUSE LLC # # All Rights Reserved. # @@ -38,7 +38,7 @@ settings.encryption.method = Y2Storage::EncryptionMethod::LUKS2 settings.encryption.pbkd_function = Y2Storage::PbkdFunction::ARGON2ID settings.space.policy = :custom - settings.space.actions = { "/dev/sda" => :force_delete } + settings.space.actions = { "/dev/sda" => :force_delete, "/dev/sdb1" => "resize" } settings.volumes = [Agama::Storage::Volume.new("/test")] end end @@ -53,7 +53,7 @@ "EncryptionMethod" => "luks2", "EncryptionPBKDFunction" => "pbkdf2", "SpacePolicy" => "keep", - "SpaceActions" => {}, + "SpaceActions" => [], "Volumes" => [] ) @@ -65,7 +65,16 @@ "EncryptionMethod" => "luks2", "EncryptionPBKDFunction" => "argon2id", "SpacePolicy" => "custom", - "SpaceActions" => { "/dev/sda" => :force_delete }, + "SpaceActions" => [ + { + "Device" => "/dev/sda", + "Action" => "force_delete" + }, + { + "Device" => "/dev/sdb1", + "Action" => "resize" + } + ], "Volumes" => [ { "MountPath" => "/test", diff --git a/service/test/agama/dbus/storage/proposal_test.rb b/service/test/agama/dbus/storage/proposal_test.rb index 47791823af..790014eeaa 100644 --- a/service/test/agama/dbus/storage/proposal_test.rb +++ b/service/test/agama/dbus/storage/proposal_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -181,6 +181,34 @@ end end + describe "#space_actions" do + context "if a proposal has not been calculated yet" do + let(:settings) { nil } + + it "returns an empty list" do + expect(subject.space_actions).to eq([]) + end + end + + context "if a proposal has been calculated" do + let(:settings) do + Agama::Storage::ProposalSettings.new.tap do |settings| + settings.space.actions = { + "/dev/vda1" => :force_delete, + "/dev/vda2" => :resize + } + end + end + + it "return a list with a hash for each action" do + expect(subject.space_actions).to contain_exactly( + { "Device" => "/dev/vda1", "Action" => "force_delete" }, + { "Device" => "/dev/vda2", "Action" => "resize" } + ) + end + end + end + describe "#volumes" do let(:settings) do Agama::Storage::ProposalSettings.new.tap { |s| s.volumes = calculated_volumes } diff --git a/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb b/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb index 9a6f4ff517..c01e40a4e9 100644 --- a/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb +++ b/service/test/agama/storage/proposal_settings_conversion/from_y2storage_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2023] SUSE LLC +# Copyright (c) [2023-2024] SUSE LLC # # All Rights Reserved. # @@ -38,6 +38,10 @@ settings.encryption_password = "notsecret" settings.encryption_method = Y2Storage::EncryptionMethod::LUKS2 settings.encryption_pbkdf = Y2Storage::PbkdFunction::ARGON2ID + settings.space_settings.actions = { + "/dev/sda" => :force_delete, + "/dev/sdb1" => :resize + } settings.volumes = [] end end @@ -57,6 +61,9 @@ method: Y2Storage::EncryptionMethod::LUKS2, pbkd_function: Y2Storage::PbkdFunction::ARGON2ID ), + space: an_object_having_attributes( + actions: { "/dev/sda" => :force_delete, "/dev/sdb1" => :resize } + ), volumes: [] ) end diff --git a/service/test/agama/storage/proposal_test.rb b/service/test/agama/storage/proposal_test.rb index d276a2f8c8..c56237b394 100644 --- a/service/test/agama/storage/proposal_test.rb +++ b/service/test/agama/storage/proposal_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Copyright (c) [2022-2023] SUSE LLC +# Copyright (c) [2022-2024] SUSE LLC # # All Rights Reserved. # @@ -155,13 +155,10 @@ volumes: contain_exactly( an_object_having_attributes(mount_path: "/") ), - # Checking space settings explicitly here because the settings converter cannot infer the - # space settings from the Y2Storage settings. The space settings are directly recovered - # from the original settings passed to #calculate. - space: an_object_having_attributes( - policy: :custom, - actions: { "/dev/sda" => :force_delete } - ) + # Checking space policy explicitly here because the settings converter cannot infer the + # space policy from the Y2Storage settings. The space policy is directly recovered from + # the original settings passed to #calculate. + space: an_object_having_attributes(policy: :custom) ) end diff --git a/web/cspell.json b/web/cspell.json index adf47ce25f..9c7619f4a9 100644 --- a/web/cspell.json +++ b/web/cspell.json @@ -75,6 +75,7 @@ "textinput", "tkip", "udev", + "unpartitioned", "wwpn", "xxxs", "zfcp" diff --git a/web/package/cockpit-agama.changes b/web/package/cockpit-agama.changes index bded8c6aff..43034076bc 100644 --- a/web/package/cockpit-agama.changes +++ b/web/package/cockpit-agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Feb 20 13:13:51 UTC 2024 - José Iván López González + +- Add section for space policy to the storage page + (gh#openSUSE/agama#1028). + ------------------------------------------------------------------- Wed Feb 14 12:42:32 UTC 2024 - David Diaz diff --git a/web/src/assets/styles/blocks.scss b/web/src/assets/styles/blocks.scss index bafe238f42..a6d42c2e68 100644 --- a/web/src/assets/styles/blocks.scss +++ b/web/src/assets/styles/blocks.scss @@ -15,7 +15,7 @@ margin-inline-end: var(--section-icon-size); &:not(:last-child) { - margin-block-end: var(--spacer-normal); + margin-block-end: var(--spacer-medium); } > h2 { @@ -431,6 +431,32 @@ ul[data-type="agama/list"][role="grid"] { } } +[data-type="agama/options-picker"] { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--spacer-smaller); + + [role="option"] { + cursor: pointer; + border: 1px solid var(--color-gray); + padding: var(--spacer-small); + border-block-end-width: 4px; + + &[aria-selected="true"] { + background: var(--color-gray-light); + border-color: var(--color-primary); + } + + >:first-child { + margin-block-end: var(--spacer-small); + } + + >:last-child { + font-size: var(--fs-small); + } + } +} + [role="dialog"] { section:not([class^="pf-c"]) { > svg:first-child { diff --git a/web/src/assets/styles/patternfly-overrides.scss b/web/src/assets/styles/patternfly-overrides.scss index 5e2d2f9fe5..90fa1bb699 100644 --- a/web/src/assets/styles/patternfly-overrides.scss +++ b/web/src/assets/styles/patternfly-overrides.scss @@ -245,9 +245,27 @@ ul { } } +// Styles for tree table used by storage page. + +.pf-v5-c-table tbody { + border-block-end: var(--pf-v5-c-table--border-width--base) solid var(--pf-v5-c-table--BorderColor); +} + +.pf-v5-c-table td > .pf-v5-c-form-control { + inline-size: max-content; +} + +.pf-v5-c-table tr[aria-level="1"] { + border-block-end: 0; + border-block-start: var(--pf-v5-c-table--border-width--base) solid var(--pf-v5-c-table--BorderColor); +} + +.pf-v5-c-table tr[aria-level="2"] { + border-block-end: 0; +} + @media screen and (width <= 768px) { .pf-m-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr):not(.pf-v5-c-table__expandable-row) { padding-inline: 0; } - } diff --git a/web/src/assets/styles/utilities.scss b/web/src/assets/styles/utilities.scss index 4452ec0c22..ae12b38719 100644 --- a/web/src/assets/styles/utilities.scss +++ b/web/src/assets/styles/utilities.scss @@ -34,6 +34,10 @@ font-weight: bold; } +.fs-small { + font-size: var(--fs-small); +} + // Utility classes for sizing icons .icon-xxxs { block-size: var(--icon-size-xxxs); diff --git a/web/src/client/storage.js b/web/src/client/storage.js index ba74801644..66a92956bf 100644 --- a/web/src/client/storage.js +++ b/web/src/client/storage.js @@ -108,6 +108,7 @@ class DevicesManager { * * @typedef {object} StorageDevice * @property {string} sid - Internal id that is used as D-Bus object basename + * @property {boolean} isDrive - Whether the device is a drive * @property {string} type - Type of device ("disk", "raid", "multipath", "dasd", "md") * @property {string} [vendor] * @property {string} [model] @@ -117,25 +118,33 @@ class DevicesManager { * @property {string} [transport] * @property {boolean} [sdCard] * @property {boolean} [dellBOOS] - * @property {string[]} [devices] - RAID devices (only for "raid" type) + * @property {string[]} [devices] - RAID devices (only for "raid" and "md" types) * @property {string[]} [wires] - Multipath wires (only for "multipath" type) * @property {string} [level] - MD RAID level (only for "md" type) * @property {string} [uuid] - * @property {string[]} [members] - Member devices for a MD RAID (only for "md" type) * @property {boolean} [active] * @property {string} [name] - Block device name * @property {number} [size] + * @property {number} [recoverableSize] * @property {string[]} [systems] - Name of the installed systems * @property {string[]} [udevIds] * @property {string[]} [udevPaths] - * @property {PartitionTableData} [partitionTable] + * @property {PartitionTable} [partitionTable] + * @property {Filesystem} [filesystem] * - * @typedef {object} PartitionTableData + * @typedef {object} PartitionTable * @property {string} type + * @property {StorageDevice[]} partitions + * @property {number} unpartitionedSize - Total size not assigned to any partition + * + * @typedef {object} Filesystem + * @property {string} type + * @property {boolean} isEFI */ async getDevices() { - const buildDevice = (path, dbusDevice) => { + const buildDevice = (path, dbusDevices) => { const addDriveProperties = (device, dbusProperties) => { + device.isDrive = true; device.type = dbusProperties.Type.v; device.vendor = dbusProperties.Vendor.v; device.model = dbusProperties.Model.v; @@ -148,41 +157,62 @@ class DevicesManager { }; const addRAIDProperties = (device, raidProperties) => { - device.devices = raidProperties.Devices.v; + device.devices = raidProperties.Devices.v.map(d => buildDevice(d, dbusDevices)); }; const addMultipathProperties = (device, multipathProperties) => { - device.wires = multipathProperties.Wires.v; + device.wires = multipathProperties.Wires.v.map(d => buildDevice(d, dbusDevices)); }; const addMDProperties = (device, mdProperties) => { device.type = "md"; device.level = mdProperties.Level.v; device.uuid = mdProperties.UUID.v; - device.members = mdProperties.Members.v; + device.devices = mdProperties.Devices.v.map(d => buildDevice(d, dbusDevices)); }; const addBlockProperties = (device, blockProperties) => { device.active = blockProperties.Active.v; device.name = blockProperties.Name.v; device.size = blockProperties.Size.v; + device.recoverableSize = blockProperties.RecoverableSize.v; device.systems = blockProperties.Systems.v; device.udevIds = blockProperties.UdevIds.v; device.udevPaths = blockProperties.UdevPaths.v; }; const addPtableProperties = (device, ptableProperties) => { + const partitions = ptableProperties.Partitions.v.map(p => buildDevice(p, dbusDevices)); device.partitionTable = { type: ptableProperties.Type.v, - partitions: ptableProperties.Partitions.v + partitions, + unpartitionedSize: device.size - partitions.reduce((s, p) => s + p.size, 0) + }; + }; + + const addFilesystemProperties = (device, filesystemProperties) => { + device.filesystem = { + type: filesystemProperties.Type.v, + isEFI: filesystemProperties.EFI.v + }; + }; + + const addComponentProperties = (device, componentProperties) => { + device.component = { + type: componentProperties.Type.v, + deviceNames: componentProperties.DeviceNames.v }; }; const device = { sid: path.split("/").pop(), + isDrive: false, type: "" }; + const dbusDevice = dbusDevices[path]; + if (!dbusDevice) return device; + const driveProperties = dbusDevice["org.opensuse.Agama.Storage1.Drive"]; if (driveProperties !== undefined) addDriveProperties(device, driveProperties); @@ -201,6 +231,12 @@ class DevicesManager { const ptableProperties = dbusDevice["org.opensuse.Agama.Storage1.PartitionTable"]; if (ptableProperties !== undefined) addPtableProperties(device, ptableProperties); + const filesystemProperties = dbusDevice["org.opensuse.Agama.Storage1.Filesystem"]; + if (filesystemProperties !== undefined) addFilesystemProperties(device, filesystemProperties); + + const componentProperties = dbusDevice["org.opensuse.Agama.Storage1.Component"]; + if (componentProperties !== undefined) addComponentProperties(device, componentProperties); + return device; }; @@ -214,7 +250,7 @@ class DevicesManager { const dbusObjects = managedObjects.shift(); const systemPaths = Object.keys(dbusObjects).filter(k => k.startsWith(this.rootPath)); - return systemPaths.map(p => buildDevice(p, dbusObjects[p])); + return systemPaths.map(p => buildDevice(p, dbusObjects)); } } @@ -241,10 +277,15 @@ class ProposalManager { * @property {string} encryptionMethod * @property {boolean} lvm * @property {string} spacePolicy + * @property {SpaceAction[]} spaceActions * @property {string[]} systemVGDevices * @property {Volume[]} volumes * @property {StorageDevice[]} installationDevices * + * @typedef {object} SpaceAction + * @property {string} device + * @property {string} action + * * @typedef {object} Volume * @property {string} mountPath * @property {string} fsType @@ -338,6 +379,13 @@ class ProposalManager { const systemDevices = await this.system.getDevices(); const buildResult = (proxy) => { + const buildSpaceAction = dbusSpaceAction => { + return { + device: dbusSpaceAction.Device.v, + action: dbusSpaceAction.Action.v + }; + }; + const buildAction = dbusAction => { return { text: dbusAction.Text.v, @@ -365,6 +413,7 @@ class ProposalManager { bootDevice: proxy.BootDevice, lvm: proxy.LVM, spacePolicy: proxy.SpacePolicy, + spaceActions: proxy.SpaceActions.map(buildSpaceAction), systemVGDevices: proxy.SystemVGDevices, encryptionPassword: proxy.EncryptionPassword, encryptionMethod: proxy.EncryptionMethod, @@ -388,7 +437,31 @@ class ProposalManager { * @param {ProposalSettings} settings * @returns {Promise} 0 on success, 1 on failure */ - async calculate({ bootDevice, encryptionPassword, encryptionMethod, lvm, spacePolicy, systemVGDevices, volumes }) { + async calculate(settings) { + const { + bootDevice, + encryptionPassword, + encryptionMethod, + lvm, + spacePolicy, + spaceActions, + systemVGDevices, + volumes + } = settings; + + const dbusSpaceActions = () => { + const dbusSpaceAction = (spaceAction) => { + return { + Device: { t: "s", v: spaceAction.device }, + Action: { t: "s", v: spaceAction.action } + }; + }; + + if (spacePolicy !== "custom") return; + + return spaceActions?.map(dbusSpaceAction); + }; + const dbusVolume = (volume) => { return removeUndefinedCockpitProperties({ MountPath: { t: "s", v: volume.mountPath }, @@ -401,18 +474,19 @@ class ProposalManager { }); }; - const settings = removeUndefinedCockpitProperties({ + const dbusSettings = removeUndefinedCockpitProperties({ BootDevice: { t: "s", v: bootDevice }, EncryptionPassword: { t: "s", v: encryptionPassword }, EncryptionMethod: { t: "s", v: encryptionMethod }, LVM: { t: "b", v: lvm }, SpacePolicy: { t: "s", v: spacePolicy }, + SpaceActions: { t: "aa{sv}", v: dbusSpaceActions() }, SystemVGDevices: { t: "as", v: systemVGDevices }, Volumes: { t: "aa{sv}", v: volumes?.map(dbusVolume) } }); const proxy = await this.proxies.proposalCalculator; - return proxy.Calculate(settings); + return proxy.Calculate(dbusSettings); } /** diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 8c3826221f..73610f9a1b 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -1,5 +1,5 @@ /* - * Copyright (c) [2022-2023] SUSE LLC + * Copyright (c) [2022-2024] SUSE LLC * * All Rights Reserved. * @@ -33,118 +33,264 @@ const cockpitCallbacks = {}; let managedObjects = {}; -const systemDevices = { - sda: { - sid: "59", - type: "disk", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, - name: "/dev/sda", - size: 1024, - systems : ["Windows", "openSUSE Leap 15.2"], - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], - partitionTable: { - type: "gpt", - partitions: ["/dev/sda1", "/dev/sda2"] - } - }, - sdb: { - sid: "60", - type: "disk", - vendor: "Samsung", - model: "Samsung Evo 8 Pro", - driver: ["ahci"], - bus: "IDE", - busId: "", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/sdb", - size: 2048, - systems : [], - udevIds: [], - udevPaths: ["pci-0000:00-19"] - }, - md0: { - sid: "62", - type: "md", - level: "raid0", - uuid: "12345:abcde", - members: ["/dev/sdb"], - active: true, - name: "/dev/md0", - size: 2048, - systems : [], - udevIds: [], - udevPaths: [] - }, - raid: { - sid: "63", - type: "raid", - devices: ["/dev/sda", "/dev/sdb"], - vendor: "Dell", - model: "Dell BOSS-N1 Modular", - driver: [], - bus: "", - busId: "", - transport: "", - dellBOSS: true, - sdCard: false, - active: true, - name: "/dev/mapper/isw_ddgdcbibhd_244", - size: 2048, - systems : [], - udevIds: [], - udevPaths: [] - }, - multipath: { - sid: "64", - type: "multipath", - wires: ["/dev/sdc", "/dev/sdd"], - vendor: "", - model: "", - driver: [], - bus: "", - busId: "", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/mapper/36005076305ffc73a00000000000013b4", - size: 2048, - systems : [], - udevIds: [], - udevPaths: [] - }, - dasd: { - sid: "65", - type: "dasd", - vendor: "IBM", - model: "IBM", - driver: [], - bus: "", - busId: "0.0.0150", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/dasda", - size: 2048, - systems : [], - udevIds: [], - udevPaths: [] - } +// Define devices + +const sda = { + sid: "59", + isDrive: true, + type: "disk", + vendor: "Micron", + model: "Micron 1100 SATA", + driver: ["ahci", "mmcblk"], + bus: "IDE", + busId: "", + transport: "usb", + dellBOSS: false, + sdCard: true, + active: true, + name: "/dev/sda", + size: 1024, + recoverableSize: 0, + systems : [], + udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], + udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], +}; + +const sda1 = { + sid: "60", + isDrive: false, + type: "", + active: true, + name: "/dev/sda1", + size: 512, + recoverableSize: 128, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sda2 = { + sid: "61", + isDrive: false, + type: "", + active: true, + name: "/dev/sda2", + size: 256, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sdb = { + sid: "62", + isDrive: true, + type: "disk", + vendor: "Samsung", + model: "Samsung Evo 8 Pro", + driver: ["ahci"], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdb", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: ["pci-0000:00-19"] +}; + +const sdc = { + sid: "63", + isDrive: true, + type: "disk", + vendor: "Disk", + model: "", + driver: [], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdc", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sdd = { + sid: "64", + isDrive: true, + type: "disk", + vendor: "Disk", + model: "", + driver: [], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sdd", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const sde = { + sid: "65", + isDrive: true, + type: "disk", + vendor: "Disk", + model: "", + driver: [], + bus: "IDE", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/sde", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const md0 = { + sid: "66", + isDrive: false, + type: "md", + level: "raid0", + uuid: "12345:abcde", + active: true, + name: "/dev/md0", + size: 2048, + recoverableSize: 0, + systems : ["openSUSE Leap 15.2"], + udevIds: [], + udevPaths: [], + filesystem: { type: "ext4", isEFI: false } +}; + +const raid = { + sid: "67", + isDrive: true, + type: "raid", + vendor: "Dell", + model: "Dell BOSS-N1 Modular", + driver: [], + bus: "", + busId: "", + transport: "", + dellBOSS: true, + sdCard: false, + active: true, + name: "/dev/mapper/isw_ddgdcbibhd_244", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const multipath = { + sid: "68", + isDrive: true, + type: "multipath", + vendor: "", + model: "", + driver: [], + bus: "", + busId: "", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/mapper/36005076305ffc73a00000000000013b4", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +const dasd = { + sid: "69", + isDrive: true, + type: "dasd", + vendor: "IBM", + model: "IBM", + driver: [], + bus: "", + busId: "0.0.0150", + transport: "", + dellBOSS: false, + sdCard: false, + active: true, + name: "/dev/dasda", + size: 2048, + recoverableSize: 0, + systems : [], + udevIds: [], + udevPaths: [] +}; + +// Define relationship between devices + +sda.partitionTable = { + type: "gpt", + partitions: [sda1, sda2], + unpartitionedSize: 256 }; +sda1.component = { + type: "md_device", + deviceNames: ["/dev/md0"] +}; + +sda2.component = { + type: "md_device", + deviceNames: ["/dev/md0"] +}; + +sdb.component = { + type: "raid_device", + deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"] +}; + +sdc.component = { + type: "raid_device", + deviceNames: ["/dev/mapper/isw_ddgdcbibhd_244"] +}; + +sdd.component = { + type: "multipath_wire", + deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"] +}; + +sde.component = { + type: "multipath_wire", + deviceNames: ["/dev/mapper/36005076305ffc73a00000000000013b4"] +}; + +md0.devices = [sda1, sda2]; + +raid.devices = [sdb, sdc]; + +multipath.wires = [sdd, sde]; + +const systemDevices = { sda, sda1, sda2, sdb, sdc, sdd, sde, md0, raid, multipath, dasd }; + const contexts = { withoutProposal: () => { cockpitProxies.proposal = null; @@ -155,7 +301,17 @@ const contexts = { LVM: true, SystemVGDevices: ["/dev/sda", "/dev/sdb"], EncryptionPassword: "00000", - SpacePolicy: "delete", + SpacePolicy: "custom", + SpaceActions: [ + { + Device: { t: "s", v: "/dev/sda" }, + Action: { t: "s", v: "force_delete" } + }, + { + Device: { t: "s", v: "/dev/sdb" }, + Action: { t: "s", v: "resize" } + } + ], Volumes: [ { MountPath: { t: "s", v: "/" }, @@ -210,7 +366,7 @@ const contexts = { withAvailableDevices: () => { cockpitProxies.proposalCalculator.AvailableDevices = [ "/org/opensuse/Agama/Storage1/system/59", - "/org/opensuse/Agama/Storage1/system/60" + "/org/opensuse/Agama/Storage1/system/62" ]; }, withoutIssues: () => { @@ -335,16 +491,52 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/sda" }, Size: { t: "x", v: 1024 }, - Systems: { t: "as", v: ["Windows", "openSUSE Leap 15.2"] }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"] }, UdevPaths: { t: "as", v: ["pci-0000:00-12", "pci-0000:00-12-ata"] } }, "org.opensuse.Agama.Storage1.PartitionTable": { Type: { t: "s", v: "gpt" }, - Partitions: { t: "as", v: ["/dev/sda1", "/dev/sda2"] } + Partitions: { + t: "as", + v: ["/org/opensuse/Agama/Storage1/system/60", "/org/opensuse/Agama/Storage1/system/61"] + } } }; managedObjects["/org/opensuse/Agama/Storage1/system/60"] = { + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sda1" }, + Size: { t: "x", v: 512 }, + RecoverableSize: { t: "x", v: 128 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "md_device" }, + DeviceNames: { t: "as", v: ["/dev/md0"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/66"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/61"] = { + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sda2" }, + Size: { t: "x", v: 256 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "md_device" }, + DeviceNames: { t: "as", v: ["/dev/md0"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/66"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/62"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "disk" }, Vendor: { t: "s", v: "Samsung" }, @@ -359,27 +551,119 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/sdb" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: ["pci-0000:00-19"] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "raid_device" }, + DeviceNames: { t: "as", v: ["/dev/mapper/isw_ddgdcbibhd_244"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/67"] } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/62"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/63"] = { + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Disk" }, + Model: { t: "s", v: "" }, + Driver: { t: "as", v: [] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sdc" }, + Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "raid_device" }, + DeviceNames: { t: "as", v: ["/dev/mapper/isw_ddgdcbibhd_244"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/67"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/64"] = { + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Disk" }, + Model: { t: "s", v: "" }, + Driver: { t: "as", v: [] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sdd" }, + Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "multipath_wire" }, + DeviceNames: { t: "as", v: ["/dev/mapper/36005076305ffc73a00000000000013b4"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/68"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/65"] = { + "org.opensuse.Agama.Storage1.Drive": { + Type: { t: "s", v: "disk" }, + Vendor: { t: "s", v: "Disk" }, + Model: { t: "s", v: "" }, + Driver: { t: "as", v: [] }, + Bus: { t: "s", v: "IDE" }, + BusId: { t: "s", v: "" }, + Transport: { t: "s", v: "" }, + Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, + }, + "org.opensuse.Agama.Storage1.Block": { + Active: { t: "b", v: true }, + Name: { t: "s", v: "/dev/sde" }, + Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: [] }, + UdevIds: { t: "as", v: [] }, + UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Component": { + Type: { t: "s", v: "multipath_wire" }, + DeviceNames: { t: "as", v: ["/dev/mapper/36005076305ffc73a00000000000013b4"] }, + Devices: { t: "ao", v: ["/org/opensuse/Agama/Storage1/system/68"] } + } + }; + managedObjects["/org/opensuse/Agama/Storage1/system/66"] = { "org.opensuse.Agama.Storage1.MD": { Level: { t: "s", v: "raid0" }, UUID: { t: "s", v: "12345:abcde" }, - Members: { t: "as", v: ["/dev/sdb"] } + Devices: { + t: "ao", + v: ["/org/opensuse/Agama/Storage1/system/60", "/org/opensuse/Agama/Storage1/system/61"] + } }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/md0" }, Size: { t: "x", v: 2048 }, - Systems: { t: "as", v: [] }, + RecoverableSize: { t: "x", v: 0 }, + Systems: { t: "as", v: ["openSUSE Leap 15.2"] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } + }, + "org.opensuse.Agama.Storage1.Filesystem": { + Type: { t: "s", v: "ext4" }, + EFI: { t: "b", v: false } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/63"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/67"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "raid" }, Vendor: { t: "s", v: "Dell" }, @@ -391,18 +675,22 @@ const contexts = { Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: true }, SDCard: { t: "b", v: false } } }, }, "org.opensuse.Agama.Storage1.RAID" : { - Devices: { t: "as", v: ["/dev/sda", "/dev/sdb"] } + Devices: { + t: "ao", + v: ["/org/opensuse/Agama/Storage1/system/62", "/org/opensuse/Agama/Storage1/system/63"] + } }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/mapper/isw_ddgdcbibhd_244" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/64"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/68"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "multipath" }, Vendor: { t: "s", v: "" }, @@ -414,18 +702,22 @@ const contexts = { Info: { t: "a{sv}", v: { DellBOSS: { t: "b", v: false }, SDCard: { t: "b", v: false } } }, }, "org.opensuse.Agama.Storage1.Multipath" : { - Wires: { t: "as", v: ["/dev/sdc", "/dev/sdd"] } + Wires: { + t: "ao", + v: ["/org/opensuse/Agama/Storage1/system/64", "/org/opensuse/Agama/Storage1/system/65"] + } }, "org.opensuse.Agama.Storage1.Block": { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/mapper/36005076305ffc73a00000000000013b4" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } } }; - managedObjects["/org/opensuse/Agama/Storage1/system/65"] = { + managedObjects["/org/opensuse/Agama/Storage1/system/69"] = { "org.opensuse.Agama.Storage1.Drive": { Type: { t: "s", v: "dasd" }, Vendor: { t: "s", v: "IBM" }, @@ -440,6 +732,7 @@ const contexts = { Active: { t: "b", v: true }, Name: { t: "s", v: "/dev/dasda" }, Size: { t: "x", v: 2048 }, + RecoverableSize: { t: "x", v: 0 }, Systems: { t: "as", v: [] }, UdevIds: { t: "as", v: [] }, UdevPaths: { t: "as", v: [] } @@ -825,7 +1118,11 @@ describe("#proposal", () => { lvm: true, systemVGDevices: ["/dev/sda", "/dev/sdb"], encryptionPassword: "00000", - spacePolicy: "delete", + spacePolicy: "custom", + spaceActions: [ + { device: "/dev/sda", action: "force_delete" }, + { device: "/dev/sdb", action: "resize" } + ], volumes: [ { mountPath: "/", @@ -895,6 +1192,8 @@ describe("#proposal", () => { encryptionPassword: "12345", lvm: true, systemVGDevices: ["/dev/sdc"], + spacePolicy: "custom", + spaceActions: [{ device: "/dev/sda", action: "resize" }], volumes: [ { mountPath: "/test1", @@ -916,6 +1215,16 @@ describe("#proposal", () => { EncryptionPassword: { t: "s", v: "12345" }, LVM: { t: "b", v: true }, SystemVGDevices: { t: "as", v: ["/dev/sdc"] }, + SpacePolicy: { t: "s", v: "custom" }, + SpaceActions: { + t: "aa{sv}", + v: [ + { + Device: { t: "s", v: "/dev/sda" }, + Action: { t: "s", v: "resize" } + } + ] + }, Volumes: { t: "aa{sv}", v: [ @@ -935,6 +1244,17 @@ describe("#proposal", () => { } }); }); + + it("calculates a proposal without space actions if the policy is not custom", async () => { + await client.proposal.calculate({ + spacePolicy: "delete", + spaceActions: [{ device: "/dev/sda", action: "resize" }], + }); + + expect(cockpitProxies.proposalCalculator.Calculate).toHaveBeenCalledWith({ + SpacePolicy: { t: "s", v: "delete" } + }); + }); }); }); diff --git a/web/src/components/core/OptionsPicker.jsx b/web/src/components/core/OptionsPicker.jsx new file mode 100644 index 0000000000..55cc44f850 --- /dev/null +++ b/web/src/components/core/OptionsPicker.jsx @@ -0,0 +1,71 @@ +/* + * 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 version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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"; + +/** + * Wrapper for OptionsPicker options + * @component + * + * @param {object} props + * @param {string} [props.title] - Text to be used as option title + * @param {string} [props.body] - Text to be used as option body + * @param {boolean} [props.isSelected=false] - Whether the option should be set as select of not + * @param {object} [props.props] - Other props sent to div#option node + */ +const Option = ({ title, body, isSelected = false, ...props }) => { + return ( +
+
{title}
+
{body}
+
+ ); +}; + +/** + * Helper component to build rich options picker + * @component + * + * @param {object} props + * @param {string} [props.ariaLabel] - Text to be used as accessible label + * @param {Array