diff --git a/libraries/pacemaker.rb b/libraries/pacemaker.rb index eb69903..4c07a13 100644 --- a/libraries/pacemaker.rb +++ b/libraries/pacemaker.rb @@ -1,3 +1,4 @@ require File.expand_path('pacemaker/resource/primitive', File.dirname(__FILE__)) require File.expand_path('pacemaker/resource/clone', File.dirname(__FILE__)) +require File.expand_path('pacemaker/resource/group', File.dirname(__FILE__)) require File.expand_path('pacemaker/constraint/colocation', File.dirname(__FILE__)) diff --git a/libraries/pacemaker/resource/group.rb b/libraries/pacemaker/resource/group.rb new file mode 100644 index 0000000..c658ff2 --- /dev/null +++ b/libraries/pacemaker/resource/group.rb @@ -0,0 +1,43 @@ +require File.expand_path('../resource', File.dirname(__FILE__)) +require File.expand_path('../mixins/resource_meta', File.dirname(__FILE__)) + +class Pacemaker::Resource::Group < Pacemaker::Resource + TYPE = 'group' + register_type TYPE + + include Pacemaker::Resource::Meta + + attr_accessor :members + + def self.from_chef_resource(resource) + attrs = %w(members meta) + new(resource.name).copy_attrs_from_chef_resource(resource, *attrs) + end + + def parse_definition + rsc_re = /(\S+?)(?::(Started|Stopped))?/ + unless definition =~ /^#{TYPE} (\S+) (.+?)(\s+\\)?$/ + raise Pacemaker::CIBObject::DefinitionParseError, \ + "Couldn't parse definition '#{definition}'" + end + self.name = $1 + members = $2.split + trim_from = members.find_index('meta') + members = members[0..trim_from-1] if trim_from + self.members = members + self.meta = self.class.extract_hash(definition, 'meta') + end + + def definition_string + str = "#{TYPE} #{name} " + members.join(' ') + unless meta.empty? + str << continuation_line(meta_string) + end + str + end + + def crm_configure_command + "crm configure " + definition_string + end + +end diff --git a/providers/group.rb b/providers/group.rb new file mode 100644 index 0000000..73317fd --- /dev/null +++ b/providers/group.rb @@ -0,0 +1,78 @@ +# Cookbook Name:: pacemaker +# Provider:: group +# +# Copyright:: 2014, SUSE +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require ::File.expand_path('../libraries/pacemaker', ::File.dirname(__FILE__)) +require ::File.expand_path('../libraries/chef/mixin/pacemaker', + ::File.dirname(__FILE__)) + +include Chef::Mixin::Pacemaker::RunnableResource + +action :create do + name = new_resource.name + + if @current_resource_definition.nil? + create_resource(name) + else + maybe_modify_resource(name) + end +end + +action :delete do + delete_runnable_resource +end + +action :start do + start_runnable_resource +end + +action :stop do + stop_runnable_resource +end + +def cib_object_class + ::Pacemaker::Resource::Group +end + +def load_current_resource + standard_load_current_resource +end + +def init_current_resource + name = @new_resource.name + @current_resource = Chef::Resource::PacemakerGroup.new(name) + attrs = [:members] + @current_cib_object.copy_attrs_to_chef_resource(@current_resource, *attrs) +end + +def create_resource(name) + standard_create_resource +end + +def maybe_modify_resource(name) + Chef::Log.info "Checking existing #{@current_cib_object} for modifications" + + desired_group = cib_object_class.from_chef_resource(new_resource) + if desired_group.definition_string != @current_cib_object.definition_string + Chef::Log.debug "changed from [#{@current_cib_object.definition_string}] to [#{desired_group.definition_string}]" + cmd = desired_group.reconfigure_command + execute cmd do + action :nothing + end.run_action(:run) + new_resource.updated_by_last_action(true) + end +end diff --git a/resources/group.rb b/resources/group.rb new file mode 100644 index 0000000..32fca08 --- /dev/null +++ b/resources/group.rb @@ -0,0 +1,25 @@ +# Cookbook Name:: pacemaker +# Resource:: group +# +# Copyright:: 2014, SUSE +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +actions :create, :delete, :start, :stop + +default_action :create + +attribute :name, :kind_of => String, :name_attribute => true +attribute :members, :kind_of => Array +attribute :meta, :kind_of => Hash diff --git a/spec/fixtures/resource_group.rb b/spec/fixtures/resource_group.rb new file mode 100644 index 0000000..485ed1c --- /dev/null +++ b/spec/fixtures/resource_group.rb @@ -0,0 +1,20 @@ +require File.expand_path('../../libraries/pacemaker/resource/group', + File.dirname(__FILE__)) + +module Chef::RSpec + module Pacemaker + module Config + RESOURCE_GROUP = \ + ::Pacemaker::Resource::Group.new('group1') + RESOURCE_GROUP.members = ['resource1', 'resource2'] + RESOURCE_GROUP.meta = [ + [ "target-role", "Started" ], + [ "is-managed", "true" ] + ] + RESOURCE_GROUP_DEFINITION = <<'EOF'.chomp +group group1 resource1 resource2 \ + meta is-managed="true" target-role="Started" +EOF + end + end +end diff --git a/spec/libraries/pacemaker/resource/group_spec.rb b/spec/libraries/pacemaker/resource/group_spec.rb new file mode 100644 index 0000000..ef3d180 --- /dev/null +++ b/spec/libraries/pacemaker/resource/group_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' +require File.expand_path('../../../../libraries/pacemaker/resource/group', + File.dirname(__FILE__)) +require File.expand_path('../../../fixtures/resource_group', File.dirname(__FILE__)) +require File.expand_path('../../../helpers/cib_object', File.dirname(__FILE__)) +require File.expand_path('../../../helpers/meta_examples', + File.dirname(__FILE__)) + +describe Pacemaker::Resource::Group do + let(:fixture) { Chef::RSpec::Pacemaker::Config::RESOURCE_GROUP.dup } + let(:fixture_definition) { + Chef::RSpec::Pacemaker::Config::RESOURCE_GROUP_DEFINITION + } + + before(:each) do + Mixlib::ShellOut.any_instance.stub(:run_command) + end + + def object_type + 'group' + end + + def pacemaker_object_class + Pacemaker::Resource::Group + end + + def fields + %w(name members) + end + + it_should_behave_like "a CIB object" + + it_should_behave_like "with meta attributes" + + describe "#definition_string" do + it "should return the definition string" do + expect(fixture.definition_string).to eq(fixture_definition) + end + + it "should return a short definition string" do + primitive = Pacemaker::Resource::Group.new('foo') + primitive.definition = \ + %!group foo member1 member2 meta target-role="Started"! + primitive.parse_definition + expect(primitive.definition_string).to eq(<<'EOF'.chomp) +group foo member1 member2 \ + meta target-role="Started" +EOF + end + end + + describe "#parse_definition" do + before(:each) do + @parsed = Pacemaker::Resource::Group.new(fixture.name) + @parsed.definition = fixture_definition + @parsed.parse_definition + end + + it "should parse the members" do + expect(@parsed.members).to eq(fixture.members) + end + end +end diff --git a/spec/providers/group_spec.rb b/spec/providers/group_spec.rb new file mode 100644 index 0000000..fbc28b6 --- /dev/null +++ b/spec/providers/group_spec.rb @@ -0,0 +1,70 @@ +require 'chef/application' +require File.expand_path('../spec_helper', File.dirname(__FILE__)) +require File.expand_path('../helpers/cib_object', File.dirname(__FILE__)) +require File.expand_path('../helpers/runnable_resource', File.dirname(__FILE__)) +require File.expand_path('../fixtures/resource_group', File.dirname(__FILE__)) + +describe "Chef::Provider::PacemakerGroup" do + # for use inside examples: + let(:fixture) { Chef::RSpec::Pacemaker::Config::RESOURCE_GROUP.dup } + # for use outside examples (e.g. when invoking shared_examples) + fixture = Chef::RSpec::Pacemaker::Config::RESOURCE_GROUP.dup + + before(:each) do + runner_opts = { + :step_into => ['pacemaker_group'] + } + @chef_run = ::ChefSpec::Runner.new(runner_opts) + @chef_run.converge "pacemaker::default" + @node = @chef_run.node + @run_context = @chef_run.run_context + + @resource = Chef::Resource::PacemakerGroup.new(fixture.name, @run_context) + @resource.members fixture.members.dup + @resource.meta Hash[fixture.meta.dup] + end + + let (:provider) { Chef::Provider::PacemakerGroup.new(@resource, @run_context) } + + def cib_object_class + Pacemaker::Resource::Group + end + + include Chef::RSpec::Pacemaker::CIBObject + + describe ":create action" do + def test_modify(expected_cmds) + yield + + expect_definition(fixture.definition_string) + + provider.run_action :create + + expected_cmds.each do |cmd| + expect(@chef_run).to run_execute(cmd) + end + expect(@resource).to be_updated + end + + it "should modify the group if it has a member resource added" do + expected = fixture.dup + expected.members = expected.members.dup + %w(resource3) + expected_configure_cmd_args = [expected.reconfigure_command] + test_modify(expected_configure_cmd_args) do + @resource.members expected.members + end + end + + it "should modify the group if it has different member resources" do + fixture.members = %w(resource1 resource3) + expected_configure_cmd_args = [fixture.reconfigure_command] + test_modify(expected_configure_cmd_args) do + @resource.members fixture.members + end + end + + end + + it_should_behave_like "a runnable resource", fixture + +end