yast/yast-storage-ng

View on GitHub
src/lib/y2partitioner/widgets/format_and_mount.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Copyright (c) [2017-2020] 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 "yast"
require "cwm"
require "yast2/popup"
require "y2storage"
require "y2storage/mountable"
require "y2storage/btrfs_subvolume"
require "y2partitioner/filesystems"
require "y2partitioner/widgets/fstab_options"
require "y2partitioner/dialogs/mkfs_options"

module Y2Partitioner
  module Widgets
    # Format options for {Y2Storage::BlkDevice}
    #
    # This widget generates a Frame Term with the format and encrypt options,
    # redrawing the interface in case of filesystem or partition selection
    # change.
    class FormatOptions < CWM::CustomWidget
      # Constructor
      # @param controller [Actions::Controllers::Filesystem]
      # @param parent_widget [#refresh_others] container widget that must be
      #   notified after every relevant update to the controller information
      def initialize(controller, parent_widget)
        super()
        textdomain "storage"

        @controller        = controller
        @encrypt_widget    = EncryptBlkDevice.new(@controller)
        @filesystem_widget = BlkDeviceFilesystem.new(@controller)
        @format_options    = FormatOptionsArea.new(@controller)
        @partition_id      = PartitionId.new(@controller)
        @parent_widget     = parent_widget

        self.handle_all_events = true
      end

      # @macro seeAbstractWidget
      def init
        refresh
      end

      # Synchronize the widget with the information from the controller
      def refresh
        @encrypt_widget.refresh
        @filesystem_widget.refresh
        @partition_id.refresh
        @format_options.refresh

        if controller.to_be_formatted?
          Yast::UI.ChangeWidget(Id(:format_device), :Value, true)

          @encrypt_widget.enable
          @filesystem_widget.enable
        else
          Yast::UI.ChangeWidget(Id(:no_format_device), :Value, true)

          # If there is already a filesystem we want to respect, we can't decide
          # on the encryption value
          controller.filesystem ? @encrypt_widget.disable : @encrypt_widget.enable
          @filesystem_widget.disable
        end
      end

      # @macro seeAbstractWidget
      def handle(event)
        case event["ID"]
        when :format_device, @filesystem_widget.event_id
          select_format
        when :no_format_device
          select_no_format
        when @partition_id.event_id
          change_partition_id
        end

        nil
      end

      def help
        text = _("<p>First, choose whether the partition should be\n" \
                 "formatted and the desired file system type.</p>")

        text +=
          _(
            "<p>If you want to encrypt all data on the\n" \
            "volume, select <b>Encrypt Device</b>. Changing the encryption on an existing\n" \
            "volume will delete all data on it.</p>\n"
          )
        text +=
          _(
            "<p>Then, choose whether the partition should\n" \
            "be mounted and enter the mount point (/, /boot, /home, /var, etc.).</p>"
          )

        text
      end

      def contents
        VBox(
          RadioButtonGroup(
            Id(:format),
            VBox(
              Left(RadioButton(Id(:format_device), Opt(:notify), _("Format device"))),
              HBox(
                HSpacing(4),
                VBox(
                  Left(@filesystem_widget),
                  Left(@format_options)
                )
              ),
              Left(RadioButton(Id(:no_format_device), Opt(:notify), _("Do not format device"))),
              @partition_id
            )
          ),
          VSpacing(1),
          Left(@encrypt_widget)
        )
      end

      private

      attr_reader :controller

      def select_format
        controller.new_filesystem(@filesystem_widget.value)
        refresh
        @parent_widget.refresh_others(self)
      end

      def select_no_format
        controller.dont_format
        refresh
        @parent_widget.refresh_others(self)
      end

      def change_partition_id
        controller.partition_id = @partition_id.value
        refresh
        @parent_widget.refresh_others(self)
      end
    end

    # Mount options for {Y2Storage::BlkDevice}
    class MountOptions < CWM::CustomWidget
      # @param controller [Actions::Controllers::Filesystem]
      # @param parent_widget [#refresh_others] container widget that must be
      #   notified after every relevant update to the controller information
      def initialize(controller, parent_widget)
        super()
        textdomain "storage"

        @controller = controller
        @parent_widget = parent_widget

        @mount_point_widget = MountPoint.new(controller)
        @fstab_options_widget = FstabOptionsButton.new(controller)

        self.handle_all_events = true
      end

      def filesystem
        @controller.filesystem
      end

      # @macro seeAbstractWidget
      def init
        refresh
      end

      # Synchronize the widget with the information from the controller
      def refresh
        @mount_point_widget.refresh

        if filesystem&.supports_mount?
          Yast::UI.ChangeWidget(Id(:mount_device), :Enabled, true)

          if filesystem.mount_point.nil?
            Yast::UI.ChangeWidget(Id(:dont_mount_device), :Value, true)
            @mount_point_widget.disable
            @fstab_options_widget.disable
          else
            Yast::UI.ChangeWidget(Id(:mount_device), :Value, true)
            @mount_point_widget.enable
            @fstab_options_widget.enable
          end
        else
          Yast::UI.ChangeWidget(Id(:mount_device), :Enabled, false)

          Yast::UI.ChangeWidget(Id(:dont_mount_device), :Value, true)
          @mount_point_widget.disable
          @fstab_options_widget.disable
        end
      end

      def contents
        VBox(
          RadioButtonGroup(
            Id(:mount),
            VBox(
              Left(RadioButton(Id(:mount_device), Opt(:notify), _("Mount device"))),
              HBox(
                HSpacing(4),
                VBox(
                  Left(@mount_point_widget),
                  Left(@fstab_options_widget)
                )
              ),
              Left(RadioButton(Id(:dont_mount_device), Opt(:notify), _("Do not mount device")))
            )
          )
        )
      end

      # @macro seeAbstractWidget
      def handle(event)
        refresh_others = true

        case event["ID"]
        when :mount_device
          mount_device
        when :dont_mount_device
          dont_mount_device
        when @mount_point_widget.widget_id
          mount_point_change
        else
          refresh_others = false
        end
        @parent_widget.refresh_others(self) if refresh_others

        nil
      end

      # @see #errors
      def validate
        return false unless system_mount_points_warning

        error = errors.first

        if error
          Yast2::Popup.show(error, headline: :error)
          return false
        end

        btrfs_subvolumes_question
        true
      end

      private

      # @return [Actions::Controllers::Filesystem]
      attr_reader :controller

      # Asks to the user what to do with the current list of Btrfs subvolumes
      #
      # Note that the user is only asked when the Btrfs filesystem does not exist on disk yet and it has
      # subvolumes.
      def btrfs_subvolumes_question
        return unless controller.new_btrfs? && controller.mount_path_modified?

        controller.restore_btrfs_subvolumes = true

        # It does not have subvolumes, so let's simply restore the default list.
        return unless controller.btrfs_subvolumes?

        message = if controller.default_btrfs_subvolumes?
          format(
            _("You have chosen to mount the device at %{mount_path}.\n\n" \
              "Do you want to remove the current subvolumes and\n" \
              "create the suggested subvolumes for %{mount_path}?"),
            mount_path: controller.mount_path
          )
        else
          _(
            "You have changed the mount point of the device.\n\n" \
            "Do you want to delete the current subvolumes?"
          )
        end

        restore = Yast2::Popup.show(message, buttons: :yes_no) == :yes

        controller.restore_btrfs_subvolumes = restore
      end

      # List of errors
      #
      # @see #validate
      #
      # @return [Array<String>]
      def errors
        [mount_by_label_error].compact
      end

      # Error when the label is missing.
      #
      # It is necessary to prevent an empty filesystem label when the option mount_by is set
      # to label by default. The label value is validated when the user gives one value
      # (by editing the fstab options), but the user could mount a device without entering
      # in that dialog for the mount options.
      #
      # Here only the presence of a label is checked. The correctness of the label is checked
      # when the label is entered (see {Dialogs::FstabOptions}).
      #
      # @return [String, nil] nil if the label is not required or it is required and given
      def mount_by_label_error
        return nil if !formatted? || !mounted? || !mounted_by_label? || !empty_label?

        # TRANSLATORS: Error message when a device should be mounted by label but no label is given.
        _("Provide a volume label to mount by label.")
      end

      # Check if a system mount point is reused without formatting the
      # partition during installation and warn the user if it is.
      #
      # @return [Boolean] true if okay, false if not
      def system_mount_points_warning
        return true unless Yast::Mode.installation
        return true if to_be_formatted?
        return true unless mounted?
        return true unless ["/", "/usr", "/boot"].include?(mount_path)

        warn_unformatted_system_mount_points
      end

      # Post a warning about reusing unformatted system mount points.
      #
      # @return [Boolean] true if the user wants to continue
      def warn_unformatted_system_mount_points
        log.info("warn_unformatted_system_mount_points")
        # Translators: popup text
        message = _(
          "\n" \
          "You chose to install onto an existing partition that will not be\n" \
          "formatted. YaST cannot guarantee your installation will succeed,\n" \
          "particularly in any of the following cases:\n"
        ) +
          # continued popup text
          _(
            "- if this is an existing Btrfs partition\n" \
            "- if this partition already contains a Linux distribution that will be\n" \
            "overwritten\n" \
            "- if this partition does not yet contain a file system\n"
          ) +
          # continued popup text
          _(
            "If in doubt, better go back and mark this partition for\n" \
            "formatting, especially if it is assigned to one of the standard mount points\n" \
            "like /, /boot, /opt or /var.\n"
          ) +
          # continued popup text
          _(
            "If you decide to format the partition, all data on it will be lost.\n" \
            "\n" \
            "Really skip formatting the partition?\n"
          )
        Yast2::Popup.show(message, headline: :warning, buttons: :yes_no) == :yes
      end

      def mount_device
        @mount_point_widget.enable
        mount_point_change
      end

      def dont_mount_device
        @controller.remove_mount_point
        @fstab_options_widget.disable
        @mount_point_widget.disable
      end

      def mount_point_change
        if mount_path.empty?
          @controller.remove_mount_point
          @fstab_options_widget.disable
        else
          @controller.create_or_update_mount_point(mount_path)
          @fstab_options_widget.enable
        end
      end

      # Value given for the mount point
      #
      # @return [String]
      def mount_path
        @mount_point_widget.value.to_s
      end

      # Whether the device has a filesystem
      #
      # @return [Boolean] true if it has a filesystem; false otherwise.
      def formatted?
        !@controller.filesystem.nil?
      end

      # Whether the device will be formatted
      #
      # @return [Boolean] true if will be formatted; false otherwise
      def to_be_formatted?
        @controller.to_be_formatted?
      end

      # Whether the device has a mount point
      #
      # @return [Boolean] true if it has a mount point; false otherwise.
      def mounted?
        !@controller.mount_point.nil?
      end

      # Whether the device is set to be mounted by label
      #
      # @return [Boolean] true if it is set to mount by label; false otherwise.
      def mounted_by_label?
        mounted? && @controller.mount_point.mount_by.is?(:label)
      end

      # Whether the filesystem has a label
      #
      # @return [Boolean] true if it has a label; false otherwise.
      def empty_label?
        !formatted? || @controller.filesystem.label.empty?
      end
    end

    # BlkDevice Filesystem selector
    class BlkDeviceFilesystem < CWM::ComboBox
      # Id for the events generated by this widget
      #
      # Useful for other widgets with handle_all_events
      alias_method :event_id, :widget_id

      # @param controller [Actions::Controllers::Filesystem]
      def initialize(controller)
        super()
        textdomain "storage"

        @controller = controller
      end

      # @macro seeAbstractWidget
      def opt
        [:hstretch, :notify]
      end

      # @macro seeAbstractWidget
      def init
        refresh
      end

      # Synchronize the widget with the information from the controller
      def refresh
        fs_type = @controller.filesystem_type
        self.value = fs_type ? fs_type.to_sym : nil
      end

      def label
        _("Filesystem")
      end

      def items
        Y2Partitioner::Filesystems.all.map { |f| [f.to_sym, f.to_human_string] }
      end
    end

    # Widget to support the current UI hack in which the snapshots checkbox and
    # the format options button are displayed alternatively based on the
    # filesystem type and other criteria.
    #
    # The behavior is not 100% the same than the old partitioner (which
    # sometimes leaded to unsupported situations), but is equivalent for all the
    # supported scenarios.
    class FormatOptionsArea < CWM::ReplacePoint
      def initialize(controller)
        @controller       = controller
        @options_button   = FormatOptionsButton.new(controller)
        @snapper_checkbox = Snapshots.new(controller)
        super(id: "format_options_area", widget: @options_button)
      end

      alias_method :show, :replace

      # @macro seeAbstractWidget
      def init
        refresh
      end

      # Synchronizes the widget with the information from the controller
      def refresh
        if @controller.snapshots_supported?
          show(@snapper_checkbox)
          @snapper_checkbox.refresh
        else
          show(@options_button)
          @controller.format_options_supported? ? @options_button.enable : @options_button.disable
        end
      end
    end

    # Push Button that launches a dialog to set speficic options for the
    # selected filesystem
    class FormatOptionsButton < CWM::PushButton
      # @param controller [Actions::Controllers::Filesystem]
      def initialize(controller)
        super()
        textdomain "storage"
        @controller = controller
      end

      def filesystem
        @controller.filesystem
      end

      # @macro seeAbstractWidget
      def opt
        [:hstretch, :notify]
      end

      def label
        _("Options...")
      end

      # @macro seeAbstractWidget
      def handle
        log.info(
          "format options before: [#{filesystem.type}] " \
          "mkfs='#{filesystem.mkfs_options}', tune='#{filesystem.tune_options}'"
        )
        Dialogs::MkfsOptions.new(@controller).run
        log.info(
          "format options after: [#{filesystem.type}] " \
          "mkfs='#{filesystem.mkfs_options}', tune='#{filesystem.tune_options}'"
        )
        nil
      end
    end

    # Btrfs snapshots selector
    class Snapshots < CWM::CheckBox
      # @param controller [Actions::Controllers::Filesystem]
      def initialize(controller)
        super()
        textdomain "storage"
        @controller = controller
      end

      # @macro seeAbstractWidget
      def label
        _("Enable Snapshots")
      end

      # @macro seeAbstractWidget
      def opt
        [:notify]
      end

      # Synchronizes the widget with the information from the controller
      def refresh
        self.value = @controller.configure_snapper
      end

      # @macro seeAbstractWidget
      def handle(event)
        @controller.configure_snapper = value if event["ID"] == widget_id
        nil
      end

      # @macro seeAbstractWidget
      def help
        format(
          # TRANSLATORS: help text, where %{label} is the label of the explained widget
          _("<p><b>%{label}</b> configures Snapper and the subvolumes of the root file " \
            "system in a way that makes possible to take snapshots of the system. That " \
            "allows to boot to any of those former snapshots, rolling back any change " \
            "done to the system and restoring its previous state.</p>"), label: label
        )
      end
    end

    # MountPoint selector
    class MountPoint < CWM::ComboBox
      # Constructor
      # @param controller [Actions::Controllers::Filesystem]
      def initialize(controller)
        super()
        textdomain "storage"
        @controller = controller
      end

      # @macro seeAbstractWidget
      def init
        refresh
      end

      # Synchronize the widget with the information from the controller
      def refresh
        self.value = controller.mount_path
      end

      def label
        _("Mount Point")
      end

      # @macro seeAbstractWidget
      def opt
        [:editable, :hstretch, :notify]
      end

      def items
        controller.mount_paths.map { |mp| [mp, mp] }
      end

      # @see #errors
      def validate
        error = errors.first

        return true unless error

        Yast2::Popup.show(error, headline: :error)
        false
      end

      private

      # @return [Actions::Controllers::Filesystem]
      attr_reader :controller

      # List of errors
      #
      # The following condintions are checked:
      # - The mount point is not empty
      # - The mount point is unique
      #
      # @return [Array<String>]
      def errors
        return [] unless enabled?

        [content_error, uniqueness_error].compact
      end

      # Error when the mount point is empty
      #
      # @return [String, nil] nil if the mount point is given
      def content_error
        return nil unless value.empty?

        _("Empty mount point not allowed.")
      end

      # Error when the mount point is not unique in the whole system
      #
      # @see #duplicated_mount_point?
      #
      # @return [String, nil] nil if the mount point is unique
      def uniqueness_error
        return nil unless duplicated_mount_point?

        _("This mount point is already in use. Select a different one.")
      end

      # Checks if the mount point is duplicated
      #
      # @return [Boolean]
      def duplicated_mount_point?
        # The special mount point "swap" can be used for several devices at the
        # same time.
        return false if value == "swap"

        controller.mounted_paths.include?(value)
      end
    end

    # Encryption selector
    class EncryptBlkDevice < CWM::CheckBox
      # @param controller [Actions::Controllers::Filesystem]
      def initialize(controller)
        super()
        textdomain "storage"
        @controller = controller
      end

      def label
        _("Encrypt Device")
      end

      # @macro seeAbstractWidget
      def opt
        [:notify]
      end

      # @macro seeAbstractWidget
      def init
        refresh
      end

      # Synchronize the widget with the information from the controller
      def refresh
        self.value = @controller.encrypt
      end

      # validates if it can be encrypted
      def validate
        return true unless value

        # FIXME: have generic check when new partition is created if it is valid
        # like e.g. if fs fits to partition, and others
        # size number is from bsc1065071#c5
        return true if @controller.blk_device.size > ::Y2Storage::DiskSize.MiB(2)

        Yast2::Popup.show(_("Only devices bigger than 2 MiB can be encrypted."), headline: :error)
        false
      end

      # @macro seeAbstractWidget
      def handle(event)
        @controller.encrypt = value if event["ID"] == widget_id
        nil
      end
    end

    # Inode Size format option
    class InodeSize < CWM::ComboBox
      SIZES = ["auto", "512", "1024", "2048", "4096"].freeze

      def initialize(options)
        super()
        textdomain "storage"
        @options = options
      end

      def label
        _("&Inode Size")
      end

      def items
        SIZES.map { |s| [s, s] }
      end
    end

    # Block Size format option
    class BlockSize < CWM::ComboBox
      SIZES = ["auto", "512", "1024", "2048", "4096"].freeze

      def initialize(options)
        super()
        textdomain "storage"
        @options = options
      end

      def label
        _("Block &Size in Bytes")
      end

      def help
        "<p><b>Block Size:</b>\nSpecify the size of blocks in bytes. " \
          "Valid block size values are 512, 1024, 2048 and 4096 bytes " \
          "per block. If auto is selected, the standard block size of " \
          "4096 is used.</p>\n"
      end

      def items
        SIZES.map { |s| [s, s] }
      end
    end

    # Partition identifier selector or empty widget if changing the partition id
    # is not possible
    class PartitionId < CWM::CustomWidget
      # @param controller [Actions::Controllers::Filesystem]
      def initialize(controller)
        super()
        textdomain "storage"
        @controller = controller
        @selector = PartitionIdComboBox.new(controller) if controller.partition_id_supported?
      end

      # @macro seeAbstractWidget
      def contents
        if @selector
          VBox(VSpacing(1), Left(@selector))
        else
          Empty()
        end
      end

      # Synchronize the widget with the information from the controller
      def refresh
        @selector&.refresh
      end

      # @macro seeAbstractWidget
      def enable
        @selector&.enable
      end

      # @macro seeAbstractWidget
      def disable
        @selector&.disable
      end

      # Id for the events generated by this widget
      #
      # Useful for other widgets with handle_all_events
      def event_id
        @selector&.widget_id
      end

      def value
        @selector&.value
      end
    end

    # Partition identifier selector
    class PartitionIdComboBox < CWM::ComboBox
      # @param controller [Actions::Controllers::Filesystem]
      def initialize(controller)
        super()
        textdomain "storage"
        @controller = controller
      end

      # @macro seeAbstractWidget
      def opt
        [:hstretch, :notify]
      end

      # @macro seeAbstractWidget
      def init
        refresh
      end

      # Synchronize the widget with the information from the controller
      def refresh
        self.value = @controller.partition_id.to_sym
      end

      def label
        _("Partition &ID:")
      end

      def items
        blk_dev = @controller.blk_device
        if blk_dev.is?(:partition)
          blk_dev.partition_table.supported_partition_ids.sort.map do |part_id|
            [part_id.to_sym, part_id.to_human_string]
          end
        else
          []
        end
      end
    end
  end
end