yast/yast-network

View on GitHub
src/lib/cfa/hosts.rb

Summary

Maintainability
A
1 hr
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 "yast"
require "yast2/target_file"

require "cfa/base_model"
require "cfa/matcher"
require "cfa/augeas_parser"

module CFA
  class AugeasTree
    def initialize_clone(source)
      # Cloning internal data representation
      @data = source.data.map do |d|
        nd = d.clone
        nd[:value] = d[:value].clone if is_a? AugeasTree
        nd
      end
    end
  end

  # class representings /etc/hosts file model. It provides helper to manipulate
  # with file. It uses CFA framework and Augeas parser.
  # @see http://www.rubydoc.info/github/config-files-api/config_files_api/CFA/BaseModel
  # @see http://www.rubydoc.info/github/config-files-api/config_files_api/CFA/AugeasParser
  class Hosts < BaseModel
    PATH = "/etc/hosts".freeze
    include Yast::Logger

    def initialize(file_handler: nil)
      super(AugeasParser.new("hosts.lns"), PATH, file_handler: file_handler)
    end

    def initialize_clone(source)
      # Cloning AugeasTree
      @data = source.data.clone
    end

    # The old format used by {Yast::HostClass}.
    # @return [Hash{String => Array<String>}] keys are IPs,
    #   values are lists of lines in /etc/hosts (not names!)
    #   with whitespace separated hostnames, where the first one is canonical
    #   and the rest are aliases
    #
    #   For example, the file contents
    #
    #       1.2.3.4 www.example.org www
    #       1.2.3.7 log.example.org log
    #       1.2.3.7 sql.example.org sql
    #
    #   is returned as
    #
    #       {
    #         "1.2.3.4" => "www.example.org www"
    #         "1.2.3.7" => [
    #           "log.example.org log",
    #           "sql.example.org sql"
    #         ]
    #       }
    def hosts
      matcher = Matcher.new { |k, _v| k =~ /^\d*$/ }
      data.select(matcher).each_with_object({}) do |host, result|
        entry = host[:value]
        result[entry["ipaddr"]] ||= []
        result[entry["ipaddr"]] << single_host_entry(entry)
      end
    end

    # Returns single entry from hosts for given ip or empty array if not found
    # @see #hosts
    # @return [Array<String>]
    def host(ip)
      hosts = data.select(ip_matcher(ip))

      hosts.map do |host|
        single_host_entry(host[:value])
      end
    end

    # deletes all occurences of given ip in host table
    # @return [void]
    def delete_by_ip(ip)
      entries = data.select(ip_matcher(ip))
      if entries.empty?
        log.info "no entry to delete for ip #{ip}"
        return
      end

      log.info "delete host with ip '#{ip}' removes more then one entry" if entries.size > 1

      entries.each do |e|
        log.info "deleting record #{e.inspect}"
        data.delete(e[:key])
      end
    end

    # Replaces or adds a new host entry.
    # If more than one entry with the given ip exists
    # then it replaces the last instance.
    # @param [String] ip
    # @param [String] canonical
    # @param [Array<String>] aliases
    # @return [void]
    def set_entry(ip, canonical, aliases = [])
      entries = data.select(ip_matcher(ip))
      if entries.empty?
        add_entry(ip, canonical, aliases)
        return
      end

      log.info "more then one entry with ip '#{ip}'. Replacing last one." if entries.size > 1

      entry = entries.last[:value]
      entry["ipaddr"] = ip
      entry["canonical"] = canonical
      # clear previous aliases
      entry.delete("alias")
      entry.delete("alias[]")
      aliases_col = entry.collection("alias")
      aliases.each do |a|
        aliases_col.add(a)
      end
    end

    # Adds new entry, even if it exists
    # @param [String] ip
    # @param [String] canonical
    # @param [Array<String>] aliases
    # @return [void]
    def add_entry(ip, canonical, aliases = [])
      log.info "adding new entry for ip #{ip}"
      entry_line = AugeasTree.new
      entry_line["ipaddr"] = ip
      entry_line["canonical"] = canonical
      aliases_col = entry_line.collection("alias")
      aliases.each do |a|
        aliases_col.add(a)
      end
      data.add(data.unique_id, entry_line)
    end

    # Removes hostname from all entries in hosts table.
    # If it is the only hostname for a given ip, the ip is removed
    # If it is canonical name, then the first alias becomes the canonical hostname
    # @param [String] hostname
    # @return [void]
    def delete_hostname(hostname)
      entries = data.select(hostname_matcher(hostname))
      entries.each do |pair|
        entry = pair[:value]
        if entry["canonical"] == hostname
          aliases = aliases_for(entry)
          if aliases.empty?
            delete_by_ip(entry["ipaddr"])
          else
            entry["canonical"] = aliases.first
            entry.delete("alias")
            entry.delete("alias[]")
            aliases_col = entry.collection("alias")
            aliases[1..-1].each do |a|
              aliases_col.add(a)
            end
          end
        else
          reduced_aliases = aliases_for(entry)
          reduced_aliases.delete(hostname)
          entry.delete("alias")
          entry.delete("alias[]")
          aliases_col = entry.collection("alias")
          reduced_aliases.each do |a|
            aliases_col.add(a)
          end
        end
      end
    end

    # returns true if hosts include entry with given IP
    def include_ip?(ip)
      entries = data.select(ip_matcher(ip))
      !entries.empty?
    end

  private

    # returns matcher for cfa to find entries with given ip
    def ip_matcher(ip)
      Matcher.new { |_k, v| v["ipaddr"] == ip }
    end

    # returns matcher for cfa to find entries with given hostname
    def hostname_matcher(hostname)
      Matcher.new do |_k, v|
        v["canonical"] == hostname || aliases_for(v).include?(hostname)
      end
    end

    #  returns aliases as array even if there is only one
    def aliases_for(entry)
      entry["alias[]"] ? entry.collection("alias").map { |a| a } : [entry["alias"]].compact
    end

    # generate old format string with first canonical and then aliases
    # all separated by space
    def single_host_entry(entry)
      result = [entry["canonical"]]
      result.concat(aliases_for(entry))
      result.join(" ")
    end
  end
end