From ebc20c216d6c294a10b5ae015495fb404a878288 Mon Sep 17 00:00:00 2001 From: Michele Catalano Date: Fri, 21 Nov 2014 16:34:23 +0100 Subject: [PATCH 1/3] Add new resource rabbitmq_queue --- .../provider/rabbitmq_queue/rabbitmqadmin.rb | 105 ++++++++++++++++++ lib/puppet/type/rabbitmq_queue.rb | 68 ++++++++++++ .../rabbitmq_queue/rabbitmqadmin_spec.rb | 60 ++++++++++ spec/unit/puppet/type/rabbitmq_queue_spec.rb | 60 ++++++++++ 4 files changed, 293 insertions(+) create mode 100644 lib/puppet/provider/rabbitmq_queue/rabbitmqadmin.rb create mode 100644 lib/puppet/type/rabbitmq_queue.rb create mode 100644 spec/unit/puppet/provider/rabbitmq_queue/rabbitmqadmin_spec.rb create mode 100644 spec/unit/puppet/type/rabbitmq_queue_spec.rb diff --git a/lib/puppet/provider/rabbitmq_queue/rabbitmqadmin.rb b/lib/puppet/provider/rabbitmq_queue/rabbitmqadmin.rb new file mode 100644 index 000000000..b0ecb1f5a --- /dev/null +++ b/lib/puppet/provider/rabbitmq_queue/rabbitmqadmin.rb @@ -0,0 +1,105 @@ +require 'json' +require 'puppet' +Puppet::Type.type(:rabbitmq_queue).provide(:rabbitmqadmin) do + + if Puppet::PUPPETVERSION.to_f < 3 + commands :rabbitmqctl => 'rabbitmqctl' + commands :rabbitmqadmin => '/usr/local/bin/rabbitmqadmin' + else + has_command(:rabbitmqctl, 'rabbitmqctl') do + environment :HOME => "/tmp" + end + has_command(:rabbitmqadmin, '/usr/local/bin/rabbitmqadmin') do + environment :HOME => "/tmp" + end + end + defaultfor :feature => :posix + + def should_vhost + if @should_vhost + @should_vhost + else + @should_vhost = resource[:name].rpartition('@').last + end + end + + def self.all_vhosts + vhosts = [] + rabbitmqctl('list_vhosts', '-q').split(/\n/).collect do |vhost| + vhosts.push(vhost) + end + vhosts + end + + def self.all_queues(vhost) + rabbitmqctl('list_queues', '-q', '-p', vhost, 'name', 'durable', 'auto_delete', 'arguments').split(/\n/) + end + + def self.instances + resources = [] + all_vhosts.each do |vhost| + all_queues(vhost).collect do |line| + name, durable, auto_delete, arguments = line.split() + # Convert output of arguments from the rabbitmqctl command to a json string. + if !arguments.nil? + arguments = arguments.gsub(/^\[(.*)\]$/, "").gsub(/\{("(?:.|\\")*?"),/, '{\1:').gsub(/\},\{/, ",") + if arguments == "" + arguments = '{}' + end + else + arguments = '{}' + end + queue = { + :durable => durable, + :auto_delete => auto_delete, + :arguments => JSON.parse(arguments), + :ensure => :present, + :name => "%s@%s" % [name, vhost], + } + resources << new(queue) if queue[:name] + end + end + resources + end + + def self.prefetch(resources) + packages = instances + resources.keys.each do |name| + if provider = packages.find{ |pkg| pkg.name == name } + resources[name].provider = provider + end + end + end + + def exists? + @property_hash[:ensure] == :present + end + + def create + vhost_opt = should_vhost ? "--vhost=#{should_vhost}" : '' + name = resource[:name].rpartition('@').first + arguments = resource[:arguments] + if arguments.nil? + arguments = {} + end + rabbitmqadmin('declare', + 'queue', + vhost_opt, + "--user=#{resource[:user]}", + "--password=#{resource[:password]}", + "name=#{name}", + "durable=#{resource[:durable]}", + "auto_delete=#{resource[:auto_delete]}", + "arguments=#{arguments.to_json}" + ) + @property_hash[:ensure] = :present + end + + def destroy + vhost_opt = should_vhost ? "--vhost=#{should_vhost}" : '' + name = resource[:name].rpartition('@').first + rabbitmqadmin('delete', 'queue', vhost_opt, "--user=#{resource[:user]}", "--password=#{resource[:password]}", "name=#{name}") + @property_hash[:ensure] = :absent + end + +end diff --git a/lib/puppet/type/rabbitmq_queue.rb b/lib/puppet/type/rabbitmq_queue.rb new file mode 100644 index 000000000..464a2ca6e --- /dev/null +++ b/lib/puppet/type/rabbitmq_queue.rb @@ -0,0 +1,68 @@ +Puppet::Type.newtype(:rabbitmq_queue) do + desc 'Native type for managing rabbitmq queue' + + ensurable do + defaultto(:present) + newvalue(:present) do + provider.create + end + newvalue(:absent) do + provider.destroy + end + end + + newparam(:name, :namevar => true) do + desc 'Name of queue' + newvalues(/^\S*@\S+$/) + end + + newparam(:durable) do + desc 'Queue is durable' + newvalues(/true|false/) + defaultto('true') + end + + newparam(:auto_delete) do + desc 'Queue will be auto deleted' + newvalues(/true|false/) + defaultto('false') + end + + newparam(:arguments) do + desc 'Queue arguments example: {x-message-ttl => 60, x-expires => 10}' + defaultto {} + validate do |value| + resource.validate_argument(value) + end + end + + newparam(:user) do + desc 'The user to use to connect to rabbitmq' + defaultto('guest') + newvalues(/^\S+$/) + end + + newparam(:password) do + desc 'The password to use to connect to rabbitmq' + defaultto('guest') + newvalues(/\S+/) + end + + autorequire(:rabbitmq_vhost) do + [self[:name].split('@')[1]] + end + + autorequire(:rabbitmq_user) do + [self[:user]] + end + + autorequire(:rabbitmq_user_permissions) do + ["#{self[:user]}@#{self[:name].split('@')[1]}"] + end + + def validate_argument(argument) + unless [Hash].include?(argument.class) + raise ArgumentError, "Invalid argument" + end + end +end diff --git a/spec/unit/puppet/provider/rabbitmq_queue/rabbitmqadmin_spec.rb b/spec/unit/puppet/provider/rabbitmq_queue/rabbitmqadmin_spec.rb new file mode 100644 index 000000000..97410a8b4 --- /dev/null +++ b/spec/unit/puppet/provider/rabbitmq_queue/rabbitmqadmin_spec.rb @@ -0,0 +1,60 @@ +require 'puppet' +require 'mocha/api' +RSpec.configure do |config| + config.mock_with :mocha +end +provider_class = Puppet::Type.type(:rabbitmq_queue).provider(:rabbitmqadmin) +describe provider_class do + before :each do + @resource = Puppet::Type::Rabbitmq_queue.new( + {:name => 'test@/', + :durable => :true, + :auto_delete => :false, + :arguments => {} + } + ) + @provider = provider_class.new(@resource) + end + + it 'should return instances' do + provider_class.expects(:rabbitmqctl).with('list_vhosts', '-q').returns <<-EOT +/ +EOT + provider_class.expects(:rabbitmqctl).with('list_queues', '-q', '-p', '/', 'name', 'durable', 'auto_delete', 'arguments').returns <<-EOT +test true false [] +test2 true false [{"x-message-ttl",342423},{"x-expires",53253232},{"x-max-length",2332},{"x-max-length-bytes",32563324242},{"x-dead-letter-exchange","amq.direct"},{"x-dead-letter-routing-key","test.routing"}] +EOT + instances = provider_class.instances + instances.size.should == 2 + end + + it 'should call rabbitmqadmin to create' do + @provider.expects(:rabbitmqadmin).with('declare', 'queue', '--vhost=/', '--user=guest', '--password=guest', 'name=test', 'durable=true', 'auto_delete=false', 'arguments={}') + @provider.create + end + + it 'should call rabbitmqadmin to destroy' do + @provider.expects(:rabbitmqadmin).with('delete', 'queue', '--vhost=/', '--user=guest', '--password=guest', 'name=test') + @provider.destroy + end + + context 'specifying credentials' do + before :each do + @resource = Puppet::Type::Rabbitmq_queue.new( + {:name => 'test@/', + :durable => 'true', + :auto_delete => 'false', + :arguments => {}, + :user => 'colin', + :password => 'secret', + } + ) + @provider = provider_class.new(@resource) + end + + it 'should call rabbitmqadmin to create' do + @provider.expects(:rabbitmqadmin).with('declare', 'queue', '--vhost=/', '--user=colin', '--password=secret', 'name=test', 'durable=true', 'auto_delete=false', 'arguments={}') + @provider.create + end + end +end diff --git a/spec/unit/puppet/type/rabbitmq_queue_spec.rb b/spec/unit/puppet/type/rabbitmq_queue_spec.rb new file mode 100644 index 000000000..4fd7b34ef --- /dev/null +++ b/spec/unit/puppet/type/rabbitmq_queue_spec.rb @@ -0,0 +1,60 @@ +require 'puppet' +require 'puppet/type/rabbitmq_queue' +require 'json' +describe Puppet::Type.type(:rabbitmq_queue) do + before :each do + @queue = Puppet::Type.type(:rabbitmq_queue).new( + :name => 'foo@bar', + :durable => :true, + :arguments => { + 'x-message-ttl' => 45, + 'x-dead-letter-exchange' => 'deadexchange' + } + ) + end + it 'should accept an queue name' do + @queue[:name] = 'dan@pl' + @queue[:name].should == 'dan@pl' + end + it 'should require a name' do + expect { + Puppet::Type.type(:rabbitmq_queue).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + it 'should not allow whitespace in the name' do + expect { + @queue[:name] = 'b r' + }.to raise_error(Puppet::Error, /Valid values match/) + end + it 'should not allow names without @' do + expect { + @queue[:name] = 'b_r' + }.to raise_error(Puppet::Error, /Valid values match/) + end + + it 'should accept an arguments with numbers value' do + @queue[:arguments] = {'x-message-ttl' => 30} + @queue[:arguments].to_json.should == "{\"x-message-ttl\":30}" + @queue[:arguments]['x-message-ttl'].should == 30 + end + + it 'should accept an arguments with string value' do + @queue[:arguments] = {'x-dead-letter-exchange' => 'catchallexchange'} + @queue[:arguments].to_json.should == "{\"x-dead-letter-exchange\":\"catchallexchange\"}" + end + + it 'should accept an queue durable' do + @queue[:durable] = :true + @queue[:durable].should == :true + end + + it 'should accept a user' do + @queue[:user] = :root + @queue[:user].should == :root + end + + it 'should accept a password' do + @queue[:password] = :PaSsw0rD + @queue[:password].should == :PaSsw0rD + end +end From d998f948fd4a5001b85fcbf4c40b3a78f08ac2f6 Mon Sep 17 00:00:00 2001 From: Michele Catalano Date: Fri, 5 Dec 2014 11:58:23 +0100 Subject: [PATCH 2/3] Add new resource rabbitmq_binding --- .../rabbitmq_binding/rabbitmqadmin.rb | 110 ++++++++++++++++++ lib/puppet/type/rabbitmq_binding.rb | 96 +++++++++++++++ .../rabbitmq_binding/rabbitmqadmin_spec.rb | 59 ++++++++++ .../unit/puppet/type/rabbitmq_binding_spec.rb | 50 ++++++++ 4 files changed, 315 insertions(+) create mode 100644 lib/puppet/provider/rabbitmq_binding/rabbitmqadmin.rb create mode 100644 lib/puppet/type/rabbitmq_binding.rb create mode 100644 spec/unit/puppet/provider/rabbitmq_binding/rabbitmqadmin_spec.rb create mode 100644 spec/unit/puppet/type/rabbitmq_binding_spec.rb diff --git a/lib/puppet/provider/rabbitmq_binding/rabbitmqadmin.rb b/lib/puppet/provider/rabbitmq_binding/rabbitmqadmin.rb new file mode 100644 index 000000000..e0eebd025 --- /dev/null +++ b/lib/puppet/provider/rabbitmq_binding/rabbitmqadmin.rb @@ -0,0 +1,110 @@ +require 'json' +require 'puppet' +Puppet::Type.type(:rabbitmq_binding).provide(:rabbitmqadmin) do + + if Puppet::PUPPETVERSION.to_f < 3 + commands :rabbitmqctl => 'rabbitmqctl' + commands :rabbitmqadmin => '/usr/local/bin/rabbitmqadmin' + else + has_command(:rabbitmqctl, 'rabbitmqctl') do + environment :HOME => "/tmp" + end + has_command(:rabbitmqadmin, '/usr/local/bin/rabbitmqadmin') do + environment :HOME => "/tmp" + end + end + defaultfor :feature => :posix + + def should_vhost + if @should_vhost + @should_vhost + else + @should_vhost = resource[:name].split('@').last + end + end + + def self.all_vhosts + vhosts = [] + rabbitmqctl('list_vhosts', '-q').split(/\n/).collect do |vhost| + vhosts.push(vhost) + end + vhosts + end + + def self.all_bindings(vhost) + rabbitmqctl('list_bindings', '-q', '-p', vhost, 'source_name', 'destination_name', 'destination_kind', 'routing_key', 'arguments').split(/\n/) + end + + def self.instances + resources = [] + all_vhosts.each do |vhost| + all_bindings(vhost).collect do |line| + source_name, destination_name, destination_type, routing_key, arguments = line.split(/\t/) + # Convert output of arguments from the rabbitmqctl command to a json string. + if !arguments.nil? + arguments = arguments.gsub(/^\[(.*)\]$/, "").gsub(/\{("(?:.|\\")*?"),/, '{\1:').gsub(/\},\{/, ",") + if arguments == "" + arguments = '{}' + end + else + arguments = '{}' + end + if (source_name != '') + binding = { + :destination_type => destination_type, + :routing_key => routing_key, + :arguments => JSON.parse(arguments), + :ensure => :present, + :name => "%s@%s@%s" % [source_name, destination_name, vhost], + } + resources << new(binding) if binding[:name] + end + end + end + resources + end + + def self.prefetch(resources) + packages = instances + resources.keys.each do |name| + if provider = packages.find{ |pkg| pkg.name == name } + resources[name].provider = provider + end + end + end + + def exists? + @property_hash[:ensure] == :present + end + + def create + vhost_opt = should_vhost ? "--vhost=#{should_vhost}" : '' + name = resource[:name].split('@').first + destination = resource[:name].split('@')[1] + arguments = resource[:arguments] + if arguments.nil? + arguments = {} + end + rabbitmqadmin('declare', + 'binding', + vhost_opt, + "--user=#{resource[:user]}", + "--password=#{resource[:password]}", + "source=#{name}", + "destination=#{destination}", + "arguments=#{arguments.to_json}", + "routing_key=#{resource[:routing_key]}", + "destination_type=#{resource[:destination_type]}" + ) + @property_hash[:ensure] = :present + end + + def destroy + vhost_opt = should_vhost ? "--vhost=#{should_vhost}" : '' + name = resource[:name].split('@').first + destination = resource[:name].split('@')[1] + rabbitmqadmin('delete', 'binding', vhost_opt, "--user=#{resource[:user]}", "--password=#{resource[:password]}", "source=#{name}", "destination_type=#{resource[:destination_type]}", "destination=#{destination}") + @property_hash[:ensure] = :absent + end + +end diff --git a/lib/puppet/type/rabbitmq_binding.rb b/lib/puppet/type/rabbitmq_binding.rb new file mode 100644 index 000000000..130948003 --- /dev/null +++ b/lib/puppet/type/rabbitmq_binding.rb @@ -0,0 +1,96 @@ +Puppet::Type.newtype(:rabbitmq_binding) do + desc 'Native type for managing rabbitmq bindings' + + ensurable do + defaultto(:present) + newvalue(:present) do + provider.create + end + newvalue(:absent) do + provider.destroy + end + end + + newparam(:name, :namevar => true) do + desc 'source and destination of bind' + newvalues(/^\S*@\S+@\S+$/) + end + + newparam(:destination_type) do + desc 'binding destination_type' + newvalues(/queue|exchange/) + defaultto('queue') + end + + newparam(:routing_key) do + desc 'binding routing_key' + newvalues(/^\S*$/) + end + + newparam(:arguments) do + desc 'binding arguments' + defaultto {} + validate do |value| + resource.validate_argument(value) + end + end + + newparam(:user) do + desc 'The user to use to connect to rabbitmq' + defaultto('guest') + newvalues(/^\S+$/) + end + + newparam(:password) do + desc 'The password to use to connect to rabbitmq' + defaultto('guest') + newvalues(/\S+/) + end + + autorequire(:rabbitmq_vhost) do + [self[:name].split('@')[2]] + end + + autorequire(:rabbitmq_exchange) do + setup_autorequire('exchange') + end + + autorequire(:rabbitmq_queue) do + setup_autorequire('queue') + end + + autorequire(:rabbitmq_user) do + [self[:user]] + end + + autorequire(:rabbitmq_user_permissions) do + [ + "#{self[:user]}@#{self[:name].split('@')[1]}", + "#{self[:user]}@#{self[:name].split('@')[0]}" + ] + end + + def setup_autorequire(type) + destination_type = value(:destination_type) + if type == 'exchange' + rval = ["#{self[:name].split('@')[0]}@#{self[:name].split('@')[2]}"] + if destination_type == type + rval.push("#{self[:name].split('@')[1]}@#{self[:name].split('@')[2]}") + end + else + if destination_type == type + rval = ["#{self[:name].split('@')[1]}@#{self[:name].split('@')[2]}"] + else + rval = [] + end + end + rval + end + + def validate_argument(argument) + unless [Hash].include?(argument.class) + raise ArgumentError, "Invalid argument" + end + end + +end diff --git a/spec/unit/puppet/provider/rabbitmq_binding/rabbitmqadmin_spec.rb b/spec/unit/puppet/provider/rabbitmq_binding/rabbitmqadmin_spec.rb new file mode 100644 index 000000000..d1ff0667c --- /dev/null +++ b/spec/unit/puppet/provider/rabbitmq_binding/rabbitmqadmin_spec.rb @@ -0,0 +1,59 @@ +require 'puppet' +require 'mocha/api' +RSpec.configure do |config| + config.mock_with :mocha +end +provider_class = Puppet::Type.type(:rabbitmq_binding).provider(:rabbitmqadmin) +describe provider_class do + before :each do + @resource = Puppet::Type::Rabbitmq_binding.new( + {:name => 'source@target@/', + :destination_type => :queue, + :routing_key => 'blablub', + :arguments => {} + } + ) + @provider = provider_class.new(@resource) + end + + it 'should return instances' do + provider_class.expects(:rabbitmqctl).with('list_vhosts', '-q').returns <<-EOT +/ +EOT + provider_class.expects(:rabbitmqctl).with('list_bindings', '-q', '-p', '/', 'source_name', 'destination_name', 'destination_kind', 'routing_key', 'arguments').returns <<-EOT + queue queue queue [] +EOT + instances = provider_class.instances + instances.size.should == 1 + end + + it 'should call rabbitmqadmin to create' do + @provider.expects(:rabbitmqadmin).with('declare', 'binding', '--vhost=/', '--user=guest', '--password=guest', 'source=source', 'destination=target', 'arguments={}', 'routing_key=blablub', 'destination_type=queue') + @provider.create + end + + it 'should call rabbitmqadmin to destroy' do + @provider.expects(:rabbitmqadmin).with('delete', 'binding', '--vhost=/', '--user=guest', '--password=guest', 'source=source', 'destination_type=queue', 'destination=target') + @provider.destroy + end + + context 'specifying credentials' do + before :each do + @resource = Puppet::Type::Rabbitmq_binding.new( + {:name => 'source@test2@/', + :destination_type => :queue, + :routing_key => 'blablubd', + :arguments => {}, + :user => 'colin', + :password => 'secret' + } + ) + @provider = provider_class.new(@resource) + end + + it 'should call rabbitmqadmin to create' do + @provider.expects(:rabbitmqadmin).with('declare', 'binding', '--vhost=/', '--user=colin', '--password=secret', 'source=source', 'destination=test2', 'arguments={}', 'routing_key=blablubd', 'destination_type=queue') + @provider.create + end + end +end diff --git a/spec/unit/puppet/type/rabbitmq_binding_spec.rb b/spec/unit/puppet/type/rabbitmq_binding_spec.rb new file mode 100644 index 000000000..b0671e7c2 --- /dev/null +++ b/spec/unit/puppet/type/rabbitmq_binding_spec.rb @@ -0,0 +1,50 @@ +require 'puppet' +require 'puppet/type/rabbitmq_binding' +describe Puppet::Type.type(:rabbitmq_binding) do + before :each do + @binding = Puppet::Type.type(:rabbitmq_binding).new( + :name => 'foo@blub@bar', + :destination_type => :queue + ) + end + it 'should accept an queue name' do + @binding[:name] = 'dan@dude@pl' + @binding[:name].should == 'dan@dude@pl' + end + it 'should require a name' do + expect { + Puppet::Type.type(:rabbitmq_binding).new({}) + }.to raise_error(Puppet::Error, 'Title or name must be provided') + end + it 'should not allow whitespace in the name' do + expect { + @binding[:name] = 'b r' + }.to raise_error(Puppet::Error, /Valid values match/) + end + it 'should not allow names without one @' do + expect { + @binding[:name] = 'b_r' + }.to raise_error(Puppet::Error, /Valid values match/) + end + + it 'should not allow names without two @' do + expect { + @binding[:name] = 'b@r' + }.to raise_error(Puppet::Error, /Valid values match/) + end + + it 'should accept an binding destination_type' do + @binding[:destination_type] = :exchange + @binding[:destination_type].should == :exchange + end + + it 'should accept a user' do + @binding[:user] = :root + @binding[:user].should == :root + end + + it 'should accept a password' do + @binding[:password] = :PaSsw0rD + @binding[:password].should == :PaSsw0rD + end +end From a2f802cc17aae331663f640337d0f2217c3e438f Mon Sep 17 00:00:00 2001 From: Michele Catalano Date: Fri, 5 Dec 2014 16:31:31 +0100 Subject: [PATCH 3/3] Add documentation for the new resources --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 2176a6a90..486c7750c 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,35 @@ rabbitmq_exchange { 'myexchange@myhost': } ``` +### rabbitmq\_queue + +```puppet +rabbitmq_queue { 'myqueue@myhost': + user => 'dan', + password => 'bar', + durable => true, + auto_delete => false, + arguments => { + x-message-ttl => 123, + x-dead-letter-exchange => 'other' + }, + ensure => present, +} +``` + +### rabbitmq\_binding + +```puppet +rabbitmq_binding { 'myexchange@myqueue@myhost': + user => 'dan', + password => 'bar', + destination_type => 'queue', + routing_key => '#', + arguments => {}, + ensure => present, +} +``` + ### rabbitmq\_user\_permissions ```puppet