yast/yast-network

View on GitHub
src/include/network/services/dns.rb

Summary

Maintainability
D
1 day
Test Coverage
# ***************************************************************************
#
# Copyright (c) 2012 Novell, Inc.
# 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 Novell, Inc.
#
# To contact Novell about this file by physical or electronic mail,
# you may find current contact information at www.novell.com
#
# **************************************************************************

require "ui/text_helpers"

module Yast
  module NetworkServicesDnsInclude
    include ::UI::TextHelpers

    # CWM wants id-value pairs
    CUSTOM_RESOLV_POLICIES = {
      "STATIC"          => "STATIC",
      "STATIC_FALLBACK" => "STATIC_FALLBACK"
    }.freeze

    def initialize_network_services_dns(include_target)
      textdomain "network"

      Yast.import "CWM"
      Yast.import "DNS"
      Yast.import "Hostname"
      Yast.import "IP"
      Yast.import "Label"
      Yast.import "Popup"
      Yast.import "Map"

      Yast.include include_target, "network/routines.rb"
      Yast.include include_target, "network/widgets.rb"
      Yast.include include_target, "network/lan/help.rb"

      # If there's a process modifying resolv.conf, we warn the user before
      # letting him change things that will be overwritten anyway.
      # See also #61000.
      @resolver_modifiable = false

      # original setup, used to determine whether data have been modified
      @settings_orig = {}

      # CWM buffer for both dialogs.  Note that NAMESERVERS and SEARCHLIST
      # are lists and their widgets are suffixed.
      @hn_settings = {}

      # TODO: It would be nice to display also transient hostname here - e.g. that
      # one received from dhcp
      @widget_descr_dns = {
        "HOSTNAME"        => {
          "widget"            => :textentry,
          "label"             => _("Static H&ostname"),
          "opt"               => [],
          "help"              => Ops.get_string(@help, "hostname_global", ""),
          "valid_chars"       => Yast::Hostname.ValidCharsDomain,
          "validate_type"     => :function_no_popup,
          "validate_function" => fun_ref(
            method(:ValidateHostname),
            "boolean (string, map)"
          ),
          # validation error popup
          "validate_help"     => Ops.add(
            _("The hostname is invalid.") + "\n",
            Hostname.ValidHost
          )
        },
        "HOSTNAME_GLOBAL" => {
          "widget" => :empty,
          "init"   => fun_ref(
            method(:initHostnameGlobal),
            "void (string)"
          ),
          "store"  => fun_ref(
            method(:storeHostnameGlobal),
            "void (string, map)"
          )
        },
        "DHCP_HOSTNAME"   => {
          "widget"        => :custom,
          "custom_widget" => HBox(
            Label(_("Set Hostname via DHCP")),
            HSpacing(2),
            ReplacePoint(Id("dhcp_hostname_method"), Empty()),
            ReplacePoint(Id("dh_host_text"), Empty())
          ),
          "init"          => fun_ref(method(:InitDhcpHostname), "void (string)"),
          "store"         => fun_ref(method(:StoreDhcpHostname), "void (string, map)")
        },
        "MODIFY_RESOLV"   => {
          "widget" => :combobox,
          "label"  => _("&Modify DNS Configuration"),
          "opt"    => [:notify],
          "items"  => [
            [:nomodify, _("Only Manually")],
            [:auto, _("Use Default Policy")],
            [:custom, _("Use Custom Policy")]
          ],
          "init"   => fun_ref(method(:initModifyResolvPolicy), "void (string)"),
          "handle" => fun_ref(
            method(:handleModifyResolvPolicy),
            "symbol (string, map)"
          ),
          "help"   => Ops.get_string(@help, "dns_config_policy", "")
        },
        "PLAIN_POLICY"    => {
          "widget" => :combobox,
          "label"  => _("&Custom Policy Rule"),
          "opt"    => [:editable],
          "items"  => CUSTOM_RESOLV_POLICIES.to_a,
          "init"   => fun_ref(method(:initPolicy), "void (string)"),
          "handle" => fun_ref(method(:handlePolicy), "symbol (string, map)"),
          "help"   => ""
        },
        "NAMESERVER_1"    => {
          "widget"            => :textentry,
          "label"             => _("Name Server &1"),
          "opt"               => [],
          "help"              => "",
          # at "SEARCHLIST_S"
          "handle"            => fun_ref(
            method(:HandleResolverData),
            "symbol (string, map)"
          ),
          "valid_chars"       => IP.ValidChars,
          "validate_type"     => :function_no_popup,
          "validate_function" => fun_ref(
            method(:ValidateIP),
            "boolean (string, map)"
          ),
          # validation error popup
          "validate_help"     => _("The IP address of the name server is invalid.") + "\n\n" +
            IP.Valid4 + "\n\n" + IP.Valid6
        },
        # NAMESERVER_2 and NAMESERVER_3 are cloned in the dialog function
        "SEARCHLIST_S"    => {
          "widget"            => :multi_line_edit,
          "label"             => _("Do&main Search"),
          "opt"               => [],
          "help"              => Ops.get_string(@help, "searchlist_s", ""),
          "handle"            => fun_ref(
            method(:HandleResolverData),
            "symbol (string, map)"
          ),
          "validate_type"     => :function,
          "validate_function" => fun_ref(
            method(:ValidateSearchList),
            "boolean (string, map)"
          )
        }
      }

      Ops.set(
        @widget_descr_dns,
        "NAMESERVER_2",
        Ops.get(@widget_descr_dns, "NAMESERVER_1", {})
      )
      Ops.set(
        @widget_descr_dns,
        "NAMESERVER_3",
        Ops.get(@widget_descr_dns, "NAMESERVER_1", {})
      )
      Ops.set(@widget_descr_dns, ["NAMESERVER_2", "label"], _("Name Server &2"))
      Ops.set(@widget_descr_dns, ["NAMESERVER_3", "label"], _("Name Server &3"))

      @dns_contents = VBox(
        VBox(
          HBox(
            "HOSTNAME",
            "HOSTNAME_GLOBAL" # global help, init, store for all dialog
          ),
          VSpacing(0.49),
          VBox(
            Left("DHCP_HOSTNAME")
          )
        ),
        VSpacing(1),
        Left(HBox("MODIFY_RESOLV", HSpacing(1), "PLAIN_POLICY")),
        MarginBox(
          0.49,
          0.49,
          Frame(
            _("Name Servers and Domain Search List"),
            VBox(
              VSquash(
                HBox(
                  HWeight(1, VBox("NAMESERVER_1", "NAMESERVER_2", "NAMESERVER_3")),
                  HSpacing(1),
                  HWeight(1, "SEARCHLIST_S")
                )
              )
            )
          )
        ),
        VStretch()
      )

      @dns_td = {
        "resolv" => {
          "header"       => _("Hostname/DNS"),
          "contents"     => @dns_contents,
          "widget_names" => [
            "HOSTNAME",
            "HOSTNAME_GLOBAL",
            "DOMAIN",
            "DHCP_HOSTNAME",
            "MODIFY_RESOLV",
            "PLAIN_POLICY",
            "NAMESERVER_1",
            "NAMESERVER_2",
            "NAMESERVER_3",
            "SEARCHLIST_S"
          ]
        }
      }
    end

    # @param [Array<String>] l list of strings
    # @return only non-empty items
    def NonEmpty(l)
      l = deep_copy(l)
      Builtins.filter(l) { |s| s != "" }
    end

    # @return initial settings for this dialog in one map, from DNS::
    def InitSettings
      settings = {
        "HOSTNAME"     => DNS.hostname,
        "PLAIN_POLICY" => DNS.resolv_conf_policy
      }
      # the rest is not so straightforward,
      # because we have list variables but non-list widgets

      # domain search
      searchstring = Builtins.mergestring(DNS.searchlist, "\n")
      # #49094: populate the search list
      # #437759: discard 'site', nobody really wants that pre-set
      if searchstring == "" && Ops.get_string(settings, "DOMAIN", "") != "site"
        searchstring = Ops.get_string(settings, "DOMAIN", "")
      end
      Ops.set(settings, "SEARCHLIST_S", searchstring)
      Ops.set(settings, "NAMESERVER_1", DNS.nameservers[0].to_s)
      Ops.set(settings, "NAMESERVER_2", DNS.nameservers[1].to_s)
      Ops.set(settings, "NAMESERVER_3", DNS.nameservers[2].to_s)

      @settings_orig = deep_copy(settings)

      deep_copy(settings)
    end

    # Stores user's input from hostname field
    #
    # @param value [String]
    # @return [String] stored hostname
    def store_hostname(value)
      hostname = config.hostname
      hostname.static = value
      hostname.installer = value if Stage.initial

      value
    end

    # @param [Hash] settings map of settings to be stored to DNS::
    def StoreSettings(settings)
      settings = deep_copy(settings)
      nameservers = [
        Ops.get_string(settings, "NAMESERVER_1", ""),
        Ops.get_string(settings, "NAMESERVER_2", ""),
        Ops.get_string(settings, "NAMESERVER_3", "")
      ]
      searchlist = Builtins.splitstring(
        Ops.get_string(settings, "SEARCHLIST_S", ""),
        " ,\n\t"
      )

      hostname = settings["HOSTNAME"] || ""
      update_hostname_hosts(hostname)
      store_hostname(hostname)

      valid_nameservers = NonEmpty(nameservers).each_with_object([]) do |ip_str, all|
        all << IPAddr.new(ip_str) if IP.Check(ip_str)
      end
      DNS.nameservers = valid_nameservers
      DNS.searchlist = NonEmpty(searchlist)
      DNS.resolv_conf_policy = settings["PLAIN_POLICY"]

      nil
    end

    # Convenience method to check the current static hostname
    #
    # @return [String]
    def current_hostname
      config&.hostname&.static || ""
    end

    # Allow to update a connection hostname using the new hostname when the
    # connection hostname was the modified hostname
    #
    # @param hostname [String] New static hostname
    def update_hostname_hosts(hostname)
      return false if hostname == current_hostname
      return false if current_hostname.empty?

      conn = config.connections.find { |c| c.hostnames&.include?(current_hostname) }
      return false if !conn

      proposed_hostname = propose_hostname_for(conn, hostname)

      return false if !Popup.YesNo(
        wrap_text(
          format(_("The interface '%s' static IP address is mapped to the hostname '%s'.\n\n" \
                   "Would you like to adapt it to '%s'?"),
            conn.name, current_hostname, proposed_hostname)
        )
      )

      conn.hostname = proposed_hostname
    end

    # Stores actual hostname settings.
    def StoreHnSettings
      StoreSettings(@hn_settings)

      nil
    end

    # Initialize internal state according current system configuration.
    def InitHnSettings
      @hn_settings = InitSettings()

      nil
    end

    # Function for updating actual hostname settings.
    # @param [String] key for known keys see hn_settings
    # @param [Object] value value for particular hn_settings key
    def SetHnItem(key, value)
      Builtins.y2milestone(
        "hn_settings[ \"%1\"] changes '%2' -> '%3'",
        key,
        @hn_settings[key].to_s,
        value
      )
      @hn_settings[key] = value

      nil
    end

    # Function for updating actual hostname.
    def SetHostname(value)
      value = deep_copy(value)
      SetHnItem("HOSTNAME", value)

      nil
    end

    # Function for updating ip address of first nameserver.
    def SetNameserver1(value)
      value = deep_copy(value)
      SetHnItem("NAMESERVER_1", value)

      nil
    end

    # Function for updating ip address of second nameserver.
    def SetNameserver2(value)
      value = deep_copy(value)
      SetHnItem("NAMESERVER_2", value)

      nil
    end

    # Function for updating ip address of third nameserver.
    def SetNameserver3(value)
      value = deep_copy(value)
      SetHnItem("NAMESERVER_3", value)

      nil
    end

    # Default function to init the value of a widget.
    # Used for push buttons.
    # @param [String] key id of the widget
    def InitHnWidget(key)
      value = Ops.get(@hn_settings, key)
      UI.ChangeWidget(Id(key), :Value, value)

      nil
    end

    # Default function to store the value of a widget.
    # @param key [String] id of the widget
    # @param _event [Hash] the event being handled
    def StoreHnWidget(key, _event)
      value = UI.QueryWidget(Id(key), :Value)
      SetHnItem(key, value)

      nil
    end

    NONE_LABEL = "no".freeze
    ANY_LABEL = "any".freeze
    NO_CHANGE_LABEL = "no_change".freeze

    # Checks whether setting hostname via DHCP is allowed
    def use_dhcp_hostname?
      UI.QueryWidget(Id("DHCP_HOSTNAME"), :Value) != NONE_LABEL
    end

    # Init handler for DHCP_HOSTNAME
    def InitDhcpHostname(_key)
      UI.ChangeWidget(Id("DHCP_HOSTNAME"), :Enabled, dhcp? && config&.backend?(:wicked))
      dhcp_hostname = DNS.dhcp_hostname

      items = [
        # translators: no device selected placeholder
        Item(Id(:none), _("no"), dhcp_hostname == :none),
        # translators: placeholder for "set hostname via any DHCP aware device"
        Item(Id(:any), _("yes: any"), dhcp_hostname == :any)
      ]

      iface_names = config.connections.to_a.select(&:dhcp?).map(&:interface)
      items += iface_names.map do |iface|
        # translators: label is in form yes: <device name>
        Item(Id(iface), format(_("yes: %s"), iface), dhcp_hostname == iface)
      end

      UI.ReplaceWidget(
        Id("dhcp_hostname_method"),
        ComboBox(
          Id("DHCP_HOSTNAME"),
          "",
          items
        )
      )

      log.info("InitDhcpHostname: preselected item = #{dhcp_hostname}")

      nil
    end

    # Store handler for DHCP_HOSTNAME
    def StoreDhcpHostname(_key, _event)
      return if !UI.QueryWidget(Id("DHCP_HOSTNAME"), :Enabled)

      DNS.dhcp_hostname = UI.QueryWidget(Id("DHCP_HOSTNAME"), :Value)

      nil
    end

    # Event handler for resolver data (nameservers, searchlist)
    # enable or disable: is DHCP available?
    # @param key [String] the widget receiving the event
    # @param _event [Hash] the event being handled
    # @return nil so that the dialog loops on
    def HandleResolverData(key, _event)
      # if this one is disabled, it means NM is in charge (see also initModifyResolvPolicy())
      if Convert.to_boolean(UI.QueryWidget(Id("MODIFY_RESOLV"), :Enabled))
        # thus, we should not re-enable already disabled widgets
        UI.ChangeWidget(Id(key), :Enabled, @resolver_modifiable)
      end
      nil
    end

    # Validator for hostname, no_popup
    # @param key [String] the widget being validated
    # @param _event [Hash] the event being handled
    # @return [Boolean] whether the current static hostname is valid or not
    def ValidateHostname(key, _event)
      value = UI.QueryWidget(Id(key), :Value).to_s

      # 1) empty hostname is allowed - /etc/hostname gets cleared in such case
      # 2) FQDN is allowed
      value.empty? || Hostname.Check(value.tr(".", ""))
    end

    # Validator for the search list
    # @param key [String] the widget being validated
    # @param _event [Hash] the event being handled
    # @return whether valid
    def ValidateSearchList(key, _event)
      value = Convert.to_string(UI.QueryWidget(Id(key), :Value))
      sl = NonEmpty(Builtins.splitstring(value, " ,\n\t"))
      error = ""

      sl.each do |s|
        next if Hostname.CheckDomain(s)

        # TRANSLATORS: Popup::Error text
        error = Builtins.sformat(_("The search domain '%1' is invalid."), s) +
          "\n" + Hostname.ValidDomain
        break
      end

      if error != ""
        UI.SetFocus(Id(key))
        Popup.Error(error)
        return false
      end
      true
    end

    def initPolicy(_key)
      # first initialize correctly
      Builtins.y2milestone(
        "initPolicy: %1",
        UI.QueryWidget(Id("MODIFY_RESOLV"), :Value)
      )
      if UI.QueryWidget(Id("MODIFY_RESOLV"), :Value) == :custom
        UI.ChangeWidget(Id("PLAIN_POLICY"), :Enabled, true)
        if UI.QueryWidget(Id("PLAIN_POLICY"), :Value) == ""
          UI.ChangeWidget(Id("PLAIN_POLICY"), :Value, DNS.resolv_conf_policy)
        end
      else
        UI.ChangeWidget(Id("PLAIN_POLICY"), :Value, "")
        UI.ChangeWidget(Id("PLAIN_POLICY"), :Enabled, false)
      end
      # then disable if needed
      disable_unconfigureable_items(["PLAIN_POLICY"], false)

      nil
    end

    def handlePolicy(_key, _event)
      Builtins.y2milestone("handlePolicy")

      case UI.QueryWidget(Id("MODIFY_RESOLV"), :Value)
      when :custom
        SetHnItem("PLAIN_POLICY", UI.QueryWidget(Id("PLAIN_POLICY"), :Value))
      when :auto
        SetHnItem("PLAIN_POLICY", "auto")
      else
        SetHnItem("PLAIN_POLICY", "")
      end

      nil
    end

    def modify_resolv_default
      if DNS.resolv_conf_policy.nil? || DNS.resolv_conf_policy == ""
        Id(:nomodify)
      elsif DNS.resolv_conf_policy == "auto" || DNS.resolv_conf_policy == "STATIC *"
        Id(:auto)
      else
        Id(:custom)
      end
    end

    def initModifyResolvPolicy(_key)
      Builtins.y2milestone("initModifyResolvPolicy")

      # first initialize correctly
      default = modify_resolv_default

      UI.ChangeWidget(Id("MODIFY_RESOLV"), :Value, default)
      # then disable if needed
      disable_unconfigureable_items(["MODIFY_RESOLV"], false)

      nil
    end

    def handleModifyResolvPolicy(key, _event)
      Builtins.y2milestone(
        "handleModifyResolvPolicy called: %1",
        UI.QueryWidget(Id("MODIFY_RESOLV"), :Value)
      )

      @resolver_modifiable = UI.QueryWidget(Id("MODIFY_RESOLV"), :Value) != :nomodify

      initPolicy(key)

      Builtins.y2milestone(
        "Exit: resolver_modifiable = %1",
        @resolver_modifiable
      )
      nil
    end

    # Used in GUI mode - initializes widgets according hn_settings
    # @param _key [String] ignored
    def initHostnameGlobal(_key)
      InitHnSettings()

      Builtins.foreach(
        Convert.convert(
          Map.Keys(@hn_settings),
          from: "list",
          to:   "list <string>"
        )
      ) { |key2| InitHnWidget(key2) }
      # disable those if NM is in charge
      disable_unconfigureable_items(
        ["NAMESERVER_1", "NAMESERVER_2", "NAMESERVER_3", "SEARCHLIST_S"],
        false
      )

      nil
    end

    # Used in GUI mode - updates and stores actuall hostname settings according dialog
    # widgets content.
    # It calls store handler for every widget from hn_settings with event as an option.
    # @param _key [String] ignored
    # @param event [Hash] user generated event
    def storeHostnameGlobal(_key, event)
      @hn_settings.each_key do |key2|
        StoreHnWidget(key2, event) if UI.QueryWidget(Id(key2), :Enabled)
      end

      StoreHnSettings()

      nil
    end

    # Standalone dialog only - embedded one is handled separately
    # via CWMTab
    def DNSMainDialog(_standalone)
      caption = _("Hostname and Name Server Configuration")

      functions = {
        "init"  => fun_ref(method(:InitHnWidget), "void (string)"),
        "store" => fun_ref(method(:StoreHnWidget), "void (string, map)"),
        :abort  => fun_ref(method(:ReallyAbort), "boolean ()")
      }

      Wizard.HideBackButton

      CWM.ShowAndRun(
        "widget_descr"       => @widget_descr_dns,
        "contents"           => @dns_contents,
        # dialog caption
        "caption"            => caption,
        "back_button"        => Label.BackButton,
        "next_button"        => Label.FinishButton,
        "fallback_functions" => functions
      )
    end

    def config
      Yast::Lan.yast_config
    end

    # Checks if any interface is configured to use DHCP
    #
    # @return [Boolean] true when an interface uses DHCP config
    def dhcp?
      return false unless config&.connections

      config.connections.any?(&:dhcp?)
    end

  private

    def propose_hostname_for(conn, hostname)
      return hostname if hostname.to_s.empty? || hostname.include?(".")

      conn.hostname.sub(/^[^.]+/, hostname)
    end
  end
end