diff --git a/bindep.txt b/bindep.txt index 01b2ca6b..17bc5314 100644 --- a/bindep.txt +++ b/bindep.txt @@ -9,4 +9,8 @@ ruby-devel [test platform:rpm] ruby-dev [test platform:dpkg] zlib1g-dev [test platform:dpkg] zlib-devel [test platform:rpm] +pacemaker-cli-utils [test platform:dpkg] +pacemaker-cli [test platform:rpm] +pcs [test platform:rpm] +pcs [test platform:dpkg] puppet [build] diff --git a/lib/puppet/provider/pcmk_common.rb b/lib/puppet/provider/pcmk_common.rb index 39613945..e61ddcbd 100644 --- a/lib/puppet/provider/pcmk_common.rb +++ b/lib/puppet/provider/pcmk_common.rb @@ -1,4 +1,5 @@ require 'digest' +require 'rexml/document' # Constants that represent the state of a resource/constraint PCMK_NOCHANGENEEDED = 0 @@ -173,6 +174,133 @@ def build_pcs_location_rule_cmd(resource) if location_rule['expression'] location_cmd += " " + location_rule['expression'].join(' ') end - Puppet.debug("build_location_rule_cmd: #{location_cmd}") + Puppet.debug("build_pcs_location_rule_cmd: #{location_cmd}") location_cmd end + +# This method runs a pcs command on an offline cib +# Much simpler logic compared to pcs() +# return output for good exit or false for failure. +def pcs_offline(cmd, cib) + Puppet.debug("pcs_offline: /usr/sbin/pcs -f #{cib} #{cmd}") + pcs_out = `/usr/sbin/pcs -f #{cib} #{cmd}` + return $?.exitstatus == 0 ? pcs_out : false +end + +# This is a loop that simply tries to push a CIB a number of time +# on to the live cluster. It does not remove the CIB except in the Error +# case. Returns nothing in case of success and errors out in case of errors +def push_cib_offline(cib, tries=1, try_sleep=0, post_success_sleep=0) + tries.times do |try| + try_text = tries > 1 ? "try #{try+1}/#{tries}: " : '' + Puppet.debug("pcs_cib_offline push #{try_text}") + if push_cib(cib) == 0 + sleep post_success_sleep + return + end + Puppet.debug("Error: #{pcs_out}") + if try == tries-1 + delete_cib(cib) + raise Puppet::Error, "push_cib_offline for #{cib} failed" + end + if try_sleep > 0 + Puppet.debug("Sleeping for #{try_sleep} seconds between tries") + sleep try_sleep + end + end +end + +# The following function will take a resource_name an xml graph file as generated by crm_simulate and +# will return true if the resource_name is contained in the transition graph (i.e. the cluster would +# restart the resource) and false if not (i.e. the cluster would not restart the resource) +def pcmk_graph_contain_id?(resource_name, graph_file, is_bundle=false) + graph = File.new(graph_file) + graph_doc = REXML::Document.new graph + xpath_query = '/transition_graph//primitive/@id' + ids = [] + REXML::XPath.each(graph_doc, xpath_query) do |element| + id = element.to_s + # if we are a bundle we compare the start of the strings + # because the primitive id will be in the form of galera-bundle-1 as opposed to galera-bundle + if is_bundle then + if id.start_with?(resource_name) then + return true + end + else + if id == resource_name then + return true + end + end + end + return false +end + +# This given a cib and a resource name, this method returns true if pacemaker +# will restart the resource false if no action will be taken by pacemaker +# Note that we need to leverage crm_simulate instead of crm_diff due to: +# https://bugzilla.redhat.com/show_bug.cgi?id=1561617 +def pcmk_restart_resource?(resource_name, cib, is_bundle=false) + tmpfile = pcmk_tmpname("#{PCMK_TMP_BASE}/puppet-cib-simulate", nil) + cmd = "/usr/sbin/crm_simulate -x #{cib} -s -G#{tmpfile}" + crm_out = `#{cmd}` + if $?.exitstatus != 0 + FileUtils.rm(tmpfile, :force => true) + delete_cib(cib) + raise Puppet::Error, "#{cmd} failed with: #{crm_out}" + end + # Now in tmpfile we have the xml of the changes to the cluster + # If tmpfile only contains one empy no changes took place + ret = pcmk_graph_contain_id?(resource_name, tmpfile, is_bundle) + FileUtils.rm(tmpfile, :force => true) + return ret +end + +# This method takes a resource and a creation command and does the following +# 1. Deletes the resource from the offline CIB +# 2. Recreates the resource on the offline CIB +# 3. Verifies if the pacemaker will restart the resource and returns true if the answer is a yes +def pcmk_resource_has_changed?(resource, cmd_create, is_bundle=false) + cib = backup_cib() + cmd_delete = "resource delete #{resource[:name]}" + ret = pcs_offline(cmd_delete, cib) + if ret == false + delete_cib(cib) + raise Puppet::Error, "#{cmd_delete} returned error. This should never happen." + end + ret = pcs_offline(cmd_create, cib) + if ret == false + delete_cib(cib) + raise Puppet::Error, "#{cmd_create} returned error. This should never happen." + end + ret = pcmk_restart_resource?(resource[:name], cib, is_bundle) + Puppet.debug("pcmk_resource_has_changed returned #{ret}") + delete_cib(cib) + return ret +end + +# This function will update a resource by making a cib backup +# removing the resource and readding it and the push the CIB +# to the cluster +def pcmk_update_resource(resource, cmd_create) + cib = backup_cib() + cmd_delete = "resource delete #{resource[:name]}" + ret = pcs_offline(cmd_delete, cib) + if ret == false + delete_cib(cib) + raise Puppet::Error, "#{cmd_delete} returned error. This should never happen." + end + ret = pcs_offline(cmd_create, cib) + if ret == false + delete_cib(cib) + raise Puppet::Error, "#{cmd_create} returned error. This should never happen." + end + if resource[:location_rule] then + cmd_location = build_pcs_location_rule_cmd(resource) + ret = pcs_offline(cmd_location, cib) + if ret == false + delete_cib(cib) + raise Puppet::Error, "#{cmd_location} returned error. This should never happen." + end + end + push_cib_offline(cib, resource[:tries], resource[:try_sleep], resource[:post_success_sleep]) +end diff --git a/spec/unit/puppet/provider/cib-orig.xml b/spec/unit/puppet/provider/cib-orig.xml new file mode 100644 index 00000000..0c3dfe9c --- /dev/null +++ b/spec/unit/puppet/provider/cib-orig.xml @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/unit/puppet/provider/pcmk_common_spec.rb b/spec/unit/puppet/provider/pcmk_common_spec.rb new file mode 100644 index 00000000..b0af6f05 --- /dev/null +++ b/spec/unit/puppet/provider/pcmk_common_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +require_relative '../../../../lib/puppet/provider/pcmk_common' + +describe "pcmk_common functions" do + before(:all) do + # FIXME: we need to stub this properly not via this hack + PCMK_TMP_BASE = "/tmp" + orig_cib = File.join File.dirname(__FILE__), 'cib-orig.xml' + FileUtils.cp orig_cib, "cib-noop.xml" + FileUtils.cp orig_cib, "cib-resource.xml" + FileUtils.cp orig_cib, "cib-bundle.xml" + end + + it "pcmk_graph_contain_id? raises proper exception" do + expect { pcmk_graph_contain_id?('foo', 'bar') }.to raise_error(Errno::ENOENT) + end + + it "pcs_offline noop update" do + expect(pcs_offline('resource update ip-172.16.11.97 cidr_netmask=32', 'cib-noop.xml')).to eq "" + end + it "pcmk_restart_resource? noop" do + expect(pcmk_restart_resource?('foo', "cib-noop.xml")).to eq false + expect(pcmk_restart_resource?('ip-172.16.11.97', "cib-noop.xml")).to eq false + end + + it "pcs_offline update to resource definition" do + expect(pcs_offline('resource update ip-172.16.11.97 cidr_netmask=31', 'cib-resource.xml')).to eq "" + end + it "pcmk_restart_resource? vip resource" do + expect(pcmk_restart_resource?('foo', "cib-resource.xml")).to eq false + expect(pcmk_restart_resource?('ip-172.16.11.97', "cib-resource.xml")).to eq true + end + + it "pcs_offline update to bundle definition" do + # We effectively change the number of replicas from 3 to 2 + expect(pcs_offline('resource delete test_bundle', 'cib-bundle.xml')).not_to eq false + cmd = 'resource bundle create test_bundle container docker image=docker.io/sdelrio/docker-minimal-nginx '\ + 'replicas=2 options="--user=root --log-driver=journald -e KOLLA_CONFIG_STRATEGY=COPY_ALWAYS" '\ + 'network=host storage-map id=haproxy-cfg-files source-dir=/var/lib/kolla/config_files/haproxy.json '\ + 'target-dir=/var/lib/kolla/config_files/config.json options=ro storage-map id=haproxy-cfg-data '\ + 'source-dir=/var/lib/config-data/puppet-generated/haproxy/ target-dir=/var/lib/kolla/config_files/src '\ + 'options=ro storage-map id=haproxy-hosts source-dir=/etc/hosts target-dir=/etc/hosts options=ro '\ + 'storage-map id=haproxy-localtime source-dir=/etc/localtime target-dir=/etc/localtime options=ro '\ + 'storage-map id=haproxy-pki-extracted source-dir=/etc/pki/ca-trust/extracted '\ + 'target-dir=/etc/pki/ca-trust/extracted options=ro storage-map id=haproxy-pki-ca-bundle-crt '\ + 'source-dir=/etc/pki/tls/certs/ca-bundle.crt target-dir=/etc/pki/tls/certs/ca-bundle.crt options=ro '\ + 'storage-map id=haproxy-pki-ca-bundle-trust-crt source-dir=/etc/pki/tls/certs/ca-bundle.trust.crt '\ + 'target-dir=/etc/pki/tls/certs/ca-bundle.trust.crt options=ro storage-map id=haproxy-pki-cert '\ + 'source-dir=/etc/pki/tls/cert.pem target-dir=/etc/pki/tls/cert.pem options=ro storage-map '\ + 'id=haproxy-dev-log source-dir=/dev/log target-dir=/dev/log options=rw' + expect(pcs_offline(cmd, 'cib-bundle.xml')).to eq "" + end + + it "pcmk_restart_resource? bundle resource" do + expect(pcmk_restart_resource?('foo', "cib-bundle.xml", true)).to eq false + expect(pcmk_restart_resource?('test_bundle', "cib-bundle.xml", true)).to eq true + end +end