diff --git a/.travis.yml b/.travis.yml index b1ebcfbd7..468ef8b8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,13 @@ compiler: before_install: # disable rvm, use system Ruby - rvm reset + # install newer augeasget repo with newer augeas, otherwise ruby-augeas fails (see https://github.com/yast/yast-network/pull/454#issuecomment-253795507) + - sudo add-apt-repository -y ppa:raphink/augeas + - sudo apt-get update + - sudo apt-get install libaugeas-dev libxml2-dev + # end of augeas install - wget https://raw.githubusercontent.com/yast/yast-devtools/master/travis-tools/travis_setup.sh - - sh ./travis_setup.sh -p "rake yast2-devtools yast2-testsuite yast2 yast2-storage yast2-proxy yast2-country yast2-packager" -g "rspec:3.3.0 yast-rake gettext rubocop:0.41.2 simplecov:0.10.0 coveralls" + - sh ./travis_setup.sh -p "ruby2.1-dev augeas-lenses libaugeas0 rake yast2-devtools yast2-testsuite yast2 yast2-storage yast2-proxy yast2-country yast2-packager" -g "rspec:3.3.0 yast-rake gettext rubocop:0.41.2 simplecov:0.10.0 coveralls cfa cheetah" script: - rubocop - rake check:syntax diff --git a/package/yast2-network.changes b/package/yast2-network.changes index 60fa30306..d0a1a8ea0 100644 --- a/package/yast2-network.changes +++ b/package/yast2-network.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Fri Oct 14 14:08:31 UTC 2016 - jreidinger@suse.com + +- optimize loading /etc/hosts if there is a lot of entries + (bsc#877047) +- 3.2.5 + ------------------------------------------------------------------- Mon Oct 10 09:11:29 UTC 2016 - mfilka@suse.com diff --git a/package/yast2-network.spec b/package/yast2-network.spec index b4925642b..ace8a61cb 100644 --- a/package/yast2-network.spec +++ b/package/yast2-network.spec @@ -17,7 +17,7 @@ Name: yast2-network -Version: 3.2.4 +Version: 3.2.5 Release: 0 BuildRoot: %{_tmppath}/%{name}-%{version}-build @@ -50,6 +50,12 @@ BuildRequires: yast2-storage >= 2.21.11 Requires: yast2-storage >= 2.21.11 # Packages::vnc_packages Requires: yast2-packager >= 3.1.47 +# cfa for parsing hosts +BuildRequires: rubygem(%rb_default_ruby_abi:cfa) +Requires: rubygem(%rb_default_ruby_abi:cfa) +# lenses are needed to use cfa +BuildRequires: augeas-lenses +Requires: augeas-lenses # testsuite BuildRequires: rubygem(rspec) @@ -93,6 +99,8 @@ rake install DESTDIR="%{buildroot}" %{yast_schemadir}/autoyast/rnc/networking.rnc %{yast_schemadir}/autoyast/rnc/host.rnc %{yast_libdir}/network +%dir %{yast_libdir}/cfa/ +%{yast_libdir}/cfa/hosts.rb %{yast_ydatadir}/network %dir %{yast_docdir} diff --git a/src/clients/host.rb b/src/clients/host.rb index 56565b1db..8269d172d 100644 --- a/src/clients/host.rb +++ b/src/clients/host.rb @@ -109,15 +109,9 @@ def HostGUI def ListHandler(_options) # Command line output Headline # configuration of hosts - summary = Ops.add( - Ops.add( - "\n" + _("Host Configuration Summary:") + "\n\n", - RichText.Rich2Plain(Host.Summary) - ), - "\n" - ) + summary = "\n" + _("Host Configuration Summary:") + "\n\n" + + RichText.Rich2Plain(Host.Summary) + "\n" - Builtins.y2debug("%1", summary) CommandLine.Print(summary) true end diff --git a/src/include/network/lan/address.rb b/src/include/network/lan/address.rb index 467118dae..6b8a20e1b 100644 --- a/src/include/network/lan/address.rb +++ b/src/include/network/lan/address.rb @@ -1530,7 +1530,7 @@ def AddressDialog ip_changed = LanItems.ipaddr != Ops.get_string(@settings, "IPADDR", "") if ip_changed - Host.set_names(LanItems.ipaddr, []) + Host.remove_ip(LanItems.ipaddr) Builtins.y2milestone("IP has changed") end @@ -1541,12 +1541,12 @@ def AddressDialog if @hostname_initial != Ops.get_string(@settings, "HOSTNAME", "") || ip_changed if Ops.get_string(@settings, "HOSTNAME", "") == "" - Host.set_names(LanItems.ipaddr, []) + Host.remove_ip(LanItems.ipaddr) else Host.Update( @hostname_initial, Ops.get_string(@settings, "HOSTNAME", ""), - [Ops.get_string(@settings, "IPADDR", "")] + Ops.get_string(@settings, "IPADDR", "") ) end end diff --git a/src/include/network/services/host.rb b/src/include/network/services/host.rb index fc38ba462..31770d511 100644 --- a/src/include/network/services/host.rb +++ b/src/include/network/services/host.rb @@ -292,10 +292,6 @@ def HostsMainDialog(standalone) key = Ops.get_string(row, 1, "") Host.add_name(key, value) end - # deleted entries need to be set to [], - # so that ini-agent does not keep them in - # config file (#455862) - Builtins.foreach(deleted_items) { |d| Host.set_names(d, []) } end break else diff --git a/src/lib/cfa/hosts.rb b/src/lib/cfa/hosts.rb new file mode 100644 index 000000000..35a9f40e5 --- /dev/null +++ b/src/lib/cfa/hosts.rb @@ -0,0 +1,200 @@ +require "yast" +require "yast2/target_file" + +require "cfa/base_model" +require "cfa/matcher" +require "cfa/augeas_parser" + +module CFA + # 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 + PARSER = AugeasParser.new("hosts.lns") + PATH = "/etc/hosts".freeze + include Yast::Logger + + def initialize(file_handler: nil) + super(PARSER, PATH, file_handler: file_handler) + end + + # The old format used by {Yast::HostClass}. + # @return [Hash{String => Array}] 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] + 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 + + if entries.size > 1 + log.info "delete host with ip '#{ip}' removes more then one entry" + end + + 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] 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 + + if entries.size > 1 + log.info "more then one entry with ip '#{ip}'. Replacing last one." + end + + 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] 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(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_host(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") + aliases[1..-1].each do |a| + aliases_col.add(a) + end + end + end + 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 + + # helper to generate unique id for cfa entry + def unique_id + id = 1 + loop do + return id.to_s unless data[id.to_s] + id += 1 + end + end + end +end diff --git a/src/modules/Host.rb b/src/modules/Host.rb index 0904da7d0..d64d4371d 100644 --- a/src/modules/Host.rb +++ b/src/modules/Host.rb @@ -21,12 +21,10 @@ # you may find current contact information at www.novell.com # # ************************************************************************** -# File: modules/Host.ycp -# Package: Network configuration -# Summary: Hosts data (/etc/hosts) -# Authors: Michal Svec # require "yast" +require "yast2/execute" +require "cfa/hosts" module Yast class HostClass < Module @@ -42,50 +40,42 @@ def main Yast.include self, "network/routines.rb" - # All hosts - # See hosts(5) - # keys: IPs, (But #35671 suggests that repeating IPs is valid) - # values: names, the first one is the canonical one - @hosts = {} - # Data was modified? @modified = false - # All hosts read at the start - @hosts_init = {} - - # "hosts" file location - @hosts_file = "/etc/hosts" - @initialized = false + + @hosts = CFA::Hosts.new end # Remove all entries from the host table. def clear - @hosts = {} + @hosts.hosts.keys.each do |ip| + @hosts.delete_by_ip(ip) + end @modified = true end # @return [hash] address->list of names def name_map - @hosts + @hosts.hosts end # @return [array] names for that address def names(address) - @hosts[address] || [] + @hosts.host(address) || [] end - # Give address a new list of names. - def set_names(address, names) - @hosts[address] = names - @modified = true + # remove all instances of ip in hosts table + def remove_ip(address) + @hosts.delete_by_ip(address) end # Add another name to the list for address (which may be empty so far) + # FIXME: used only in one place, which looks wrong def add_name(address, name) - @hosts[address] ||= [] - @hosts[address] << name + canonical, *aliases = name.split(" ") + @hosts.add_entry(address, canonical, aliases) @modified = true end @@ -103,15 +93,12 @@ def EnsureHostnameResolvable # being unable to resolve hostname (bnc#304632) fqhostname = Hostname.MergeFQ(DNS.hostname, DNS.domain) - Ops.set( - @hosts, - local_ip, - [Ops.add(Ops.add(fqhostname, " "), DNS.hostname)] - ) + set_names(local_ip, ["#{fqhostname} #{DNS.hostname}"]) elsif Builtins.haskey(@hosts, local_ip) # Do not add it if product default says no # and remove 127.0.02 entry if it exists - Ops.set(@hosts, local_ip, []) + + @hosts.delete_by_ip(local_ip) end @modified = true @@ -121,27 +108,19 @@ def EnsureHostnameResolvable # Read hosts settings # @return true if success def Read - return true if @initialized == true + return true if @initialized # read /etc/hosts - if Ops.greater_than(SCR.Read(path(".target.size"), @hosts_file), 0) - hostlist = SCR.Dir(path(".etc.hosts")) - @hosts = Builtins.listmap(hostlist) do |host| - names = Convert.convert( - SCR.Read( - Builtins.topath(Builtins.sformat(".etc.hosts.\"%1\"", host)) - ), - from: "any", - to: "list " - ) - next { host => names } if names != [] - end + if Ops.greater_than(SCR.Read(path(".target.size"), CFA::Hosts::PATH), 0) + @hosts = CFA::Hosts.new + @hosts.load end # save hosts to check for changes later - @hosts_init = deep_copy(@hosts) + @hosts_init = CFA::Hosts.new + @hosts_init.load - Builtins.y2debug("hosts=%1", @hosts) + Builtins.y2debug("hosts=#{@hosts.inspect}") @initialized = true true end @@ -165,48 +144,21 @@ def Write steps = [_("Update /etc/hosts")] caption = _("Saving Hostname Configuration") - sl = 500 # sleep for longer time, so that progress does not disappear right afterwards - Progress.New(caption, " ", Builtins.size(steps), steps, [], "") + Progress.New(caption, " ", steps.size, steps, [], "") ProgressNextStage(_("Updating /etc/hosts ...")) - # Create if not exists, otherwise backup - if Ops.less_than(SCR.Read(path(".target.size"), @hosts_file), 0) - SCR.Write(path(".target.string"), @hosts_file, "") - else - SCR.Execute( - path(".target.bash"), - Ops.add( - Ops.add(Ops.add(Ops.add("/bin/cp ", @hosts_file), " "), @hosts_file), - ".YaST2save" - ) - ) + # backup if exists + if SCR.Read(path(".target.size"), CFA::Hosts::PATH) >= 0 + Yast::Execute.on_target("cp", CFA::Hosts::PATH, "#{CFA::Hosts::PATH}.YaST2save") end - ret = false - if @hosts == {} || @hosts.nil? - # Workaround bug [#4476] - ret = SCR.Write(path(".target.string"), @hosts_file, "") - else - # Update the hosts config - Builtins.y2milestone("hosts=%1", @hosts) - Builtins.maplist(@hosts) do |ho, names| - Builtins.y2milestone( - "%1 (%2:%3)", - ho, - names, - Ops.get(@hosts_init, ho) - ) - SCR.Write(Builtins.add(path(".etc.hosts"), ho), names) - end - ret = true - end + @hosts.save - SCR.Write(path(".etc.hosts"), nil) - Builtins.sleep(sl) Progress.NextStage - ret == true + + true end # Get all the Hosts configuration from a map. @@ -215,34 +167,38 @@ def Write # @param [Hash] settings autoinstallation settings # @return true if success def Import(settings) - settings = deep_copy(settings) @modified = true # trigger Write @initialized = true # don't let Read discard our data - @hosts = Builtins.eval(Ops.get_map(settings, "hosts", {})) + @hosts = CFA::Hosts.new + imported_hosts = Builtins.eval(Ops.get_map(settings, "hosts", {})) # convert from old format to the new one # use ::1 entry as a reference - if Ops.greater_than(Builtins.size(Ops.get(@hosts, "::1", [])), 1) - Builtins.foreach(@hosts) do |ip, hn| - Ops.set(@hosts, ip, [Builtins.mergestring(hn, " ")]) + if (imported_hosts["::1"] || []).size > 1 + imported_hosts.each_pair do |k, v| + imported_hosts[k] = v.join(" ") end end + + imported_hosts.each_pair do |ip, names| + set_names(ip, names) + end + true end # Dump the Hosts settings to a map, for autoinstallation use. # @return autoinstallation settings def Export - return {} if @hosts.empty? + exported_hosts = @hosts.hosts + return {} if exported_hosts.empty? # Filter out IPs with empty hostname (so that valid autoyast # profile is created)(#335120) - # FIXME: this also removes records with empty names from @hosts. Such - # side effect is unexpected and should be removed. - @hosts.keep_if { |_, names| !names.empty? } + exported_hosts.keep_if { |_, names| !names.empty? } - { "hosts" => @hosts } + { "hosts" => exported_hosts } end # Return "system" predefined hosts (should be present all the time) @@ -260,60 +216,44 @@ def GetSystemHosts end # Update hosts according to the current hostname - # (only one hostname, assigned to all IPs) + # (only one hostname, assigned to all IP) # @param hostname current hostname # @param domain current domain name - # @param [Array] iplist localhost IP addresses + # @param String ip to assign # @return true if success - def Update(oldhn, newhn, iplist) - iplist = deep_copy(iplist) - ips = Builtins.filter(iplist) { |ip| ip != "127.0.0.1" } - - Builtins.y2milestone("Hosts: %1", @hosts) + def Update(oldhn, newhn, ip) Builtins.y2milestone( "Updating /etc/hosts: %1 -> %2: %3", oldhn, newhn, - ips + ip ) @modified = true - nick = Ops.get(Hostname.SplitFQ(newhn), 0, "") - # Remove old hostname from hosts - if !oldhn.empty? - Builtins.foreach(@hosts) do |ip, hs| - wrk = Builtins.maplist(hs) { |s| Builtins.splitstring(s, " ") } - wrk = Builtins.filter(wrk) { |lst| !Builtins.contains(lst, oldhn) } - Ops.set(@hosts, ip, Builtins.maplist(wrk) do |lst| - Builtins.mergestring(lst, " ") - end) - end - end - - # Resurect the rest of oldhnlist without old hostname - # FIXME: maybe + @hosts.delete_hostname(oldhn) if !oldhn.empty? # Add localhost if missing - if !Builtins.haskey(@hosts, "127.0.0.1") - Ops.set(@hosts, "127.0.0.1", ["localhost"]) + if @hosts.host("127.0.0.1").empty? + @hosts.add_entry("127.0.0.1", "localhost") end - # Add hostname/ip for all ips - nickadded = false - Builtins.maplist(ips) do |ip| - # Omit some IP addresses - next if ip == "" || ip.nil? || ip == "127.0.0.1" - name = newhn - # Add nick for the first one - if !nickadded && name != "" - nickadded = true - name = Ops.add(Ops.add(newhn, " "), nick) - end - Ops.set(@hosts, ip, Builtins.add(Ops.get(@hosts, ip, []), name)) - end + # Omit some IP addresses + return true if ["127.0.0.1", "", nil].include?(ip) + # Omit invalid newhn + return true if [nil, ""].include?(newhn) - Builtins.y2milestone("Hosts: %1", @hosts) + nick = Hostname.SplitFQ(newhn)[0] || "" + nick = nick.empty? ? [] : [nick] + hosts = @hosts.host(ip) + if hosts.empty? + @hosts.add_entry(ip, newhn, nick) + else + canonical, *aliases = hosts.last.split(" ") + aliases << newhn + aliases.concat(nick) + @hosts.set_entry(ip, canonical, aliases) + end true end @@ -322,10 +262,10 @@ def Update(oldhn, newhn, iplist) # @return summary text def Summary summary = "" - return Summary.NotConfigured if @hosts == {} + return Summary.NotConfigured if @hosts.hosts.empty? summary = Summary.OpenList(summary) - Builtins.foreach(@hosts) do |k, v| + @hosts.hosts.each do |k, v| Builtins.foreach(v) do |hn| summary = Summary.AddListItem(summary, Ops.add(Ops.add(k, " - "), hn)) end if !Builtins.contains( @@ -386,6 +326,18 @@ def SetModified publish function: :ResolveHostnameToStaticIPs, type: "void ()" publish function: :GetModified, type: "boolean ()" publish function: :SetModified, type: "void ()" + + private + + # Give address a new list of names. + def set_names(address, names) + @hosts.delete_by_ip(address) + names.each do |name| + canonical, *aliases = name.split(" ") + @hosts.add_entry(address, canonical, aliases) + end + @modified = true + end end Host = HostClass.new diff --git a/src/modules/LanItems.rb b/src/modules/LanItems.rb index 93bc84f54..a56d7aa57 100644 --- a/src/modules/LanItems.rb +++ b/src/modules/LanItems.rb @@ -2405,7 +2405,7 @@ def s390_correct_lladdr(lladdr) # Removes all records connected to the ip from /etc/hosts def drop_hosts(ip) log.info("Deleting hostnames assigned to #{ip} from /etc/hosts") - Host.set_names(ip, []) + Host.remove_ip(ip) end # Exports udev rules for AY profile diff --git a/test/data/hosts b/test/data/hosts new file mode 100644 index 000000000..cd5c1450c --- /dev/null +++ b/test/data/hosts @@ -0,0 +1,24 @@ +# +# hosts This file describes a number of hostname-to-address +# mappings for the TCP/IP subsystem. It is mostly +# used at boot time, when no name servers are running. +# On small systems, this file can be used instead of a +# "named" name server. +# Syntax: +# +# IP-Address Full-Qualified-Hostname Short-Hostname +# + +127.0.0.1 localhost + +# special IPv6 addresses +::1 localhost ipv6-localhost ipv6-loopback + +fe00::0 ipv6-localnet + +ff00::0 ipv6-mcastprefix +ff02::1 ipv6-allnodes +ff02::2 ipv6-allrouters +ff02::3 ipv6-allhosts +10.100.128.72 pepa.labs.suse.cz pepa pepa2 +10.254.128.1 pepa1.labs.suse.cz pepa1 diff --git a/test/host_test.rb b/test/host_test.rb index d83c43a0e..d53904940 100755 --- a/test/host_test.rb +++ b/test/host_test.rb @@ -3,13 +3,118 @@ require_relative "test_helper" require "yast" +require "cfa/memory_file" +require "cfa/base_model" +require "cfa/hosts" Yast.import "Host" -describe "Host" do - subject(:host) { Yast::Host } +describe Yast::Host do + let(:file) do + file_path = File.expand_path("../data/hosts", __FILE__) + CFA::MemoryFile.new(File.read(file_path)) + end + + before do + # use only testing file + CFA::BaseModel.default_file_handler = file + + allow(Yast::SCR).to receive(:Read).with(path(".target.size"), "/etc/hosts").and_return(50) + + # reset internal caches + Yast::Host.instance_variable_set(:"@modified", false) + Yast::Host.instance_variable_set(:"@initialized", false) + + # do nothing on system + allow(Yast::Execute).to receive(:on_target) + end + + describe ".Read" do + it "reads hosts configuration from system" do + Yast::Host.Read + + expect(Yast::Host.name_map).to_not be_empty + end + end + + describe ".clear" do + it "removes all entries from host table" do + Yast::Host.Read + Yast::Host.clear + + expect(Yast::Host.name_map).to be_empty + end + end - describe "#Export" do + describe ".name_map" do + # FIXME: make value API better + it "returns hash with ip as key and hostnames as value" do + Yast::Host.Read + + name_map = Yast::Host.name_map + expect(name_map["10.100.128.72"]).to eq(["pepa.labs.suse.cz pepa pepa2"]) + end + end + + describe ".names" do + it "returns empty array if given ip is not is hosts table" do + Yast::Host.Read + + expect(Yast::Host.names("1.1.1.1")).to eq [] + end + + # FIXME: better API + it "returns single element array with string containing canonical name and aliases separated by space" do + Yast::Host.Read + + expect(Yast::Host.names("10.100.128.72")).to eq(["pepa.labs.suse.cz pepa pepa2"]) + end + end + + describe ".add_name" do + it "adds host to hosts entry even if it is already there" do + Yast::Host.Read + Yast::Host.add_name("10.100.128.72", "test test2.suse.cz") + Yast::Host.add_name("10.100.128.72", "test3 test3.suse.cz") + + expect(Yast::Host.names("10.100.128.72")).to eq([ + "pepa.labs.suse.cz pepa pepa2", + "test test2.suse.cz", + "test3 test3.suse.cz" + ]) + end + end + + describe ".Write" do + it "do nothing if not modified" do + expect(file).to_not receive(:write) + Yast::Host.Read + Yast::Host.Write + end + + it "writes content of file" do + Yast::Host.Read + Yast::Host.add_name("10.100.128.72", "test test2.suse.cz") + Yast::Host.add_name("10.100.128.72", "test3 test3.suse.cz") + Yast::Host.Write + + content = file.content + + expect(content.lines).to include("10.100.128.72\ttest test2.suse.cz\n") + expect(content.lines).to include("10.100.128.72\ttest3 test3.suse.cz\n") + end + + it "creates backup of file" do + expect(Yast::Execute).to receive(:on_target).with("cp", "/etc/hosts", "/etc/hosts.YaST2save") + + Yast::Host.Read + Yast::Host.add_name("10.100.128.72", "test test2.suse.cz") + Yast::Host.add_name("10.100.128.72", "test3 test3.suse.cz") + Yast::Host.Write + end + end + + describe ".Export" do let(:etc_hosts) do { "127.0.0.1" => ["localhost localhost.localdomain"], @@ -18,22 +123,22 @@ end it "Successfully exports stored mapping" do - host.Import("hosts" => etc_hosts) - expect(host.Export).to eql("hosts" => etc_hosts) + Yast::Host.Import("hosts" => etc_hosts) + expect(Yast::Host.Export).to eql("hosts" => etc_hosts) end it "removes empty name lists" do - host.Import("hosts" => { "127.0.0.1" => ["localhost"], "10.0.0.1" => [] }) - expect(host.Export).to eql("hosts" => { "127.0.0.1" => ["localhost"] }) + Yast::Host.Import("hosts" => { "127.0.0.1" => ["localhost"], "10.0.0.1" => [] }) + expect(Yast::Host.Export).to eql("hosts" => { "127.0.0.1" => ["localhost"] }) end it "exports empty hash when no mapping is defined" do - host.Import("hosts" => {}) - expect(host.Export).to be_empty + Yast::Host.Import("hosts" => {}) + expect(Yast::Host.Export).to be_empty end end - describe "#Update" do + describe ".Update" do let(:etc_hosts) do { "127.0.0.1" => ["localhost localhost.localdomain"], @@ -41,12 +146,48 @@ } end - it "doesn't drop records with two spaces" do - host.Import("hosts" => etc_hosts) - host.Update("", "newname", ["10.0.0.42"]) + let(:etc_hosts_new) do + { + "127.0.0.1" => ["localhost localhost.localdomain"], + "10.0.0.1" => ["somehost.example.com notice-two-spaces"] + } + end + + it "doesn't drop records with two spaces but make it single space" do + Yast::Host.Import("hosts" => etc_hosts) + Yast::Host.Update("", "newname", "10.0.0.42") tested_ip = "10.0.0.1" - expect(host.name_map[tested_ip]).to eql etc_hosts[tested_ip] + expect(Yast::Host.name_map[tested_ip]).to eql etc_hosts_new[tested_ip] + end + + it "adds alias for added hostname" do + Yast::Host.Import("hosts" => etc_hosts) + Yast::Host.Update("", "newname.suse.cz", "10.0.0.42") + + tested_ip = "10.0.0.42" + expect(Yast::Host.name_map[tested_ip]).to eql ["newname.suse.cz newname"] + end + + it "deletes old hostnames passed as first parameter" do + Yast::Host.Read + Yast::Host.Update("pepa.labs.suse.cz", "newname.suse.cz", "10.0.0.42") + Yast::Host.Write + + content = file.content + + expect(content.lines).to include("10.100.128.72 pepa pepa2\n") + expect(content.lines).to include("10.0.0.42\tnewname.suse.cz newname\n") + end + + it "adds hostname as alias if ip have already its entry" do + Yast::Host.Read + Yast::Host.Update("pepa.labs.suse.cz", "newname.suse.cz", "10.100.128.72") + Yast::Host.Write + + content = file.content + + expect(content.lines).to include("10.100.128.72 pepa pepa2 newname.suse.cz newname\n") end end end