diff --git a/lib/puppet/provider/postgresql_replication_slot/ruby.rb b/lib/puppet/provider/postgresql_replication_slot/ruby.rb new file mode 100644 index 0000000..cc49f7b --- /dev/null +++ b/lib/puppet/provider/postgresql_replication_slot/ruby.rb @@ -0,0 +1,68 @@ +Puppet::Type.type(:postgresql_replication_slot).provide(:ruby) do + # For confinement + commands :psql => 'psql' + + def self.instances + run_sql_command('SELECT * FROM pg_replication_slots;')[0].split("\n").select { |l| l =~ /\|/ }.map do |l| + name, *others = l.strip.split(/\s+\|\s+/) + new({ + :name => name, + :ensure => :present, + }) + end + end + + def self.prefetch(resources) + instances.each do |i| + if slot = resources[i.name] + slot.provider = i + end + end + end + + def exists? + @property_hash[:ensure] == :present + end + + def create + output = self.class.run_sql_command("SELECT * FROM pg_create_physical_replication_slot('#{resource[:name]}');") + if output[1].success? + @property_hash[:ensure] = :present + else + raise Puppet::Error, "Failed to create replication slot #{resource[:name]}:\n#{output[0]}" + end + end + + def destroy + output = self.class.run_sql_command("SELECT pg_drop_replication_slot('#{resource[:name]}');") + if output[1].success? + @property_hash[:ensure] = :absent + else + raise Puppet::Error, "Failed to destroy replication slot #{resource[:name]}:\n#{output[0]}" + end + end + + private + + def self.run_sql_command(sql) + command = ['psql', '-t', '-c', sql] + + self.run_command(command, 'postgres', 'postgres') + end + + def self.run_command(command, user, group) + if Puppet::PUPPETVERSION.to_f < 3.4 + Puppet::Util::SUIDManager.run_and_capture(command, user, group) + else + output = Puppet::Util::Execution.execute(command, { + :uid => user, + :gid => group, + :failonfail => false, + :combine => true, + :override_locale => true, + :custom_environment => {} + }) + [output, $CHILD_STATUS.dup] + end + end +end diff --git a/lib/puppet/type/postgresql_replication_slot.rb b/lib/puppet/type/postgresql_replication_slot.rb new file mode 100644 index 0000000..b5b317c --- /dev/null +++ b/lib/puppet/type/postgresql_replication_slot.rb @@ -0,0 +1,16 @@ +Puppet::Type.newtype(:postgresql_replication_slot) do + @doc = "Manages Postgresql replication slots. + +This type allows to create and destroy replication slots +to register warm standby replication on a Postgresql +master server. +" + + ensurable + + newparam(:name) do + desc "The name of the slot to create. Must be a valid replication slot name." + isnamevar + newvalues /^[a-z0-9_]+$/ + end +end diff --git a/spec/unit/puppet/provider/postgresql_replication_slot/ruby_spec.rb b/spec/unit/puppet/provider/postgresql_replication_slot/ruby_spec.rb new file mode 100644 index 0000000..69b63f3 --- /dev/null +++ b/spec/unit/puppet/provider/postgresql_replication_slot/ruby_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +type = Puppet::Type.type(:postgresql_replication_slot) +describe type.provider(:ruby) do + let(:name) { 'standby' } + let(:resource) do + type.new({ :name => name, :provider => :ruby }.merge attributes) + end + + let(:sql_instances) do + "abc | | physical | | | t | | | 0/3000420 +def | | physical | | | t | | | 0/3000420\n" + end + + class SuccessStatus + def success? + true + end + end + let(:success_status) { SuccessStatus.new } + + class FailStatus + def success? + false + end + end + let(:fail_status) { FailStatus.new } + + let(:provider) { resource.provider } + + context 'when listing instances' do + let(:attributes) do { } end + + it 'should list instances' do + provider.class.expects(:run_command).with( + ['psql', '-t', '-c', 'SELECT * FROM pg_replication_slots;'], + 'postgres', 'postgres').returns([sql_instances, nil]) + instances = type.instances + expect(instances.size).to eq 2 + expect(instances[0].name).to eq 'abc' + expect(instances[1].name).to eq 'def' + end + end + + context 'when creating slot' do + let(:attributes) do { :ensure => 'present' } end + + context 'when creation works' do + it 'should call psql and succeed' do + provider.class.expects(:run_command).with( + ['psql', '-t', '-c', "SELECT * FROM pg_create_physical_replication_slot('standby');"], + 'postgres', 'postgres').returns([nil, success_status]) + + expect { provider.create }.not_to raise_error + end + end + + context 'when creation fails' do + it 'should call psql and fail' do + provider.class.expects(:run_command).with( + ['psql', '-t', '-c', "SELECT * FROM pg_create_physical_replication_slot('standby');"], + 'postgres', 'postgres').returns([nil, fail_status]) + + expect { provider.create }.to raise_error(Puppet::Error, /Failed to create replication slot standby:/) + end + end + end + + context 'when destroying slot' do + let(:attributes) do { :ensure => 'absent' } end + + context 'when destruction works' do + it 'should call psql and succeed' do + provider.class.expects(:run_command).with( + ['psql', '-t', '-c', "SELECT pg_drop_replication_slot('standby');"], + 'postgres', 'postgres').returns([nil, success_status]) + + expect { provider.destroy }.not_to raise_error + end + end + + context 'when destruction fails' do + it 'should call psql and fail' do + provider.class.expects(:run_command).with( + ['psql', '-t', '-c', "SELECT pg_drop_replication_slot('standby');"], + 'postgres', 'postgres').returns([nil, fail_status]) + + expect { provider.destroy }.to raise_error(Puppet::Error, /Failed to destroy replication slot standby:/) + end + end + end +end diff --git a/spec/unit/puppet/type/postgresql_replication_slot_spec.rb b/spec/unit/puppet/type/postgresql_replication_slot_spec.rb new file mode 100644 index 0000000..0d7c668 --- /dev/null +++ b/spec/unit/puppet/type/postgresql_replication_slot_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Puppet::Type.type(:postgresql_replication_slot) do + subject do + Puppet::Type.type(:postgresql_psql).new({:name => 'standby'}) + end + + it 'should have a name parameter' do + expect(subject[:name]).to eq 'standby' + end +end