From a82266c256784c4af229e026b00a4dcf9e779270 Mon Sep 17 00:00:00 2001 From: Eli Young Date: Mon, 26 Jan 2015 19:17:53 -0800 Subject: [PATCH] (MODULES-1715) Add fqdn_rand string generators --- README.markdown | 17 ++++ .../parser/functions/fqdn_rand_string.rb | 34 +++++++ spec/acceptance/fqdn_rand_base64_spec.rb | 60 ++++++++++++ spec/acceptance/fqdn_rand_string_spec.rb | 60 ++++++++++++ spec/functions/fqdn_rand_string_spec.rb | 91 +++++++++++++++++++ 5 files changed, 262 insertions(+) create mode 100644 lib/puppet/parser/functions/fqdn_rand_string.rb create mode 100644 spec/acceptance/fqdn_rand_base64_spec.rb create mode 100644 spec/acceptance/fqdn_rand_string_spec.rb create mode 100644 spec/functions/fqdn_rand_string_spec.rb diff --git a/README.markdown b/README.markdown index 293a81aca..724a48e36 100644 --- a/README.markdown +++ b/README.markdown @@ -244,6 +244,23 @@ This function flattens any deeply nested arrays and returns a single flat array Returns the largest integer less than or equal to the argument. Takes a single numeric value as an argument. *Type*: rvalue +#### `fqdn_rand_string` + +Generates a random alphanumeric string using an optionally-specified character set (default is alphanumeric), combining the `$fqdn` fact and an optional seed for repeatable randomness. + +*Usage:* +``` +fqdn_rand_string(LENGTH, [CHARSET], [SEED]) +``` +*Examples:* +``` +fqdn_rand_string(10) +fqdn_rand_string(10, 'ABCDEF!@#$%^') +fqdn_rand_string(10, '', 'custom seed') +``` + +*Type*: rvalue + #### `fqdn_rotate` Rotates an array a random number of times based on a node's fqdn. *Type*: rvalue diff --git a/lib/puppet/parser/functions/fqdn_rand_string.rb b/lib/puppet/parser/functions/fqdn_rand_string.rb new file mode 100644 index 000000000..785c9fd51 --- /dev/null +++ b/lib/puppet/parser/functions/fqdn_rand_string.rb @@ -0,0 +1,34 @@ +Puppet::Parser::Functions::newfunction( + :fqdn_rand_string, + :arity => -2, + :type => :rvalue, + :doc => "Usage: `fqdn_rand_string(LENGTH, [CHARSET], [SEED])`. LENGTH is + required and must be a positive integer. CHARSET is optional and may be + `undef` or a string. SEED is optional and may be any number or string. + + Generates a random string LENGTH characters long using the character set + provided by CHARSET, combining the `$fqdn` fact and the value of SEED for + repeatable randomness. (That is, each node will get a different random + string from this function, but a given node's result will be the same every + time unless its hostname changes.) Adding a SEED can be useful if you need + more than one unrelated string. CHARSET will default to alphanumeric if + `undef` or an empty string.") do |args| + raise(ArgumentError, "fqdn_rand_string(): wrong number of arguments (0 for 1)") if args.size == 0 + Puppet::Parser::Functions.function('is_integer') + raise(ArgumentError, "fqdn_rand_base64(): first argument must be a positive integer") unless function_is_integer([args[0]]) and args[0].to_i > 0 + raise(ArgumentError, "fqdn_rand_base64(): second argument must be undef or a string") unless args[1].nil? or args[1].is_a? String + + Puppet::Parser::Functions.function('fqdn_rand') + + length = args.shift.to_i + charset = args.shift.to_s.chars.to_a + + charset = (0..9).map { |i| i.to_s } + ('A'..'Z').to_a + ('a'..'z').to_a if charset.empty? + + rand_string = '' + for current in 1..length + rand_string << charset[function_fqdn_rand([charset.size, (args + [current.to_s]).join(':')]).to_i] + end + + rand_string +end diff --git a/spec/acceptance/fqdn_rand_base64_spec.rb b/spec/acceptance/fqdn_rand_base64_spec.rb new file mode 100644 index 000000000..7c5894271 --- /dev/null +++ b/spec/acceptance/fqdn_rand_base64_spec.rb @@ -0,0 +1,60 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper_acceptance' + +describe 'fqdn_rand_base64 function', :unless => unsupported_platforms.include?(fact('operatingsystem')) do + describe 'success' do + let(:facts_d) do + if fact('is_pe', '--puppet') == "true" + if fact('osfamily') =~ /windows/i + if fact('kernelmajversion').to_f < 6.0 + 'c:/documents and settings/all users/application data/puppetlabs/facter/facts.d' + else + 'c:/programdata/puppetlabs/facter/facts.d' + end + else + '/etc/puppetlabs/facter/facts.d' + end + else + '/etc/facter/facts.d' + end + end + after :each do + shell("if [ -f '#{facts_d}/fqdn.txt' ] ; then rm '#{facts_d}/fqdn.txt' ; fi") + end + before :each do + #no need to create on windows, pe creates by default + if fact('osfamily') !~ /windows/i + shell("mkdir -p '#{facts_d}'") + end + end + it 'generates random base64 strings' do + shell("echo fqdn=fakehost.localdomain > '#{facts_d}/fqdn.txt'") + pp = <<-eos + $l = 10 + $o = fqdn_rand_base64($l) + notice(inline_template('fqdn_rand_base64 is <%= @o.inspect %>')) + eos + + apply_manifest(pp, :catch_failures => true) do |r| + expect(r.stdout).to match(/fqdn_rand_base64 is "8ySYp0dq0B"/) + end + end + it 'generates random base64 strings with custom seeds' do + shell("echo fqdn=fakehost.localdomain > '#{facts_d}/fqdn.txt'") + pp = <<-eos + $l = 10 + $s = 'seed' + $o = fqdn_rand_base64($l, $s) + notice(inline_template('fqdn_rand_base64 is <%= @o.inspect %>')) + eos + + apply_manifest(pp, :catch_failures => true) do |r| + expect(r.stdout).to match(/fqdn_rand_base64 is "6J2c4oMRUJ"/) + end + end + end + describe 'failure' do + it 'handles improper argument counts' + it 'handles non-numbers for length argument' + end +end diff --git a/spec/acceptance/fqdn_rand_string_spec.rb b/spec/acceptance/fqdn_rand_string_spec.rb new file mode 100644 index 000000000..0e79723ac --- /dev/null +++ b/spec/acceptance/fqdn_rand_string_spec.rb @@ -0,0 +1,60 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper_acceptance' + +describe 'fqdn_rand_string function', :unless => unsupported_platforms.include?(fact('operatingsystem')) do + describe 'success' do + let(:facts_d) do + if fact('is_pe', '--puppet') == "true" + if fact('osfamily') =~ /windows/i + if fact('kernelmajversion').to_f < 6.0 + 'c:/documents and settings/all users/application data/puppetlabs/facter/facts.d' + else + 'c:/programdata/puppetlabs/facter/facts.d' + end + else + '/etc/puppetlabs/facter/facts.d' + end + else + '/etc/facter/facts.d' + end + end + after :each do + shell("if [ -f '#{facts_d}/fqdn.txt' ] ; then rm '#{facts_d}/fqdn.txt' ; fi") + end + before :each do + #no need to create on windows, pe creates by default + if fact('osfamily') !~ /windows/i + shell("mkdir -p '#{facts_d}'") + end + end + it 'generates random alphanumeric strings' do + shell("echo fqdn=fakehost.localdomain > '#{facts_d}/fqdn.txt'") + pp = <<-eos + $l = 10 + $o = fqdn_rand_string($l) + notice(inline_template('fqdn_rand_string is <%= @o.inspect %>')) + eos + + apply_manifest(pp, :catch_failures => true) do |r| + expect(r.stdout).to match(/fqdn_rand_string is "7oDp0KOr1b"/) + end + end + it 'generates random alphanumeric strings with custom seeds' do + shell("echo fqdn=fakehost.localdomain > '#{facts_d}/fqdn.txt'") + pp = <<-eos + $l = 10 + $s = 'seed' + $o = fqdn_rand_string($l, $s) + notice(inline_template('fqdn_rand_string is <%= @o.inspect %>')) + eos + + apply_manifest(pp, :catch_failures => true) do |r| + expect(r.stdout).to match(/fqdn_rand_string is "3HS4mbuI3E"/) + end + end + end + describe 'failure' do + it 'handles improper argument counts' + it 'handles non-numbers for length argument' + end +end diff --git a/spec/functions/fqdn_rand_string_spec.rb b/spec/functions/fqdn_rand_string_spec.rb new file mode 100644 index 000000000..949d930c7 --- /dev/null +++ b/spec/functions/fqdn_rand_string_spec.rb @@ -0,0 +1,91 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the fqdn_rand_string function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + expect(Puppet::Parser::Functions.function("fqdn_rand_string")).to eq("function_fqdn_rand_string") + end + + it "should raise an ArgumentError if there is less than 1 argument" do + expect { fqdn_rand_string() }.to( raise_error(ArgumentError, /wrong number of arguments/)) + end + + it "should raise an ArgumentError if argument 1 isn't a positive integer" do + expect { fqdn_rand_string(0) }.to( raise_error(ArgumentError, /first argument must be a positive integer/)) + expect { fqdn_rand_string(-1) }.to( raise_error(ArgumentError, /first argument must be a positive integer/)) + expect { fqdn_rand_string(0.5) }.to( raise_error(ArgumentError, /first argument must be a positive integer/)) + end + + it "provides a valid alphanumeric string when no character set is provided" do + length = 100 + string = %r{\A[a-zA-Z0-9]{#{length}}\z} + expect(fqdn_rand_string(length).match(string)).not_to eq(nil) + end + + it "provides a valid alphanumeric string when an undef character set is provided" do + length = 100 + string = %r{\A[a-zA-Z0-9]{#{length}}\z} + expect(fqdn_rand_string(length, :charset => nil).match(string)).not_to eq(nil) + end + + it "provides a valid alphanumeric string when an empty character set is provided" do + length = 100 + string = %r{\A[a-zA-Z0-9]{#{length}}\z} + expect(fqdn_rand_string(length, :charset => '').match(string)).not_to eq(nil) + end + + it "uses a provided character set" do + length = 100 + charset = '!@#$%^&*()-_=+' + string = %r{\A[#{charset}]{#{length}}\z} + expect(fqdn_rand_string(length, :charset => charset).match(string)).not_to eq(nil) + end + + it "provides a random string exactly as long as the given length" do + expect(fqdn_rand_string(10).size).to eql(10) + end + + it "provides the same 'random' value on subsequent calls for the same host" do + expect(fqdn_rand_string(10)).to eql(fqdn_rand_string(10)) + end + + it "considers the same host and same extra arguments to have the same random sequence" do + first_random = fqdn_rand_string(10, :extra_identifier => [1, "same", "host"]) + second_random = fqdn_rand_string(10, :extra_identifier => [1, "same", "host"]) + + expect(first_random).to eql(second_random) + end + + it "allows extra arguments to control the random value on a single host" do + first_random = fqdn_rand_string(10, :extra_identifier => [1, "different", "host"]) + second_different_random = fqdn_rand_string(10, :extra_identifier => [2, "different", "host"]) + + expect(first_random).not_to eql(second_different_random) + end + + it "should return different strings for different hosts" do + val1 = fqdn_rand_string(10, :host => "first.host.com") + val2 = fqdn_rand_string(10, :host => "second.host.com") + + expect(val1).not_to eql(val2) + end + + def fqdn_rand_string(max, args = {}) + host = args[:host] || '127.0.0.1' + charset = args[:charset] + extra = args[:extra_identifier] || [] + + scope = PuppetlabsSpec::PuppetInternals.scope + scope.stubs(:[]).with("::fqdn").returns(host) + scope.stubs(:lookupvar).with("::fqdn").returns(host) + + function_args = [max] + if args.has_key?(:charset) or !extra.empty? + function_args << charset + end + function_args += extra + scope.function_fqdn_rand_string(function_args) + end +end