From 108f1e3a69f4ff98a748d1b6433d62ff45d1a714 Mon Sep 17 00:00:00 2001 From: dn1s Date: Thu, 30 Aug 2018 16:01:34 +0200 Subject: [PATCH] Add auto cluster configuration support - added clustername fact and spec tests for it - added rabbitmq_cluster type and provider with tests - added related doc - based from PR#736, credits to @vchepkov --- REFERENCE.md | 51 ++++++++++++ data/common.yaml | 1 + .../cluster/join_cluster_and_change_name.pp | 5 ++ lib/facter/rabbitmq_clustername.rb | 15 ++++ .../provider/rabbitmq_cluster/rabbitmqctl.rb | 40 ++++++++++ lib/puppet/type/rabbitmq_cluster.rb | 50 ++++++++++++ manifests/init.pp | 17 +++- spec/acceptance/clustering_spec.rb | 16 ++++ .../util/fact_rabbitmq_clustername_spec.rb | 77 +++++++++++++++++++ .../rabbitmq_cluster/rabbitmqctl_spec.rb | 39 ++++++++++ .../unit/puppet/type/rabbitmq_cluster_spec.rb | 28 +++++++ 11 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 examples/cluster/join_cluster_and_change_name.pp create mode 100644 lib/facter/rabbitmq_clustername.rb create mode 100644 lib/puppet/provider/rabbitmq_cluster/rabbitmqctl.rb create mode 100644 lib/puppet/type/rabbitmq_cluster.rb create mode 100644 spec/unit/facter/util/fact_rabbitmq_clustername_spec.rb create mode 100644 spec/unit/puppet/provider/rabbitmq_cluster/rabbitmqctl_spec.rb create mode 100644 spec/unit/puppet/type/rabbitmq_cluster_spec.rb diff --git a/REFERENCE.md b/REFERENCE.md index 587b67487..8267abbc4 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -22,6 +22,7 @@ _Private Classes_ **Resource types** * [`rabbitmq_binding`](#rabbitmq_binding): Native type for managing rabbitmq bindings rabbitmq_binding { 'binding 1': ensure => present, source => 'myexchange' +* [`rabbitmq_cluster`](#rabbitmq_cluster): Type to manage a rabbitmq cluster * [`rabbitmq_erlang_cookie`](#rabbitmq_erlang_cookie): Type to manage the rabbitmq erlang cookie securely This is essentially a private type used by the rabbitmq::config class to manage the erlan * [`rabbitmq_exchange`](#rabbitmq_exchange): Native type for managing rabbitmq exchanges * [`rabbitmq_parameter`](#rabbitmq_parameter): Type for managing rabbitmq parameters @@ -148,6 +149,21 @@ class { 'rabbitmq': } ``` +To create and join the cluster: +```puppet +class { 'rabbitmq': + config_cluster => true, + cluster_nodes => ['rabbit1', 'rabbit2'], + cluster => { + 'name' => 'test_cluster', + 'init_node' => 'hostname' + }, + cluster_node_type => 'ram', + erlang_cookie => 'A_SECRET_COOKIE_STRING', + wipe_db_on_cookie_change => true, +} +``` + #### Parameters The following parameters are available in the `rabbitmq` class. @@ -213,6 +229,17 @@ Value to set for `cluster_partition_handling` RabbitMQ configuration variable. Default value: 'ignore' +##### `cluster` + +Data type: `Hash` + +If both `name` and `init_node` keys are set then the rabbitmq node is added to +a cluster named after the corresponding key by joining `init_node`. +Note: `init_node` must be included in the [`cluster_nodes`](#cluster_nodes) +parameter. + +Default value: '{}' + ##### `collect_statistics_interval` Data type: `Optional[Integer]` @@ -1132,6 +1159,30 @@ The password to use to connect to rabbitmq Default value: guest +### rabbitmq_cluster + +Type to manage a rabbitmq cluster + +#### Properties + +The following properties are available in the `rabbitmq_cluster` type. + +#### `init_node` + +Data type: `String` + +The node to join to initiate the cluster. It is mandatory. + +Default value: unset + +#### `node_disc_type` + +Data type: `Enum['ram', 'disc']` + +Choose between disc and ram cluster nodes. + +Default value: disc + ### rabbitmq_erlang_cookie Type to manage the rabbitmq erlang cookie securely diff --git a/data/common.yaml b/data/common.yaml index ce9887fc3..f130e56a4 100644 --- a/data/common.yaml +++ b/data/common.yaml @@ -2,6 +2,7 @@ rabbitmq::admin_enable: true rabbitmq::management_enable: false rabbitmq::use_config_file_for_plugins: false +rabbitmq::cluster: {} rabbitmq::cluster_node_type: 'disc' rabbitmq::cluster_nodes: [] rabbitmq::config: 'rabbitmq/rabbitmq.config.erb' diff --git a/examples/cluster/join_cluster_and_change_name.pp b/examples/cluster/join_cluster_and_change_name.pp new file mode 100644 index 000000000..7b1dda49f --- /dev/null +++ b/examples/cluster/join_cluster_and_change_name.pp @@ -0,0 +1,5 @@ +# This sets the cluster name to `test_cluster` +# If run on another host than host1, this will join the host1's cluster +rabbitmq_cluster { 'test_cluster': + init_node => 'host1', +} diff --git a/lib/facter/rabbitmq_clustername.rb b/lib/facter/rabbitmq_clustername.rb new file mode 100644 index 000000000..7a178dfe4 --- /dev/null +++ b/lib/facter/rabbitmq_clustername.rb @@ -0,0 +1,15 @@ +Facter.add(:rabbitmq_clustername) do + setcode do + if Facter::Util::Resolution.which('rabbitmqctl') + ret = nil + cluster_status = Facter::Core::Execution.execute('rabbitmqctl -q cluster_status 2>&1') + [%r!{cluster_name,<<"(\S+)">>}!, %r!^Cluster name: (\S+)$!].each do |r| + if (data = r.match(cluster_status)) + ret = data[1] + break + end + end + end + ret + end +end diff --git a/lib/puppet/provider/rabbitmq_cluster/rabbitmqctl.rb b/lib/puppet/provider/rabbitmq_cluster/rabbitmqctl.rb new file mode 100644 index 000000000..f65326dae --- /dev/null +++ b/lib/puppet/provider/rabbitmq_cluster/rabbitmqctl.rb @@ -0,0 +1,40 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'rabbitmq_cli')) +Puppet::Type.type(:rabbitmq_cluster).provide( + :rabbitmqctl, + parent: Puppet::Provider::RabbitmqCli +) do + confine feature: :posix + + def exists? + cluster_name == @resource[:name].to_s + end + + def create + storage_type = @resource[:node_disc_type].to_s + + init_node = @resource[:init_node].to_s.gsub(%r{^.*@}, '') + + if [Facter.value(:hostname), Facter.value(:fqdn)].include? init_node + return rabbitmqctl('set_cluster_name', @resource[:name]) unless cluster_name == resource[:name].to_s + else + rabbitmqctl('stop_app') + rabbitmqctl('join_cluster', "rabbit@#{init_node}", "--#{storage_type}") + rabbitmqctl('start_app') + end + end + + def destroy + rabbitmqctl('stop_app') + rabbitmqctl('reset') + rabbitmqctl('start_app') + end + + def cluster_name + cluster_status = rabbitmqctl('-q', 'cluster_status') + [%r!{cluster_name,<<"(\S+)">>}!, %r!^Cluster name: (\S+)$!].each do |r| + if (data = r.match(cluster_status)) + return data[1] + end + end + end +end diff --git a/lib/puppet/type/rabbitmq_cluster.rb b/lib/puppet/type/rabbitmq_cluster.rb new file mode 100644 index 000000000..f48106567 --- /dev/null +++ b/lib/puppet/type/rabbitmq_cluster.rb @@ -0,0 +1,50 @@ +Puppet::Type.newtype(:rabbitmq_cluster) do + desc <<-DESC +Native type for managing rabbitmq cluster + +@example Configure a cluster, rabbit_cluster + rabbitmq_cluster { 'rabbit_cluster': + init_node => 'host1' + } + +@example Optional parameter tags will set further rabbitmq tags like monitoring, policymaker, etc. + To set the cluster name use cluster_name. + rabbitmq_cluster { 'rabbit_cluster': + init_node => 'host1', + node_disc_type => 'ram', + } +DESC + + ensurable do + defaultto(:present) + newvalue(:present) do + provider.create + end + newvalue(:absent) do + provider.destroy + end + end + + autorequire(:service) { 'rabbitmq-server' } + + newparam(:name, namevar: true) do + desc 'The cluster name' + end + + newparam(:init_node) do + desc 'Name of which cluster node to join.' + validate do |value| + resource.validate_init_node(value) + end + end + + newparam(:node_disc_type) do + desc 'Storage type of node, default disc.' + newvalues(%r{disc|ram}) + defaultto('disc') + end + + def validate_init_node(value) + raise ArgumentError, 'init_node must be defined' if value.empty? + end +end diff --git a/manifests/init.pp b/manifests/init.pp index 71097b9e9..12a71985e 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -72,6 +72,10 @@ # # @example Use RabbitMQ clustering facilities # class { 'rabbitmq': +# cluster => { +# 'name' => 'test_cluster', +# 'init_node' => 'hostname' +# }, # config_cluster => true, # cluster_nodes => ['rabbit1', 'rabbit2'], # cluster_node_type => 'ram', @@ -92,6 +96,7 @@ # An array specifying authorization/authentication backend to use. Single quotes should be placed around array entries, # ex. `['{foo, baz}', 'baz']` Defaults to [rabbit_auth_backend_internal], and if using LDAP defaults to [rabbit_auth_backend_internal, # rabbit_auth_backend_ldap]. +# @param cluster Join cluster and change name of cluster. # @param cluster_node_type # Choose between disc and ram nodes. # @param cluster_nodes @@ -313,6 +318,7 @@ Boolean $admin_enable = true, Boolean $management_enable = false, Boolean $use_config_file_for_plugins = false, + Hash $cluster = $rabbitmq::cluster, Enum['ram', 'disc'] $cluster_node_type = 'disc', Array $cluster_nodes = [], String $config = 'rabbitmq/rabbitmq.config.erb', @@ -525,6 +531,15 @@ Class['rabbitmq::install::rabbitmqadmin'] -> Rabbitmq_exchange<| |> } + if $config_cluster and $cluster['name'] and $cluster['init_node'] { + create_resources('rabbitmq_cluster', { + $cluster['name'] => { + 'init_node' => $cluster['init_node'], + 'node_disc_type' => $cluster_node_type, + } + }) + } + if ($service_restart) { Class['rabbitmq::config'] ~> Class['rabbitmq::service'] } @@ -535,5 +550,5 @@ -> Class['rabbitmq::management'] # Make sure the various providers have their requirements in place. - Class['rabbitmq::install'] -> Rabbitmq_plugin<| |> + Class['rabbitmq::install'] -> Rabbitmq_plugin<| |> -> Rabbitmq_cluster<| |> } diff --git a/spec/acceptance/clustering_spec.rb b/spec/acceptance/clustering_spec.rb index 204eef052..634992909 100644 --- a/spec/acceptance/clustering_spec.rb +++ b/spec/acceptance/clustering_spec.rb @@ -5,9 +5,11 @@ it 'runs successfully' do pp = <<-EOS class { 'rabbitmq': + cluster => { 'name' => 'rabbit_cluster', 'init_node' => $facts['fqdn'] }, config_cluster => true, cluster_nodes => ['rabbit1', 'rabbit2'], cluster_node_type => 'ram', + environment_variables => { 'RABBITMQ_USE_LONGNAME' => true }, erlang_cookie => 'TESTCOOKIE', wipe_db_on_cookie_change => false, } @@ -28,9 +30,11 @@ class { 'erlang': epel_enable => true} it 'runs successfully' do pp = <<-EOS class { 'rabbitmq': + cluster => { 'name' => 'rabbit_cluster', 'init_node' => $facts['fqdn'] }, config_cluster => true, cluster_nodes => ['rabbit1', 'rabbit2'], cluster_node_type => 'ram', + environment_variables => { 'RABBITMQ_USE_LONGNAME' => true }, erlang_cookie => 'TESTCOOKIE', wipe_db_on_cookie_change => true, } @@ -55,5 +59,17 @@ class { 'erlang': epel_enable => true} it { is_expected.to be_file } it { is_expected.to contain 'TESTCOOKIE' } end + + describe 'rabbitmq_cluster' do + context 'cluster_name => rabbit_cluster' do + it 'cluster has name' do + shell('rabbitmqctl -q cluster_status') do |r| + expect(r.stdout).to match(%r!({cluster_name,<<"rabbit_cluster">>}|^Cluster name: rabbit_cluster$)!) + expect(r.exit_code).to be_zero + end + # rubocop:enable RSpec/MultipleExpectations + end + end + end end end diff --git a/spec/unit/facter/util/fact_rabbitmq_clustername_spec.rb b/spec/unit/facter/util/fact_rabbitmq_clustername_spec.rb new file mode 100644 index 000000000..1f7488430 --- /dev/null +++ b/spec/unit/facter/util/fact_rabbitmq_clustername_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Facter::Util::Fact do + before do + Facter.clear + end + + describe 'rabbitmq_clusternam' do + context 'with value' do + it do + Facter::Util::Resolution.expects(:which).with('rabbitmqctl').returns(true) + Facter::Core::Execution.expects(:execute).with('rabbitmqctl -q cluster_status 2>&1').returns(' {cluster_name,<<"monty">>},') + expect(Facter.fact(:rabbitmq_clustername).value).to eq('monty') + end + end + + context 'with dashes in hostname' do + it do + Facter::Util::Resolution.expects(:which).with('rabbitmqctl').returns(true) + Facter::Core::Execution.expects(:execute).with('rabbitmqctl -q cluster_status 2>&1').returns('Cluster name: rabbit-1') + expect(Facter.fact(:rabbitmq_clustername).value).to eq('rabbit-1') + end + end + + context 'with dashes in clustername/hostname' do + it do + Facter::Util::Resolution.expects(:which).with('rabbitmqctl').returns(true) + Facter::Core::Execution.expects(:execute).with('rabbitmqctl -q cluster_status 2>&1').returns(' {cluster_name,<<"monty-python@rabbit-1">>},') + expect(Facter.fact(:rabbitmq_clustername).value).to eq('monty-python@rabbit-1') + end + end + + context 'with quotes around node name' do + it do + Facter::Util::Resolution.expects(:which).with('rabbitmqctl').returns(true) + Facter::Core::Execution.expects(:execute).with('rabbitmqctl -q cluster_status 2>&1').returns("monty\npython\nCluster name: 'monty@rabbit-1'\nend\nof\nfile") + expect(Facter.fact(:rabbitmq_clustername).value).to eq("'monty@rabbit-1'") + end + end + + context 'rabbitmq is not running' do + it do + error_string = <<-EOS +Status of node 'monty@rabbit-1' ... +Error: unable to connect to node 'monty@rabbit-1': nodedown + +DIAGNOSTICS +=========== + +attempted to contact: ['monty@rabbit-1'] + +monty@rabbit-1: + * connected to epmd (port 4369) on centos-7-x64 + * epmd reports: node 'rabbit' not running at all + no other nodes on centos-7-x64 + * suggestion: start the node + +current node details: +- node name: 'rabbitmq-cli-73@centos-7-x64' +- home dir: /var/lib/rabbitmq +- cookie hash: 6WdP0nl6d3HYqA5vTKMkIg== + + EOS + Facter::Util::Resolution.expects(:which).with('rabbitmqctl').returns(true) + Facter::Core::Execution.expects(:execute).with('rabbitmqctl -q cluster_status 2>&1').returns(error_string) + expect(Facter.fact(:rabbitmq_clustername).value).to be_nil + end + end + + context 'rabbitmqctl is not in path' do + it do + Facter::Util::Resolution.expects(:which).with('rabbitmqctl').returns(false) + expect(Facter.fact(:rabbitmq_clustername).value).to be_nil + end + end + end +end diff --git a/spec/unit/puppet/provider/rabbitmq_cluster/rabbitmqctl_spec.rb b/spec/unit/puppet/provider/rabbitmq_cluster/rabbitmqctl_spec.rb new file mode 100644 index 000000000..f59cda96b --- /dev/null +++ b/spec/unit/puppet/provider/rabbitmq_cluster/rabbitmqctl_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +provider_class = Puppet::Type.type(:rabbitmq_cluster).provider(:rabbitmqctl) +describe provider_class do + let(:resource) do + Puppet::Type::Rabbitmq_cluster.new( + name: 'test_cluster', + init_node: 'host1' + ) + end + let(:provider) { provider_class.new(resource) } + + describe '#exists?' do + it { + provider.expects(:rabbitmqctl).with('-q', 'cluster_status').returns( + 'Cluster name: test_cluster' + ) + expect(provider.exists?).to be true + } + end + + describe '#create on every other node' do + it 'joins a cluster or changes the cluster name' do + provider.expects(:rabbitmqctl).with('stop_app') + provider.expects(:rabbitmqctl).with('join_cluster', 'rabbit@host1', '--disc') + provider.expects(:rabbitmqctl).with('start_app') + provider.create + end + end + + describe '#destroy' do + it 'remove cluster setup' do + provider.expects(:rabbitmqctl).with('stop_app') + provider.expects(:rabbitmqctl).with('reset') + provider.expects(:rabbitmqctl).with('start_app') + provider.destroy + end + end +end diff --git a/spec/unit/puppet/type/rabbitmq_cluster_spec.rb b/spec/unit/puppet/type/rabbitmq_cluster_spec.rb new file mode 100644 index 000000000..2a0257037 --- /dev/null +++ b/spec/unit/puppet/type/rabbitmq_cluster_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' +describe Puppet::Type.type(:rabbitmq_cluster) do + let(:rabbitmq_cluster) do + Puppet::Type.type(:rabbitmq_cluster).new(name: 'test_cluster') + end + + it 'accepts a cluster name' do + rabbitmq_cluster[:name] = 'test_cluster' + expect(rabbitmq_cluster[:name]).to eq('test_cluster') + end + it 'requires a name' do + expect do + Puppet::Type.type(:rabbitmq_cluster).new({}) + end.to raise_error(Puppet::Error, 'Title or name must be provided') + end + it 'check if init_node set to host1' do + rabbitmq_cluster[:init_node] = 'host1' + expect(rabbitmq_cluster[:init_node]).to eq('host1') + end + it 'try to set node_disc_type to ram' do + rabbitmq_cluster[:node_disc_type] = 'ram' + expect(rabbitmq_cluster[:node_disc_type]).to eq('ram') + end + it 'node_disc_type not set should default to disc' do + rabbitmq_cluster[:name] = 'test_cluster' + expect(rabbitmq_cluster[:node_disc_type]).to eq('disc') + end +end