From 7a74431e8819354fdae489552c494543981b65d5 Mon Sep 17 00:00:00 2001 From: Adam Spiers Date: Tue, 28 Jan 2014 17:58:49 +0000 Subject: [PATCH] switch to OO classes modelling Pacemaker objects --- Guardfile | 23 ++- libraries/cib_objects.rb | 94 ------------ libraries/pacemaker.rb | 2 + libraries/pacemaker/cib_object.rb | 91 ++++++++++++ libraries/pacemaker/resource.rb | 23 +++ libraries/pacemaker/resource/clone.rb | 8 ++ libraries/pacemaker/resource/primitive.rb | 124 ++++++++++++++++ providers/clone.rb | 2 +- providers/colocation.rb | 2 +- providers/location.rb | 2 +- providers/ms.rb | 2 +- providers/order.rb | 2 +- providers/primitive.rb | 59 ++++---- providers/property.rb | 2 +- spec/helpers/clone_resource.rb | 13 ++ spec/helpers/keystone_config.rb | 38 ----- spec/helpers/keystone_primitive.rb | 31 ++++ spec/libraries/cib_object_spec.rb | 112 +++++++++++++++ spec/libraries/cib_objects_spec.rb | 167 ---------------------- spec/libraries/primitive_spec.rb | 108 ++++++++++++++ spec/libraries/resource_spec.rb | 27 ++++ spec/providers/primitive_spec.rb | 144 ++++++++++--------- spec/spec_helper.rb | 2 +- 23 files changed, 665 insertions(+), 413 deletions(-) delete mode 100644 libraries/cib_objects.rb create mode 100644 libraries/pacemaker.rb create mode 100644 libraries/pacemaker/cib_object.rb create mode 100644 libraries/pacemaker/resource.rb create mode 100644 libraries/pacemaker/resource/clone.rb create mode 100644 libraries/pacemaker/resource/primitive.rb create mode 100644 spec/helpers/clone_resource.rb delete mode 100644 spec/helpers/keystone_config.rb create mode 100644 spec/helpers/keystone_primitive.rb create mode 100644 spec/libraries/cib_object_spec.rb delete mode 100644 spec/libraries/cib_objects_spec.rb create mode 100644 spec/libraries/primitive_spec.rb create mode 100644 spec/libraries/resource_spec.rb diff --git a/Guardfile b/Guardfile index 8b4bb5b..5781319 100644 --- a/Guardfile +++ b/Guardfile @@ -7,25 +7,24 @@ guard_opts = { all_after_pass: true, } -def startup_guards - watch(%r{^Gemfile$}) { yield } - watch(%r{^Gemfile.lock$}) { yield } - watch(%r{^spec/spec_helper\.rb$}) { yield } -end - def all_specs 'spec' end group :rspec do guard 'rspec', guard_opts do - startup_guards { all_specs } - watch(%r{^spec_helper\.rb$}) { all_specs } - watch(%r{^helpers/(.+)\.rb$}) { all_specs } + watch(%r{^Gemfile$}) { all_specs } + watch(%r{^Gemfile.lock$}) { all_specs } + watch(%r{^spec/spec_helper\.rb$}) { all_specs } + watch(%r{^spec/helpers/(.+)\.rb$}) { all_specs } + watch(%r{^libraries/pacemaker\.rb$}) { all_specs } watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^(libraries|providers)/(.+)\.rb$}) do |m| - "spec/#{m[1]}/#{m[2]}_spec.rb" - end + watch(%r{^libraries/pacemaker/(.+)\.rb$}) { |m| + "spec/libraries/#{m[1]}_spec.rb" + } + watch(%r{^(resources|providers)/(.+)\.rb$}) { |m| + "spec/providers/#{m[1]}_spec.rb" + } end end diff --git a/libraries/cib_objects.rb b/libraries/cib_objects.rb deleted file mode 100644 index 788b920..0000000 --- a/libraries/cib_objects.rb +++ /dev/null @@ -1,94 +0,0 @@ -require 'shellwords' - -module Chef::Libraries - module Pacemaker - module CIBObjects - include Chef::Mixin::ShellOut - - def get_cib_object_definition(name) - cmd = Mixlib::ShellOut.new("crm configure show #{name}") - cmd.environment['HOME'] = ENV.fetch('HOME', '/root') - cmd.run_command - begin - cmd.error! - cmd.stdout - rescue - nil - end - end - - def cib_object_exists?(name) - dfn = get_cib_object_definition(name) - !! (dfn && ! dfn.empty?) - end - - def cib_object_type(obj_definition) - unless obj_definition =~ /\A(\w+)\s/ - raise "Couldn't extract CIB object type from '#{obj_definition}'" - end - return $1 - end - - def pacemaker_resource_running?(name) - cmd = shell_out! "crm", "resource", "status", name - Chef::Log.info cmd.stdout - cmd.stdout.include? "resource #{name} is running" - end - - def resource_params_string(params) - return "" if ! params or params.empty? - s = " params" - params.sort.each do |key, value| - s << %' #{key}="#{value}"' - end - s - end - - def resource_meta_string(meta) - return "" if ! meta or meta.empty? - s = " meta" - meta.sort.each do |key, value| - s << %' #{key}="#{value}"' - end - s - end - - def resource_op_string(ops) - return "" if ! ops or ops.empty? - s = " op" - ops.sort.each do |op, attrs| - s << " #{op}" - attrs.sort.each do |key, value| - s << %' #{key}="#{value}"' - end - end - s - end - - # CIB object definitions look something like: - # - # primitive keystone ocf:openstack:keystone \ - # params os_username="crowbar" os_password="crowbar" os_tenant_name="openstack" \ - # meta target-role="Started" is-managed="true" \ - # op monitor interval="10" timeout=30s \ - # op start interval="10s" timeout="240" \ - # - # This method extracts a Hash from one of the params / meta / op lines. - def extract_hash(name, obj_definition, data_type) - unless obj_definition =~ /^\s+#{data_type} (.+?)(\s*\\)?$/ - raise "Couldn't retrieve #{data_type} for '#{name}' CIB object from [#{obj_definition}]" - end - - h = {} - Shellwords.split($1).each do |kvpair| - unless kvpair =~ /^(.+?)=(.+)$/ - raise "Couldn't understand '#{kvpair}' for #{data_type} section of '#{name}' primitive" - end - h[$1] = $2.sub(/^"(.*)"$/, "\1") - end - h - end - - end - end -end diff --git a/libraries/pacemaker.rb b/libraries/pacemaker.rb new file mode 100644 index 0000000..cb108c6 --- /dev/null +++ b/libraries/pacemaker.rb @@ -0,0 +1,2 @@ +require_relative 'pacemaker/resource/primitive' +require_relative 'pacemaker/resource/clone' diff --git a/libraries/pacemaker/cib_object.rb b/libraries/pacemaker/cib_object.rb new file mode 100644 index 0000000..39ab545 --- /dev/null +++ b/libraries/pacemaker/cib_object.rb @@ -0,0 +1,91 @@ +require 'mixlib/shellout' + +module Pacemaker + class ObjectTypeMismatch < StandardError + end + + class CIBObject + attr_accessor :name, :definition + + @@subclasses = { } + + class << self + attr_reader :object_type + + def register_type(type_name) + @object_type = type_name + @@subclasses[type_name] = self + end + + def get_definition(name) + cmd = Mixlib::ShellOut.new("crm configure show #{name}") + cmd.environment['HOME'] = ENV.fetch('HOME', '/root') + cmd.run_command + begin + cmd.error! + cmd.stdout + rescue + nil + end + end + + def type(definition) + unless definition =~ /\A(\w+)\s/ + raise "Couldn't extract CIB object type from '#{definition}'" + end + return $1 + end + + def from_name(name) + definition = get_definition(name) + return nil unless definition + obj_type = type(definition) + subclass = @@subclasses[obj_type] + unless subclass + raise "No subclass of #{self.name} was registered with type '#{obj_type}'" + end + obj = subclass.from_definition(definition) + unless name == obj.name + raise "Name '#{obj.name}' in definition didn't match name '#{name}' used for retrieval" + end + obj + end + + def from_definition(definition) + obj = new(name) + obj.definition = definition + obj.parse_definition + obj + end + end + + def initialize(name) + @name = name + @definition = nil + end + + def load_definition + @definition = self.class.get_definition(name) + + if @definition and ! @definition.empty? and type != self.class.object_type + raise ObjectTypeMismatch, "Expected #{self.class.object_type} type but loaded definition was type #{type}" + end + end + + def parse_definition + raise NotImplementedError, "#{self.class} must implement #parse_definition" + end + + def exists? + !! (definition && ! definition.empty?) + end + + def type + self.class.type(definition) + end + + def delete_command + "crm configure delete '#{name}'" + end + end +end diff --git a/libraries/pacemaker/resource.rb b/libraries/pacemaker/resource.rb new file mode 100644 index 0000000..3d0639a --- /dev/null +++ b/libraries/pacemaker/resource.rb @@ -0,0 +1,23 @@ +require 'chef/mixin/shell_out' +require_relative 'cib_object' + +module Pacemaker + class Resource < Pacemaker::CIBObject + include Chef::Mixin::ShellOut + + def running? + cmd = shell_out! "crm", "resource", "status", name + Chef::Log.info cmd.stdout + !! cmd.stdout.include?("resource #{name} is running") + end + + def start_command + "crm resource start '#{name}'" + end + + def stop_command + "crm resource stop '#{name}'" + end + + end +end diff --git a/libraries/pacemaker/resource/clone.rb b/libraries/pacemaker/resource/clone.rb new file mode 100644 index 0000000..6cb8433 --- /dev/null +++ b/libraries/pacemaker/resource/clone.rb @@ -0,0 +1,8 @@ +require_relative File::join(%w(.. resource)) + +class Pacemaker::Resource::Clone < Pacemaker::Resource + register_type 'clone' + + attr_accessor :primitive + +end diff --git a/libraries/pacemaker/resource/primitive.rb b/libraries/pacemaker/resource/primitive.rb new file mode 100644 index 0000000..45370cb --- /dev/null +++ b/libraries/pacemaker/resource/primitive.rb @@ -0,0 +1,124 @@ +require 'shellwords' +require_relative File::join(%w(.. resource)) + +class Pacemaker::Resource::Primitive < Pacemaker::Resource + TYPE = 'primitive' + + register_type TYPE + + attr_accessor :agent, :params, :meta, :op + + def initialize(*args) + super(*args) + + @agent = nil + end + + def self.from_chef_resource(resource) + primitive = new(resource.name) + %w(agent params meta op).each do |data| + value = resource.send(data.to_sym) + writer = (data + '=').to_sym + primitive.send(writer, value) + end + return primitive + end + + def parse_definition + unless definition =~ /\Aprimitive (\S+) (\S+)/ + raise RuntimeError, "Couldn't parse definition '#{definition}'" + end + self.name = $1 + self.agent = $2 + + %w(params meta).each do |data_type| + hash = self.class.extract_hash(definition, data_type) + writer = (data_type + '=').to_sym + send(writer, hash) + end + + # FIXME: deal with op + end + + def params_string + self.class.params_string(params) + end + + def meta_string + self.class.meta_string(meta) + end + + def op_string + self.class.op_string(op) + end + + def definition_string + return < e + Chef::Log.warn e.message return end - agent = $1 - @current_resource_definition = obj_definition + if ! primitive.definition or primitive.definition.empty? + Chef::Log.debug "CIB object definition nil or empty" + return + end + Chef::Log.debug "CIB object definition #{primitive.definition}" + @current_resource_definition = primitive.definition + primitive.parse_definition + + @current_primitive = primitive @current_resource = Chef::Resource::PacemakerPrimitive.new(name) - @current_resource.agent(agent) - + @current_resource.agent(primitive.agent) %w(params meta).each do |data_type| - h = extract_hash(name, obj_definition, data_type) - @current_resource.send(data_type.to_sym, h) - Chef::Log.debug "detected #{name} has #{data_type} #{h}" + method = data_type.to_sym + value = primitive.send(method) + @current_resource.send(method, value) + Chef::Log.debug "detected #{name} has #{data_type} #{value}" end end def create_resource(name) - cmd = "crm configure primitive #{name} #{new_resource.agent}" - cmd << resource_params_string(new_resource.params) - cmd << resource_meta_string(new_resource.meta) - cmd << resource_op_string(new_resource.op) + primitive = Pacemaker::Resource::Primitive.from_chef_resource(new_resource) + cmd = primitive.crm_configure_command Chef::Log.info "Creating new resource primitive #{name}" @@ -121,7 +126,7 @@ def create_resource(name) action :nothing end.run_action(:run) - if cib_object_exists?(name) + if primitive.exists? new_resource.updated_by_last_action(true) Chef::Log.info "Successfully configured primitive '#{name}'." else diff --git a/providers/property.rb b/providers/property.rb index 90f15a7..c734476 100644 --- a/providers/property.rb +++ b/providers/property.rb @@ -17,7 +17,7 @@ # limitations under the License. # -require ::File.join(::File.dirname(__FILE__), *%w(.. libraries cib_objects)) +require ::File.join(::File.dirname(__FILE__), *%w(.. libraries pacemaker cib_object)) action :create do name = new_resource.name diff --git a/spec/helpers/clone_resource.rb b/spec/helpers/clone_resource.rb new file mode 100644 index 0000000..ebd4e96 --- /dev/null +++ b/spec/helpers/clone_resource.rb @@ -0,0 +1,13 @@ +#require ::File.join(::File.dirname(__FILE__), *%w(.. .. libraries pacemaker clone)) +require_relative 'keystone_primitive' + +module Chef::RSpec + module Pacemaker + module Config + include Chef::RSpec::Pacemaker::Config + + CLONE = ::Pacemaker::Clone.new('clone1') + CLONE.primitive = KEYSTONE + end + end +end diff --git a/spec/helpers/keystone_config.rb b/spec/helpers/keystone_config.rb deleted file mode 100644 index f7df900..0000000 --- a/spec/helpers/keystone_config.rb +++ /dev/null @@ -1,38 +0,0 @@ -require ::File.join(::File.dirname(__FILE__), *%w(.. .. libraries cib_objects)) - -module Chef::RSpec - module Pacemaker - module Config - extend Chef::Libraries::Pacemaker::CIBObjects - - RA = { - :name => "keystone", - :agent => "ocf:openstack:keystone", - :params => [ - [ "os_password", "adminpw" ], - [ "os_auth_url", "http://node1:5000/v2.0" ], - [ "os_username", "admin" ], - [ "os_tenant_name", "openstack" ], - [ "user", "openstack-keystone" ], - ], - :meta => [ - [ "target-role", "Started" ], - [ "is-managed", "true" ] - ], - :op => [ - [ "monitor", { "timeout" => "60", "interval" => "10s" } ], - [ "start", { "timeout" => "240", "interval" => "10s" } ] - ], - } - RA[:params_string] = resource_params_string(RA[:params]) - RA[:meta_string] = resource_meta_string(RA[:meta]) - RA[:op_string] = resource_op_string(RA[:op]) - RA[:config] = < "60", "interval" => "10s" } ], + [ "start", { "timeout" => "240", "interval" => "10s" } ] + ] + KEYSTONE_PRIMITIVE_DEFINITION = <<'EOF' +primitive keystone ocf:openstack:keystone \ + params os_auth_url="http://node1:5000/v2.0" os_password="adminpw" os_tenant_name="openstack" os_username="admin" user="openstack-keystone" \ + meta is-managed="true" target-role="Started" \ + op monitor interval="10s" timeout="60" start interval="10s" timeout="240" +EOF + end + end +end diff --git a/spec/libraries/cib_object_spec.rb b/spec/libraries/cib_object_spec.rb new file mode 100644 index 0000000..3545716 --- /dev/null +++ b/spec/libraries/cib_object_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' +require 'mixlib/shellout' + +require_relative File.join(%w(.. .. libraries pacemaker)) +require_relative File.join(%w(.. helpers keystone_primitive)) + +describe Pacemaker::CIBObject do + + before(:each) do + Mixlib::ShellOut.any_instance.stub(:run_command) + end + + let(:cib_object) { Chef::RSpec::Pacemaker::Config::KEYSTONE_PRIMITIVE.dup } + + ##################################################################### + # examples start here + + context "no CIB object" do + before(:each) do + expect_any_instance_of(Mixlib::ShellOut) \ + .to receive(:error!) \ + .and_raise(RuntimeError) + end + + describe "#load_definition" do + it "should return nil cluster config" do + cib_object.load_definition + expect(cib_object.definition).to eq(nil) + end + end + + describe "#exists?" do + it "should return false" do + cib_object.load_definition + expect(cib_object.exists?).to be(false) + end + end + end + + context "keystone primitive resource CIB object" do + before(:each) do + Mixlib::ShellOut.any_instance.stub(:error!) + expect_any_instance_of(Mixlib::ShellOut) \ + .to receive(:stdout) \ + .and_return(cib_object.definition_string) + end + + context "with definition loaded" do + before(:each) do + cib_object.load_definition + end + + describe "#exists?" do + it "should return true" do + expect(cib_object.exists?).to be(true) + end + end + + describe "#load_definition" do + it "should retrieve cluster config" do + expect(cib_object.definition).to eq(cib_object.definition_string) + end + end + + describe "#type" do + it "should return primitive" do + expect(cib_object.type).to eq("primitive") + end + end + end + + describe "::from_name" do + it "should instantiate a Pacemaker::Resource::Primitive" do + obj = Pacemaker::CIBObject.from_name(cib_object.name) + expect(obj.is_a? Pacemaker::Resource::Primitive).to be_true + end + end + end + + context "CIB object with unregistered type" do + before(:each) do + Mixlib::ShellOut.any_instance.stub(:error!) + end + + describe "::from_name" do + it "should refuse to instantiate from any subclass" do + expect_any_instance_of(Mixlib::ShellOut) \ + .to receive(:stdout) \ + .and_return("unregistered #{cib_object.name} ") + expect { + Pacemaker::CIBObject.from_name(cib_object.name) + }.to raise_error "No subclass of Pacemaker::CIBObject was registered with type 'unregistered'" + end + end + end + + context "invalid CIB object definition" do + before(:each) do + Mixlib::ShellOut.any_instance.stub(:error!) + expect_any_instance_of(Mixlib::ShellOut) \ + .to receive(:stdout) \ + .and_return("nonsense") + end + + describe "#type" do + it "should raise an error without a valid definition" do + expect { cib_object.load_definition }.to \ + raise_error(RuntimeError, "Couldn't extract CIB object type from 'nonsense'") + end + end + end +end diff --git a/spec/libraries/cib_objects_spec.rb b/spec/libraries/cib_objects_spec.rb deleted file mode 100644 index dd6e4bb..0000000 --- a/spec/libraries/cib_objects_spec.rb +++ /dev/null @@ -1,167 +0,0 @@ -require 'spec_helper' -require_relative File.join(%w(.. .. libraries cib_objects)) -require_relative File.join(%w(.. helpers keystone_config)) - -describe Chef::Libraries::Pacemaker::CIBObjects do - include Chef::Libraries::Pacemaker::CIBObjects - - shared_context "shellout stubs" do - before(:each) do - Mixlib::ShellOut.any_instance.stub(:run_command) - end - end - - shared_context "keystone config" do - let(:ra) { Chef::RSpec::Pacemaker::Config::RA } - end - - shared_context "keystone primitive" do - include_context "shellout stubs" - include_context "keystone config" - - before(:each) do - Mixlib::ShellOut.any_instance.stub(:error!) - expect_any_instance_of(Mixlib::ShellOut) \ - .to receive(:stdout) \ - .and_return(ra[:config]) - end - end - - shared_context "no keystone primitive" do - include_context "shellout stubs" - before(:each) do - expect_any_instance_of(Mixlib::ShellOut) \ - .to receive(:error!) \ - .and_raise(RuntimeError) - end - end - - describe "#get_cib_object_definition" do - include_context "keystone primitive" - - it "should retrieve cluster config" do - expect(get_cib_object_definition("keystone")).to eq(ra[:config]) - end - end - - describe "#get_cib_object_definition" do - include_context "no keystone primitive" - - it "should return nil cluster config" do - expect(get_cib_object_definition("keystone")).to eq(nil) - end - end - - describe "#cib_object_exists?" do - include_context "keystone primitive" - - it "should return true" do - expect(cib_object_exists?("keystone")).to be(true) - end - end - - describe "#cib_object_exists?" do - include_context "no keystone primitive" - - it "should return false" do - expect(cib_object_exists?("keystone")).to be(false) - end - end - - describe "#cib_object_type" do - include_context "keystone config" - - it "should return primitive" do - expect(cib_object_type(ra[:config])).to eq("primitive") - end - - it "should raise an error without valid config" do - expect { cib_object_type("nonsense") }.to raise_error - end - end - - describe "#pacemaker_resource_running?" do - before(:each) do - @cmd = double(Mixlib::ShellOut) - expect(self).to receive(:shell_out!) \ - .with(*%w(crm resource status keystone)).ordered \ - .and_return(@cmd) - end - - it "should return true" do - expect(@cmd).to receive(:stdout).at_least(:once).and_return("resource keystone is running") - expect(pacemaker_resource_running?("keystone")).to be(true) - end - - it "should return false" do - expect(@cmd).to receive(:stdout).at_least(:once).and_return("resource keystone is not running") - expect(pacemaker_resource_running?("keystone")).to be(false) - end - end - - describe "#resource_params_string" do - it "should return empty string with nil params" do - expect(resource_params_string(nil)).to eq("") - end - - it "should return empty string with empty params" do - expect(resource_params_string({})).to eq("") - end - - it "should return a resource params string" do - params = { - "foo" => "bar", - "baz" => "qux", - } - expect(resource_params_string(params)).to eq(%' params baz="qux" foo="bar"') - end - end - - describe "#resource_meta_string" do - it "should return empty string with nil meta" do - expect(resource_meta_string(nil)).to eq("") - end - - it "should return empty string with empty meta" do - expect(resource_meta_string({})).to eq("") - end - - it "should return a resource meta string" do - meta = { - "foo" => "bar", - "baz" => "qux", - } - expect(resource_meta_string(meta)).to eq(%' meta baz="qux" foo="bar"') - end - end - - describe "#resource_op_string" do - it "should return empty string with nil op" do - expect(resource_op_string(nil)).to eq("") - end - - it "should return empty string with empty op" do - expect(resource_op_string({})).to eq("") - end - - it "should return a resource op string" do - op = { - "monitor" => { - "foo" => "bar", - "baz" => "qux", - } - } - expect(resource_op_string(op)).to eq(%' op monitor baz="qux" foo="bar"') - end - end - - describe "#extract_hash" do - include_context "keystone config" - - it "should extract a hash from config" do - expect(extract_hash("keystone", ra[:config], "params")).to eq(Hash[ra[:params]]) - end - - end - -end diff --git a/spec/libraries/primitive_spec.rb b/spec/libraries/primitive_spec.rb new file mode 100644 index 0000000..4619d8c --- /dev/null +++ b/spec/libraries/primitive_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' +require_relative File.join(%w(.. .. libraries pacemaker resource primitive)) +require_relative File.join(%w(.. helpers keystone_primitive)) + +describe Pacemaker::Resource::Primitive do + before(:each) do + @primitive = Chef::RSpec::Pacemaker::Config::KEYSTONE_PRIMITIVE.dup + Mixlib::ShellOut.any_instance.stub(:run_command) + end + + it "should barf if the loaded definition's type is not primitive" do + Mixlib::ShellOut.any_instance.stub(:error!) + expect_any_instance_of(Mixlib::ShellOut) \ + .to receive(:stdout) \ + .and_return("clone foo blah blah") + expect { @primitive.load_definition }.to \ + raise_error(Pacemaker::ObjectTypeMismatch, + "Expected primitive type but loaded definition was type clone") + end + + describe "#params_string" do + it "should return empty string with nil params" do + @primitive.params = nil + expect(@primitive.params_string).to eq("") + end + + it "should return empty string with empty params" do + @primitive.params = {} + expect(@primitive.params_string).to eq("") + end + + it "should return a resource params string" do + @primitive.params = { + "foo" => "bar", + "baz" => "qux", + } + expect(@primitive.params_string).to eq(%' params baz="qux" foo="bar"') + end + end + + describe "#meta_string" do + it "should return empty string with nil meta" do + @primitive.meta = nil + expect(@primitive.meta_string).to eq("") + end + + it "should return empty string with empty meta" do + @primitive.meta = {} + expect(@primitive.meta_string).to eq("") + end + + it "should return a resource meta string" do + @primitive.meta = { + "foo" => "bar", + "baz" => "qux", + } + expect(@primitive.meta_string).to eq(%' meta baz="qux" foo="bar"') + end + end + + describe "#op_string" do + it "should return empty string with nil op" do + @primitive.op = nil + expect(@primitive.op_string).to eq("") + end + + it "should return empty string with empty op" do + @primitive.op = {} + expect(@primitive.op_string).to eq("") + end + + it "should return a resource op string" do + @primitive.op = { + "monitor" => { + "foo" => "bar", + "baz" => "qux", + } + } + expect(@primitive.op_string).to eq(%' op monitor baz="qux" foo="bar"') + end + end + + describe "::extract_hash" do + it "should extract a hash from config" do + expect(@primitive.class.extract_hash(@primitive.definition_string, "params")).to \ + eq(Hash[@primitive.params]) + end + end + + describe "#definition_string" do + it "should return the definition string" do + expect(@primitive.definition_string).to \ + eq(Chef::RSpec::Pacemaker::Config::KEYSTONE_PRIMITIVE_DEFINITION) + end + end + + describe "#parse_definition" do + before(:each) do + @parsed = Pacemaker::Resource::Primitive.new(@primitive.name) + @parsed.definition = Chef::RSpec::Pacemaker::Config::KEYSTONE_PRIMITIVE_DEFINITION + @parsed.parse_definition + end + + it "should parse the agent" do + expect(@parsed.agent).to eq(@primitive.agent) + end + end +end diff --git a/spec/libraries/resource_spec.rb b/spec/libraries/resource_spec.rb new file mode 100644 index 0000000..a203449 --- /dev/null +++ b/spec/libraries/resource_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require_relative File.join(%w(.. .. libraries pacemaker resource)) + +describe Pacemaker::Resource do + describe "#running?" do + let(:rsc) { Pacemaker::Resource.new('keystone') } + + before(:each) do + @cmd = double(Mixlib::ShellOut) + expect(rsc).to receive(:shell_out!) \ + .with(*%w(crm resource status keystone)) \ + .and_return(@cmd) + end + + it "should return true" do + expect(@cmd).to receive(:stdout).at_least(:once) \ + .and_return("resource #{rsc.name} is running on: d52-54-00-e5-6b-a0") + expect(rsc.running?).to be(true) + end + + it "should return false" do + expect(@cmd).to receive(:stdout).at_least(:once) \ + .and_return("resource #{rsc.name} is NOT running") + expect(rsc.running?).to be(false) + end + end +end diff --git a/spec/providers/primitive_spec.rb b/spec/providers/primitive_spec.rb index c1d037e..db944e8 100644 --- a/spec/providers/primitive_spec.rb +++ b/spec/providers/primitive_spec.rb @@ -1,12 +1,12 @@ require 'chef/application' require_relative File.join(%w(.. spec_helper)) -require_relative File.join(%w(.. helpers keystone_config)) +require_relative File.join(%w(.. helpers keystone_primitive)) describe "Chef::Provider::PacemakerPrimitive" do # for use inside examples: - let(:ra) { Chef::RSpec::Pacemaker::Config::RA } + let(:rsc) { Chef::RSpec::Pacemaker::Config::KEYSTONE_PRIMITIVE } # for use outside examples (e.g. when invoking shared_examples) - ra = Chef::RSpec::Pacemaker::Config::RA + rsc = Chef::RSpec::Pacemaker::Config::KEYSTONE_PRIMITIVE before(:each) do runner_opts = { @@ -17,27 +17,48 @@ describe "Chef::Provider::PacemakerPrimitive" do @node = @chef_run.node @run_context = @chef_run.run_context - @resource = Chef::Resource::PacemakerPrimitive.new(ra[:name], @run_context) - @resource.agent ra[:agent] - @resource.params Hash[ra[:params]] - @resource.meta Hash[ra[:meta]] - @resource.op Hash[ra[:op]] + @resource = Chef::Resource::PacemakerPrimitive.new(rsc.name, @run_context) + @resource.agent rsc.agent + @resource.params Hash[rsc.params] + @resource.meta Hash[rsc.meta] + @resource.op Hash[rsc.op] end let (:provider) { Chef::Provider::PacemakerPrimitive.new(@resource, @run_context) } + # "crm configure show" is executed by load_current_resource, and + # again later on for the :create action, to see whether to create or + # modify. + def expect_definition(definition) + Mixlib::ShellOut.any_instance.stub(:run_command) + Mixlib::ShellOut.any_instance.stub(:error!) + expect_any_instance_of(Mixlib::ShellOut) \ + .to receive(:stdout) \ + .and_return(definition) + end + + def expect_exists(exists) + expect_any_instance_of(Pacemaker::Resource::Primitive) \ + .to receive(:exists?) \ + .and_return(exists) + end + + def expect_running(running) + expect_any_instance_of(Pacemaker::Resource::Primitive) \ + .to receive(:running?) \ + .and_return(running) + end + describe ":create action" do it "should modify the primitive if it already exists" do - new_params = Hash[ra[:params]].merge("os_password" => "newpasswd") + new_params = Hash[rsc.params].merge("os_password" => "newpasswd") new_params.delete("os_tenant_name") @resource.params new_params - @resource.meta Hash[ra[:meta]].merge("target-role" => "Stopped") + @resource.meta Hash[rsc.meta].merge("target-role" => "Stopped") - # get_cib_object_definition is invoked by load_current_resource - # and later used to see whether to create or modify. - expect(provider).to receive(:get_cib_object_definition).and_return(ra[:config]) + expect_definition(rsc.definition_string) - configure_cmd_prefix = "crm_resource --resource #{ra[:name]}" + configure_cmd_prefix = "crm_resource --resource #{rsc.name}" expected_configure_cmd_args = [ %'--set-parameter "os_password" --parameter-value "newpasswd"', %'--delete-parameter "os_tenant_name"', @@ -45,6 +66,7 @@ describe "Chef::Provider::PacemakerPrimitive" do ] provider.run_action :create + expected_configure_cmd_args.each do |args| cmd = configure_cmd_prefix + " " + args expect(@chef_run).to run_execute(cmd) @@ -53,33 +75,24 @@ describe "Chef::Provider::PacemakerPrimitive" do end it "should create a primitive if it doesn't already exist" do - # get_cib_object_definition is invoked by load_current_resource - # and later used to see whether to create or modify. - expect(provider).to receive(:get_cib_object_definition).and_return("") - - # Later, the :create action calls cib_object_exists? to check + expect_definition("") + # Later, the :create action calls Pacemaker::Resource::Primitive#exists? to check # that creation succeeded. - expect(provider).to receive(:cib_object_exists?).and_return(true) + expect_exists(true) provider.run_action :create - cmd = "crm configure primitive #{ra[:name]} #{ra[:agent]}" + \ - ra[:params_string] + ra[:meta_string] + ra[:op_string] - expect(@chef_run).to run_execute(cmd) - + expect(@chef_run).to run_execute(rsc.crm_configure_command) expect(@resource).to be_updated end - it "should barf if the primitive has the wrong agent" do + it "should barf if the primitive is already defined with the wrong agent" do existing_agent = "ocf:openstack:something-else" - config = ra[:config].sub(ra[:agent], existing_agent) - - # get_cib_object_definition is invoked by load_current_resource - # and later used to see whether to create or modify. - expect(provider).to receive(:get_cib_object_definition).and_return(config) + definition = rsc.definition_string.sub(rsc.agent, existing_agent) + expect_definition(definition) expected_error = \ - "Existing resource primitive '#{ra[:name]}' has agent '#{existing_agent}' " \ + "Existing resource primitive '#{rsc.name}' has agent '#{existing_agent}' " \ "but recipe wanted '#{@resource.agent}'" expect { provider.run_action :create }.to \ raise_error(RuntimeError, expected_error) @@ -90,8 +103,7 @@ describe "Chef::Provider::PacemakerPrimitive" do shared_examples "action on non-existent resource" do |action, cmd, expected_error| it "should not attempt to #{action.to_s} a non-existent resource" do - # get_cib_object_definition is invoked by load_current_resource - expect(provider).to receive(:get_cib_object_definition).once.and_return("") + expect_definition("") if expected_error expect { provider.run_action action }.to \ @@ -107,28 +119,28 @@ describe "Chef::Provider::PacemakerPrimitive" do describe ":delete action" do it_should_behave_like "action on non-existent resource", \ - :delete, "crm configure delete #{ra[:name]}", nil + :delete, "crm configure delete #{rsc.name}", nil it "should not delete a running resource" do - # get_cib_object_definition is invoked by load_current_resource - expect(provider).to receive(:get_cib_object_definition).once.and_return(ra[:config]) + expect_definition(rsc.definition_string) + expect_running(true) - expect(provider).to receive(:pacemaker_resource_running?).once.and_return(true) - cmd = "crm configure delete #{ra[:name]}" - expected_error = "Cannot delete running resource primitive #{ra[:name]}" + expected_error = "Cannot delete running resource primitive #{rsc.name}" expect { provider.run_action :delete }.to \ raise_error(RuntimeError, expected_error) + + cmd = "crm configure delete '#{rsc.name}'" expect(@chef_run).not_to run_execute(cmd) expect(@resource).not_to be_updated end it "should delete a non-running resource" do - # get_cib_object_definition is invoked by load_current_resource - expect(provider).to receive(:get_cib_object_definition).once.and_return(ra[:config]) + expect_definition(rsc.definition_string) + expect_running(false) - expect(provider).to receive(:pacemaker_resource_running?).once.and_return(false) - cmd = "crm configure delete #{ra[:name]}" provider.run_action :delete + + cmd = "crm configure delete '#{rsc.name}'" expect(@chef_run).to run_execute(cmd) expect(@resource).to be_updated end @@ -137,30 +149,28 @@ describe "Chef::Provider::PacemakerPrimitive" do describe ":start action" do it_should_behave_like "action on non-existent resource", \ :start, - "crm resource start #{ra[:name]}", \ - "Cannot start non-existent resource primitive '#{ra[:name]}'" + "crm resource start #{rsc.name}", \ + "Cannot start non-existent resource primitive '#{rsc.name}'" it "should do nothing to a started resource" do - # get_cib_object_definition is invoked by load_current_resource - expect(provider).to receive(:get_cib_object_definition).and_return(ra[:config]) + expect_definition(rsc.definition_string) + expect_running(true) - expect(provider).to receive(:pacemaker_resource_running?).once.and_return(true) - - cmd = "crm resource start #{ra[:name]}" provider.run_action :start + + cmd = "crm resource start #{rsc.name}" expect(@chef_run).not_to run_execute(cmd) expect(@resource).not_to be_updated end it "should start a stopped resource" do - config = ra[:config].sub("Started", "Stopped") - # get_cib_object_definition is invoked by load_current_resource - expect(provider).to receive(:get_cib_object_definition).and_return(config) + config = rsc.definition_string.sub("Started", "Stopped") + expect_definition(config) + expect_running(false) - expect(provider).to receive(:pacemaker_resource_running?).once.and_return(false) - - cmd = "crm resource start #{ra[:name]}" provider.run_action :start + + cmd = "crm resource start '#{rsc.name}'" expect(@chef_run).to run_execute(cmd) expect(@resource).to be_updated end @@ -169,29 +179,27 @@ describe "Chef::Provider::PacemakerPrimitive" do describe ":stop action" do it_should_behave_like "action on non-existent resource", \ :stop, - "crm resource stop #{ra[:name]}", \ - "Cannot stop non-existent resource primitive '#{ra[:name]}'" + "crm resource stop #{rsc.name}", \ + "Cannot stop non-existent resource primitive '#{rsc.name}'" it "should do nothing to a stopped resource" do - # get_cib_object_definition is invoked by load_current_resource - expect(provider).to receive(:get_cib_object_definition).and_return(ra[:config]) + expect_definition(rsc.definition_string) + expect_running(false) - expect(provider).to receive(:pacemaker_resource_running?).once.and_return(false) - - cmd = "crm resource start #{ra[:name]}" provider.run_action :stop + + cmd = "crm resource start #{rsc.name}" expect(@chef_run).not_to run_execute(cmd) expect(@resource).not_to be_updated end it "should stop a started resource" do - # get_cib_object_definition is invoked by load_current_resource - expect(provider).to receive(:get_cib_object_definition).and_return(ra[:config]) + expect_definition(rsc.definition_string) + expect_running(true) - expect(provider).to receive(:pacemaker_resource_running?).once.and_return(true) - - cmd = "crm resource stop #{ra[:name]}" provider.run_action :stop + + cmd = "crm resource stop '#{rsc.name}'" expect(@chef_run).to run_execute(cmd) expect(@resource).to be_updated end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d580114..10b218c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -17,7 +17,7 @@ RSpec.configure do |config| #config.role_path = '/var/roles' # Specify the Chef log_level (default: :warn) - #config.log_level = :debug + config.log_level = ENV['CHEF_LOG_LEVEL'].to_sym if ENV['CHEF_LOG_LEVEL'] # Specify the path to a local JSON file with Ohai data (default: nil) #config.path = 'ohai.json'