diff options
| author | Josh Cooper <josh@puppet.com> | 2018-07-09 17:15:39 -0700 | 
|---|---|---|
| committer | Josh Cooper <josh@puppet.com> | 2018-07-09 19:44:47 -0700 | 
| commit | 2ac89832b7e065df4490d81b3080b2b570a172ad (patch) | |
| tree | f9f4f28f30a77c958e8e850acf954336b2301dc9 | |
| parent | 1c6ae8bfde5175c5d3512f5acb5352fda94a4801 (diff) | |
| download | puppet-hosts_core-2ac89832b7e065df4490d81b3080b2b570a172ad.tar.gz puppet-hosts_core-2ac89832b7e065df4490d81b3080b2b570a172ad.tar.bz2 | |
Initial host import from puppet#ee7cf4d28077be7d1bdbbe934ea012d41d33deff
| -rw-r--r-- | lib/puppet/provider/host/parsed.rb | 46 | ||||
| -rw-r--r-- | lib/puppet/type/host.rb | 95 | ||||
| -rw-r--r-- | spec/acceptance/tests/pup_2289_should_not_destroy_data_when_malformed.rb | 32 | ||||
| -rw-r--r-- | spec/acceptance/tests/should_create.rb | 23 | ||||
| -rw-r--r-- | spec/acceptance/tests/should_create_aliases.rb | 24 | ||||
| -rw-r--r-- | spec/acceptance/tests/should_destroy.rb | 27 | ||||
| -rw-r--r-- | spec/acceptance/tests/should_modify_alias.rb | 27 | ||||
| -rw-r--r-- | spec/acceptance/tests/should_modify_ip.rb | 27 | ||||
| -rw-r--r-- | spec/acceptance/tests/should_not_create_existing.rb | 24 | ||||
| -rw-r--r-- | spec/acceptance/tests/should_query_all.rb | 34 | ||||
| -rw-r--r-- | spec/acceptance/tests/ticket_4131_should_not_create_without_ip.rb | 29 | ||||
| -rw-r--r-- | spec/fixtures/unit/provider/host/parsed/valid_hosts | 19 | ||||
| -rw-r--r-- | spec/lib/puppet_spec/files.rb | 108 | ||||
| -rw-r--r-- | spec/shared_behaviours/all_parsedfile_providers.rb | 21 | ||||
| -rw-r--r-- | spec/unit/provider/host/parsed_spec.rb | 233 | ||||
| -rw-r--r-- | spec/unit/type/host_spec.rb | 681 | 
16 files changed, 1450 insertions, 0 deletions
| diff --git a/lib/puppet/provider/host/parsed.rb b/lib/puppet/provider/host/parsed.rb new file mode 100644 index 0000000..d3d0039 --- /dev/null +++ b/lib/puppet/provider/host/parsed.rb @@ -0,0 +1,46 @@ +require 'puppet/provider/parsedfile' + +hosts = nil +case Facter.value(:osfamily) +when "Solaris"; hosts = "/etc/inet/hosts" +when "windows" +  require 'win32/resolv' +  hosts = Win32::Resolv.get_hosts_path +else +  hosts = "/etc/hosts" +end + + +Puppet::Type.type(:host).provide(:parsed,:parent => Puppet::Provider::ParsedFile, +  :default_target => hosts,:filetype => :flat) do +  confine :exists => hosts + +  text_line :comment, :match => /^#/ +  text_line :blank, :match => /^\s*$/ +  hosts_pattern = '^([0-9a-f:]\S+)\s+([^#\s+]\S+)\s*(.*?)?(?:\s*#\s*(.*))?$' +  record_line :parsed, :fields => %w{ip name host_aliases comment}, +    :optional => %w{host_aliases comment}, +    :match    => /#{hosts_pattern}/, +    :post_parse => proc { |hash| +      # An absent comment should match "comment => ''" +      hash[:comment] = '' if hash[:comment].nil? or hash[:comment] == :absent +      unless hash[:host_aliases].nil? or hash[:host_aliases] == :absent +        hash[:host_aliases].gsub!(/\s+/,' ') # Change delimiter +      end +    }, +    :to_line  => proc { |hash| +      [:ip, :name].each do |n| +        raise ArgumentError, _("%{attr} is a required attribute for hosts") % { attr: n } unless hash[n] and hash[n] != :absent +      end +      str = "#{hash[:ip]}\t#{hash[:name]}" +      if hash.include? :host_aliases and !hash[:host_aliases].nil? and hash[:host_aliases] != :absent +        str += "\t#{hash[:host_aliases]}" +      end +      if hash.include? :comment and !hash[:comment].empty? +        str += "\t# #{hash[:comment]}" +      end +      str +    } + +  text_line :incomplete, :match => /(?! (#{hosts_pattern}))/ +end diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb new file mode 100644 index 0000000..3bfcc7e --- /dev/null +++ b/lib/puppet/type/host.rb @@ -0,0 +1,95 @@ +require 'puppet/property/ordered_list' + +module Puppet +  Type.newtype(:host) do +    ensurable + +    newproperty(:ip) do +      desc "The host's IP address, IPv4 or IPv6." + + +      def valid_v4?(addr) +        if /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/ =~ addr +          return $~.captures.all? {|i| i = i.to_i; i >= 0 and i <= 255 } +        end +        return false +      end + +      def valid_v6?(addr) +        # http://forums.dartware.com/viewtopic.php?t=452 +        # ...and, yes, it is this hard. Doing it programmatically is harder. +        return true if addr =~ /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/ + +        return false +      end +      def valid_newline?(addr) +        return false if (addr =~ /\n/ || addr =~ /\r/) +        return true +      end + +      validate do |value| +        return true if ((valid_v4?(value) || valid_v6?(value)) && (valid_newline?(value))) +        raise Puppet::Error, _("Invalid IP address %{value}") % { value: value.inspect } +      end +    end + +    # for now we use OrderedList to indicate that the order does matter. +    newproperty(:host_aliases, :parent => Puppet::Property::OrderedList) do +      desc "Any aliases the host might have.  Multiple values must be +        specified as an array." + +      def delimiter +        " " +      end + +      def inclusive? +        true +      end + +      validate do |value| +        # This regex already includes newline check. +        raise Puppet::Error, _("Host aliases cannot include whitespace") if value =~ /\s/ +        raise Puppet::Error, _("Host aliases cannot be an empty string. Use an empty array to delete all host_aliases ") if value =~ /^\s*$/ +      end + +    end + +    newproperty(:comment) do +      desc "A comment that will be attached to the line with a # character." +      validate do |value| +        raise Puppet::Error, _("Comment cannot include newline") if (value =~ /\n/ || value =~ /\r/) +      end +    end + +    newproperty(:target) do +      desc "The file in which to store service information.  Only used by +        those providers that write to disk. On most systems this defaults to `/etc/hosts`." + +      defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) +        @resource.class.defaultprovider.default_target +        else +          nil +        end +      } +    end + +    newparam(:name) do +      desc "The host name." + +      isnamevar + +      validate do |value| +        value.split('.').each do |hostpart| +          unless hostpart =~ /^([\w]+|[\w][\w\-]+[\w])$/ +            raise Puppet::Error, _("Invalid host name") +          end +        end +        raise Puppet::Error, _("Hostname cannot include newline") if (value =~ /\n/ || value =~ /\r/) +      end +    end + +    @doc = "Installs and manages host entries.  For most systems, these +      entries will just be in `/etc/hosts`, but some systems (notably OS X) +      will have different solutions." +  end +end diff --git a/spec/acceptance/tests/pup_2289_should_not_destroy_data_when_malformed.rb b/spec/acceptance/tests/pup_2289_should_not_destroy_data_when_malformed.rb new file mode 100644 index 0000000..670651d --- /dev/null +++ b/spec/acceptance/tests/pup_2289_should_not_destroy_data_when_malformed.rb @@ -0,0 +1,32 @@ +test_name "should not delete data when existing content is malformed" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  file = agent.tmpfile('host-not-delete-data') + +  teardown do +    on(agent, "rm -f #{file}", :acceptable_exit_codes => (0..255)) +  end + +  step "(setup) populate test file with host information" +  on(agent, "printf '127.0.0.2 existing alias\n' > #{file}") + +  step "(setup) populate test file with a malformed line" +  on(agent, "printf '==\n' >> #{file}") + +  step "tell puppet to add another host entry" +  on(agent, puppet_resource('host', 'test', "target=#{file}", +    'ensure=present', 'ip=127.0.0.3', 'host_aliases=foo')) + +  step "verify that the initial host entry was not deleted" +  on(agent, "cat #{file}") do |res| +    fail_test "existing host data was deleted" unless +      res.stdout.include? 'existing' +  end + +end diff --git a/spec/acceptance/tests/should_create.rb b/spec/acceptance/tests/should_create.rb new file mode 100644 index 0000000..b590f85 --- /dev/null +++ b/spec/acceptance/tests/should_create.rb @@ -0,0 +1,23 @@ +test_name "host should create" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  target = agent.tmpfile('host-create') + +  step "clean up for the test" +  on agent, "rm -f #{target}" + +  step "create the host record" +  on(agent, puppet_resource("host", "test", "ensure=present", +              "ip=127.0.0.1", "target=#{target}")) + +  step "verify that the record was created" +  on(agent, "cat #{target} ; rm -f #{target}") do +    fail_test "record was not present" unless stdout =~ /^127\.0\.0\.1[[:space:]]+test/ +  end +end diff --git a/spec/acceptance/tests/should_create_aliases.rb b/spec/acceptance/tests/should_create_aliases.rb new file mode 100644 index 0000000..be4a134 --- /dev/null +++ b/spec/acceptance/tests/should_create_aliases.rb @@ -0,0 +1,24 @@ +test_name "host should create aliases" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  target  = agent.tmpfile('host-create-aliases') + +  step "clean up the system for testing" +  on agent, "rm -f #{target}" + +  step "create the record" +  on(agent, puppet_resource('host', 'test', "ensure=present", +              "ip=127.0.0.7", "target=#{target}", "host_aliases=alias")) + +  step "verify that the aliases were added" +  on(agent, "cat #{target} ; rm -f #{target}") do +    fail_test "alias was missing" unless +      stdout =~ /^127\.0\.0\.7[[:space:]]+test[[:space:]]alias/ +  end +end diff --git a/spec/acceptance/tests/should_destroy.rb b/spec/acceptance/tests/should_destroy.rb new file mode 100644 index 0000000..97f1477 --- /dev/null +++ b/spec/acceptance/tests/should_destroy.rb @@ -0,0 +1,27 @@ +test_name "should be able to remove a host record" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  file = agent.tmpfile('host-destroy') +  line = "127.0.0.7 test1" + +  step "set up files for the test" +  on agent, "printf '#{line}\n' > #{file}" + +  step "delete the resource from the file" +  on(agent, puppet_resource('host', 'test1', "target=#{file}", +              'ensure=absent', 'ip=127.0.0.7')) + +  step "verify that the content was removed" +  on(agent, "cat #{file}; rm -f #{file}") do +    fail_test "the content was still present" if stdout.include? line +  end + +  step "clean up after the test" +  on agent, "rm -f #{file}" +end diff --git a/spec/acceptance/tests/should_modify_alias.rb b/spec/acceptance/tests/should_modify_alias.rb new file mode 100644 index 0000000..07198b1 --- /dev/null +++ b/spec/acceptance/tests/should_modify_alias.rb @@ -0,0 +1,27 @@ +test_name "should be able to modify a host alias" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  file = agent.tmpfile('host-modify-alias') + +  step "set up files for the test" +  on agent, "printf '127.0.0.8 test alias\n' > #{file}" + +  step "modify the resource" +  on(agent, puppet_resource('host', 'test', "target=#{file}", +              'ensure=present', 'ip=127.0.0.8', 'host_aliases=banzai')) + +  step "verify that the content was updated" +  on(agent, "cat #{file}; rm -f #{file}") do +    fail_test "the alias was not updated" unless +      stdout =~ /^127\.0\.0\.8[[:space:]]+test[[:space:]]+banzai[[:space:]]*$/ +  end + +  step "clean up after the test" +  on agent, "rm -f #{file}" +end diff --git a/spec/acceptance/tests/should_modify_ip.rb b/spec/acceptance/tests/should_modify_ip.rb new file mode 100644 index 0000000..10c5adf --- /dev/null +++ b/spec/acceptance/tests/should_modify_ip.rb @@ -0,0 +1,27 @@ +test_name "should be able to modify a host address" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  file = agent.tmpfile('host-modify-ip') + +  step "set up files for the test" +  on agent, "printf '127.0.0.9 test alias\n' > #{file}" + +  step "modify the resource" +  on(agent, puppet_resource('host', 'test', "target=#{file}", +              'ensure=present', 'ip=127.0.0.10', 'host_aliases=alias')) + +  step "verify that the content was updated" +  on(agent, "cat #{file}; rm -f #{file}") do +    fail_test "the address was not updated" unless +      stdout =~ /^127\.0\.0\.10[[:space:]]+test[[:space:]]+alias[[:space:]]*$/ +  end + +  step "clean up after the test" +  on agent, "rm -f #{file}" +end diff --git a/spec/acceptance/tests/should_not_create_existing.rb b/spec/acceptance/tests/should_not_create_existing.rb new file mode 100644 index 0000000..0cdc87b --- /dev/null +++ b/spec/acceptance/tests/should_not_create_existing.rb @@ -0,0 +1,24 @@ +test_name "should not create host if it exists" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  file = agent.tmpfile('host-not-create-existing') + +  step "set up the system for the test" +  on agent, "printf '127.0.0.2 test alias\n' > #{file}" + +  step "tell puppet to ensure the host exists" +  on(agent, puppet_resource('host', 'test', "target=#{file}", +              'ensure=present', 'ip=127.0.0.2', 'host_aliases=alias')) do +    fail_test "darn, we created the host record" if +      stdout.include? '/Host[test1]/ensure: created' +  end + +  step "clean up after we created things" +  on agent, "rm -f #{file}" +end diff --git a/spec/acceptance/tests/should_query_all.rb b/spec/acceptance/tests/should_query_all.rb new file mode 100644 index 0000000..e60756d --- /dev/null +++ b/spec/acceptance/tests/should_query_all.rb @@ -0,0 +1,34 @@ +test_name "should query all hosts from hosts file" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +content = %q{127.0.0.1 test1 test1.local +127.0.0.2 test2 test2.local +127.0.0.3 test3 test3.local +127.0.0.4 test4 test4.local +} + +agents.each do |agent| +  backup = agent.tmpfile('host-query-all') + +  step "configure the system for testing (including file backups)" +  on agent, "cp /etc/hosts #{backup}" +  on agent, "cat > /etc/hosts", :stdin => content + +  step "query all host records using puppet" +  on(agent, puppet_resource('host')) do +    found = stdout.scan(/host \{ '([^']+)'/).flatten.sort +    fail_test "the list of returned hosts was wrong: #{found.join(', ')}" unless +      found == %w{test1 test2 test3 test4} + +    count = stdout.scan(/ensure\s+=>\s+'present'/).length +    fail_test "found #{count} records, wanted 4" unless count == 4 +  end + +  step "clean up the system afterwards" +  on agent, "cat #{backup} > /etc/hosts && rm -f #{backup}" +end diff --git a/spec/acceptance/tests/ticket_4131_should_not_create_without_ip.rb b/spec/acceptance/tests/ticket_4131_should_not_create_without_ip.rb new file mode 100644 index 0000000..867ff6c --- /dev/null +++ b/spec/acceptance/tests/ticket_4131_should_not_create_without_ip.rb @@ -0,0 +1,29 @@ +test_name "#4131: should not create host without IP attribute" + +tag 'audit:low', +    'audit:refactor',  # Use block style `test_name` +    'audit:acceptance' # Could be done at the integration (or unit) layer though +                       # actual changing of resources could irreparably damage a +                       # host running this, or require special permissions. + +agents.each do |agent| +  file = agent.tmpfile('4131-require-ip') + +  step "configure the target system for the test" +  on agent, "rm -rf #{file} ; touch #{file}" + +  step "try to create the host, which should fail" +  # REVISIT: This step should properly need to handle the non-zero exit code, +  # and #5668 has been filed to record that.  When it is fixed this test will +  # start to fail, and this comment will tell you why. --daniel 2010-12-24 +  on(agent, puppet_resource('host', 'test', "target=#{file}", +              "host_aliases=alias")) do +    fail_test "puppet didn't complain about the missing attribute" unless +      stderr.include? 'ip is a required attribute for hosts' unless agent['locale'] == 'ja' +  end + +  step "verify that the host was not added to the file" +  on(agent, "cat #{file} ; rm -f #{file}") do +    fail_test "the host was apparently added to the file" if stdout.include? 'test' +  end +end diff --git a/spec/fixtures/unit/provider/host/parsed/valid_hosts b/spec/fixtures/unit/provider/host/parsed/valid_hosts new file mode 100644 index 0000000..2463629 --- /dev/null +++ b/spec/fixtures/unit/provider/host/parsed/valid_hosts @@ -0,0 +1,19 @@ +# Some leading comment, that should be ignored +# The next line is empty so it should be ignored + +::1 localhost + +# We now try another delimiter: Several tabs +127.0.0.1         localhost + +# No test trailing spaces +10.0.0.1         host1          + +# Ok its time to test aliases +2001:252:0:1::2008:8 ipv6host alias1 +192.168.0.1 ipv4host alias2 alias3 + +# Testing inlinecomments now +192.168.0.2 host3 # This is host3 +192.168.0.3 host4 alias10 # This is host4 +192.168.0.4 host5   alias11             alias12      #     This is host5 diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb new file mode 100644 index 0000000..b34daed --- /dev/null +++ b/spec/lib/puppet_spec/files.rb @@ -0,0 +1,108 @@ +require 'fileutils' +require 'tempfile' +require 'tmpdir' +require 'pathname' + +# A support module for testing files. +module PuppetSpec::Files +  def self.cleanup +    $global_tempfiles ||= [] +    while path = $global_tempfiles.pop do +      begin +        Dir.unstub(:entries) +        FileUtils.rm_rf path, :secure => true +      rescue Errno::ENOENT +        # nothing to do +      end +    end +  end + +  module_function + +  def make_absolute(path) +    path = File.expand_path(path) +    path[0] = 'c' if Puppet.features.microsoft_windows? +    path +  end + +  def tmpfile(name, dir = nil) +    dir ||= Dir.tmpdir +    path = Puppet::FileSystem.expand_path(make_tmpname(name, nil).encode(Encoding::UTF_8), dir) +    record_tmp(File.expand_path(path)) + +    path +  end + +  def file_containing(name, contents) +    file = tmpfile(name) +    File.open(file, 'wb') { |f| f.write(contents) } +    file +  end + +  def script_containing(name, contents) +    file = tmpfile(name) +    if Puppet.features.microsoft_windows? +      file += '.bat' +      text = contents[:windows] +    else +      text = contents[:posix] +    end +    File.open(file, 'wb') { |f| f.write(text) } +    Puppet::FileSystem.chmod(0755, file) +    file +  end + +  def tmpdir(name) +    dir = Puppet::FileSystem.expand_path(Dir.mktmpdir(name).encode!(Encoding::UTF_8)) + +    record_tmp(dir) + +    dir +  end + +  # Copied from ruby 2.4 source +  def make_tmpname((prefix, suffix), n) +    prefix = (String.try_convert(prefix) or +              raise ArgumentError, "unexpected prefix: #{prefix.inspect}") +    suffix &&= (String.try_convert(suffix) or +                raise ArgumentError, "unexpected suffix: #{suffix.inspect}") +    t = Time.now.strftime("%Y%m%d") +    path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}".dup +    path << "-#{n}" if n +    path << suffix if suffix +    path +  end + +  def dir_containing(name, contents_hash) +    dir_contained_in(tmpdir(name), contents_hash) +  end + +  def dir_contained_in(dir, contents_hash) +    contents_hash.each do |k,v| +      if v.is_a?(Hash) +        Dir.mkdir(tmp = File.join(dir,k)) +        dir_contained_in(tmp, v) +      else +        file = File.join(dir, k) +        File.open(file, 'wb') {|f| f.write(v) } +      end +    end +    dir +  end + +  def record_tmp(tmp) +    # ...record it for cleanup, +    $global_tempfiles ||= [] +    $global_tempfiles << tmp +  end + +  def expect_file_mode(file, mode) +    actual_mode = "%o" % Puppet::FileSystem.stat(file).mode +    target_mode = if Puppet.features.microsoft_windows? +      mode +    else +      "10" + "%04i" % mode.to_i +    end +    expect(actual_mode).to eq(target_mode) +  end +end diff --git a/spec/shared_behaviours/all_parsedfile_providers.rb b/spec/shared_behaviours/all_parsedfile_providers.rb new file mode 100644 index 0000000..9fdf54b --- /dev/null +++ b/spec/shared_behaviours/all_parsedfile_providers.rb @@ -0,0 +1,21 @@ +shared_examples_for "all parsedfile providers" do |provider, *files| +  if files.empty? then +    files = my_fixtures +  end + +  files.flatten.each do |file| +    it "should rewrite #{file} reasonably unchanged" do +      provider.stubs(:default_target).returns(file) +      provider.prefetch + +      text = provider.to_file(provider.target_records(file)) +      text.gsub!(/^# HEADER.+\n/, '') + +      oldlines = File.readlines(file) +      newlines = text.chomp.split "\n" +      oldlines.zip(newlines).each do |old, new| +        expect(new.gsub(/\s+/, '')).to eq(old.chomp.gsub(/\s+/, '')) +      end +    end +  end +end diff --git a/spec/unit/provider/host/parsed_spec.rb b/spec/unit/provider/host/parsed_spec.rb new file mode 100644 index 0000000..b2cba76 --- /dev/null +++ b/spec/unit/provider/host/parsed_spec.rb @@ -0,0 +1,233 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'shared_behaviours/all_parsedfile_providers' + +require 'puppet_spec/files' + +provider_class = Puppet::Type.type(:host).provider(:parsed) + +describe provider_class do +  include PuppetSpec::Files + +  before do +    @host_class = Puppet::Type.type(:host) +    @provider = @host_class.provider(:parsed) +    @hostfile = tmpfile('hosts') +    @provider.any_instance.stubs(:target).returns @hostfile +  end + +  after :each do +    @provider.initvars +  end + +  def mkhost(args) +    hostresource = Puppet::Type::Host.new(:name => args[:name]) +    hostresource.stubs(:should).with(:target).returns @hostfile + +    # Using setters of provider to build our testobject +    # Note: We already proved, that in case of host_aliases +    # the provider setter "host_aliases=(value)" will be +    # called with the joined array, so we just simulate that +    host = @provider.new(hostresource) +    args.each do |property,value| +      value = value.join(" ") if property == :host_aliases and value.is_a?(Array) +      host.send("#{property}=", value) +    end +    host +  end + +  def genhost(host) +    @provider.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam) +    File.stubs(:chown) +    File.stubs(:chmod) +    Puppet::Util::SUIDManager.stubs(:asuser).yields +    host.flush +    @provider.target_object(@hostfile).read +  end + +  describe "when parsing on incomplete line" do + +    it "should work for only ip" do +      expect(@provider.parse_line("127.0.0.1")[:line]).to eq("127.0.0.1") +    end + +    it "should work for only hostname" do +      expect(@provider.parse_line("www.example.com")[:line]).to eq("www.example.com") +    end + +    it "should work for ip and space" do +      expect(@provider.parse_line("127.0.0.1 ")[:line]).to eq("127.0.0.1 ") +    end + +    it "should work for hostname and space" do +      expect(@provider.parse_line("www.example.com ")[:line]).to eq("www.example.com ") +    end + +    it "should work for hostname and host_aliases" do +      expect(@provider.parse_line("www.example.com  www xyz")[:line]).to eq("www.example.com  www xyz") +    end + +    it "should work for ip and comment" do +      expect(@provider.parse_line("127.0.0.1  #www xyz")[:line]).to eq("127.0.0.1  #www xyz") +    end + +    it "should work for hostname and comment" do +      expect(@provider.parse_line("xyz  #www test123")[:line]).to eq("xyz  #www test123") +    end + +    it "should work for crazy incomplete lines" do +      expect(@provider.parse_line("%th1s is a\t cr$zy    !incompl1t line")[:line]).to eq("%th1s is a\t cr$zy    !incompl1t line") +    end + +  end + +  describe "when parsing a line with ip and hostname" do + +    it "should parse an ipv4 from the first field" do +      expect(@provider.parse_line("127.0.0.1    localhost")[:ip]).to eq("127.0.0.1") +    end + +    it "should parse an ipv6 from the first field" do +      expect(@provider.parse_line("::1     localhost")[:ip]).to eq("::1") +    end + +    it "should parse the name from the second field" do +      expect(@provider.parse_line("::1     localhost")[:name]).to eq("localhost") +    end + +    it "should set an empty comment" do +      expect(@provider.parse_line("::1     localhost")[:comment]).to eq("") +    end + +    it "should set host_aliases to :absent" do +      expect(@provider.parse_line("::1     localhost")[:host_aliases]).to eq(:absent) +    end + +  end + +  describe "when parsing a line with ip, hostname and comment" do +    before do +      @testline = "127.0.0.1   localhost # A comment with a #-char" +    end + +    it "should parse the ip from the first field" do +      expect(@provider.parse_line(@testline)[:ip]).to eq("127.0.0.1") +    end + +    it "should parse the hostname from the second field" do +      expect(@provider.parse_line(@testline)[:name]).to eq("localhost") +    end + +    it "should parse the comment after the first '#' character" do +      expect(@provider.parse_line(@testline)[:comment]).to eq('A comment with a #-char') +    end + +  end + +  describe "when parsing a line with ip, hostname and aliases" do + +    it "should parse alias from the third field" do +      expect(@provider.parse_line("127.0.0.1   localhost   localhost.localdomain")[:host_aliases]).to eq("localhost.localdomain") +    end + +    it "should parse multiple aliases" do +      expect(@provider.parse_line("127.0.0.1 host alias1 alias2")[:host_aliases]).to eq('alias1 alias2') +      expect(@provider.parse_line("127.0.0.1 host alias1\talias2")[:host_aliases]).to eq('alias1 alias2') +      expect(@provider.parse_line("127.0.0.1 host alias1\talias2   alias3")[:host_aliases]).to eq('alias1 alias2 alias3') +    end + +  end + +  describe "when parsing a line with ip, hostname, aliases and comment" do + +    before do +      # Just playing with a few different delimiters +      @testline = "127.0.0.1\t   host  alias1\talias2   alias3   #   A comment with a #-char" +    end + +    it "should parse the ip from the first field" do +      expect(@provider.parse_line(@testline)[:ip]).to eq("127.0.0.1") +    end + +    it "should parse the hostname from the second field" do +      expect(@provider.parse_line(@testline)[:name]).to eq("host") +    end + +    it "should parse all host_aliases from the third field" do +      expect(@provider.parse_line(@testline)[:host_aliases]).to eq('alias1 alias2 alias3') +    end + +    it "should parse the comment after the first '#' character" do +      expect(@provider.parse_line(@testline)[:comment]).to eq('A comment with a #-char') +    end + +  end + +  describe "when operating on /etc/hosts like files" do +    it_should_behave_like "all parsedfile providers", +      provider_class, my_fixtures('valid*') + +    it "should be able to generate a simple hostfile entry" do +      host = mkhost( +        :name   => 'localhost', +        :ip     => '127.0.0.1', +        :ensure => :present +      ) +      expect(genhost(host)).to eq("127.0.0.1\tlocalhost\n") +    end + +    it "should be able to generate an entry with one alias" do +      host = mkhost( +        :name   => 'localhost.localdomain', +        :ip     => '127.0.0.1', +        :host_aliases => 'localhost', +        :ensure => :present +      ) +      expect(genhost(host)).to eq("127.0.0.1\tlocalhost.localdomain\tlocalhost\n") +    end + +    it "should be able to generate an entry with more than one alias" do +      host = mkhost( +        :name       => 'host', +        :ip         => '192.0.0.1', +        :host_aliases => [ 'a1','a2','a3','a4' ], +        :ensure     => :present +      ) +      expect(genhost(host)).to eq("192.0.0.1\thost\ta1 a2 a3 a4\n") +    end + +    it "should be able to generate a simple hostfile entry with comments" do +      host = mkhost( +        :name    => 'localhost', +        :ip      => '127.0.0.1', +        :comment => 'Bazinga!', +        :ensure  => :present +      ) +      expect(genhost(host)).to eq("127.0.0.1\tlocalhost\t# Bazinga!\n") +    end + +    it "should be able to generate an entry with one alias and a comment" do +      host = mkhost( +        :name   => 'localhost.localdomain', +        :ip     => '127.0.0.1', +        :host_aliases => 'localhost', +        :comment => 'Bazinga!', +        :ensure => :present +      ) +      expect(genhost(host)).to eq("127.0.0.1\tlocalhost.localdomain\tlocalhost\t# Bazinga!\n") +    end + +    it "should be able to generate an entry with more than one alias and a comment" do +      host = mkhost( +        :name         => 'host', +        :ip           => '192.0.0.1', +        :host_aliases => [ 'a1','a2','a3','a4' ], +        :comment      => 'Bazinga!', +        :ensure       => :present +      ) +      expect(genhost(host)).to eq("192.0.0.1\thost\ta1 a2 a3 a4\t# Bazinga!\n") +    end + +  end + +end diff --git a/spec/unit/type/host_spec.rb b/spec/unit/type/host_spec.rb new file mode 100644 index 0000000..ecce6e2 --- /dev/null +++ b/spec/unit/type/host_spec.rb @@ -0,0 +1,681 @@ +#! /usr/bin/env ruby +require 'spec_helper' + +host = Puppet::Type.type(:host) + +describe host do +  FakeHostProvider = Struct.new(:ip, :host_aliases, :comment) +  before do +    @class = host +    @catalog = Puppet::Resource::Catalog.new +    @provider = FakeHostProvider.new +    @resource = stub 'resource', :resource => nil, :provider => @provider +  end + +  it "should have :name be its namevar" do +    expect(@class.key_attributes).to eq([:name]) +  end + +  describe "when validating attributes" do +    [:name, :provider ].each do |param| +      it "should have a #{param} parameter" do +        expect(@class.attrtype(param)).to eq(:param) +      end +    end + +    [:ip, :target, :host_aliases, :comment, :ensure].each do |property| +      it "should have a #{property} property" do +        expect(@class.attrtype(property)).to eq(:property) +      end +    end + +    it "should have a list host_aliases" do +      expect(@class.attrclass(:host_aliases).ancestors).to be_include(Puppet::Property::OrderedList) +    end + +  end + +  describe "when validating values" do +    it "should support present as a value for ensure" do +      expect { @class.new(:name => "foo", :ensure => :present) }.not_to raise_error +    end + +    it "should support absent as a value for ensure" do +      expect { @class.new(:name => "foo", :ensure => :absent) }.not_to raise_error +    end + +    it "should accept IPv4 addresses" do +      expect { @class.new(:name => "foo", :ip => '10.96.0.1') }.not_to raise_error +    end + +    it "should accept long IPv6 addresses" do +      # Taken from wikipedia article about ipv6 +      expect { @class.new(:name => "foo", :ip => '2001:0db8:85a3:08d3:1319:8a2e:0370:7344') }.not_to raise_error +    end + +    it "should accept one host_alias" do +      expect { @class.new(:name => "foo", :host_aliases => 'alias1') }.not_to raise_error +    end + +    it "should accept multiple host_aliases" do +      expect { @class.new(:name => "foo", :host_aliases => [ 'alias1', 'alias2' ]) }.not_to raise_error +    end + +    it "should accept shortened IPv6 addresses" do +      expect { @class.new(:name => "foo", :ip => '2001:db8:0:8d3:0:8a2e:70:7344') }.not_to raise_error +      expect { @class.new(:name => "foo", :ip => '::ffff:192.0.2.128') }.not_to raise_error +      expect { @class.new(:name => "foo", :ip => '::1') }.not_to raise_error +    end + +    it "should not accept malformed IPv4 addresses like 192.168.0.300" do +      expect { @class.new(:name => "foo", :ip => '192.168.0.300') }.to raise_error(Puppet::ResourceError, /Parameter ip failed/) +    end + +    it "should reject over-long IPv4 addresses" do +      expect { @class.new(:name => "foo", :ip => '10.10.10.10.10') }.to raise_error(Puppet::ResourceError, /Parameter ip failed/) +    end + +    it "should not accept malformed IP addresses like 2001:0dg8:85a3:08d3:1319:8a2e:0370:7344" do +      expect { @class.new(:name => "foo", :ip => '2001:0dg8:85a3:08d3:1319:8a2e:0370:7344') }.to raise_error(Puppet::ResourceError, /Parameter ip failed/) +    end + +    # Assorted, annotated IPv6 passes. +    ["::1",                          # loopback, compressed, non-routable +     "::",                           # unspecified, compressed, non-routable +     "0:0:0:0:0:0:0:1",              # loopback, full +     "0:0:0:0:0:0:0:0",              # unspecified, full +     "2001:DB8:0:0:8:800:200C:417A", # unicast, full +     "FF01:0:0:0:0:0:0:101",         # multicast, full +     "2001:DB8::8:800:200C:417A",    # unicast, compressed +     "FF01::101",                    # multicast, compressed +     # Some more test cases that should pass. +     "2001:0000:1234:0000:0000:C1C0:ABCD:0876", +     "3ffe:0b00:0000:0000:0001:0000:0000:000a", +     "FF02:0000:0000:0000:0000:0000:0000:0001", +     "0000:0000:0000:0000:0000:0000:0000:0001", +     "0000:0000:0000:0000:0000:0000:0000:0000", +     # Assorted valid, compressed IPv6 addresses. +     "2::10", +     "ff02::1", +     "fe80::", +     "2002::", +     "2001:db8::", +     "2001:0db8:1234::", +     "::ffff:0:0", +     "::1", +     "1:2:3:4:5:6:7:8", +     "1:2:3:4:5:6::8", +     "1:2:3:4:5::8", +     "1:2:3:4::8", +     "1:2:3::8", +     "1:2::8", +     "1::8", +     "1::2:3:4:5:6:7", +     "1::2:3:4:5:6", +     "1::2:3:4:5", +     "1::2:3:4", +     "1::2:3", +     "1::8", +     "::2:3:4:5:6:7:8", +     "::2:3:4:5:6:7", +     "::2:3:4:5:6", +     "::2:3:4:5", +     "::2:3:4", +     "::2:3", +     "::8", +     "1:2:3:4:5:6::", +     "1:2:3:4:5::", +     "1:2:3:4::", +     "1:2:3::", +     "1:2::", +     "1::", +     "1:2:3:4:5::7:8", +     "1:2:3:4::7:8", +     "1:2:3::7:8", +     "1:2::7:8", +     "1::7:8", +     # IPv4 addresses as dotted-quads +     "1:2:3:4:5:6:1.2.3.4", +     "1:2:3:4:5::1.2.3.4", +     "1:2:3:4::1.2.3.4", +     "1:2:3::1.2.3.4", +     "1:2::1.2.3.4", +     "1::1.2.3.4", +     "1:2:3:4::5:1.2.3.4", +     "1:2:3::5:1.2.3.4", +     "1:2::5:1.2.3.4", +     "1::5:1.2.3.4", +     "1::5:11.22.33.44", +     "fe80::217:f2ff:254.7.237.98", +     "::ffff:192.168.1.26", +     "::ffff:192.168.1.1", +     "0:0:0:0:0:0:13.1.68.3", # IPv4-compatible IPv6 address, full, deprecated +     "0:0:0:0:0:FFFF:129.144.52.38", # IPv4-mapped IPv6 address, full +     "::13.1.68.3",             # IPv4-compatible IPv6 address, compressed, deprecated +     "::FFFF:129.144.52.38",    # IPv4-mapped IPv6 address, compressed +     "fe80:0:0:0:204:61ff:254.157.241.86", +     "fe80::204:61ff:254.157.241.86", +     "::ffff:12.34.56.78", +     "::ffff:192.0.2.128",      # this is OK, since there's a single zero digit in IPv4 +     "fe80:0000:0000:0000:0204:61ff:fe9d:f156", +     "fe80:0:0:0:204:61ff:fe9d:f156", +     "fe80::204:61ff:fe9d:f156", +     "::1", +     "fe80::", +     "fe80::1", +     "::ffff:c000:280", + +     # Additional test cases from http://rt.cpan.org/Public/Bug/Display.html?id=50693 +     "2001:0db8:85a3:0000:0000:8a2e:0370:7334", +     "2001:db8:85a3:0:0:8a2e:370:7334", +     "2001:db8:85a3::8a2e:370:7334", +     "2001:0db8:0000:0000:0000:0000:1428:57ab", +     "2001:0db8:0000:0000:0000::1428:57ab", +     "2001:0db8:0:0:0:0:1428:57ab", +     "2001:0db8:0:0::1428:57ab", +     "2001:0db8::1428:57ab", +     "2001:db8::1428:57ab", +     "0000:0000:0000:0000:0000:0000:0000:0001", +     "::1", +     "::ffff:0c22:384e", +     "2001:0db8:1234:0000:0000:0000:0000:0000", +     "2001:0db8:1234:ffff:ffff:ffff:ffff:ffff", +     "2001:db8:a::123", +     "fe80::", + +     "1111:2222:3333:4444:5555:6666:7777:8888", +     "1111:2222:3333:4444:5555:6666:7777::", +     "1111:2222:3333:4444:5555:6666::", +     "1111:2222:3333:4444:5555::", +     "1111:2222:3333:4444::", +     "1111:2222:3333::", +     "1111:2222::", +     "1111::", +     "1111:2222:3333:4444:5555:6666::8888", +     "1111:2222:3333:4444:5555::8888", +     "1111:2222:3333:4444::8888", +     "1111:2222:3333::8888", +     "1111:2222::8888", +     "1111::8888", +     "::8888", +     "1111:2222:3333:4444:5555::7777:8888", +     "1111:2222:3333:4444::7777:8888", +     "1111:2222:3333::7777:8888", +     "1111:2222::7777:8888", +     "1111::7777:8888", +     "::7777:8888", +     "1111:2222:3333:4444::6666:7777:8888", +     "1111:2222:3333::6666:7777:8888", +     "1111:2222::6666:7777:8888", +     "1111::6666:7777:8888", +     "::6666:7777:8888", +     "1111:2222:3333::5555:6666:7777:8888", +     "1111:2222::5555:6666:7777:8888", +     "1111::5555:6666:7777:8888", +     "::5555:6666:7777:8888", +     "1111:2222::4444:5555:6666:7777:8888", +     "1111::4444:5555:6666:7777:8888", +     "::4444:5555:6666:7777:8888", +     "1111::3333:4444:5555:6666:7777:8888", +     "::3333:4444:5555:6666:7777:8888", +     "::2222:3333:4444:5555:6666:7777:8888", +     "1111:2222:3333:4444:5555:6666:123.123.123.123", +     "1111:2222:3333:4444:5555::123.123.123.123", +     "1111:2222:3333:4444::123.123.123.123", +     "1111:2222:3333::123.123.123.123", +     "1111:2222::123.123.123.123", +     "1111::123.123.123.123", +     "::123.123.123.123", +     "1111:2222:3333:4444::6666:123.123.123.123", +     "1111:2222:3333::6666:123.123.123.123", +     "1111:2222::6666:123.123.123.123", +     "1111::6666:123.123.123.123", +     "::6666:123.123.123.123", +     "1111:2222:3333::5555:6666:123.123.123.123", +     "1111:2222::5555:6666:123.123.123.123", +     "1111::5555:6666:123.123.123.123", +     "::5555:6666:123.123.123.123", +     "1111:2222::4444:5555:6666:123.123.123.123", +     "1111::4444:5555:6666:123.123.123.123", +     "::4444:5555:6666:123.123.123.123", +     "1111::3333:4444:5555:6666:123.123.123.123", +     "::2222:3333:4444:5555:6666:123.123.123.123", + +     # Playing with combinations of "0" and "::"; these are all sytactically +     # correct, but are bad form because "0" adjacent to "::" should be +     # combined into "::" +     "::0:0:0:0:0:0:0", +     "::0:0:0:0:0:0", +     "::0:0:0:0:0", +     "::0:0:0:0", +     "::0:0:0", +     "::0:0", +     "::0", +     "0:0:0:0:0:0:0::", +     "0:0:0:0:0:0::", +     "0:0:0:0:0::", +     "0:0:0:0::", +     "0:0:0::", +     "0:0::", +     "0::", + +     # Additional cases: http://crisp.tweakblogs.net/blog/2031/ipv6-validation-%28and-caveats%29.html +     "0:a:b:c:d:e:f::", +     "::0:a:b:c:d:e:f", # syntactically correct, but bad form (::0:... could be combined) +     "a:b:c:d:e:f:0::", +    ].each do |ip| +      it "should accept #{ip.inspect} as an IPv6 address" do +        expect { @class.new(:name => "foo", :ip => ip) }.not_to raise_error +      end +    end + +    # ...aaaand, some failure cases. +    [":", +     "02001:0000:1234:0000:0000:C1C0:ABCD:0876",     # extra 0 not allowed! +     "2001:0000:1234:0000:00001:C1C0:ABCD:0876",     # extra 0 not allowed! +     "2001:0000:1234:0000:0000:C1C0:ABCD:0876  0",   # junk after valid address +     "2001:0000:1234: 0000:0000:C1C0:ABCD:0876",     # internal space +     "3ffe:0b00:0000:0001:0000:0000:000a",           # seven segments +     "FF02:0000:0000:0000:0000:0000:0000:0000:0001", # nine segments +     "3ffe:b00::1::a",                               # double "::" +     "::1111:2222:3333:4444:5555:6666::",            # double "::" +     "1:2:3::4:5::7:8",                              # Double "::" +     "12345::6:7:8", +     # IPv4 embedded, but bad... +     "1::5:400.2.3.4", "1::5:260.2.3.4", "1::5:256.2.3.4", "1::5:1.256.3.4", +     "1::5:1.2.256.4", "1::5:1.2.3.256", "1::5:300.2.3.4", "1::5:1.300.3.4", +     "1::5:1.2.300.4", "1::5:1.2.3.300", "1::5:900.2.3.4", "1::5:1.900.3.4", +     "1::5:1.2.900.4", "1::5:1.2.3.900", "1::5:300.300.300.300", "1::5:3000.30.30.30", +     "1::400.2.3.4", "1::260.2.3.4", "1::256.2.3.4", "1::1.256.3.4", +     "1::1.2.256.4", "1::1.2.3.256", "1::300.2.3.4", "1::1.300.3.4", +     "1::1.2.300.4", "1::1.2.3.300", "1::900.2.3.4", "1::1.900.3.4", +     "1::1.2.900.4", "1::1.2.3.900", "1::300.300.300.300", "1::3000.30.30.30", +     "::400.2.3.4", "::260.2.3.4", "::256.2.3.4", "::1.256.3.4", +     "::1.2.256.4", "::1.2.3.256", "::300.2.3.4", "::1.300.3.4", +     "::1.2.300.4", "::1.2.3.300", "::900.2.3.4", "::1.900.3.4", +     "::1.2.900.4", "::1.2.3.900", "::300.300.300.300", "::3000.30.30.30", +     "2001:1:1:1:1:1:255Z255X255Y255", # garbage instead of "." in IPv4 +     "::ffff:192x168.1.26",            # ditto +     "::ffff:2.3.4", +     "::ffff:257.1.2.3", +     "1.2.3.4:1111:2222:3333:4444::5555", +     "1.2.3.4:1111:2222:3333::5555", +     "1.2.3.4:1111:2222::5555", +     "1.2.3.4:1111::5555", +     "1.2.3.4::5555", +     "1.2.3.4::", + +     # Testing IPv4 addresses represented as dotted-quads Leading zero's in +     # IPv4 addresses not allowed: some systems treat the leading "0" in +     # ".086" as the start of an octal number Update: The BNF in RFC-3986 +     # explicitly defines the dec-octet (for IPv4 addresses) not to have a +     # leading zero +     "fe80:0000:0000:0000:0204:61ff:254.157.241.086", +     "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:1.2.3.4", +     "1111:2222:3333:4444:5555:6666:00.00.00.00", +     "1111:2222:3333:4444:5555:6666:000.000.000.000", +     "1111:2222:3333:4444:5555:6666:256.256.256.256", + +     "1111:2222:3333:4444::5555:", +     "1111:2222:3333::5555:", +     "1111:2222::5555:", +     "1111::5555:", +     "::5555:", +     ":::", +     "1111:", +     ":", + +     ":1111:2222:3333:4444::5555", +     ":1111:2222:3333::5555", +     ":1111:2222::5555", +     ":1111::5555", +     ":::5555", +     ":::", + +     # Additional test cases from http://rt.cpan.org/Public/Bug/Display.html?id=50693 +     "123", +     "ldkfj", +     "2001::FFD3::57ab", +     "2001:db8:85a3::8a2e:37023:7334", +     "2001:db8:85a3::8a2e:370k:7334", +     "1:2:3:4:5:6:7:8:9", +     "1::2::3", +     "1:::3:4:5", +     "1:2:3::4:5:6:7:8:9", + +     # Invalid data +     "XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX", + +     # Too many components +     "1111:2222:3333:4444:5555:6666:7777:8888:9999", +     "1111:2222:3333:4444:5555:6666:7777:8888::", +     "::2222:3333:4444:5555:6666:7777:8888:9999", + +     # Too few components +     "1111:2222:3333:4444:5555:6666:7777", +     "1111:2222:3333:4444:5555:6666", +     "1111:2222:3333:4444:5555", +     "1111:2222:3333:4444", +     "1111:2222:3333", +     "1111:2222", +     "1111", + +     # Missing : +     "11112222:3333:4444:5555:6666:7777:8888", +     "1111:22223333:4444:5555:6666:7777:8888", +     "1111:2222:33334444:5555:6666:7777:8888", +     "1111:2222:3333:44445555:6666:7777:8888", +     "1111:2222:3333:4444:55556666:7777:8888", +     "1111:2222:3333:4444:5555:66667777:8888", +     "1111:2222:3333:4444:5555:6666:77778888", + +     # Missing : intended for :: +     "1111:2222:3333:4444:5555:6666:7777:8888:", +     "1111:2222:3333:4444:5555:6666:7777:", +     "1111:2222:3333:4444:5555:6666:", +     "1111:2222:3333:4444:5555:", +     "1111:2222:3333:4444:", +     "1111:2222:3333:", +     "1111:2222:", +     "1111:", +     ":", +     ":8888", +     ":7777:8888", +     ":6666:7777:8888", +     ":5555:6666:7777:8888", +     ":4444:5555:6666:7777:8888", +     ":3333:4444:5555:6666:7777:8888", +     ":2222:3333:4444:5555:6666:7777:8888", +     ":1111:2222:3333:4444:5555:6666:7777:8888", + +     # ::: +     ":::2222:3333:4444:5555:6666:7777:8888", +     "1111:::3333:4444:5555:6666:7777:8888", +     "1111:2222:::4444:5555:6666:7777:8888", +     "1111:2222:3333:::5555:6666:7777:8888", +     "1111:2222:3333:4444:::6666:7777:8888", +     "1111:2222:3333:4444:5555:::7777:8888", +     "1111:2222:3333:4444:5555:6666:::8888", +     "1111:2222:3333:4444:5555:6666:7777:::", + +     # Double ::", +     "::2222::4444:5555:6666:7777:8888", +     "::2222:3333::5555:6666:7777:8888", +     "::2222:3333:4444::6666:7777:8888", +     "::2222:3333:4444:5555::7777:8888", +     "::2222:3333:4444:5555:7777::8888", +     "::2222:3333:4444:5555:7777:8888::", + +     "1111::3333::5555:6666:7777:8888", +     "1111::3333:4444::6666:7777:8888", +     "1111::3333:4444:5555::7777:8888", +     "1111::3333:4444:5555:6666::8888", +     "1111::3333:4444:5555:6666:7777::", + +     "1111:2222::4444::6666:7777:8888", +     "1111:2222::4444:5555::7777:8888", +     "1111:2222::4444:5555:6666::8888", +     "1111:2222::4444:5555:6666:7777::", + +     "1111:2222:3333::5555::7777:8888", +     "1111:2222:3333::5555:6666::8888", +     "1111:2222:3333::5555:6666:7777::", + +     "1111:2222:3333:4444::6666::8888", +     "1111:2222:3333:4444::6666:7777::", + +     "1111:2222:3333:4444:5555::7777::", + + +     # Too many components" +     "1111:2222:3333:4444:5555:6666:7777:8888:1.2.3.4", +     "1111:2222:3333:4444:5555:6666:7777:1.2.3.4", +     "1111:2222:3333:4444:5555:6666::1.2.3.4", +     "::2222:3333:4444:5555:6666:7777:1.2.3.4", +     "1111:2222:3333:4444:5555:6666:1.2.3.4.5", + +     # Too few components +     "1111:2222:3333:4444:5555:1.2.3.4", +     "1111:2222:3333:4444:1.2.3.4", +     "1111:2222:3333:1.2.3.4", +     "1111:2222:1.2.3.4", +     "1111:1.2.3.4", + +     # Missing : +     "11112222:3333:4444:5555:6666:1.2.3.4", +     "1111:22223333:4444:5555:6666:1.2.3.4", +     "1111:2222:33334444:5555:6666:1.2.3.4", +     "1111:2222:3333:44445555:6666:1.2.3.4", +     "1111:2222:3333:4444:55556666:1.2.3.4", +     "1111:2222:3333:4444:5555:66661.2.3.4", + +     # Missing . +     "1111:2222:3333:4444:5555:6666:255255.255.255", +     "1111:2222:3333:4444:5555:6666:255.255255.255", +     "1111:2222:3333:4444:5555:6666:255.255.255255", + +     # Missing : intended for :: +     ":1.2.3.4", +     ":6666:1.2.3.4", +     ":5555:6666:1.2.3.4", +     ":4444:5555:6666:1.2.3.4", +     ":3333:4444:5555:6666:1.2.3.4", +     ":2222:3333:4444:5555:6666:1.2.3.4", +     ":1111:2222:3333:4444:5555:6666:1.2.3.4", + +     # ::: +     ":::2222:3333:4444:5555:6666:1.2.3.4", +     "1111:::3333:4444:5555:6666:1.2.3.4", +     "1111:2222:::4444:5555:6666:1.2.3.4", +     "1111:2222:3333:::5555:6666:1.2.3.4", +     "1111:2222:3333:4444:::6666:1.2.3.4", +     "1111:2222:3333:4444:5555:::1.2.3.4", + +     # Double :: +     "::2222::4444:5555:6666:1.2.3.4", +     "::2222:3333::5555:6666:1.2.3.4", +     "::2222:3333:4444::6666:1.2.3.4", +     "::2222:3333:4444:5555::1.2.3.4", + +     "1111::3333::5555:6666:1.2.3.4", +     "1111::3333:4444::6666:1.2.3.4", +     "1111::3333:4444:5555::1.2.3.4", + +     "1111:2222::4444::6666:1.2.3.4", +     "1111:2222::4444:5555::1.2.3.4", + +     "1111:2222:3333::5555::1.2.3.4", + +     # Missing parts +     "::.", +     "::..", +     "::...", +     "::1...", +     "::1.2..", +     "::1.2.3.", +     "::.2..", +     "::.2.3.", +     "::.2.3.4", +     "::..3.", +     "::..3.4", +     "::...4", + +     # Extra : in front +     ":1111:2222:3333:4444:5555:6666:7777::", +     ":1111:2222:3333:4444:5555:6666::", +     ":1111:2222:3333:4444:5555::", +     ":1111:2222:3333:4444::", +     ":1111:2222:3333::", +     ":1111:2222::", +     ":1111::", +     ":::", +     ":1111:2222:3333:4444:5555:6666::8888", +     ":1111:2222:3333:4444:5555::8888", +     ":1111:2222:3333:4444::8888", +     ":1111:2222:3333::8888", +     ":1111:2222::8888", +     ":1111::8888", +     ":::8888", +     ":1111:2222:3333:4444:5555::7777:8888", +     ":1111:2222:3333:4444::7777:8888", +     ":1111:2222:3333::7777:8888", +     ":1111:2222::7777:8888", +     ":1111::7777:8888", +     ":::7777:8888", +     ":1111:2222:3333:4444::6666:7777:8888", +     ":1111:2222:3333::6666:7777:8888", +     ":1111:2222::6666:7777:8888", +     ":1111::6666:7777:8888", +     ":::6666:7777:8888", +     ":1111:2222:3333::5555:6666:7777:8888", +     ":1111:2222::5555:6666:7777:8888", +     ":1111::5555:6666:7777:8888", +     ":::5555:6666:7777:8888", +     ":1111:2222::4444:5555:6666:7777:8888", +     ":1111::4444:5555:6666:7777:8888", +     ":::4444:5555:6666:7777:8888", +     ":1111::3333:4444:5555:6666:7777:8888", +     ":::3333:4444:5555:6666:7777:8888", +     ":::2222:3333:4444:5555:6666:7777:8888", +     ":1111:2222:3333:4444:5555:6666:1.2.3.4", +     ":1111:2222:3333:4444:5555::1.2.3.4", +     ":1111:2222:3333:4444::1.2.3.4", +     ":1111:2222:3333::1.2.3.4", +     ":1111:2222::1.2.3.4", +     ":1111::1.2.3.4", +     ":::1.2.3.4", +     ":1111:2222:3333:4444::6666:1.2.3.4", +     ":1111:2222:3333::6666:1.2.3.4", +     ":1111:2222::6666:1.2.3.4", +     ":1111::6666:1.2.3.4", +     ":::6666:1.2.3.4", +     ":1111:2222:3333::5555:6666:1.2.3.4", +     ":1111:2222::5555:6666:1.2.3.4", +     ":1111::5555:6666:1.2.3.4", +     ":::5555:6666:1.2.3.4", +     ":1111:2222::4444:5555:6666:1.2.3.4", +     ":1111::4444:5555:6666:1.2.3.4", +     ":::4444:5555:6666:1.2.3.4", +     ":1111::3333:4444:5555:6666:1.2.3.4", +     ":::2222:3333:4444:5555:6666:1.2.3.4", + +     # Extra : at end +     "1111:2222:3333:4444:5555:6666:7777:::", +     "1111:2222:3333:4444:5555:6666:::", +     "1111:2222:3333:4444:5555:::", +     "1111:2222:3333:4444:::", +     "1111:2222:3333:::", +     "1111:2222:::", +     "1111:::", +     ":::", +     "1111:2222:3333:4444:5555:6666::8888:", +     "1111:2222:3333:4444:5555::8888:", +     "1111:2222:3333:4444::8888:", +     "1111:2222:3333::8888:", +     "1111:2222::8888:", +     "1111::8888:", +     "::8888:", +     "1111:2222:3333:4444:5555::7777:8888:", +     "1111:2222:3333:4444::7777:8888:", +     "1111:2222:3333::7777:8888:", +     "1111:2222::7777:8888:", +     "1111::7777:8888:", +     "::7777:8888:", +     "1111:2222:3333:4444::6666:7777:8888:", +     "1111:2222:3333::6666:7777:8888:", +     "1111:2222::6666:7777:8888:", +     "1111::6666:7777:8888:", +     "::6666:7777:8888:", +     "1111:2222:3333::5555:6666:7777:8888:", +     "1111:2222::5555:6666:7777:8888:", +     "1111::5555:6666:7777:8888:", +     "::5555:6666:7777:8888:", +     "1111:2222::4444:5555:6666:7777:8888:", +     "1111::4444:5555:6666:7777:8888:", +     "::4444:5555:6666:7777:8888:", +     "1111::3333:4444:5555:6666:7777:8888:", +     "::3333:4444:5555:6666:7777:8888:", +     "::2222:3333:4444:5555:6666:7777:8888:", +    ].each do |ip| +      it "should reject #{ip.inspect} as an IPv6 address" do +        expect { @class.new(:name => "foo", :ip => ip) }.to raise_error(Puppet::ResourceError, /Parameter ip failed/) +      end +    end + +    it "should not accept newlines in resourcename" do +      expect { @class.new(:name => "fo\no", :ip => '127.0.0.1' ) }.to  raise_error(Puppet::ResourceError, /Hostname cannot include newline/) +    end + +    it "should not accept newlines in ipaddress" do +      expect { @class.new(:name => "foo", :ip => "127.0.0.1\n") }.to raise_error(Puppet::ResourceError, /Invalid IP address/) +    end + +    it "should not accept newlines in host_aliases" do +      expect { @class.new(:name => "foo", :ip => '127.0.0.1', :host_aliases => [ 'well_formed', "thisalias\nhavenewline" ] ) }.to raise_error(Puppet::ResourceError, /Host aliases cannot include whitespace/) +    end + +    it "should not accept newlines in comment" do +      expect { @class.new(:name => "foo", :ip => '127.0.0.1', :comment => "Test of comment blah blah \n test 123" ) }.to raise_error(Puppet::ResourceError, /Comment cannot include newline/) +    end + +    it "should not accept spaces in resourcename" do +      expect { @class.new(:name => "foo bar") }.to raise_error(Puppet::ResourceError, /Invalid host name/) +    end + +    it "should not accept host_aliases with spaces" do +      expect { @class.new(:name => "foo", :host_aliases => [ 'well_formed', 'not wellformed' ]) }.to raise_error(Puppet::ResourceError, /Host aliases cannot include whitespace/) +    end + +    it "should not accept empty host_aliases" do +      expect { @class.new(:name => "foo", :host_aliases => ['alias1','']) }.to raise_error(Puppet::ResourceError, /Host aliases cannot be an empty string/) +    end +  end + +  describe "when syncing" do + +    it "should send the first value to the provider for ip property" do +      @ip = @class.attrclass(:ip).new(:resource => @resource, :should => %w{192.168.0.1 192.168.0.2}) + +      @ip.sync + +      expect(@provider.ip).to eq('192.168.0.1') +    end + +    it "should send the first value to the provider for comment property" do +      @comment = @class.attrclass(:comment).new(:resource => @resource, :should => %w{Bazinga Notme}) + +      @comment.sync + +      expect(@provider.comment).to eq('Bazinga') +    end + +    it "should send the joined array to the provider for host_alias" do +      @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) + +      @host_aliases.sync + +      expect(@provider.host_aliases).to eq('foo bar') +    end + +    it "should also use the specified delimiter for joining" do +      @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) +      @host_aliases.stubs(:delimiter).returns "\t" + +      @host_aliases.sync + +      expect(@provider.host_aliases).to eq("foo\tbar") +    end + +    it "should care about the order of host_aliases" do +      @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) +      expect(@host_aliases.insync?(%w{foo bar})).to eq(true) +      expect(@host_aliases.insync?(%w{bar foo})).to eq(false) +    end + +    it "should not consider aliases to be in sync if should is a subset of current" do +      @host_aliases = @class.attrclass(:host_aliases).new(:resource => @resource, :should => %w{foo bar}) +      expect(@host_aliases.insync?(%w{foo bar anotherone})).to eq(false) +    end + +  end +end | 
