From 3d9ae52169163436ccad4782dce7c7096daa87b0 Mon Sep 17 00:00:00 2001 From: Garrett Honeycutt Date: Fri, 20 Mar 2020 23:04:47 -0400 Subject: [PATCH] Add facter::structured_data_fact to allow for structured data --- README.md | 38 ++++++- manifests/init.pp | 10 ++ manifests/structured_data_fact.pp | 36 +++++++ spec/acceptance/facter_spec.rb | 50 +++++++++ spec/classes/init_spec.rb | 69 ++++++++++++ spec/defines/structured_data_fact_spec.rb | 122 ++++++++++++++++++++++ templates/structured_data_fact.erb | 3 + 7 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 manifests/structured_data_fact.pp create mode 100644 spec/defines/structured_data_fact_spec.rb create mode 100644 templates/structured_data_fact.erb diff --git a/README.md b/README.md index 36c5ca5..2bdbb0b 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,12 @@ and populate it with `facts.txt` which is used for external facts. It can optionally create a symlink such as `/usr/local/bin/facter` to point to facter in the puppet package that may not be in your `$PATH`. -It has a defined type that allows for specifying external facts. This -allows you to seed already known information on the system. +It has defined types for specifying external facts. This allows you to +seed already known information on the system. `facter::fact` is for +traditional key=value where the value is a string. +`facter::structured_data_fact` allows values to be structured data such +as arrays and hashes. To achive this, the structured data is converted +to YAML format. ### Beginning with facter @@ -39,6 +43,8 @@ class. To specify external facts, use the `facter::fact` defined type. You can optionally specify a hash of external facts in Hiera. +### `facter::fact` + ```yaml --- facter::facts_hash: @@ -62,6 +68,34 @@ It would also produce `/etc/facter/facts.d/location.txt` with the following cont location=RNB ``` +### `facter::structured_data_fact` + +```yaml +--- +facter::structured_data_facts_hash: +foo: + data: + my_array: + - one + - two + - three + my_hash: + k: v +bar: + data: + bar_array: + - one + - two + - three + file: bar.yaml + facts_dir: /factsdir +``` + +The above configuration would create `/etc/facter/facts.d/facts.yaml` +with the fact `my_array`. It would create `/factsdir/bar.yaml` with the +fact `bar_array`. + + ### Minimum usage ```puppet diff --git a/manifests/init.pp b/manifests/init.pp index fee7521..2b15f9f 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -33,6 +33,9 @@ # @param facts_hash # A hash of `facter::fact` entries. # +# @param structured_data_facts_hash +# A hash of `facter::structured_data_fact` entries. +# # @param facts_file # The file in which the text based external facts are stored. This file must # end with '.txt'. @@ -57,6 +60,7 @@ Stdlib::Absolutepath $path_to_facter_symlink = '/usr/local/bin/facter', Boolean $ensure_facter_symlink = false, Hash $facts_hash = {}, + Hash $structured_data_facts_hash = {}, Pattern[/\.txt*\Z/] $facts_file = 'facts.txt', String[1] $facts_file_owner = 'root', String[1] $facts_file_group = 'root', @@ -126,4 +130,10 @@ * => $v, } } + + $structured_data_facts_hash.each |$k, $v| { + facter::structured_data_fact { $k: + * => $v, + } + } } diff --git a/manifests/structured_data_fact.pp b/manifests/structured_data_fact.pp new file mode 100644 index 0000000..8168b6a --- /dev/null +++ b/manifests/structured_data_fact.pp @@ -0,0 +1,36 @@ +# @summary Define YAML based external structured data facts. +# +# @param data +# A hash of facts. All keys must be strings. +# +# @param file +# File in which the fact will be placed. The file name must end with '.yaml'. +# +# @param facts_dir +# Directory in which the file will be placed. If not specified, use the +# default facts_d_dir. +# +define facter::structured_data_fact ( + Hash[String[1], Data] $data, + Pattern[/\.yaml*\Z/] $file = 'facts.yaml', + Optional[Stdlib::Absolutepath] $facts_dir = undef, +) { + + include facter + + $facts_dir_path = pick($facts_dir, $facter::facts_d_dir) + if $facts['os']['family'] == 'windows' { + $facts_file_path = "${facts_dir_path}\\${file}" + } else { + $facts_file_path = "${facts_dir_path}/${file}" + } + + file { "structured_data_fact_${file}": + ensure => file, + path => $facts_file_path, + content => template('facter/structured_data_fact.erb'), + owner => $facter::facts_file_owner, + group => $facter::facts_file_group, + mode => $facter::facts_file_mode, + } +} diff --git a/spec/acceptance/facter_spec.rb b/spec/acceptance/facter_spec.rb index 37dfdab..2b559d4 100644 --- a/spec/acceptance/facter_spec.rb +++ b/spec/acceptance/facter_spec.rb @@ -8,6 +8,7 @@ facts_d_group = 'NT AUTHORITY\SYSTEM' facts_d_mode = nil test_facts_file = 'C:\ProgramData\PuppetLabs\facter\facts.d\\test.txt' + test_yaml_file = 'C:\ProgramData\PuppetLabs\facter\facts.d\\test.yaml' facts_file = 'C:\ProgramData\PuppetLabs\facter\facts.d\\facts.txt' facts_file_owner = 'NT AUTHORITY\SYSTEM' facts_file_group = 'NT AUTHORITY\SYSTEM' @@ -18,6 +19,7 @@ facts_d_group = 'root' facts_d_mode = '755' test_facts_file = '/etc/facter/facts.d/test.txt' + test_yaml_file = '/etc/facter/facts.d/test.yaml' facts_file = '/etc/facter/facts.d/facts.txt' facts_file_owner = 'root' facts_file_group = 'root' @@ -126,6 +128,54 @@ class { 'facter': end end + context 'with using facter::structured_data_fact' do + context 'should apply the manifest' do + pp = <<-EOS + facter::structured_data_fact { 'test': + data => { + 'my_hash' => { + 'a' => 1, + 'b' => 2, + }, + }, + file => 'test.yaml', + } + EOS + + if Gem.win_platform? + manifest = 'C:\manifest-facter_yaml.pp' + + it 'creates manifest' do + File.open(manifest, 'w') { |f| f.write(pp) } + puts manifest + puts File.read(manifest) + end + + describe command("PsExec -accepteula -s 'C:\\Program Files\\Puppet Labs\\Puppet\\bin\\puppet.bat' apply --debug #{manifest}") do + its(:stderr) { should contain('with error code 0') } + end + else + it 'should work with no errors' do + apply_manifest(pp, :catch_failures => true) + end + end + end + + context 'and should contain facts' do + describe file(test_yaml_file) do + it { should exist } + end + + describe command('puppet facts my_hash.a') do + its(:stdout) { should contain('1') } + end + + describe command('puppet facts my_hash.b') do + its(:stdout) { should contain('2') } + end + end + end + context 'with setting ensure_facter_symlink to true' do if host_inventory['platform'] == 'windows' before { skip('Windows does not have symlinks. Skipping test.') } diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb index f534add..ab46484 100644 --- a/spec/classes/init_spec.rb +++ b/spec/classes/init_spec.rb @@ -153,6 +153,75 @@ it { should contain_exec('mkdir_p-/etc/facter/facts.d') } end + context 'with structured_data_facts_hash specified' do + let(:params) do + { + :structured_data_facts_hash => { + 'foo' => { + 'data' => { + 'my_array' => ['one', 'two', 'three'], + 'my_hash' => { 'k' => 'v' }, + }, + }, + 'bar' => { + 'data' => { + 'bar_array' => ['one', 'two', 'three'], + }, + 'file' => 'bar.yaml', + 'facts_dir' => '/factsdir', + }, + } + } + end + + foo_content = <<-END.gsub(/^\s+\|/, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |--- + |my_array: + |- one + |- two + |- three + |my_hash: + | k: v + END + + bar_content = <<-END.gsub(/^\s+\|/, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |--- + |bar_array: + |- one + |- two + |- three + END + + it { should contain_facter__structured_data_fact('foo') } + it { should contain_facter__structured_data_fact('bar') } + + it { + should contain_file('structured_data_fact_facts.yaml').with({ + 'ensure' => 'file', + 'path' => '/etc/facter/facts.d/facts.yaml', + 'content' => foo_content, + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0644', + }) + } + + it { + should contain_file('structured_data_fact_bar.yaml').with({ + 'ensure' => 'file', + 'path' => '/factsdir/bar.yaml', + 'content' => bar_content, + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0644', + }) + } + end + context 'with facts specified as a hash with different file and facts_dir' do let(:params) do { diff --git a/spec/defines/structured_data_fact_spec.rb b/spec/defines/structured_data_fact_spec.rb new file mode 100644 index 0000000..af30d78 --- /dev/null +++ b/spec/defines/structured_data_fact_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +describe 'facter::structured_data_fact' do + context 'on RedHat' do + redhat = { + supported_os: [ + { + 'operatingsystem' => 'RedHat', + 'operatingsystemrelease' => ['7'], + }, + ], + } + + on_supported_os(redhat).each do |_os, os_facts| + let(:facts) do + os_facts + end + + context 'with file and facts_dir specified' do + let(:title) { 'fact1' } + let(:params) do + { + :data => { + 'my_array' => ['one', 'two', 'three'], + 'my_hash' => { 'k' => 'v' }, + }, + :file => 'custom.yaml', + :facts_dir => '/factsdir', + } + end + + it { should contain_class('facter') } + + # These must exist or the coverage report lists these incorrectly as + # untouched resources. These resources are all from the facter class. + it { should contain_file('facts_file') } + it { should contain_file('facts_d_directory') } + it { should contain_exec('mkdir_p-/etc/facter/facts.d') } + + content = <<-END.gsub(/^\s+\|/, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |--- + |my_array: + |- one + |- two + |- three + |my_hash: + | k: v + END + + it { + should contain_file('structured_data_fact_custom.yaml').with({ + 'ensure' => 'file', + 'path' => '/factsdir/custom.yaml', + 'content' => content, + 'owner' => 'root', + 'group' => 'root', + 'mode' => '0644', + }) + } + end + end # end on_supported_os(redhat) + end # context "on RedHat" + + context 'on windows' do + windows = { + supported_os: [ + { + 'operatingsystem' => 'windows', + 'operatingsystemrelease' => ['2016'], + }, + ], + } + + on_supported_os(windows).each do |_os, os_facts| + let(:facts) do + os_facts + end + + context 'with fact and facts_dir specified' do + let(:title) { 'fact1' } + let(:params) do + { + :data => { + 'my_array' => ['one', 'two', 'three'], + 'my_hash' => { 'k' => 'v' }, + }, + :file => 'custom.yaml', + :facts_dir => 'C:\factsdir', + } + end + + # These must exist or the coverage report lists these incorrectly as + # untouched resources. These resources are all from the facter class. + it { should contain_exec('mkdir_p-C:\ProgramData\PuppetLabs\facter\facts.d') } + + content = <<-END.gsub(/^\s+\|/, '') + |# This file is being maintained by Puppet. + |# DO NOT EDIT + |--- + |my_array: + |- one + |- two + |- three + |my_hash: + | k: v + END + + it { + should contain_file('structured_data_fact_custom.yaml').with({ + 'ensure' => 'file', + 'content' => content, + 'path' => 'C:\factsdir\custom.yaml', + 'owner' => 'NT AUTHORITY\SYSTEM', + 'group' => 'NT AUTHORITY\SYSTEM', + }) + } + end + end # end on_supported_os(windows) + end # context "on windows" +end diff --git a/templates/structured_data_fact.erb b/templates/structured_data_fact.erb new file mode 100644 index 0000000..affc1e8 --- /dev/null +++ b/templates/structured_data_fact.erb @@ -0,0 +1,3 @@ +# This file is being maintained by Puppet. +# DO NOT EDIT +<%= @data.to_yaml -%>