yast/yast-network

View on GitHub
src/lib/y2network/widgets/bond_port.rb

Summary

Maintainability
A
35 mins
Test Coverage
# Copyright (c) [2019] 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 "ui/text_helpers"
require "yast"
require "cwm/custom_widget"
require "y2network/widgets/port_items"

Yast.import "Label"
Yast.import "Lan"
Yast.import "Popup"
Yast.import "UI"

module Y2Network
  module Widgets
    class BondPort < CWM::CustomWidget
      include PortItems
      include ::UI::TextHelpers

      def initialize(settings)
        super()
        textdomain "network"
        @settings = settings
      end

      def contents
        Frame(
          _("Bond Ports and Order"),
          VBox(
            MultiSelectionBox(Id(:bond_ports_items), Opt(:notify), "", []),
            HBox(
              # TRANSLATORS: this means "move this line upwards"
              PushButton(Id(:bond_ports_up), Opt(:disabled), _("Up")),
              # TRANSLATORS: this means "move this line downwards"
              PushButton(Id(:bond_ports_down), Opt(:disabled), _("Down"))
            )
          )
        )
      end

      def handle(event)
        if event["EventReason"] == "SelectionChanged"
          enable_position_buttons
        elsif event["EventReason"] == "Activated" && event["WidgetClass"] == :PushButton
          items = ui_items || []
          current = value.to_s
          index = value_index
          case event["ID"]
          when :bond_ports_up
            items[index], items[index - 1] = items[index - 1], items[index]
          when :bond_ports_down
            items[index], items[index + 1] = items[index + 1], items[index]
          else
            log.warn("unknown action #{event["ID"]}")
            return nil
          end
          Yast::UI.ChangeWidget(:bond_ports_items, :Items, items)
          Yast::UI.ChangeWidget(:bond_ports_items, :CurrentItem, current)
          enable_position_buttons
        else
          log.debug("event:#{event}")
        end

        nil
      end

      def help
        # TODO: write it
        _(
          "<p>Select a devices for including into the bond.\n" \
          "Only devices with the device activation set to <b>Never</b> " \
          "and with <b>No Address Setup</b> are available.</p>"
        )
      end

      # Default function to init the value of port devices box for bonding.
      def init
        ports = @settings.ports
        # TODO: use def items, but problem now is that port_items returns term and not array
        items = port_items_from(
          @settings.bondable_interfaces.map(&:name),
          ports,
          Yast::Lan.yast_config # ideally get it from builder?
        )

        # reorder the items
        l1, l2 = items.partition { |t| ports.include? t[0][0] }

        items = l1 + l2.sort_by { |t| justify_dev_name(t[0][0]) }

        Yast::UI.ChangeWidget(:bond_ports_items, :Items, items)

        Yast::UI.ChangeWidget(
          :bond_ports_items,
          :SelectedItems,
          ports
        )

        enable_position_buttons

        nil
      end

      # Default function to store the value of port devices box.
      def store
        @settings.ports = selected_items
      end

      # Validates created bonding. Currently just prevent the user to create a
      # bond with more than one interface sharing the same physical port id
      #
      # @return true if valid or user decision if not
      def validate
        physical_ports = repeated_physical_port_ids(selected_items)

        return false if !physical_ports.empty? && !continue_with_duplicates?(physical_ports)

        if @settings.require_adaptation?(selected_items || [])
          Yast::Popup.ContinueCancel(
            _(
              "At least one selected device is already configured.\n" \
              "Adapt the configuration for bonding?\n"
            )
          )
        else
          true
        end
      end

      def value
        # TODO: it is multiselection, so does it make sense?
        Yast::UI.QueryWidget(:bond_ports_items, :CurrentItem)
      end

      def selected_items
        Yast::UI.QueryWidget(:bond_ports_items, :SelectedItems) || []
      end

      def ui_items
        Yast::UI.QueryWidget(:bond_ports_items, :Items) || []
      end

      def value_index
        ui_items.index { |i| i[0] == Id(value) }
      end

      def enable_position_buttons
        if value_index
          Yast::UI.ChangeWidget(:bond_ports_up, :Enabled, value_index > 0)
          Yast::UI.ChangeWidget(:bond_ports_down, :Enabled, value_index < ui_items.size - 1)
        else
          Yast::UI.ChangeWidget(:bond_ports_up, :Enabled, false)
          Yast::UI.ChangeWidget(:bond_ports_down, :Enabled, false)
        end
      end

      # A helper for sort devices by name. It justify at right with 0's numeric parts of given
      # device name until 5 digits.
      #
      # TODO: should not be in CWM
      # ==== Examples
      #
      #   justify_dev_name("eth0") # => "eth00000"
      #   justify_dev_name("eth111") # => "eth00111"
      #   justify_dev_name("enp0s25") # => "enp00000s00025"
      #
      # @param name [String] device name
      # @return [String] given name with numbers justified at right
      def justify_dev_name(name)
        splited_dev_name = name.scan(/\p{Alpha}+|\p{Digit}+/)
        splited_dev_name.map! do |d|
          if d =~ /\p{Digit}+/
            d.rjust(5, "0")
          else
            d
          end
        end.join
      end

      # Given a list of device names returns a hash of physical port ids mapping
      # device names if at least two devices shared the same physical port id
      # TODO: backend method
      #
      # NOTE: term port is slightly overloaded here. Name of method refers to
      # physical ports of a NIC card (one card can have multiple "plugs" - ports).
      # On the other hand param name refers to pure virtual bonding ports (network
      # devices provided by the system which are virtualy tighted together into a
      # virtual bond device)
      #
      # @param b_ports [Array<String>] bond ports = devices included in the bond
      # @return [Hash{String => Array<String>}]
      #   maps physical ports to non-singleton arrays of bond ports
      def repeated_physical_port_ids(b_ports)
        physical_port_ids = {}

        b_ports.each do |b_port|
          next unless physical_port_id?(b_port)

          p_port = physical_port_id(b_port)
          ps = physical_port_ids[p_port] ||= []
          ps << b_port
        end

        physical_port_ids.select! { |_k, v| v.size > 1 }

        physical_port_ids
      end

      # Given a map of duplicated port ids with device names, aks the user if he
      # would like to continue or not.
      #
      # @param physical_ports [Hash{String => Array<String>}] hash of duplicated physical port ids
      # mapping to an array of device names
      # @return [Boolean] true if continue with duplicates, otherwise false
      def continue_with_duplicates?(physical_ports)
        message = physical_ports.map do |p_port, b_ports|
          wrap_text("PhysicalPortID (#{p_port}): #{b_ports.join(", ")}")
        end.join("\n")

        Yast::Popup.YesNoHeadline(
          Yast::Label.WarningMsg,
          # Translators: Warn the user about not desired effect
          _("The interfaces selected share the same physical port and bonding " \
            "them \nmay not have the desired effect of redundancy.\n\n%s\n\n" \
            "Really continue?\n") % message
        )
      end
    end
  end
end