diff --git a/examples/mysql_user.pp b/examples/mysql_user.pp index c47186866..6c312ce40 100644 --- a/examples/mysql_user.pp +++ b/examples/mysql_user.pp @@ -6,18 +6,18 @@ mysql_user{ 'redmine@localhost': ensure => present, - password_hash => mysql_password('redmine'), + password_hash => mysql::password('redmine'), require => Class['mysql::server'], } mysql_user{ 'dan@localhost': ensure => present, - password_hash => mysql_password('blah') + password_hash => mysql::password('blah') } mysql_user{ 'dan@%': ensure => present, - password_hash => mysql_password('blah'), + password_hash => mysql::password('blah'), } mysql_user{ 'socketplugin@%': @@ -27,6 +27,6 @@ mysql_user{ 'socketplugin@%': ensure => present, - password_hash => mysql_password('blah'), + password_hash => mysql::password('blah'), plugin => 'mysql_native_password', } diff --git a/lib/puppet/functions/mysql/deepmerge.rb b/lib/puppet/functions/mysql/deepmerge.rb new file mode 100644 index 000000000..3cda71d18 --- /dev/null +++ b/lib/puppet/functions/mysql/deepmerge.rb @@ -0,0 +1,66 @@ +# @summary Recursively merges two or more hashes together and returns the resulting hash. +# +# @example +# $hash1 = {'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } } +# $hash2 = {'two' => 'dos', 'three' => { 'five' => 5 } } +# $merged_hash = mysql_deepmerge($hash1, $hash2) +# # The resulting hash is equivalent to: +# # $merged_hash = { 'one' => 1, 'two' => 'dos', 'three' => { 'four' => 4, 'five' => 5 } } +# +# - When there is a duplicate key that is a hash, they are recursively merged. +# - When there is a duplicate key that is not a hash, the key in the rightmost hash will "win." +# - When there are conficting uses of dashes and underscores in two keys (which mysql would otherwise equate), the rightmost style will win. +# +Puppet::Functions.create_function(:'mysql::deepmerge') do + def deepmerge(*args) + if args.length < 2 + raise Puppet::ParseError, _('mysql_deepmerge(): wrong number of arguments (%{args_length}; must be at least 2)') % { args_length: args.length } + end + + result = {} + args.each do |arg| + next if arg.is_a?(String) && arg.empty? # empty string is synonym for puppet's undef + # If the argument was not a hash, skip it. + unless arg.is_a?(Hash) + raise Puppet::ParseError, _('mysql_deepmerge: unexpected argument type %{arg_class}, only expects hash arguments.') % { args_class: args.class } + end + + # We need to make a copy of the hash since it is frozen by puppet + current = deep_copy(arg) + + # Now we have to traverse our hash assigning our non-hash values + # to the matching keys in our result while following our hash values + # and repeating the process. + overlay(result, current) + end + result + end + + def normalized?(hash, key) + return true if hash.key?(key) + return false unless key =~ %r{-|_} + other_key = key.include?('-') ? key.tr('-', '_') : key.tr('_', '-') + return false unless hash.key?(other_key) + hash[key] = hash.delete(other_key) + true + end + + def overlay(hash1, hash2) + hash2.each do |key, value| + if normalized?(hash1, key) && value.is_a?(Hash) && hash1[key].is_a?(Hash) + overlay(hash1[key], value) + else + hash1[key] = value + end + end + end + + def deep_copy(inputhash) + return inputhash unless inputhash.is_a? Hash + hash = {} + inputhash.each do |k, v| + hash.store(k, deep_copy(v)) + end + hash + end +end diff --git a/lib/puppet/functions/mysql/dirname.rb b/lib/puppet/functions/mysql/dirname.rb new file mode 100644 index 000000000..4074a4241 --- /dev/null +++ b/lib/puppet/functions/mysql/dirname.rb @@ -0,0 +1,20 @@ +# @summary +# Returns the dirname of a path +# +Puppet::Functions.create_function(:'mysql::dirname') do + # @param path + # Path to find the dirname for. + # + # @return + # Directory name of path. + # + dispatch :dirname do + required_param 'Variant[String, Undef]', :path + return_type 'String' + end + + def dirname(path) + return '' if path.nil? + File.dirname(path) + end +end diff --git a/lib/puppet/functions/mysql/password.rb b/lib/puppet/functions/mysql/password.rb new file mode 100644 index 000000000..24d1cc5b1 --- /dev/null +++ b/lib/puppet/functions/mysql/password.rb @@ -0,0 +1,21 @@ +require 'digest/sha1' +# @summary +# Hash a string as mysql's "PASSWORD()" function would do it +# +Puppet::Functions.create_function(:'mysql::password') do + # @param password + # Plain text password. + # + # @return the mysql password hash from the clear text password. + # + dispatch :password do + required_param 'String', :password + return_type 'String' + end + + def password(password) + return '' if password.empty? + return password if password =~ %r{\*[A-F0-9]{40}$} + '*' + Digest::SHA1.hexdigest(Digest::SHA1.digest(password)).upcase + end +end diff --git a/lib/puppet/functions/mysql/strip_hash.rb b/lib/puppet/functions/mysql/strip_hash.rb new file mode 100644 index 000000000..90a74e686 --- /dev/null +++ b/lib/puppet/functions/mysql/strip_hash.rb @@ -0,0 +1,19 @@ +# @summary +# When given a hash this function strips out all blank entries. +# +Puppet::Functions.create_function(:'mysql::strip_hash') do + # @param hash + # Hash to be stripped + # + dispatch :strip_hash do + required_param 'Hash', :hash + return_type 'Hash' + end + + def strip_hash(hash) + # Filter out all the top level blanks. + hash.reject { |_k, v| v == '' }.each do |_k, v| + v.reject! { |_ki, vi| vi == '' } if v.is_a?(Hash) + end + end +end diff --git a/lib/puppet/parser/functions/mysql_deepmerge.rb b/lib/puppet/parser/functions/mysql_deepmerge.rb index 97d10229b..766028e89 100644 --- a/lib/puppet/parser/functions/mysql_deepmerge.rb +++ b/lib/puppet/parser/functions/mysql_deepmerge.rb @@ -1,21 +1,20 @@ -# Recursively merges two or more hashes together and returns the resulting hash. module Puppet::Parser::Functions newfunction(:mysql_deepmerge, type: :rvalue, doc: <<-'ENDHEREDOC') do |args| - Recursively merges two or more hashes together and returns the resulting hash. - - For example: + @summary Recursively merges two or more hashes together and returns the resulting hash. + @example $hash1 = {'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } } $hash2 = {'two' => 'dos', 'three' => { 'five' => 5 } } $merged_hash = mysql_deepmerge($hash1, $hash2) # The resulting hash is equivalent to: # $merged_hash = { 'one' => 1, 'two' => 'dos', 'three' => { 'four' => 4, 'five' => 5 } } - When there is a duplicate key that is a hash, they are recursively merged. - When there is a duplicate key that is not a hash, the key in the rightmost hash will "win." - When there are conficting uses of dashes and underscores in two keys (which mysql would otherwise equate), + - When there is a duplicate key that is a hash, they are recursively merged. + - When there is a duplicate key that is not a hash, the key in the rightmost hash will "win." + - When there are conficting uses of dashes and underscores in two keys (which mysql would otherwise equate), the rightmost style will win. + @return [Hash] ENDHEREDOC if args.length < 2 diff --git a/lib/puppet/parser/functions/mysql_dirname.rb b/lib/puppet/parser/functions/mysql_dirname.rb index e1a21202d..40a8ad64f 100644 --- a/lib/puppet/parser/functions/mysql_dirname.rb +++ b/lib/puppet/parser/functions/mysql_dirname.rb @@ -1,7 +1,13 @@ -# Returns the dirname of a path. module Puppet::Parser::Functions newfunction(:mysql_dirname, type: :rvalue, doc: <<-EOS - Returns the dirname of a path. + @summary + Returns the dirname of a path + + @param [String] path + Path to find the dirname for. + + @return [String] + Directory name of path. EOS ) do |arguments| diff --git a/lib/puppet/parser/functions/mysql_password.rb b/lib/puppet/parser/functions/mysql_password.rb index 4169bf433..53ba580b2 100644 --- a/lib/puppet/parser/functions/mysql_password.rb +++ b/lib/puppet/parser/functions/mysql_password.rb @@ -1,9 +1,12 @@ require 'digest/sha1' -# Returns the mysql password hash from the clear text password. -# Hash a string as mysql's "PASSWORD()" function would do it module Puppet::Parser::Functions newfunction(:mysql_password, type: :rvalue, doc: <<-EOS - Returns the mysql password hash from the clear text password. + @summary + Hash a string as mysql's "PASSWORD()" function would do it + + @param [String] password Plain text password. + + @return [String] the mysql password hash from the clear text password. EOS ) do |args| diff --git a/manifests/backup/mysqlbackup.pp b/manifests/backup/mysqlbackup.pp index 08a5cc1d7..460ba6b8b 100644 --- a/manifests/backup/mysqlbackup.pp +++ b/manifests/backup/mysqlbackup.pp @@ -25,7 +25,7 @@ mysql_user { "${backupuser}@localhost": ensure => $ensure, - password_hash => mysql_password($backuppassword), + password_hash => mysql::password($backuppassword), require => Class['mysql::server::root_password'], } @@ -88,7 +88,7 @@ 'password' => $backuppassword, } } - $options = mysql_deepmerge($default_options, $mysql::server::override_options) + $options = mysql::deepmerge($default_options, $mysql::server::override_options) file { 'mysqlbackup-config-file': path => '/etc/mysql/conf.d/meb.cnf', diff --git a/manifests/backup/mysqldump.pp b/manifests/backup/mysqldump.pp index e8988dfc4..2a64a6429 100644 --- a/manifests/backup/mysqldump.pp +++ b/manifests/backup/mysqldump.pp @@ -30,7 +30,7 @@ mysql_user { "${backupuser}@localhost": ensure => $ensure, - password_hash => mysql_password($backuppassword), + password_hash => mysql::password($backuppassword), require => Class['mysql::server::root_password'], } diff --git a/manifests/backup/xtrabackup.pp b/manifests/backup/xtrabackup.pp index e0365588a..e1f451129 100644 --- a/manifests/backup/xtrabackup.pp +++ b/manifests/backup/xtrabackup.pp @@ -33,7 +33,7 @@ if $backupuser and $backuppassword { mysql_user { "${backupuser}@localhost": ensure => $ensure, - password_hash => mysql_password($backuppassword), + password_hash => mysql::password($backuppassword), require => Class['mysql::server::root_password'], } diff --git a/manifests/db.pp b/manifests/db.pp index 6b6c39b9b..385235b40 100644 --- a/manifests/db.pp +++ b/manifests/db.pp @@ -31,7 +31,7 @@ $user_resource = { ensure => $ensure, - password_hash => mysql_password($password), + password_hash => mysql::password($password), } ensure_resource('mysql_user', "${user}@${host}", $user_resource) diff --git a/manifests/server.pp b/manifests/server.pp index 11e074880..b8e47e6c2 100644 --- a/manifests/server.pp +++ b/manifests/server.pp @@ -49,7 +49,7 @@ } # Create a merged together set of options. Rightmost hashes win over left. - $options = mysql_deepmerge($mysql::params::default_options, $override_options) + $options = mysql::deepmerge($mysql::params::default_options, $override_options) Class['mysql::server::root_password'] -> Mysql::Db <| |> diff --git a/manifests/server/binarylog.pp b/manifests/server/binarylog.pp index 459915d0c..7e4279d7e 100644 --- a/manifests/server/binarylog.pp +++ b/manifests/server/binarylog.pp @@ -7,7 +7,7 @@ $logbin = pick($options['mysqld']['log-bin'], $options['mysqld']['log_bin'], false) if $logbin { - $logbindir = mysql_dirname($logbin) + $logbindir = mysql::dirname($logbin) #Stop puppet from managing directory if just a filename/prefix is specified if $logbindir != '.' { diff --git a/manifests/server/config.pp b/manifests/server/config.pp index de97bea5e..44aad19b9 100644 --- a/manifests/server/config.pp +++ b/manifests/server/config.pp @@ -20,7 +20,7 @@ # on some systems this is /etc/my.cnf.d, while Debian has /etc/mysql/conf.d and FreeBSD something in /usr/local. For the latter systems, # managing this basedir is also required, to have it available before the package is installed. - $includeparentdir = mysql_dirname($includedir) + $includeparentdir = mysql::dirname($includedir) if $includeparentdir != '/' and $includeparentdir != '/etc' { file { $includeparentdir: ensure => directory, @@ -39,9 +39,9 @@ # on mariadb systems, $includedir is not defined, but /etc/my.cnf.d has # to be managed to place the server.cnf there - $configparentdir = mysql_dirname($mysql::server::config_file) + $configparentdir = mysql::dirname($mysql::server::config_file) if $configparentdir != '/' and $configparentdir != '/etc' and $configparentdir - != $includedir and $configparentdir != mysql_dirname($includedir) { + != $includedir and $configparentdir != mysql::dirname($includedir) { file { $configparentdir: ensure => directory, mode => '0755', diff --git a/manifests/server/monitor.pp b/manifests/server/monitor.pp index 6b1860983..3c6d694cc 100644 --- a/manifests/server/monitor.pp +++ b/manifests/server/monitor.pp @@ -9,7 +9,7 @@ mysql_user { "${mysql_monitor_username}@${mysql_monitor_hostname}": ensure => present, - password_hash => mysql_password($mysql_monitor_password), + password_hash => mysql::password($mysql_monitor_password), require => Class['mysql::server::service'], } diff --git a/manifests/server/root_password.pp b/manifests/server/root_password.pp index 9ebc10398..d9c38d45f 100644 --- a/manifests/server/root_password.pp +++ b/manifests/server/root_password.pp @@ -22,7 +22,7 @@ if $mysql::server::create_root_user == true and $mysql::server::root_password != 'UNSET' { mysql_user { 'root@localhost': ensure => present, - password_hash => mysql_password($mysql::server::root_password), + password_hash => mysql::password($mysql::server::root_password), require => Exec['remove install pass'] } } diff --git a/spec/functions/mysql_deepmerge_spec.rb b/spec/functions/mysql_deepmerge_spec.rb new file mode 100644 index 000000000..e44db6cd2 --- /dev/null +++ b/spec/functions/mysql_deepmerge_spec.rb @@ -0,0 +1,94 @@ +#! /usr/bin/env ruby -S rspec # rubocop:disable Lint/ScriptPermission + +require 'spec_helper' + +describe 'mysql::deepmerge' do + it 'exists' do + is_expected.not_to eq(nil) + end + + it 'throws error with no arguments' do + is_expected.to run.with_params.and_raise_error(Puppet::ParseError) + end + + it 'throws error with only one argument' do + is_expected.to run.with_params('one' => 1).and_raise_error(Puppet::ParseError) + end + + it 'accepts empty strings as puppet undef' do + is_expected.to run.with_params({}, '') + end + + # rubocop:disable RSpec/NamedSubject + index_values = %w[one two three] + expected_values_one = %w[1 2 2] + it 'is able to mysql_deepmerge two hashes' do + new_hash = subject.execute({ 'one' => '1', 'two' => '1' }, 'two' => '2', 'three' => '2') + index_values.each_with_index do |index, expected| + expect(new_hash[index]).to eq(expected_values_one[expected]) + end + end + + it 'mysql_deepmerges multiple hashes' do + hash = subject.execute({ 'one' => 1 }, { 'one' => '2' }, 'one' => '3') + expect(hash['one']).to eq('3') + end + + it 'accepts empty hashes' do + is_expected.to run.with_params({}, {}, {}).and_return({}) + end + + expected_values_two = [1, 2, 'four' => 4] + it 'mysql_deepmerges subhashes' do + hash = subject.execute({ 'one' => 1 }, 'two' => 2, 'three' => { 'four' => 4 }) + index_values.each_with_index do |index, expected| + expect(hash[index]).to eq(expected_values_two[expected]) + end + end + + it 'appends to subhashes' do + hash = subject.execute({ 'one' => { 'two' => 2 } }, 'one' => { 'three' => 3 }) + expect(hash['one']).to eq('two' => 2, 'three' => 3) + end + + expected_values_three = [1, 'dos', { 'four' => 4, 'five' => 5 }] + it 'appends to subhashes 2' do + hash = subject.execute({ 'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } }, 'two' => 'dos', 'three' => { 'five' => 5 }) + index_values.each_with_index do |index, expected| + expect(hash[index]).to eq(expected_values_three[expected]) + end + end + + index_values_two = %w[key1 key2] + expected_values_four = [{ 'a' => 1, 'b' => 99 }, 'c' => 3] + it 'appends to subhashes 3' do + hash = subject.execute({ 'key1' => { 'a' => 1, 'b' => 2 }, 'key2' => { 'c' => 3 } }, 'key1' => { 'b' => 99 }) + index_values_two.each_with_index do |index, expected| + expect(hash[index]).to eq(expected_values_four[expected]) + end + end + + it 'equates keys mod dash and underscore #value' do + hash = subject.execute({ 'a-b-c' => 1 }, 'a_b_c' => 10) + expect(hash['a_b_c']).to eq(10) + end + it 'equates keys mod dash and underscore #not' do + hash = subject.execute({ 'a-b-c' => 1 }, 'a_b_c' => 10) + expect(hash).not_to have_key('a-b-c') + end + + index_values_three = ['a_b_c', 'b-c-d'] + expected_values_five = [10, { 'e-f-g' => 3, 'c_d_e' => 12 }] + index_values_error = ['a-b-c', 'b_c_d'] + index_values_three.each_with_index do |index, expected| + it 'keeps style of the last when keys are equal mod dash and underscore #value' do + hash = subject.execute({ 'a-b-c' => 1, 'b_c_d' => { 'c-d-e' => 2, 'e-f-g' => 3 } }, 'a_b_c' => 10, 'b-c-d' => { 'c_d_e' => 12 }) + expect(hash[index]).to eq(expected_values_five[expected]) + end + it 'keeps style of the last when keys are equal mod dash and underscore #not' do + hash = subject.execute({ 'a-b-c' => 1, 'b_c_d' => { 'c-d-e' => 2, 'e-f-g' => 3 } }, 'a_b_c' => 10, 'b-c-d' => { 'c_d_e' => 12 }) + expect(hash).not_to have_key(index_values_error[expected]) + end + end + # rubocop:enable RSpec/NamedSubject +end diff --git a/spec/functions/mysql_dirname_spec.rb b/spec/functions/mysql_dirname_spec.rb new file mode 100644 index 000000000..3e407ec0c --- /dev/null +++ b/spec/functions/mysql_dirname_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe 'mysql::dirname' do + it 'exists' do + is_expected.not_to eq(nil) + end + + it 'raises a ArgumentError if there is less than 1 arguments' do + is_expected.to run.with_params.and_raise_error(ArgumentError) + end + + it 'raises a ArgumentError if there is more than 1 arguments' do + is_expected.to run.with_params('foo', 'bar').and_raise_error(ArgumentError) + end + + it 'returns path of file' do + is_expected.to run.with_params('spec/functions/mysql_dirname_spec.rb').and_return('spec/functions') + end +end diff --git a/spec/functions/mysql_password_spec.rb b/spec/functions/mysql_password_spec.rb new file mode 100644 index 000000000..8ca4fb859 --- /dev/null +++ b/spec/functions/mysql_password_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'mysql::password' do + it 'exists' do + is_expected.not_to eq(nil) + end + + it 'raises a ArgumentError if there is less than 1 arguments' do + is_expected.to run.with_params.and_raise_error(ArgumentError) + end + + it 'raises a ArgumentError if there is more than 1 arguments' do + is_expected.to run.with_params('foo', 'bar').and_raise_error(ArgumentError) + end + + it 'converts password into a hash' do + is_expected.to run.with_params('password').and_return('*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19') + end + + it 'converts an empty password into a empty string' do + is_expected.to run.with_params('').and_return('') + end + + it 'does not convert a password that is already a hash' do + is_expected.to run.with_params('*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19').and_return('*2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19') + end +end diff --git a/spec/functions/mysql_strip_hash_spec.rb b/spec/functions/mysql_strip_hash_spec.rb new file mode 100644 index 000000000..be6704653 --- /dev/null +++ b/spec/functions/mysql_strip_hash_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe 'mysql::strip_hash' do + it 'exists' do + is_expected.not_to eq(nil) + end + + it 'raises a ArgumentError if there is less than 1 arguments' do + is_expected.to run.with_params.and_raise_error(ArgumentError) + end + + it 'raises a ArgumentError if there is more than 1 arguments' do + is_expected.to run.with_params({ 'foo' => 1 }, 'bar' => 2).and_raise_error(ArgumentError) + end + + it 'raises a ArgumentError if argument is not a hash' do + is_expected.to run.with_params('foo').and_raise_error(ArgumentError) + end + + it 'passes a hash without blanks through' do + is_expected.to run.with_params('one' => 1, 'two' => 2, 'three' => 3).and_return('one' => 1, 'two' => 2, 'three' => 3) + end + + it 'removes blank hash elements' do + is_expected.to run.with_params('one' => 1, 'two' => '', 'three' => nil, 'four' => 4).and_return('one' => 1, 'three' => nil, 'four' => 4) + end +end