diff --git a/cts/cli/regression.validity.exp b/cts/cli/regression.validity.exp
index 6a56f9d212..f020b20635 100644
--- a/cts/cli/regression.validity.exp
+++ b/cts/cli/regression.validity.exp
@@ -1,424 +1,424 @@
Created new pacemaker configuration
A new shadow instance was created. To begin using it, enter the following into your shell:
export CIB_shadow=cts-cli
=#=#=#= Begin test: Try to make resulting CIB invalid (enum violation) =#=#=#=
Call failed: Update does not conform to the configured schema
=#=#=#= Current cib after: Try to make resulting CIB invalid (enum violation) =#=#=#=
=#=#=#= End test: Try to make resulting CIB invalid (enum violation) - Invalid configuration (78) =#=#=#=
* Passed: cibadmin - Try to make resulting CIB invalid (enum violation)
=#=#=#= Begin test: Run crm_simulate with invalid CIB (enum violation) =#=#=#=
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-1.2 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-1.3 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.0 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.1 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.2 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.3 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.4 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.5 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.6 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.7 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.8 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.9 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.10 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.0 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.1 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.2 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.3 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.4 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.5 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.6 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.7 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.8 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.9 does not validate
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.10 does not validate
Cannot upgrade configuration (claiming pacemaker-1.2 schema) to at least pacemaker-3.0 because it does not validate with any schema from pacemaker-1.2 to the latest
=#=#=#= End test: Run crm_simulate with invalid CIB (enum violation) - Invalid configuration (78) =#=#=#=
* Passed: crm_simulate - Run crm_simulate with invalid CIB (enum violation)
=#=#=#= Begin test: Try to make resulting CIB invalid (unrecognized validate-with) =#=#=#=
Call failed: Update does not conform to the configured schema
=#=#=#= Current cib after: Try to make resulting CIB invalid (unrecognized validate-with) =#=#=#=
=#=#=#= End test: Try to make resulting CIB invalid (unrecognized validate-with) - Invalid configuration (78) =#=#=#=
* Passed: cibadmin - Try to make resulting CIB invalid (unrecognized validate-with)
=#=#=#= Begin test: Run crm_simulate with invalid CIB (unrecognized validate-with) =#=#=#=
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-1.0 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-1.2 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-1.3 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.0 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.1 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.2 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.3 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.4 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.5 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.6 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.7 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.8 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.9 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-2.10 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.0 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.1 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.2 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.3 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.4 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.5 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.6 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.7 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.8 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.9 does not validate
element cib: Relax-NG validity error : Invalid attribute validate-with for element cib
pcmk__update_schema debug: Schema pacemaker-3.10 does not validate
Cannot upgrade configuration (claiming pacemaker-9999.0 schema) to at least pacemaker-3.0 because it does not validate with any schema from the first to the latest
=#=#=#= End test: Run crm_simulate with invalid CIB (unrecognized validate-with) - Invalid configuration (78) =#=#=#=
* Passed: crm_simulate - Run crm_simulate with invalid CIB (unrecognized validate-with)
=#=#=#= Begin test: Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1) =#=#=#=
Call failed: Update does not conform to the configured schema
=#=#=#= Current cib after: Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1) =#=#=#=
=#=#=#= End test: Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1) - Invalid configuration (78) =#=#=#=
* Passed: cibadmin - Try to make resulting CIB invalid, but possibly recoverable (valid with X.Y+1)
=#=#=#= Begin test: Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1) =#=#=#=
element tags: Relax-NG validity error : Element configuration has extra content: tags
pcmk__update_schema debug: Schema pacemaker-1.2 does not validate
pcmk__update_schema debug: Schema pacemaker-1.3 validates
pcmk__update_schema debug: Schema pacemaker-2.0 validates
pcmk__update_schema debug: Schema pacemaker-2.1 validates
pcmk__update_schema debug: Schema pacemaker-2.2 validates
pcmk__update_schema debug: Schema pacemaker-2.3 validates
pcmk__update_schema debug: Schema pacemaker-2.4 validates
pcmk__update_schema debug: Schema pacemaker-2.5 validates
pcmk__update_schema debug: Schema pacemaker-2.6 validates
pcmk__update_schema debug: Schema pacemaker-2.7 validates
pcmk__update_schema debug: Schema pacemaker-2.8 validates
pcmk__update_schema debug: Schema pacemaker-2.9 validates
pcmk__update_schema debug: Schema pacemaker-2.10 validates
pcmk__update_schema debug: Schema pacemaker-3.0 validates
pcmk__update_schema debug: Schema pacemaker-3.1 validates
pcmk__update_schema debug: Schema pacemaker-3.2 validates
pcmk__update_schema debug: Schema pacemaker-3.3 validates
pcmk__update_schema debug: Schema pacemaker-3.4 validates
pcmk__update_schema debug: Schema pacemaker-3.5 validates
pcmk__update_schema debug: Schema pacemaker-3.6 validates
pcmk__update_schema debug: Schema pacemaker-3.7 validates
pcmk__update_schema debug: Schema pacemaker-3.8 validates
pcmk__update_schema debug: Schema pacemaker-3.9 validates
pcmk__update_schema debug: Schema pacemaker-3.10 validates
pcmk__update_schema info: Transformed the configuration schema to pacemaker-3.10
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Current cluster status:
* Full List of Resources:
* dummy1 (ocf:pacemaker:Dummy): Stopped
* dummy2 (ocf:pacemaker:Dummy): Stopped
Transition Summary:
Executing Cluster Transition:
Revised Cluster Status:
* Full List of Resources:
* dummy1 (ocf:pacemaker:Dummy): Stopped
* dummy2 (ocf:pacemaker:Dummy): Stopped
=#=#=#= End test: Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1) - OK (0) =#=#=#=
* Passed: crm_simulate - Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1)
=#=#=#= Begin test: Make resulting CIB valid, although without validate-with attribute =#=#=#=
=#=#=#= Current cib after: Make resulting CIB valid, although without validate-with attribute =#=#=#=
=#=#=#= End test: Make resulting CIB valid, although without validate-with attribute - OK (0) =#=#=#=
* Passed: cibadmin - Make resulting CIB valid, although without validate-with attribute
=#=#=#= Begin test: Run crm_simulate with valid CIB, but without validate-with attribute =#=#=#=
-Schema validation of configuration is disabled (enabling is encouraged and prevents common misconfigurations)
+Schema validation of configuration is disabled (support for validate-with set to "none" is deprecated and will be removed in a future release)
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Current cluster status:
* Full List of Resources:
* dummy1 (ocf:pacemaker:Dummy): Stopped
* dummy2 (ocf:pacemaker:Dummy): Stopped
Transition Summary:
Executing Cluster Transition:
Revised Cluster Status:
* Full List of Resources:
* dummy1 (ocf:pacemaker:Dummy): Stopped
* dummy2 (ocf:pacemaker:Dummy): Stopped
=#=#=#= End test: Run crm_simulate with valid CIB, but without validate-with attribute - OK (0) =#=#=#=
* Passed: crm_simulate - Run crm_simulate with valid CIB, but without validate-with attribute
=#=#=#= Begin test: Make resulting CIB invalid, and without validate-with attribute =#=#=#=
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
=#=#=#= Current cib after: Make resulting CIB invalid, and without validate-with attribute =#=#=#=
=#=#=#= End test: Make resulting CIB invalid, and without validate-with attribute - OK (0) =#=#=#=
* Passed: cibadmin - Make resulting CIB invalid, and without validate-with attribute
=#=#=#= Begin test: Run crm_simulate with invalid CIB, also without validate-with attribute =#=#=#=
-Schema validation of configuration is disabled (enabling is encouraged and prevents common misconfigurations)
+Schema validation of configuration is disabled (support for validate-with set to "none" is deprecated and will be removed in a future release)
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Invalid attribute first-action for element rsc_order
validity.bad.xml:10: element rsc_order: Relax-NG validity error : Element constraints has extra content: rsc_order
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
unpack_resources error: Resource start-up disabled since no STONITH resources have been defined
unpack_resources error: Either configure some or disable STONITH with the stonith-enabled option
unpack_resources error: NOTE: Clusters with shared data need STONITH to ensure data integrity
Current cluster status:
* Full List of Resources:
* dummy1 (ocf:pacemaker:Dummy): Stopped
* dummy2 (ocf:pacemaker:Dummy): Stopped
Transition Summary:
Executing Cluster Transition:
Revised Cluster Status:
* Full List of Resources:
* dummy1 (ocf:pacemaker:Dummy): Stopped
* dummy2 (ocf:pacemaker:Dummy): Stopped
=#=#=#= End test: Run crm_simulate with invalid CIB, also without validate-with attribute - OK (0) =#=#=#=
* Passed: crm_simulate - Run crm_simulate with invalid CIB, also without validate-with attribute
diff --git a/cts/scheduler/stderr/order-wrong-kind.stderr b/cts/scheduler/stderr/order-wrong-kind.stderr
index 70903689a6..db35666903 100644
--- a/cts/scheduler/stderr/order-wrong-kind.stderr
+++ b/cts/scheduler/stderr/order-wrong-kind.stderr
@@ -1 +1 @@
-Schema validation of configuration is disabled (enabling is encouraged and prevents common misconfigurations)
+Schema validation of configuration is disabled (support for validate-with set to "none" is deprecated and will be removed in a future release)
\ No newline at end of file
diff --git a/cts/scheduler/summary/order-wrong-kind.summary b/cts/scheduler/summary/order-wrong-kind.summary
index 0e00bdf2fd..903a25c723 100644
--- a/cts/scheduler/summary/order-wrong-kind.summary
+++ b/cts/scheduler/summary/order-wrong-kind.summary
@@ -1,29 +1,29 @@
-Schema validation of configuration is disabled (enabling is encouraged and prevents common misconfigurations)
+Schema validation of configuration is disabled (support for validate-with set to "none" is deprecated and will be removed in a future release)
Current cluster status:
* Node List:
* Online: [ node1 ]
* Full List of Resources:
* rsc1 (ocf:heartbeat:apache): Stopped
* rsc2 (ocf:heartbeat:apache): Started node1
* rsc3 (ocf:heartbeat:apache): Stopped
* rsc4 (ocf:heartbeat:apache): Started node1
Transition Summary:
* Start rsc1 ( node1 )
* Restart rsc2 ( node1 ) due to required rsc1 start
Executing Cluster Transition:
* Resource action: rsc1 start on node1
* Resource action: rsc2 stop on node1
* Resource action: rsc2 start on node1
Revised Cluster Status:
* Node List:
* Online: [ node1 ]
* Full List of Resources:
* rsc1 (ocf:heartbeat:apache): Started node1
* rsc2 (ocf:heartbeat:apache): Started node1
* rsc3 (ocf:heartbeat:apache): Stopped
* rsc4 (ocf:heartbeat:apache): Started node1
diff --git a/daemons/based/based_io.c b/daemons/based/based_io.c
index f22582de40..7410b03287 100644
--- a/daemons/based/based_io.c
+++ b/daemons/based/based_io.c
@@ -1,477 +1,478 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
crm_trigger_t *cib_writer = NULL;
int write_cib_contents(gpointer p);
static void
cib_rename(const char *old)
{
int new_fd;
char *new = crm_strdup_printf("%s/cib.auto.XXXXXX", cib_root);
umask(S_IWGRP | S_IWOTH | S_IROTH);
new_fd = mkstemp(new);
if ((new_fd < 0) || (rename(old, new) < 0)) {
crm_err("Couldn't archive unusable file %s (disabling disk writes and continuing)",
old);
cib_writes_enabled = FALSE;
} else {
crm_err("Archived unusable file %s as %s", old, new);
}
if (new_fd > 0) {
close(new_fd);
}
free(new);
}
/*
* It is the callers responsibility to free the output of this function
*/
static xmlNode *
retrieveCib(const char *filename, const char *sigfile)
{
xmlNode *root = NULL;
crm_info("Reading cluster configuration file %s (digest: %s)",
filename, sigfile);
switch (cib_file_read_and_verify(filename, sigfile, &root)) {
case -pcmk_err_cib_corrupt:
crm_warn("Continuing but %s will NOT be used.", filename);
break;
case -pcmk_err_cib_modified:
/* Archive the original files so the contents are not lost */
crm_warn("Continuing but %s will NOT be used.", filename);
cib_rename(filename);
cib_rename(sigfile);
break;
}
return root;
}
/*
* for OSs without support for direntry->d_type, like Solaris
*/
#ifndef DT_UNKNOWN
# define DT_UNKNOWN 0
# define DT_FIFO 1
# define DT_CHR 2
# define DT_DIR 4
# define DT_BLK 6
# define DT_REG 8
# define DT_LNK 10
# define DT_SOCK 12
# define DT_WHT 14
#endif /*DT_UNKNOWN*/
static int cib_archive_filter(const struct dirent * a)
{
int rc = 0;
/* Looking for regular files (d_type = 8) starting with 'cib-' and not ending in .sig */
struct stat s;
char *a_path = crm_strdup_printf("%s/%s", cib_root, a->d_name);
if(stat(a_path, &s) != 0) {
rc = errno;
crm_trace("%s - stat failed: %s (%d)", a->d_name, pcmk_rc_str(rc), rc);
rc = 0;
} else if ((s.st_mode & S_IFREG) != S_IFREG) {
unsigned char dtype;
#ifdef HAVE_STRUCT_DIRENT_D_TYPE
dtype = a->d_type;
#else
switch (s.st_mode & S_IFMT) {
case S_IFREG: dtype = DT_REG; break;
case S_IFDIR: dtype = DT_DIR; break;
case S_IFCHR: dtype = DT_CHR; break;
case S_IFBLK: dtype = DT_BLK; break;
case S_IFLNK: dtype = DT_LNK; break;
case S_IFIFO: dtype = DT_FIFO; break;
case S_IFSOCK: dtype = DT_SOCK; break;
default: dtype = DT_UNKNOWN; break;
}
#endif
crm_trace("%s - wrong type (%d)", a->d_name, dtype);
} else if(strstr(a->d_name, "cib-") != a->d_name) {
crm_trace("%s - wrong prefix", a->d_name);
} else if (pcmk__ends_with_ext(a->d_name, ".sig")) {
crm_trace("%s - wrong suffix", a->d_name);
} else {
crm_debug("%s - candidate", a->d_name);
rc = 1;
}
free(a_path);
return rc;
}
static int cib_archive_sort(const struct dirent ** a, const struct dirent **b)
{
/* Order by creation date - most recently created file first */
int rc = 0;
struct stat buf;
time_t a_age = 0;
time_t b_age = 0;
char *a_path = crm_strdup_printf("%s/%s", cib_root, a[0]->d_name);
char *b_path = crm_strdup_printf("%s/%s", cib_root, b[0]->d_name);
if(stat(a_path, &buf) == 0) {
a_age = buf.st_ctime;
}
if(stat(b_path, &buf) == 0) {
b_age = buf.st_ctime;
}
free(a_path);
free(b_path);
if(a_age > b_age) {
rc = 1;
} else if(a_age < b_age) {
rc = -1;
}
crm_trace("%s (%lu) vs. %s (%lu) : %d",
a[0]->d_name, (unsigned long)a_age,
b[0]->d_name, (unsigned long)b_age, rc);
return rc;
}
xmlNode *
readCibXmlFile(const char *dir, const char *file, gboolean discard_status)
{
struct dirent **namelist = NULL;
int lpc = 0;
char *sigfile = NULL;
char *sigfilepath = NULL;
char *filename = NULL;
const char *name = NULL;
const char *value = NULL;
const char *validation = NULL;
const char *use_valgrind = pcmk__env_option(PCMK__ENV_VALGRIND_ENABLED);
xmlNode *root = NULL;
xmlNode *status = NULL;
sigfile = crm_strdup_printf("%s.sig", file);
if (pcmk__daemon_can_write(dir, file) == FALSE
|| pcmk__daemon_can_write(dir, sigfile) == FALSE) {
cib_status = -EACCES;
return NULL;
}
filename = crm_strdup_printf("%s/%s", dir, file);
sigfilepath = crm_strdup_printf("%s/%s", dir, sigfile);
free(sigfile);
cib_status = pcmk_ok;
root = retrieveCib(filename, sigfilepath);
free(filename);
free(sigfilepath);
if (root == NULL) {
crm_warn("Primary configuration corrupt or unusable, trying backups in %s", cib_root);
lpc = scandir(cib_root, &namelist, cib_archive_filter, cib_archive_sort);
if (lpc < 0) {
crm_err("scandir(%s) failed: %s", cib_root, pcmk_rc_str(errno));
}
}
while (root == NULL && lpc > 1) {
crm_debug("Testing %d candidates", lpc);
lpc--;
filename = crm_strdup_printf("%s/%s", cib_root, namelist[lpc]->d_name);
sigfile = crm_strdup_printf("%s.sig", filename);
crm_info("Reading cluster configuration file %s (digest: %s)",
filename, sigfile);
if (cib_file_read_and_verify(filename, sigfile, &root) < 0) {
crm_warn("Continuing but %s will NOT be used.", filename);
} else {
crm_notice("Continuing with last valid configuration archive: %s", filename);
}
free(namelist[lpc]);
free(filename);
free(sigfile);
}
free(namelist);
if (root == NULL) {
root = createEmptyCib(0);
crm_warn("Continuing with an empty configuration.");
}
if (cib_writes_enabled && use_valgrind &&
(crm_is_true(use_valgrind) || strstr(use_valgrind, "pacemaker-based"))) {
cib_writes_enabled = FALSE;
crm_err("*** Disabling disk writes to avoid confusing Valgrind ***");
}
status = pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL);
if (discard_status && status != NULL) {
// Strip out the PCMK_XE_STATUS section if there is one
free_xml(status);
status = NULL;
}
if (status == NULL) {
pcmk__xe_create(root, PCMK_XE_STATUS);
}
/* Do this before schema validation happens */
/* fill in some defaults */
name = PCMK_XA_ADMIN_EPOCH;
value = crm_element_value(root, name);
if (value == NULL) {
crm_warn("No value for %s was specified in the configuration.", name);
crm_warn("The recommended course of action is to shutdown,"
" run crm_verify and fix any errors it reports.");
crm_warn("We will default to zero and continue but may get"
" confused about which configuration to use if"
" multiple nodes are powered up at the same time.");
crm_xml_add_int(root, name, 0);
}
name = PCMK_XA_EPOCH;
value = crm_element_value(root, name);
if (value == NULL) {
crm_xml_add_int(root, name, 0);
}
name = PCMK_XA_NUM_UPDATES;
value = crm_element_value(root, name);
if (value == NULL) {
crm_xml_add_int(root, name, 0);
}
// Unset (DC should set appropriate value)
pcmk__xe_remove_attr(root, PCMK_XA_DC_UUID);
if (discard_status) {
crm_log_xml_trace(root, "[on-disk]");
}
validation = crm_element_value(root, PCMK_XA_VALIDATE_WITH);
if (!pcmk__configured_schema_validates(root)) {
crm_err("CIB does not validate with %s",
pcmk__s(validation, "no schema specified"));
cib_status = -pcmk_err_schema_validation;
+ // @COMPAT Not specifying validate-with is deprecated since 2.1.8
} else if (validation == NULL) {
pcmk__update_schema(&root, NULL, false, false);
validation = crm_element_value(root, PCMK_XA_VALIDATE_WITH);
if (validation != NULL) {
crm_notice("Enabling %s validation on"
" the existing (sane) configuration", validation);
} else {
crm_err("CIB does not validate with any known schema");
cib_status = -pcmk_err_schema_validation;
}
}
return root;
}
gboolean
uninitializeCib(void)
{
xmlNode *tmp_cib = the_cib;
if (tmp_cib == NULL) {
crm_debug("The CIB has already been deallocated.");
return FALSE;
}
the_cib = NULL;
crm_debug("Deallocating the CIB.");
free_xml(tmp_cib);
crm_debug("The CIB has been deallocated.");
return TRUE;
}
/*
* This method will free the old CIB pointer on success and the new one
* on failure.
*/
int
activateCibXml(xmlNode * new_cib, gboolean to_disk, const char *op)
{
if (new_cib) {
xmlNode *saved_cib = the_cib;
CRM_ASSERT(new_cib != saved_cib);
the_cib = new_cib;
free_xml(saved_cib);
if (cib_writes_enabled && cib_status == pcmk_ok && to_disk) {
crm_debug("Triggering CIB write for %s op", op);
mainloop_set_trigger(cib_writer);
}
return pcmk_ok;
}
crm_err("Ignoring invalid CIB");
if (the_cib) {
crm_warn("Reverting to last known CIB");
} else {
crm_crit("Could not write out new CIB and no saved version to revert to");
}
return -ENODATA;
}
static void
cib_diskwrite_complete(mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode)
{
const char *errmsg = "Could not write CIB to disk";
if ((exitcode != 0) && cib_writes_enabled) {
cib_writes_enabled = FALSE;
errmsg = "Disabling CIB disk writes after failure";
}
if ((signo == 0) && (exitcode == 0)) {
crm_trace("Disk write [%d] succeeded", (int) pid);
} else if (signo == 0) {
crm_err("%s: process %d exited %d", errmsg, (int) pid, exitcode);
} else {
crm_err("%s: process %d terminated with signal %d (%s)%s",
errmsg, (int) pid, signo, strsignal(signo),
(core? " and dumped core" : ""));
}
mainloop_trigger_complete(cib_writer);
}
int
write_cib_contents(gpointer p)
{
int exit_rc = pcmk_ok;
xmlNode *cib_local = NULL;
/* Make a copy of the CIB to write (possibly in a forked child) */
if (p) {
/* Synchronous write out */
cib_local = pcmk__xml_copy(NULL, p);
} else {
int pid = 0;
int bb_state = qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0);
/* Turn it off before the fork() to avoid:
* - 2 processes writing to the same shared mem
* - the child needing to disable it
* (which would close it from underneath the parent)
* This way, the shared mem files are already closed
*/
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE);
pid = fork();
if (pid < 0) {
crm_err("Disabling disk writes after fork failure: %s", pcmk_rc_str(errno));
cib_writes_enabled = FALSE;
return FALSE;
}
if (pid) {
/* Parent */
mainloop_child_add(pid, 0, "disk-writer", NULL, cib_diskwrite_complete);
if (bb_state == QB_LOG_STATE_ENABLED) {
/* Re-enable now that it it safe */
qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE);
}
return -1; /* -1 means 'still work to do' */
}
/* Asynchronous write-out after a fork() */
/* In theory, we can scribble on the_cib here and not affect the parent,
* but let's be safe anyway.
*/
cib_local = pcmk__xml_copy(NULL, the_cib);
}
/* Write the CIB */
exit_rc = cib_file_write_with_digest(cib_local, cib_root, "cib.xml");
/* A nonzero exit code will cause further writes to be disabled */
free_xml(cib_local);
if (p == NULL) {
crm_exit_t exit_code = CRM_EX_OK;
switch (exit_rc) {
case pcmk_ok:
exit_code = CRM_EX_OK;
break;
case pcmk_err_cib_modified:
exit_code = CRM_EX_DIGEST; // Existing CIB doesn't match digest
break;
case pcmk_err_cib_backup: // Existing CIB couldn't be backed up
case pcmk_err_cib_save: // New CIB couldn't be saved
exit_code = CRM_EX_CANTCREAT;
break;
default:
exit_code = CRM_EX_ERROR;
break;
}
/* Use _exit() because exit() could affect the parent adversely */
_exit(exit_code);
}
return exit_rc;
}
diff --git a/include/crm/common/schemas_internal.h b/include/crm/common/schemas_internal.h
index 425b539b0f..c12a820a7a 100644
--- a/include/crm/common/schemas_internal.h
+++ b/include/crm/common/schemas_internal.h
@@ -1,37 +1,38 @@
/*
* Copyright 2006-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#ifndef PCMK__SCHEMAS_INTERNAL__H
# define PCMK__SCHEMAS_INTERNAL__H
#include // GList, gboolean
#include // xmlNode, xmlRelaxNGValidityErrorFunc
void crm_schema_init(void);
void crm_schema_cleanup(void);
void pcmk__load_schemas_from_dir(const char *dir);
void pcmk__sort_schemas(void);
GList *pcmk__schema_files_later_than(const char *name);
void pcmk__build_schema_xml_node(xmlNode *parent, const char *name,
GList **already_included);
const char *pcmk__remote_schema_dir(void);
GList *pcmk__get_schema(const char *name);
const char *pcmk__highest_schema_name(void);
int pcmk__cmp_schemas_by_name(const char *schema1_name,
const char *schema2_name);
bool pcmk__validate_xml(xmlNode *xml_blob, const char *validation,
xmlRelaxNGValidityErrorFunc error_handler,
void *error_handler_context);
bool pcmk__configured_schema_validates(xmlNode *xml);
bool pcmk__update_configured_schema(xmlNode **xml, bool to_logs);
int pcmk__update_schema(xmlNode **xml, const char *max_schema_name,
bool transform, bool to_logs);
+void pcmk__warn_if_schema_deprecated(const char *schema);
#endif // PCMK__SCHEMAS_INTERNAL__H
diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c
index e80c7412a3..7ef378929e 100644
--- a/lib/cib/cib_utils.c
+++ b/lib/cib/cib_utils.c
@@ -1,1098 +1,1099 @@
/*
* Original copyright 2004 International Business Machines
* Later changes copyright 2008-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
gboolean
cib_version_details(xmlNode * cib, int *admin_epoch, int *epoch, int *updates)
{
*epoch = -1;
*updates = -1;
*admin_epoch = -1;
if (cib == NULL) {
return FALSE;
} else {
crm_element_value_int(cib, PCMK_XA_EPOCH, epoch);
crm_element_value_int(cib, PCMK_XA_NUM_UPDATES, updates);
crm_element_value_int(cib, PCMK_XA_ADMIN_EPOCH, admin_epoch);
}
return TRUE;
}
gboolean
cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates,
int *_admin_epoch, int *_epoch, int *_updates)
{
int add[] = { 0, 0, 0 };
int del[] = { 0, 0, 0 };
xml_patch_versions(diff, add, del);
*admin_epoch = add[0];
*epoch = add[1];
*updates = add[2];
*_admin_epoch = del[0];
*_epoch = del[1];
*_updates = del[2];
return TRUE;
}
/*!
* \internal
* \brief Get the XML patchset from a CIB diff notification
*
* \param[in] msg CIB diff notification
* \param[out] patchset Where to store XML patchset
*
* \return Standard Pacemaker return code
*/
int
cib__get_notify_patchset(const xmlNode *msg, const xmlNode **patchset)
{
int rc = pcmk_err_generic;
xmlNode *wrapper = NULL;
CRM_ASSERT(patchset != NULL);
*patchset = NULL;
if (msg == NULL) {
crm_err("CIB diff notification received with no XML");
return ENOMSG;
}
if ((crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc) != 0)
|| (rc != pcmk_ok)) {
crm_warn("Ignore failed CIB update: %s " CRM_XS " rc=%d",
pcmk_strerror(rc), rc);
crm_log_xml_debug(msg, "failed");
return pcmk_legacy2rc(rc);
}
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_UPDATE_RESULT, NULL, NULL);
*patchset = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (*patchset == NULL) {
crm_err("CIB diff notification received with no patchset");
return ENOMSG;
}
return pcmk_rc_ok;
}
#define XPATH_DIFF_V1 "//" PCMK__XE_CIB_UPDATE_RESULT "//" PCMK__XE_DIFF_ADDED
/*!
* \internal
* \brief Check whether a given CIB element was modified in a CIB patchset (v1)
*
* \param[in] patchset CIB XML patchset
* \param[in] element XML tag of CIB element to check (\c NULL is equivalent
* to \c PCMK_XE_CIB)
*
* \return \c true if \p element was modified, or \c false otherwise
*/
static bool
element_in_patchset_v1(const xmlNode *patchset, const char *element)
{
char *xpath = crm_strdup_printf(XPATH_DIFF_V1 "//%s",
pcmk__s(element, PCMK_XE_CIB));
xmlXPathObject *xpath_obj = xpath_search(patchset, xpath);
free(xpath);
if (xpath_obj == NULL) {
return false;
}
freeXpathObject(xpath_obj);
return true;
}
/*!
* \internal
* \brief Check whether a given CIB element was modified in a CIB patchset (v2)
*
* \param[in] patchset CIB XML patchset
* \param[in] element XML tag of CIB element to check (\c NULL is equivalent
* to \c PCMK_XE_CIB). Supported values include any CIB
* element supported by \c pcmk__cib_abs_xpath_for().
*
* \return \c true if \p element was modified, or \c false otherwise
*/
static bool
element_in_patchset_v2(const xmlNode *patchset, const char *element)
{
const char *element_xpath = pcmk__cib_abs_xpath_for(element);
const char *parent_xpath = pcmk_cib_parent_name_for(element);
char *element_regex = NULL;
bool rc = false;
CRM_CHECK(element_xpath != NULL, return false); // Unsupported element
// Matches if and only if element_xpath is part of a changed path
element_regex = crm_strdup_printf("^%s(/|$)", element_xpath);
for (const xmlNode *change = pcmk__xe_first_child(patchset, PCMK_XE_CHANGE,
NULL, NULL);
change != NULL; change = pcmk__xe_next_same(change)) {
const char *op = crm_element_value(change, PCMK__XA_CIB_OP);
const char *diff_xpath = crm_element_value(change, PCMK_XA_PATH);
if (pcmk__str_eq(diff_xpath, element_regex, pcmk__str_regex)) {
// Change to an existing element
rc = true;
break;
}
if (pcmk__str_eq(op, PCMK_VALUE_CREATE, pcmk__str_none)
&& pcmk__str_eq(diff_xpath, parent_xpath, pcmk__str_none)
&& pcmk__xe_is(pcmk__xe_first_child(change, NULL, NULL, NULL),
element)) {
// Newly added element
rc = true;
break;
}
}
free(element_regex);
return rc;
}
/*!
* \internal
* \brief Check whether a given CIB element was modified in a CIB patchset
*
* \param[in] patchset CIB XML patchset
* \param[in] element XML tag of CIB element to check (\c NULL is equivalent
* to \c PCMK_XE_CIB). Supported values include any CIB
* element supported by \c pcmk__cib_abs_xpath_for().
*
* \return \c true if \p element was modified, or \c false otherwise
*/
bool
cib__element_in_patchset(const xmlNode *patchset, const char *element)
{
int format = 1;
CRM_ASSERT(patchset != NULL);
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
switch (format) {
case 1:
return element_in_patchset_v1(patchset, element);
case 2:
return element_in_patchset_v2(patchset, element);
default:
crm_warn("Unknown patch format: %d", format);
return false;
}
}
/*!
* \brief Create XML for a new (empty) CIB
*
* \param[in] cib_epoch What to use as \c PCMK_XA_EPOCH CIB attribute
*
* \return Newly created XML for empty CIB
* \note It is the caller's responsibility to free the result with free_xml().
*/
xmlNode *
createEmptyCib(int cib_epoch)
{
xmlNode *cib_root = NULL, *config = NULL;
cib_root = pcmk__xe_create(NULL, PCMK_XE_CIB);
crm_xml_add(cib_root, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET);
crm_xml_add(cib_root, PCMK_XA_VALIDATE_WITH, pcmk__highest_schema_name());
crm_xml_add_int(cib_root, PCMK_XA_EPOCH, cib_epoch);
crm_xml_add_int(cib_root, PCMK_XA_NUM_UPDATES, 0);
crm_xml_add_int(cib_root, PCMK_XA_ADMIN_EPOCH, 0);
config = pcmk__xe_create(cib_root, PCMK_XE_CONFIGURATION);
pcmk__xe_create(cib_root, PCMK_XE_STATUS);
pcmk__xe_create(config, PCMK_XE_CRM_CONFIG);
pcmk__xe_create(config, PCMK_XE_NODES);
pcmk__xe_create(config, PCMK_XE_RESOURCES);
pcmk__xe_create(config, PCMK_XE_CONSTRAINTS);
#if PCMK__RESOURCE_STICKINESS_DEFAULT != 0
{
xmlNode *rsc_defaults = pcmk__xe_create(config, PCMK_XE_RSC_DEFAULTS);
xmlNode *meta = pcmk__xe_create(rsc_defaults, PCMK_XE_META_ATTRIBUTES);
xmlNode *nvpair = pcmk__xe_create(meta, PCMK_XE_NVPAIR);
crm_xml_add(meta, PCMK_XA_ID, "build-resource-defaults");
crm_xml_add(nvpair, PCMK_XA_ID, "build-" PCMK_META_RESOURCE_STICKINESS);
crm_xml_add(nvpair, PCMK_XA_NAME, PCMK_META_RESOURCE_STICKINESS);
crm_xml_add_int(nvpair, PCMK_XA_VALUE,
PCMK__RESOURCE_STICKINESS_DEFAULT);
}
#endif
return cib_root;
}
static bool
cib_acl_enabled(xmlNode *xml, const char *user)
{
bool rc = FALSE;
if(pcmk_acl_required(user)) {
const char *value = NULL;
GHashTable *options = pcmk__strkey_table(free, free);
cib_read_config(options, xml);
value = pcmk__cluster_option(options, PCMK_OPT_ENABLE_ACL);
rc = crm_is_true(value);
g_hash_table_destroy(options);
}
crm_trace("CIB ACL is %s", rc ? "enabled" : "disabled");
return rc;
}
/*!
* \internal
* \brief Determine whether to perform operations on a scratch copy of the CIB
*
* \param[in] op CIB operation
* \param[in] section CIB section
* \param[in] call_options CIB call options
*
* \return \p true if we should make a copy of the CIB, or \p false otherwise
*/
static bool
should_copy_cib(const char *op, const char *section, int call_options)
{
if (pcmk_is_set(call_options, cib_dryrun)) {
// cib_dryrun implies a scratch copy by definition; no side effects
return true;
}
if (pcmk__str_eq(op, PCMK__CIB_REQUEST_COMMIT_TRANSACT, pcmk__str_none)) {
/* Commit-transaction must make a copy for atomicity. We must revert to
* the original CIB if the entire transaction cannot be applied
* successfully.
*/
return true;
}
if (pcmk_is_set(call_options, cib_transaction)) {
/* If cib_transaction is set, then we're in the process of committing a
* transaction. The commit-transaction request already made a scratch
* copy, and we're accumulating changes in that copy.
*/
return false;
}
if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_none)) {
/* Copying large CIBs accounts for a huge percentage of our CIB usage,
* and this avoids some of it.
*
* @TODO: Is this safe? See discussion at
* https://github.com/ClusterLabs/pacemaker/pull/3094#discussion_r1211400690.
*/
return false;
}
// Default behavior is to operate on a scratch copy
return true;
}
int
cib_perform_op(cib_t *cib, const char *op, int call_options, cib__op_fn_t fn,
bool is_query, const char *section, xmlNode *req, xmlNode *input,
bool manage_counters, bool *config_changed, xmlNode **current_cib,
xmlNode **result_cib, xmlNode **diff, xmlNode **output)
{
int rc = pcmk_ok;
bool check_schema = true;
bool make_copy = true;
xmlNode *top = NULL;
xmlNode *scratch = NULL;
xmlNode *patchset_cib = NULL;
xmlNode *local_diff = NULL;
const char *user = crm_element_value(req, PCMK__XA_CIB_USER);
bool with_digest = false;
crm_trace("Begin %s%s%s op",
(pcmk_is_set(call_options, cib_dryrun)? "dry run of " : ""),
(is_query? "read-only " : ""), op);
CRM_CHECK(output != NULL, return -ENOMSG);
CRM_CHECK(current_cib != NULL, return -ENOMSG);
CRM_CHECK(result_cib != NULL, return -ENOMSG);
CRM_CHECK(config_changed != NULL, return -ENOMSG);
if(output) {
*output = NULL;
}
*result_cib = NULL;
*config_changed = false;
if (fn == NULL) {
return -EINVAL;
}
if (is_query) {
xmlNode *cib_ro = *current_cib;
xmlNode *cib_filtered = NULL;
if (cib_acl_enabled(cib_ro, user)
&& xml_acl_filtered_copy(user, *current_cib, *current_cib,
&cib_filtered)) {
if (cib_filtered == NULL) {
crm_debug("Pre-filtered the entire cib");
return -EACCES;
}
cib_ro = cib_filtered;
crm_log_xml_trace(cib_ro, "filtered");
}
rc = (*fn) (op, call_options, section, req, input, cib_ro, result_cib, output);
if(output == NULL || *output == NULL) {
/* nothing */
} else if(cib_filtered == *output) {
cib_filtered = NULL; /* Let them have this copy */
} else if (*output == *current_cib) {
/* They already know not to free it */
} else if(cib_filtered && (*output)->doc == cib_filtered->doc) {
/* We're about to free the document of which *output is a part */
*output = pcmk__xml_copy(NULL, *output);
} else if ((*output)->doc == (*current_cib)->doc) {
/* Give them a copy they can free */
*output = pcmk__xml_copy(NULL, *output);
}
free_xml(cib_filtered);
return rc;
}
make_copy = should_copy_cib(op, section, call_options);
if (!make_copy) {
/* Conditional on v2 patch style */
scratch = *current_cib;
// Make a copy of the top-level element to store version details
top = pcmk__xe_create(NULL, (const char *) scratch->name);
pcmk__xe_copy_attrs(top, scratch, pcmk__xaf_none);
patchset_cib = top;
xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
rc = (*fn) (op, call_options, section, req, input, scratch, &scratch, output);
/* If scratch points to a new object now (for example, after an erase
* operation), then *current_cib should point to the same object.
*/
*current_cib = scratch;
} else {
scratch = pcmk__xml_copy(NULL, *current_cib);
patchset_cib = *current_cib;
xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user));
rc = (*fn) (op, call_options, section, req, input, *current_cib,
&scratch, output);
if ((scratch != NULL) && !xml_tracking_changes(scratch)) {
crm_trace("Inferring changes after %s op", op);
xml_track_changes(scratch, user, *current_cib,
cib_acl_enabled(*current_cib, user));
xml_calculate_changes(*current_cib, scratch);
}
CRM_CHECK(*current_cib != scratch, return -EINVAL);
}
xml_acl_disable(scratch); /* Allow the system to make any additional changes */
if (rc == pcmk_ok && scratch == NULL) {
rc = -EINVAL;
goto done;
} else if(rc == pcmk_ok && xml_acl_denied(scratch)) {
crm_trace("ACL rejected part or all of the proposed changes");
rc = -EACCES;
goto done;
} else if (rc != pcmk_ok) {
goto done;
}
/* If the CIB is from a file, we don't need to check that the feature set is
* supported. All we care about in that case is the schema version, which
* is checked elsewhere.
*/
if (scratch && (cib == NULL || cib->variant != cib_file)) {
const char *new_version = crm_element_value(scratch, PCMK_XA_CRM_FEATURE_SET);
rc = pcmk__check_feature_set(new_version);
if (rc != pcmk_rc_ok) {
pcmk__config_err("Discarding update with feature set '%s' greater than our own '%s'",
new_version, CRM_FEATURE_SET);
rc = pcmk_rc2legacy(rc);
goto done;
}
}
if (patchset_cib != NULL) {
int old = 0;
int new = 0;
crm_element_value_int(scratch, PCMK_XA_ADMIN_EPOCH, &new);
crm_element_value_int(patchset_cib, PCMK_XA_ADMIN_EPOCH, &old);
if (old > new) {
crm_err("%s went backwards: %d -> %d (Opts: %#x)",
PCMK_XA_ADMIN_EPOCH, old, new, call_options);
crm_log_xml_warn(req, "Bad Op");
crm_log_xml_warn(input, "Bad Data");
rc = -pcmk_err_old_data;
} else if (old == new) {
crm_element_value_int(scratch, PCMK_XA_EPOCH, &new);
crm_element_value_int(patchset_cib, PCMK_XA_EPOCH, &old);
if (old > new) {
crm_err("%s went backwards: %d -> %d (Opts: %#x)",
PCMK_XA_EPOCH, old, new, call_options);
crm_log_xml_warn(req, "Bad Op");
crm_log_xml_warn(input, "Bad Data");
rc = -pcmk_err_old_data;
}
}
}
crm_trace("Massaging CIB contents");
pcmk__strip_xml_text(scratch);
if (!make_copy) {
/* At this point, patchset_cib is just the PCMK_XE_CIB tag and its
* properties.
*
* The v1 format would barf on this, but we know the v2 patch
* format only needs it for the top-level version fields
*/
local_diff = xml_create_patchset(2, patchset_cib, scratch,
config_changed, manage_counters);
} else {
static time_t expires = 0;
time_t tm_now = time(NULL);
if (expires < tm_now) {
expires = tm_now + 60; /* Validate clients are correctly applying v2-style diffs at most once a minute */
with_digest = true;
}
local_diff = xml_create_patchset(0, patchset_cib, scratch,
config_changed, manage_counters);
}
pcmk__log_xml_changes(LOG_TRACE, scratch);
xml_accept_changes(scratch);
if(local_diff) {
patchset_process_digest(local_diff, patchset_cib, scratch, with_digest);
pcmk__log_xml_patchset(LOG_INFO, local_diff);
crm_log_xml_trace(local_diff, "raw patch");
}
if (make_copy && (local_diff != NULL)) {
// Original to compare against doesn't exist
pcmk__if_tracing(
{
// Validate the calculated patch set
int test_rc = pcmk_ok;
int format = 1;
xmlNode *cib_copy = pcmk__xml_copy(NULL, patchset_cib);
crm_element_value_int(local_diff, PCMK_XA_FORMAT, &format);
test_rc = xml_apply_patchset(cib_copy, local_diff,
manage_counters);
if (test_rc != pcmk_ok) {
save_xml_to_file(cib_copy, "PatchApply:calculated", NULL);
save_xml_to_file(patchset_cib, "PatchApply:input", NULL);
save_xml_to_file(scratch, "PatchApply:actual", NULL);
save_xml_to_file(local_diff, "PatchApply:diff", NULL);
crm_err("v%d patchset error, patch failed to apply: %s "
"(%d)",
format, pcmk_rc_str(pcmk_legacy2rc(test_rc)),
test_rc);
}
free_xml(cib_copy);
},
{}
);
}
if (pcmk__str_eq(section, PCMK_XE_STATUS, pcmk__str_casei)) {
/* Throttle the amount of costly validation we perform due to status updates
* a) we don't really care whats in the status section
* b) we don't validate any of its contents at the moment anyway
*/
check_schema = false;
}
/* === scratch must not be modified after this point ===
* Exceptions, anything in:
static filter_t filter[] = {
{ 0, PCMK_XA_CRM_DEBUG_ORIGIN },
{ 0, PCMK_XA_CIB_LAST_WRITTEN },
{ 0, PCMK_XA_UPDATE_ORIGIN },
{ 0, PCMK_XA_UPDATE_CLIENT },
{ 0, PCMK_XA_UPDATE_USER },
};
*/
if (*config_changed && !pcmk_is_set(call_options, cib_no_mtime)) {
const char *schema = crm_element_value(scratch, PCMK_XA_VALIDATE_WITH);
pcmk__xe_add_last_written(scratch);
+ pcmk__warn_if_schema_deprecated(schema);
/* Make values of origin, client, and user in scratch match
* the ones in req (if the schema allows the attributes)
*/
if (pcmk__cmp_schemas_by_name(schema, "pacemaker-1.2") >= 0) {
const char *origin = crm_element_value(req, PCMK__XA_SRC);
const char *client = crm_element_value(req,
PCMK__XA_CIB_CLIENTNAME);
if (origin != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_ORIGIN, origin);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_ORIGIN);
}
if (client != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_CLIENT, user);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_CLIENT);
}
if (user != NULL) {
crm_xml_add(scratch, PCMK_XA_UPDATE_USER, user);
} else {
pcmk__xe_remove_attr(scratch, PCMK_XA_UPDATE_USER);
}
}
}
crm_trace("Perform validation: %s", pcmk__btoa(check_schema));
if ((rc == pcmk_ok) && check_schema
&& !pcmk__configured_schema_validates(scratch)) {
const char *current_schema = crm_element_value(scratch,
PCMK_XA_VALIDATE_WITH);
crm_warn("Updated CIB does not validate against %s schema",
pcmk__s(current_schema, "unspecified"));
rc = -pcmk_err_schema_validation;
}
done:
*result_cib = scratch;
/* @TODO: This may not work correctly with !make_copy, since we don't
* keep the original CIB.
*/
if ((rc != pcmk_ok) && cib_acl_enabled(patchset_cib, user)
&& xml_acl_filtered_copy(user, patchset_cib, scratch, result_cib)) {
if (*result_cib == NULL) {
crm_debug("Pre-filtered the entire cib result");
}
free_xml(scratch);
}
if(diff) {
*diff = local_diff;
} else {
free_xml(local_diff);
}
free_xml(top);
crm_trace("Done");
return rc;
}
int
cib__create_op(cib_t *cib, const char *op, const char *host,
const char *section, xmlNode *data, int call_options,
const char *user_name, const char *client_name,
xmlNode **op_msg)
{
CRM_CHECK((cib != NULL) && (op_msg != NULL), return -EPROTO);
*op_msg = pcmk__xe_create(NULL, PCMK__XE_CIB_COMMAND);
cib->call_id++;
if (cib->call_id < 1) {
cib->call_id = 1;
}
crm_xml_add(*op_msg, PCMK__XA_T, PCMK__VALUE_CIB);
crm_xml_add(*op_msg, PCMK__XA_CIB_OP, op);
crm_xml_add(*op_msg, PCMK__XA_CIB_HOST, host);
crm_xml_add(*op_msg, PCMK__XA_CIB_SECTION, section);
crm_xml_add(*op_msg, PCMK__XA_CIB_USER, user_name);
crm_xml_add(*op_msg, PCMK__XA_CIB_CLIENTNAME, client_name);
crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLID, cib->call_id);
crm_trace("Sending call options: %.8lx, %d", (long)call_options, call_options);
crm_xml_add_int(*op_msg, PCMK__XA_CIB_CALLOPT, call_options);
if (data != NULL) {
xmlNode *wrapper = pcmk__xe_create(*op_msg, PCMK__XE_CIB_CALLDATA);
pcmk__xml_copy(wrapper, data);
}
if (pcmk_is_set(call_options, cib_inhibit_bcast)) {
CRM_CHECK(pcmk_is_set(call_options, cib_scope_local),
free_xml(*op_msg); return -EPROTO);
}
return pcmk_ok;
}
/*!
* \internal
* \brief Check whether a CIB request is supported in a transaction
*
* \param[in] request CIB request
*
* \return Standard Pacemaker return code
*/
static int
validate_transaction_request(const xmlNode *request)
{
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *host = crm_element_value(request, PCMK__XA_CIB_HOST);
const cib__operation_t *operation = NULL;
int rc = cib__get_operation(op, &operation);
if (rc != pcmk_rc_ok) {
// cib__get_operation() logs error
return rc;
}
if (!pcmk_is_set(operation->flags, cib__op_attr_transaction)) {
crm_err("Operation %s is not supported in CIB transactions", op);
return EOPNOTSUPP;
}
if (host != NULL) {
crm_err("Operation targeting a specific node (%s) is not supported in "
"a CIB transaction",
host);
return EOPNOTSUPP;
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Append a CIB request to a CIB transaction
*
* \param[in,out] cib CIB client whose transaction to extend
* \param[in,out] request Request to add to transaction
*
* \return Legacy Pacemaker return code
*/
int
cib__extend_transaction(cib_t *cib, xmlNode *request)
{
int rc = pcmk_rc_ok;
CRM_ASSERT((cib != NULL) && (request != NULL));
rc = validate_transaction_request(request);
if ((rc == pcmk_rc_ok) && (cib->transaction == NULL)) {
rc = pcmk_rc_no_transaction;
}
if (rc == pcmk_rc_ok) {
pcmk__xml_copy(cib->transaction, request);
} else {
const char *op = crm_element_value(request, PCMK__XA_CIB_OP);
const char *client_id = NULL;
cib->cmds->client_id(cib, NULL, &client_id);
crm_err("Failed to add '%s' operation to transaction for client %s: %s",
op, pcmk__s(client_id, "(unidentified)"), pcmk_rc_str(rc));
crm_log_xml_info(request, "failed");
}
return pcmk_rc2legacy(rc);
}
void
cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc)
{
xmlNode *output = NULL;
cib_callback_client_t *blob = NULL;
if (msg != NULL) {
xmlNode *wrapper = NULL;
crm_element_value_int(msg, PCMK__XA_CIB_RC, &rc);
crm_element_value_int(msg, PCMK__XA_CIB_CALLID, &call_id);
wrapper = pcmk__xe_first_child(msg, PCMK__XE_CIB_CALLDATA, NULL, NULL);
output = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
}
blob = cib__lookup_id(call_id);
if (blob == NULL) {
crm_trace("No callback found for call %d", call_id);
}
if (cib == NULL) {
crm_debug("No cib object supplied");
}
if (rc == -pcmk_err_diff_resync) {
/* This is an internal value that clients do not and should not care about */
rc = pcmk_ok;
}
if (blob && blob->callback && (rc == pcmk_ok || blob->only_success == FALSE)) {
crm_trace("Invoking callback %s for call %d",
pcmk__s(blob->id, "without ID"), call_id);
blob->callback(msg, call_id, rc, output, blob->user_data);
} else if (cib && cib->op_callback == NULL && rc != pcmk_ok) {
crm_warn("CIB command failed: %s", pcmk_strerror(rc));
crm_log_xml_debug(msg, "Failed CIB Update");
}
/* This may free user_data, so do it after the callback */
if (blob) {
remove_cib_op_callback(call_id, FALSE);
}
if (cib && cib->op_callback != NULL) {
crm_trace("Invoking global callback for call %d", call_id);
cib->op_callback(msg, call_id, rc, output);
}
crm_trace("OP callback activated for %d", call_id);
}
void
cib_native_notify(gpointer data, gpointer user_data)
{
xmlNode *msg = user_data;
cib_notify_client_t *entry = data;
const char *event = NULL;
if (msg == NULL) {
crm_warn("Skipping callback - NULL message");
return;
}
event = crm_element_value(msg, PCMK__XA_SUBT);
if (entry == NULL) {
crm_warn("Skipping callback - NULL callback client");
return;
} else if (entry->callback == NULL) {
crm_warn("Skipping callback - NULL callback");
return;
} else if (!pcmk__str_eq(entry->event, event, pcmk__str_casei)) {
crm_trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event);
return;
}
crm_trace("Invoking callback for %p/%s event...", entry, event);
entry->callback(event, msg);
crm_trace("Callback invoked...");
}
gboolean
cib_read_config(GHashTable * options, xmlNode * current_cib)
{
xmlNode *config = NULL;
crm_time_t *now = NULL;
if (options == NULL || current_cib == NULL) {
return FALSE;
}
now = crm_time_new(NULL);
g_hash_table_remove_all(options);
config = pcmk_find_cib_element(current_cib, PCMK_XE_CRM_CONFIG);
if (config) {
pe_unpack_nvpairs(current_cib, config, PCMK_XE_CLUSTER_PROPERTY_SET,
NULL, options, PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS, TRUE,
now, NULL);
}
pcmk__validate_cluster_options(options);
crm_time_free(now);
return TRUE;
}
int
cib_internal_op(cib_t * cib, const char *op, const char *host,
const char *section, xmlNode * data,
xmlNode ** output_data, int call_options, const char *user_name)
{
int (*delegate) (cib_t * cib, const char *op, const char *host,
const char *section, xmlNode * data,
xmlNode ** output_data, int call_options, const char *user_name) =
cib->delegate_fn;
if(user_name == NULL) {
user_name = getenv("CIB_user");
}
return delegate(cib, op, host, section, data, output_data, call_options, user_name);
}
/*!
* \brief Apply a CIB update patch to a given CIB
*
* \param[in] event CIB update patch
* \param[in] input CIB to patch
* \param[out] output Resulting CIB after patch
* \param[in] level Log the patch at this log level (unless LOG_CRIT)
*
* \return Legacy Pacemaker return code
* \note sbd calls this function
*/
int
cib_apply_patch_event(xmlNode *event, xmlNode *input, xmlNode **output,
int level)
{
int rc = pcmk_err_generic;
xmlNode *wrapper = NULL;
xmlNode *diff = NULL;
CRM_ASSERT(event);
CRM_ASSERT(input);
CRM_ASSERT(output);
crm_element_value_int(event, PCMK__XA_CIB_RC, &rc);
wrapper = pcmk__xe_first_child(event, PCMK__XE_CIB_UPDATE_RESULT, NULL,
NULL);
diff = pcmk__xe_first_child(wrapper, NULL, NULL, NULL);
if (rc < pcmk_ok || diff == NULL) {
return rc;
}
if (level > LOG_CRIT) {
pcmk__log_xml_patchset(level, diff);
}
if (input != NULL) {
rc = cib_process_diff(NULL, cib_none, NULL, event, diff, input, output,
NULL);
if (rc != pcmk_ok) {
crm_debug("Update didn't apply: %s (%d) %p",
pcmk_strerror(rc), rc, *output);
if (rc == -pcmk_err_old_data) {
crm_trace("Masking error, we already have the supplied update");
return pcmk_ok;
}
free_xml(*output);
*output = NULL;
return rc;
}
}
return rc;
}
#define log_signon_query_err(out, fmt, args...) do { \
if (out != NULL) { \
out->err(out, fmt, ##args); \
} else { \
crm_err(fmt, ##args); \
} \
} while (0)
int
cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object)
{
int rc = pcmk_rc_ok;
cib_t *cib_conn = NULL;
CRM_ASSERT(cib_object != NULL);
if (cib == NULL) {
cib_conn = cib_new();
} else {
if (*cib == NULL) {
*cib = cib_new();
}
cib_conn = *cib;
}
if (cib_conn == NULL) {
return ENOMEM;
}
if (cib_conn->state == cib_disconnected) {
rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command);
rc = pcmk_legacy2rc(rc);
}
if (rc != pcmk_rc_ok) {
log_signon_query_err(out, "Could not connect to the CIB: %s",
pcmk_rc_str(rc));
goto done;
}
if (out != NULL) {
out->transient(out, "Querying CIB...");
}
rc = cib_conn->cmds->query(cib_conn, NULL, cib_object,
cib_scope_local|cib_sync_call);
rc = pcmk_legacy2rc(rc);
if (rc != pcmk_rc_ok) {
log_signon_query_err(out, "CIB query failed: %s", pcmk_rc_str(rc));
}
done:
if (cib == NULL) {
cib__clean_up_connection(&cib_conn);
}
if ((rc == pcmk_rc_ok) && (*cib_object == NULL)) {
return pcmk_rc_no_input;
}
return rc;
}
int
cib__clean_up_connection(cib_t **cib)
{
int rc;
if (*cib == NULL) {
return pcmk_rc_ok;
}
rc = (*cib)->cmds->signoff(*cib);
cib_delete(*cib);
*cib = NULL;
return pcmk_legacy2rc(rc);
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include
xmlNode *
cib_get_generation(cib_t * cib)
{
xmlNode *the_cib = NULL;
xmlNode *generation = pcmk__xe_create(NULL, PCMK__XE_GENERATION_TUPLE);
cib->cmds->query(cib, NULL, &the_cib, cib_scope_local | cib_sync_call);
if (the_cib != NULL) {
pcmk__xe_copy_attrs(generation, the_cib, pcmk__xaf_none);
free_xml(the_cib);
}
return generation;
}
const char *
get_object_path(const char *object_type)
{
return pcmk_cib_xpath_for(object_type);
}
const char *
get_object_parent(const char *object_type)
{
return pcmk_cib_parent_name_for(object_type);
}
xmlNode *
get_object_root(const char *object_type, xmlNode *the_root)
{
return pcmk_find_cib_element(the_root, object_type);
}
const char *
cib_pref(GHashTable * options, const char *name)
{
return pcmk__cluster_option(options, name);
}
void
cib_metadata(void)
{
pcmk__output_t *out = NULL;
int rc = pcmk__output_new(&out, "text", NULL, NULL);
if (rc != pcmk_rc_ok) {
crm_err("Unable to output metadata: %s", pcmk_rc_str(rc));
return;
}
pcmk__daemon_metadata(out, "pacemaker-based",
"Cluster Information Base manager options",
"Cluster options used by Pacemaker's Cluster "
"Information Base manager",
pcmk__opt_based);
out->finish(out, CRM_EX_OK, true, NULL);
pcmk__output_free(out);
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/schemas.c b/lib/common/schemas.c
index 6e0a182153..b19b5dffe7 100644
--- a/lib/common/schemas.c
+++ b/lib/common/schemas.c
@@ -1,1723 +1,1744 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include /* PCMK__XML_LOG_BASE */
#include "crmcommon_private.h"
#define SCHEMA_ZERO { .v = { 0, 0 } }
#define schema_strdup_printf(prefix, version, suffix) \
crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1])
typedef struct {
xmlRelaxNGPtr rng;
xmlRelaxNGValidCtxtPtr valid;
xmlRelaxNGParserCtxtPtr parser;
} relaxng_ctx_cache_t;
static GList *known_schemas = NULL;
static bool initialized = false;
static bool silent_logging = FALSE;
static void G_GNUC_PRINTF(2, 3)
xml_log(int priority, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
if (silent_logging == FALSE) {
/* XXX should not this enable dechunking as well? */
PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap);
}
va_end(ap);
}
static int
xml_latest_schema_index(void)
{
/* This function assumes that crm_schema_init() has been called beforehand,
* so we have at least three schemas (one real schema, the "pacemaker-next"
* schema, and the "none" schema).
*
- * @COMPAT: pacemaker-next is deprecated since 2.1.5.
- * Update this when we drop that schema.
+ * @COMPAT: pacemaker-next is deprecated since 2.1.5 and none since 2.1.8.
+ * Update this when we drop those.
*/
return g_list_length(known_schemas) - 3;
}
/*!
* \internal
* \brief Return the schema entry of the highest-versioned schema
*
* \return Schema entry of highest-versioned schema (or NULL on error)
*/
static GList *
get_highest_schema(void)
{
/* The highest numerically versioned schema is the one before pacemaker-next
*
* @COMPAT pacemaker-next is deprecated since 2.1.5
*/
GList *entry = pcmk__get_schema("pacemaker-next");
CRM_ASSERT((entry != NULL) && (entry->prev != NULL));
return entry->prev;
}
/*!
* \internal
* \brief Return the name of the highest-versioned schema
*
* \return Name of highest-versioned schema (or NULL on error)
*/
const char *
pcmk__highest_schema_name(void)
{
GList *entry = get_highest_schema();
return ((pcmk__schema_t *)(entry->data))->name;
}
/*!
* \internal
* \brief Find first entry of highest major schema version series
*
* \return Schema entry of first schema with highest major version
*/
GList *
pcmk__find_x_0_schema(void)
{
#if defined(PCMK__UNIT_TESTING)
/* If we're unit testing, this can't be static because it'll stick
* around from one test run to the next. It needs to be cleared out
* every time.
*/
GList *x_0_entry = NULL;
#else
static GList *x_0_entry = NULL;
#endif
pcmk__schema_t *highest_schema = NULL;
if (x_0_entry != NULL) {
return x_0_entry;
}
x_0_entry = get_highest_schema();
highest_schema = x_0_entry->data;
for (GList *iter = x_0_entry->prev; iter != NULL; iter = iter->prev) {
pcmk__schema_t *schema = iter->data;
/* We've found a schema in an older major version series. Return
* the index of the first one in the same major version series as
* the highest schema.
*/
if (schema->version.v[0] < highest_schema->version.v[0]) {
x_0_entry = iter->next;
break;
}
/* We're out of list to examine. This probably means there was only
* one major version series, so return the first schema entry.
*/
if (iter->prev == NULL) {
x_0_entry = known_schemas->data;
break;
}
}
return x_0_entry;
}
static inline bool
version_from_filename(const char *filename, pcmk__schema_version_t *version)
{
if (pcmk__ends_with(filename, ".rng")) {
return sscanf(filename, "pacemaker-%hhu.%hhu.rng", &(version->v[0]), &(version->v[1])) == 2;
} else {
return sscanf(filename, "pacemaker-%hhu.%hhu", &(version->v[0]), &(version->v[1])) == 2;
}
}
static int
schema_filter(const struct dirent *a)
{
int rc = 0;
pcmk__schema_version_t version = SCHEMA_ZERO;
if (strstr(a->d_name, "pacemaker-") != a->d_name) {
/* crm_trace("%s - wrong prefix", a->d_name); */
} else if (!pcmk__ends_with_ext(a->d_name, ".rng")) {
/* crm_trace("%s - wrong suffix", a->d_name); */
} else if (!version_from_filename(a->d_name, &version)) {
/* crm_trace("%s - wrong format", a->d_name); */
} else {
/* crm_debug("%s - candidate", a->d_name); */
rc = 1;
}
return rc;
}
static int
schema_cmp(pcmk__schema_version_t a_version, pcmk__schema_version_t b_version)
{
for (int i = 0; i < 2; ++i) {
if (a_version.v[i] < b_version.v[i]) {
return -1;
} else if (a_version.v[i] > b_version.v[i]) {
return 1;
}
}
return 0;
}
static int
schema_cmp_directory(const struct dirent **a, const struct dirent **b)
{
pcmk__schema_version_t a_version = SCHEMA_ZERO;
pcmk__schema_version_t b_version = SCHEMA_ZERO;
if (!version_from_filename(a[0]->d_name, &a_version)
|| !version_from_filename(b[0]->d_name, &b_version)) {
// Shouldn't be possible, but makes static analysis happy
return 0;
}
return schema_cmp(a_version, b_version);
}
/*!
* \internal
* \brief Add given schema + auxiliary data to internal bookkeeping.
*
* \note When providing \p version, should not be called directly but
* through \c add_schema_by_version.
*/
static void
add_schema(enum pcmk__schema_validator validator, const pcmk__schema_version_t *version,
const char *name, const char *transform,
const char *transform_enter, bool transform_onleave)
{
pcmk__schema_t *schema = NULL;
schema = pcmk__assert_alloc(1, sizeof(pcmk__schema_t));
schema->validator = validator;
schema->version.v[0] = version->v[0];
schema->version.v[1] = version->v[1];
schema->transform_onleave = transform_onleave;
// schema->schema_index is set after all schemas are loaded and sorted
if (version->v[0] || version->v[1]) {
schema->name = schema_strdup_printf("pacemaker-", *version, "");
} else {
schema->name = pcmk__str_copy(name);
}
if (transform) {
schema->transform = pcmk__str_copy(transform);
}
if (transform_enter) {
schema->transform_enter = pcmk__str_copy(transform_enter);
}
known_schemas = g_list_prepend(known_schemas, schema);
}
/*!
* \internal
* \brief Add version-specified schema + auxiliary data to internal bookkeeping.
* \return Standard Pacemaker return value (the only possible values are
* \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise.
*
* \note There's no reliance on the particular order of schemas entering here.
*
* \par A bit of theory
* We track 3 XSLT stylesheets that differ per usage:
* - "upgrade":
* . sparsely spread over the sequence of all available schemas,
* as they are only relevant when major version of the schema
* is getting bumped -- in that case, it MUST be set
* . name convention: upgrade-X.Y.xsl
* - "upgrade-enter":
* . may only accompany "upgrade" occurrence, but doesn't need to
* be present anytime such one is, i.e., it MAY not be set when
* "upgrade" is
* . name convention: upgrade-X.Y-enter.xsl,
* when not present: upgrade-enter.xsl
* - "upgrade-leave":
* . like "upgrade-enter", but SHOULD be present whenever
* "upgrade-enter" is (and vice versa, but that's only
* to prevent confusion based on observing the files,
* it would get ignored regardless)
* . name convention: (see "upgrade-enter")
*/
static int
add_schema_by_version(const pcmk__schema_version_t *version, bool transform_expected)
{
bool transform_onleave = FALSE;
int rc = pcmk_rc_ok;
struct stat s;
char *xslt = NULL,
*transform_upgrade = NULL,
*transform_enter = NULL;
/* prologue for further transform_expected handling */
if (transform_expected) {
/* check if there's suitable "upgrade" stylesheet */
transform_upgrade = schema_strdup_printf("upgrade-", *version, );
xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform_upgrade);
}
if (!transform_expected) {
/* jump directly to the end */
} else if (stat(xslt, &s) == 0) {
/* perhaps there's also a targeted "upgrade-enter" stylesheet */
transform_enter = schema_strdup_printf("upgrade-", *version, "-enter");
free(xslt);
xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform_enter);
if (stat(xslt, &s) != 0) {
/* or initially, at least a generic one */
crm_debug("Upgrade-enter transform %s.xsl not found", xslt);
free(xslt);
free(transform_enter);
transform_enter = strdup("upgrade-enter");
xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform_enter);
if (stat(xslt, &s) != 0) {
crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt);
free(xslt);
xslt = NULL;
}
}
/* xslt contains full path to "upgrade-enter" stylesheet */
if (xslt != NULL) {
/* then there should be "upgrade-leave" counterpart (enter->leave) */
memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1);
transform_onleave = (stat(xslt, &s) == 0);
free(xslt);
} else {
free(transform_enter);
transform_enter = NULL;
}
} else {
crm_err("Upgrade transform %s not found", xslt);
free(xslt);
free(transform_upgrade);
transform_upgrade = NULL;
rc = ENOENT;
}
add_schema(pcmk__schema_validator_rng, version, NULL,
transform_upgrade, transform_enter, transform_onleave);
free(transform_upgrade);
free(transform_enter);
return rc;
}
static void
wrap_libxslt(bool finalize)
{
static xsltSecurityPrefsPtr secprefs;
int ret = 0;
/* security framework preferences */
if (!finalize) {
CRM_ASSERT(secprefs == NULL);
secprefs = xsltNewSecurityPrefs();
ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE,
xsltSecurityForbid)
| xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY,
xsltSecurityForbid)
| xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK,
xsltSecurityForbid)
| xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK,
xsltSecurityForbid);
if (ret != 0) {
return;
}
} else {
xsltFreeSecurityPrefs(secprefs);
secprefs = NULL;
}
/* cleanup only */
if (finalize) {
xsltCleanupGlobals();
}
}
void
pcmk__load_schemas_from_dir(const char *dir)
{
int lpc, max;
struct dirent **namelist = NULL;
max = scandir(dir, &namelist, schema_filter, schema_cmp_directory);
if (max < 0) {
crm_warn("Could not load schemas from %s: %s", dir, strerror(errno));
return;
}
for (lpc = 0; lpc < max; lpc++) {
bool transform_expected = false;
pcmk__schema_version_t version = SCHEMA_ZERO;
if (!version_from_filename(namelist[lpc]->d_name, &version)) {
// Shouldn't be possible, but makes static analysis happy
crm_warn("Skipping schema '%s': could not parse version",
namelist[lpc]->d_name);
continue;
}
if ((lpc + 1) < max) {
pcmk__schema_version_t next_version = SCHEMA_ZERO;
if (version_from_filename(namelist[lpc+1]->d_name, &next_version)
&& (version.v[0] < next_version.v[0])) {
transform_expected = true;
}
}
if (add_schema_by_version(&version, transform_expected) != pcmk_rc_ok) {
break;
}
}
for (lpc = 0; lpc < max; lpc++) {
free(namelist[lpc]);
}
free(namelist);
}
static gint
schema_sort_GCompareFunc(gconstpointer a, gconstpointer b)
{
const pcmk__schema_t *schema_a = a;
const pcmk__schema_t *schema_b = b;
+ // @COMPAT pacemaker-next is deprecated since 2.1.5 and none since 2.1.8
if (pcmk__str_eq(schema_a->name, "pacemaker-next", pcmk__str_none)) {
if (pcmk__str_eq(schema_b->name, PCMK_VALUE_NONE, pcmk__str_none)) {
return -1;
} else {
return 1;
}
} else if (pcmk__str_eq(schema_a->name, PCMK_VALUE_NONE, pcmk__str_none)) {
return 1;
} else if (pcmk__str_eq(schema_b->name, "pacemaker-next", pcmk__str_none)) {
return -1;
} else {
return schema_cmp(schema_a->version, schema_b->version);
}
}
/*!
* \internal
* \brief Sort the list of known schemas such that all pacemaker-X.Y are in
* version order, then pacemaker-next, then none
*
* This function should be called whenever additional schemas are loaded using
* pcmk__load_schemas_from_dir(), after the initial sets in crm_schema_init().
*/
void
pcmk__sort_schemas(void)
{
known_schemas = g_list_sort(known_schemas, schema_sort_GCompareFunc);
}
/*!
* \internal
* \brief Load pacemaker schemas into cache
*
* \note This currently also serves as an entry point for the
* generic initialization of the libxslt library.
*/
void
crm_schema_init(void)
{
if (!initialized) {
const char *remote_schema_dir = pcmk__remote_schema_dir();
char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng);
const pcmk__schema_version_t zero = SCHEMA_ZERO;
int schema_index = 0;
initialized = true;
wrap_libxslt(false);
pcmk__load_schemas_from_dir(base);
pcmk__load_schemas_from_dir(remote_schema_dir);
// @COMPAT: Deprecated since 2.1.5
add_schema(pcmk__schema_validator_rng, &zero, "pacemaker-next", NULL,
NULL, FALSE);
+ // @COMPAT Deprecated since 2.1.8
add_schema(pcmk__schema_validator_none, &zero, PCMK_VALUE_NONE, NULL,
NULL, FALSE);
/* add_schema() prepends items to the list, so in the simple case, this
* just reverses the list. However if there were any remote schemas,
* sorting is necessary.
*/
pcmk__sort_schemas();
// Now set the schema indexes and log the final result
for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
pcmk__schema_t *schema = iter->data;
if (schema->transform == NULL) {
crm_debug("Loaded schema %d: %s", schema_index, schema->name);
} else {
crm_debug("Loaded schema %d: %s (upgrades with %s.xsl)",
schema_index, schema->name, schema->transform);
}
schema->schema_index = schema_index++;
}
}
}
static bool
validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler,
void *error_handler_context, const char *relaxng_file,
relaxng_ctx_cache_t **cached_ctx)
{
int rc = 0;
bool valid = true;
relaxng_ctx_cache_t *ctx = NULL;
CRM_CHECK(doc != NULL, return false);
CRM_CHECK(relaxng_file != NULL, return false);
if (cached_ctx && *cached_ctx) {
ctx = *cached_ctx;
} else {
crm_debug("Creating RNG parser context");
ctx = pcmk__assert_alloc(1, sizeof(relaxng_ctx_cache_t));
ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file);
CRM_CHECK(ctx->parser != NULL, goto cleanup);
if (error_handler) {
xmlRelaxNGSetParserErrors(ctx->parser,
(xmlRelaxNGValidityErrorFunc) error_handler,
(xmlRelaxNGValidityWarningFunc) error_handler,
error_handler_context);
} else {
xmlRelaxNGSetParserErrors(ctx->parser,
(xmlRelaxNGValidityErrorFunc) fprintf,
(xmlRelaxNGValidityWarningFunc) fprintf,
stderr);
}
ctx->rng = xmlRelaxNGParse(ctx->parser);
CRM_CHECK(ctx->rng != NULL,
crm_err("Could not find/parse %s", relaxng_file);
goto cleanup);
ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng);
CRM_CHECK(ctx->valid != NULL, goto cleanup);
if (error_handler) {
xmlRelaxNGSetValidErrors(ctx->valid,
(xmlRelaxNGValidityErrorFunc) error_handler,
(xmlRelaxNGValidityWarningFunc) error_handler,
error_handler_context);
} else {
xmlRelaxNGSetValidErrors(ctx->valid,
(xmlRelaxNGValidityErrorFunc) fprintf,
(xmlRelaxNGValidityWarningFunc) fprintf,
stderr);
}
}
rc = xmlRelaxNGValidateDoc(ctx->valid, doc);
if (rc > 0) {
valid = false;
} else if (rc < 0) {
crm_err("Internal libxml error during validation");
}
cleanup:
if (cached_ctx) {
*cached_ctx = ctx;
} else {
if (ctx->parser != NULL) {
xmlRelaxNGFreeParserCtxt(ctx->parser);
}
if (ctx->valid != NULL) {
xmlRelaxNGFreeValidCtxt(ctx->valid);
}
if (ctx->rng != NULL) {
xmlRelaxNGFree(ctx->rng);
}
free(ctx);
}
return valid;
}
static void
free_schema(gpointer data)
{
pcmk__schema_t *schema = data;
relaxng_ctx_cache_t *ctx = NULL;
switch (schema->validator) {
case pcmk__schema_validator_none: // not cached
break;
case pcmk__schema_validator_rng: // cached
ctx = (relaxng_ctx_cache_t *) schema->cache;
if (ctx == NULL) {
break;
}
if (ctx->parser != NULL) {
xmlRelaxNGFreeParserCtxt(ctx->parser);
}
if (ctx->valid != NULL) {
xmlRelaxNGFreeValidCtxt(ctx->valid);
}
if (ctx->rng != NULL) {
xmlRelaxNGFree(ctx->rng);
}
free(ctx);
schema->cache = NULL;
break;
}
free(schema->name);
free(schema->transform);
free(schema->transform_enter);
}
/*!
* \internal
* \brief Clean up global memory associated with XML schemas
*/
void
crm_schema_cleanup(void)
{
g_list_free_full(known_schemas, free_schema);
known_schemas = NULL;
initialized = false;
wrap_libxslt(true);
}
/*!
* \internal
* \brief Get schema list entry corresponding to a schema name
*
* \param[in] name Name of schema to get
*
* \return Schema list entry corresponding to \p name, or NULL if unknown
*/
GList *
pcmk__get_schema(const char *name)
{
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
if (name == NULL) {
name = PCMK_VALUE_NONE;
}
for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
pcmk__schema_t *schema = iter->data;
if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
return iter;
}
}
return NULL;
}
/*!
* \internal
* \brief Compare two schema version numbers given the schema names
*
* \param[in] schema1 Name of first schema to compare
* \param[in] schema2 Name of second schema to compare
*
* \return Standard comparison result (negative integer if \p schema1 has the
* lower version number, positive integer if \p schema1 has the higher
* version number, of 0 if the version numbers are equal)
*/
int
pcmk__cmp_schemas_by_name(const char *schema1_name, const char *schema2_name)
{
GList *entry1 = pcmk__get_schema(schema1_name);
GList *entry2 = pcmk__get_schema(schema2_name);
if (entry1 == NULL) {
return (entry2 == NULL)? 0 : -1;
} else if (entry2 == NULL) {
return 1;
} else {
pcmk__schema_t *schema1 = entry1->data;
pcmk__schema_t *schema2 = entry2->data;
return schema1->schema_index - schema2->schema_index;
}
}
static bool
validate_with(xmlNode *xml, pcmk__schema_t *schema,
xmlRelaxNGValidityErrorFunc error_handler,
void *error_handler_context)
{
bool valid = false;
char *file = NULL;
relaxng_ctx_cache_t **cache = NULL;
if (schema == NULL) {
return false;
}
if (schema->validator == pcmk__schema_validator_none) {
return true;
}
- if (pcmk__str_eq(schema->name, "pacemaker-next", pcmk__str_none)) {
- crm_warn("The pacemaker-next schema is deprecated and will be removed "
- "in a future release.");
- }
-
file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng,
schema->name);
crm_trace("Validating with %s (type=%d)",
pcmk__s(file, "missing schema"), schema->validator);
switch (schema->validator) {
case pcmk__schema_validator_rng:
cache = (relaxng_ctx_cache_t **) &(schema->cache);
valid = validate_with_relaxng(xml->doc, error_handler, error_handler_context, file, cache);
break;
default:
crm_err("Unknown validator type: %d", schema->validator);
break;
}
free(file);
return valid;
}
static bool
validate_with_silent(xmlNode *xml, pcmk__schema_t *schema)
{
bool rc, sl_backup = silent_logging;
silent_logging = TRUE;
rc = validate_with(xml, schema, (xmlRelaxNGValidityErrorFunc) xml_log, GUINT_TO_POINTER(LOG_ERR));
silent_logging = sl_backup;
return rc;
}
bool
pcmk__validate_xml(xmlNode *xml_blob, const char *validation,
xmlRelaxNGValidityErrorFunc error_handler,
void *error_handler_context)
{
GList *entry = NULL;
pcmk__schema_t *schema = NULL;
CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false);
if (validation == NULL) {
validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH);
}
+ pcmk__warn_if_schema_deprecated(validation);
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
if (validation == NULL) {
bool valid = false;
for (entry = known_schemas; entry != NULL; entry = entry->next) {
schema = entry->data;
if (validate_with(xml_blob, schema, NULL, NULL)) {
valid = true;
crm_xml_add(xml_blob, PCMK_XA_VALIDATE_WITH, schema->name);
crm_info("XML validated against %s", schema->name);
}
}
return valid;
}
entry = pcmk__get_schema(validation);
- if (entry != NULL) {
- schema = entry->data;
- return validate_with(xml_blob, schema, error_handler,
- error_handler_context);
+ if (entry == NULL) {
+ pcmk__config_err("Cannot validate CIB with " PCMK_XA_VALIDATE_WITH
+ " set to an unknown schema such as '%s' (manually"
+ " edit to use a known schema)",
+ validation);
+ return false;
}
- crm_err("Unknown validator: %s", validation);
- return false;
+ schema = entry->data;
+ return validate_with(xml_blob, schema, error_handler,
+ error_handler_context);
}
/*!
* \internal
* \brief Validate XML using its configured schema (and send errors to logs)
*
* \param[in] xml XML to validate
*
* \return true if XML validates, otherwise false
*/
bool
pcmk__configured_schema_validates(xmlNode *xml)
{
return pcmk__validate_xml(xml, NULL,
(xmlRelaxNGValidityErrorFunc) xml_log,
GUINT_TO_POINTER(LOG_ERR));
}
/* With this arrangement, an attempt to identify the message severity
as explicitly signalled directly from XSLT is performed in rather
a smart way (no reliance on formatting string + arguments being
always specified as ["%s", purposeful_string], as it can also be
["%s: %s", some_prefix, purposeful_string] etc. so every argument
pertaining %s specifier is investigated), and if such a mark found,
the respective level is determined and, when the messages are to go
to the native logs, the mark itself gets dropped
(by the means of string shift).
NOTE: whether the native logging is the right sink is decided per
the ctx parameter -- NULL denotes this case, otherwise it
carries a pointer to the numeric expression of the desired
target logging level (messages with higher level will be
suppressed)
NOTE: on some architectures, this string shift may not have any
effect, but that's an acceptable tradeoff
The logging level for not explicitly designated messages
(suspicious, likely internal errors or some runaways) is
LOG_WARNING.
*/
static void G_GNUC_PRINTF(2, 3)
cib_upgrade_err(void *ctx, const char *fmt, ...)
{
va_list ap, aq;
char *arg_cur;
bool found = FALSE;
const char *fmt_iter = fmt;
uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */
const unsigned * log_level = (const unsigned *) ctx;
enum {
escan_seennothing,
escan_seenpercent,
} scan_state = escan_seennothing;
va_start(ap, fmt);
va_copy(aq, ap);
while (!found && *fmt_iter != '\0') {
/* while casing schema borrowed from libqb:qb_vsnprintf_serialize */
switch (*fmt_iter++) {
case '%':
if (scan_state == escan_seennothing) {
scan_state = escan_seenpercent;
} else if (scan_state == escan_seenpercent) {
scan_state = escan_seennothing;
}
break;
case 's':
if (scan_state == escan_seenpercent) {
scan_state = escan_seennothing;
arg_cur = va_arg(aq, char *);
if (arg_cur != NULL) {
switch (arg_cur[0]) {
case 'W':
if (!strncmp(arg_cur, "WARNING: ",
sizeof("WARNING: ") - 1)) {
msg_log_level = LOG_WARNING;
}
if (ctx == NULL) {
memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1,
strlen(arg_cur + sizeof("WARNING: ") - 1) + 1);
}
found = TRUE;
break;
case 'I':
if (!strncmp(arg_cur, "INFO: ",
sizeof("INFO: ") - 1)) {
msg_log_level = LOG_INFO;
}
if (ctx == NULL) {
memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1,
strlen(arg_cur + sizeof("INFO: ") - 1) + 1);
}
found = TRUE;
break;
case 'D':
if (!strncmp(arg_cur, "DEBUG: ",
sizeof("DEBUG: ") - 1)) {
msg_log_level = LOG_DEBUG;
}
if (ctx == NULL) {
memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1,
strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1);
}
found = TRUE;
break;
}
}
}
break;
case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.':
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '*':
break;
case 'l':
case 'z':
case 't':
case 'j':
case 'd': case 'i':
case 'o':
case 'u':
case 'x': case 'X':
case 'e': case 'E':
case 'f': case 'F':
case 'g': case 'G':
case 'a': case 'A':
case 'c':
case 'p':
if (scan_state == escan_seenpercent) {
(void) va_arg(aq, void *); /* skip forward */
scan_state = escan_seennothing;
}
break;
default:
scan_state = escan_seennothing;
break;
}
}
if (log_level != NULL) {
/* intention of the following offset is:
cibadmin -V -> start showing INFO labelled messages */
if (*log_level + 4 >= msg_log_level) {
vfprintf(stderr, fmt, ap);
}
} else {
PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap);
}
va_end(aq);
va_end(ap);
}
/*!
* \internal
* \brief Apply a single XSL transformation to given XML
*
* \param[in] xml XML to transform
* \param[in] transform XSL name
* \param[in] to_logs If false, certain validation errors will be sent to
* stderr rather than logged
*
* \return Transformed XML on success, otherwise NULL
*/
static xmlNode *
apply_transformation(const xmlNode *xml, const char *transform,
gboolean to_logs)
{
char *xform = NULL;
xmlNode *out = NULL;
xmlDocPtr res = NULL;
xsltStylesheet *xslt = NULL;
xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt,
transform);
/* for capturing, e.g., what's emitted via */
if (to_logs) {
xsltSetGenericErrorFunc(NULL, cib_upgrade_err);
} else {
xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err);
}
xslt = xsltParseStylesheetFile((pcmkXmlStr) xform);
CRM_CHECK(xslt != NULL, goto cleanup);
res = xsltApplyStylesheet(xslt, xml->doc, NULL);
CRM_CHECK(res != NULL, goto cleanup);
xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */
out = xmlDocGetRootElement(res);
cleanup:
if (xslt) {
xsltFreeStylesheet(xslt);
}
free(xform);
return out;
}
/*!
* \internal
* \brief Perform all transformations needed to upgrade XML to next schema
*
* A schema upgrade can require up to three XSL transformations: an "enter"
* transform, the main upgrade transform, and a "leave" transform. Perform
* all needed transforms to upgrade given XML to the next schema.
*
* \param[in] original_xml XML to transform
* \param[in] schema_index Index of schema that successfully validates
* \p original_xml
* \param[in] to_logs If false, certain validation errors will be sent to
* stderr rather than logged
*
* \return XML result of schema transforms if successful, otherwise NULL
*/
static xmlNode *
apply_upgrade(const xmlNode *original_xml, int schema_index, gboolean to_logs)
{
pcmk__schema_t *schema = g_list_nth_data(known_schemas, schema_index);
pcmk__schema_t *upgraded_schema = g_list_nth_data(known_schemas,
schema_index + 1);
bool transform_onleave = false;
char *transform_leave;
const xmlNode *xml = original_xml;
xmlNode *upgrade = NULL;
xmlNode *final = NULL;
xmlRelaxNGValidityErrorFunc error_handler = NULL;
CRM_ASSERT((schema != NULL) && (upgraded_schema != NULL));
if (to_logs) {
error_handler = (xmlRelaxNGValidityErrorFunc) xml_log;
}
transform_onleave = schema->transform_onleave;
if (schema->transform_enter != NULL) {
crm_debug("Upgrading schema from %s to %s: "
"applying pre-upgrade XSL transform %s",
schema->name, upgraded_schema->name, schema->transform_enter);
upgrade = apply_transformation(xml, schema->transform_enter, to_logs);
if (upgrade == NULL) {
crm_warn("Pre-upgrade XSL transform %s failed, "
"will skip post-upgrade transform",
schema->transform_enter);
transform_onleave = FALSE;
} else {
xml = upgrade;
}
}
crm_debug("Upgrading schema from %s to %s: "
"applying upgrade XSL transform %s",
schema->name, upgraded_schema->name, schema->transform);
final = apply_transformation(xml, schema->transform, to_logs);
if (upgrade != xml) {
free_xml(upgrade);
upgrade = NULL;
}
if ((final != NULL) && transform_onleave) {
upgrade = final;
/* following condition ensured in add_schema_by_version */
CRM_ASSERT(schema->transform_enter != NULL);
transform_leave = strdup(schema->transform_enter);
/* enter -> leave */
memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1);
crm_debug("Upgrading schema from %s to %s: "
"applying post-upgrade XSL transform %s",
schema->name, upgraded_schema->name, transform_leave);
final = apply_transformation(upgrade, transform_leave, to_logs);
if (final == NULL) {
crm_warn("Ignoring failure of post-upgrade XSL transform %s",
transform_leave);
final = upgrade;
} else {
free_xml(upgrade);
}
free(transform_leave);
}
if (final == NULL) {
return NULL;
}
// Ensure result validates with its new schema
if (!validate_with(final, upgraded_schema, error_handler,
GUINT_TO_POINTER(LOG_ERR))) {
crm_err("Schema upgrade from %s to %s failed: "
"XSL transform %s produced an invalid configuration",
schema->name, upgraded_schema->name, schema->transform);
crm_log_xml_debug(final, "bad-transform-result");
free_xml(final);
return NULL;
}
crm_info("Schema upgrade from %s to %s succeeded",
schema->name, upgraded_schema->name);
return final;
}
/*!
* \internal
* \brief Get the schema list entry corresponding to XML configuration
*
* \param[in] xml CIB XML to check
*
* \return List entry of schema configured in \p xml
*/
static GList *
get_configured_schema(const xmlNode *xml)
{
const char *schema_name = crm_element_value(xml, PCMK_XA_VALIDATE_WITH);
+ pcmk__warn_if_schema_deprecated(schema_name);
if (schema_name == NULL) {
return NULL;
}
return pcmk__get_schema(schema_name);
}
/*!
* \brief Update CIB XML to latest schema that validates it
*
* \param[in,out] xml XML to update (may be freed and replaced
* after being transformed)
* \param[in] max_schema_name If not NULL, do not update \p xml to any
* schema later than this one
* \param[in] transform If false, do not update \p xml to any schema
* that requires an XSL transform
* \param[in] to_logs If false, certain validation errors will be
* sent to stderr rather than logged
*
* \return Standard Pacemaker return code
*/
int
pcmk__update_schema(xmlNode **xml, const char *max_schema_name, bool transform,
bool to_logs)
{
int max_stable_schemas = xml_latest_schema_index();
int max_schema_index = 0;
int rc = pcmk_rc_ok;
GList *entry = NULL;
pcmk__schema_t *best_schema = NULL;
pcmk__schema_t *original_schema = NULL;
xmlRelaxNGValidityErrorFunc error_handler =
to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL;
CRM_CHECK((xml != NULL) && (*xml != NULL) && ((*xml)->doc != NULL),
return EINVAL);
if (max_schema_name != NULL) {
GList *max_entry = pcmk__get_schema(max_schema_name);
if (max_entry != NULL) {
pcmk__schema_t *max_schema = max_entry->data;
max_schema_index = max_schema->schema_index;
}
}
if ((max_schema_index < 1) || (max_schema_index > max_stable_schemas)) {
max_schema_index = max_stable_schemas;
}
entry = get_configured_schema(*xml);
if (entry == NULL) {
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
entry = known_schemas;
} else {
original_schema = entry->data;
if (original_schema->schema_index >= max_schema_index) {
return pcmk_rc_ok;
}
}
for (; entry != NULL; entry = entry->next) {
pcmk__schema_t *current_schema = entry->data;
xmlNode *upgrade = NULL;
if (current_schema->schema_index > max_schema_index) {
break;
}
if (!validate_with(*xml, current_schema, error_handler,
GUINT_TO_POINTER(LOG_ERR))) {
crm_debug("Schema %s does not validate", current_schema->name);
if (best_schema != NULL) {
/* we've satisfied the validation, no need to check further */
break;
}
rc = pcmk_rc_schema_validation;
continue; // Try again with the next higher schema
}
crm_debug("Schema %s validates", current_schema->name);
rc = pcmk_rc_ok;
best_schema = current_schema;
if (current_schema->schema_index == max_schema_index) {
break; // No further transformations possible
}
if (!transform || (current_schema->transform == NULL)
|| validate_with_silent(*xml, entry->next->data)) {
/* The next schema either doesn't require a transform or validates
* successfully even without the transform. Skip the transform and
* try the next schema with the same XML.
*/
continue;
}
upgrade = apply_upgrade(*xml, current_schema->schema_index, to_logs);
if (upgrade == NULL) {
/* The transform failed, so this schema can't be used. Later
* schemas are unlikely to validate, but try anyway until we
* run out of options.
*/
rc = pcmk_rc_transform_failed;
} else {
best_schema = current_schema;
free_xml(*xml);
*xml = upgrade;
}
}
if (best_schema != NULL) {
if ((original_schema == NULL)
|| (best_schema->schema_index > original_schema->schema_index)) {
crm_info("%s the configuration schema to %s",
(transform? "Transformed" : "Upgraded"),
best_schema->name);
crm_xml_add(*xml, PCMK_XA_VALIDATE_WITH, best_schema->name);
}
}
return rc;
}
/*!
* \internal
* \brief Update XML from its configured schema to the latest major series
*
* \param[in,out] xml XML to update
* \param[in] to_logs If false, certain validation errors will be
* sent to stderr rather than logged
*
* \return true if XML was successfully updated, otherwise false
*/
bool
pcmk__update_configured_schema(xmlNode **xml, bool to_logs)
{
bool rc = true;
char *original_schema_name = NULL;
+
+ // @COMPAT Not specifying a schema name is deprecated since 2.1.8
const char *effective_original_name = "the first";
+
int orig_version = -1;
pcmk__schema_t *x_0_schema = pcmk__find_x_0_schema()->data;
GList *entry = NULL;
original_schema_name = crm_element_value_copy(*xml, PCMK_XA_VALIDATE_WITH);
+ pcmk__warn_if_schema_deprecated(original_schema_name);
entry = pcmk__get_schema(original_schema_name);
if (entry != NULL) {
pcmk__schema_t *original_schema = entry->data;
effective_original_name = original_schema->name;
orig_version = original_schema->schema_index;
}
if (orig_version < x_0_schema->schema_index) {
// Current configuration schema is not acceptable, try to update
xmlNode *converted = NULL;
const char *new_schema_name = NULL;
pcmk__schema_t *schema = NULL;
entry = NULL;
converted = pcmk__xml_copy(NULL, *xml);
if (pcmk__update_schema(&converted, NULL, true, to_logs) == pcmk_rc_ok) {
new_schema_name = crm_element_value(converted,
PCMK_XA_VALIDATE_WITH);
entry = pcmk__get_schema(new_schema_name);
}
schema = (entry == NULL)? NULL : entry->data;
if ((schema == NULL)
|| (schema->schema_index < x_0_schema->schema_index)) {
// Updated configuration schema is still not acceptable
if ((orig_version == -1) || (schema == NULL)
|| (schema->schema_index < orig_version)) {
// We couldn't validate any schema at all
if (to_logs) {
pcmk__config_err("Cannot upgrade configuration (claiming "
"%s schema) to at least %s because it "
"does not validate with any schema from "
"%s to the latest",
pcmk__s(original_schema_name, "no"),
x_0_schema->name, effective_original_name);
} else {
fprintf(stderr, "Cannot upgrade configuration (claiming "
"%s schema) to at least %s because it "
"does not validate with any schema from "
"%s to the latest\n",
pcmk__s(original_schema_name, "no"),
x_0_schema->name, effective_original_name);
}
} else {
// We updated configuration successfully, but still too low
if (to_logs) {
pcmk__config_err("Cannot upgrade configuration (claiming "
"%s schema) to at least %s because it "
"would not upgrade past %s",
pcmk__s(original_schema_name, "no"),
x_0_schema->name,
pcmk__s(new_schema_name, "unspecified version"));
} else {
fprintf(stderr, "Cannot upgrade configuration (claiming "
"%s schema) to at least %s because it "
"would not upgrade past %s\n",
pcmk__s(original_schema_name, "no"),
x_0_schema->name,
pcmk__s(new_schema_name, "unspecified version"));
}
}
free_xml(converted);
converted = NULL;
rc = false;
} else {
// Updated configuration schema is acceptable
free_xml(*xml);
*xml = converted;
if (schema->schema_index < xml_latest_schema_index()) {
if (to_logs) {
pcmk__config_warn("Configuration with %s schema was "
"internally upgraded to acceptable (but "
"not most recent) %s",
pcmk__s(original_schema_name, "no"),
schema->name);
}
} else if (to_logs) {
crm_info("Configuration with %s schema was internally "
"upgraded to latest version %s",
pcmk__s(original_schema_name, "no"),
schema->name);
}
}
} else {
+ // @COMPAT the none schema is deprecated since 2.1.8
pcmk__schema_t *none_schema = NULL;
entry = pcmk__get_schema(PCMK_VALUE_NONE);
CRM_ASSERT((entry != NULL) && (entry->data != NULL));
none_schema = entry->data;
- if (orig_version >= none_schema->schema_index) {
- // Schema validation is disabled
- if (to_logs) {
- pcmk__config_warn("Schema validation of configuration is "
- "disabled (enabling is encouraged and "
- "prevents common misconfigurations)");
-
- } else {
- fprintf(stderr, "Schema validation of configuration is "
- "disabled (enabling is encouraged and "
- "prevents common misconfigurations)\n");
- }
+ if (!to_logs && (orig_version >= none_schema->schema_index)) {
+ fprintf(stderr, "Schema validation of configuration is "
+ "disabled (support for " PCMK_XA_VALIDATE_WITH
+ " set to \"" PCMK_VALUE_NONE "\" is deprecated"
+ " and will be removed in a future release)\n");
}
}
free(original_schema_name);
return rc;
}
/*!
* \internal
* \brief Return a list of all schema files and any associated XSLT files
* later than the given one
* \brief Return a list of all schema versions later than the given one
*
* \param[in] schema The schema to compare against (for example,
* "pacemaker-3.1.rng" or "pacemaker-3.1")
*
* \note The caller is responsible for freeing both the returned list and
* the elements of the list
*/
GList *
pcmk__schema_files_later_than(const char *name)
{
GList *lst = NULL;
pcmk__schema_version_t ver;
if (!version_from_filename(name, &ver)) {
return lst;
}
for (GList *iter = g_list_nth(known_schemas, xml_latest_schema_index());
iter != NULL; iter = iter->prev) {
pcmk__schema_t *schema = iter->data;
char *s = NULL;
if (schema_cmp(ver, schema->version) != -1) {
continue;
}
s = crm_strdup_printf("%s.rng", schema->name);
lst = g_list_prepend(lst, s);
if (schema->transform != NULL) {
char *xform = crm_strdup_printf("%s.xsl", schema->transform);
lst = g_list_prepend(lst, xform);
}
if (schema->transform_enter != NULL) {
char *enter = crm_strdup_printf("%s.xsl", schema->transform_enter);
lst = g_list_prepend(lst, enter);
if (schema->transform_onleave) {
int last_dash = strrchr(enter, '-') - enter;
char *leave = crm_strdup_printf("%.*s-leave.xsl", last_dash, enter);
lst = g_list_prepend(lst, leave);
}
}
}
return lst;
}
static void
append_href(xmlNode *xml, void *user_data)
{
GList **list = user_data;
char *href = crm_element_value_copy(xml, "href");
if (href == NULL) {
return;
}
*list = g_list_prepend(*list, href);
}
static void
external_refs_in_schema(GList **list, const char *contents)
{
/* local-name()= is needed to ignore the xmlns= setting at the top of
* the XML file. Otherwise, the xpath query will always return nothing.
*/
const char *search = "//*[local-name()='externalRef'] | //*[local-name()='include']";
xmlNode *xml = pcmk__xml_parse(contents);
crm_foreach_xpath_result(xml, search, append_href, list);
free_xml(xml);
}
static int
read_file_contents(const char *file, char **contents)
{
int rc = pcmk_rc_ok;
char *path = NULL;
if (pcmk__ends_with(file, ".rng")) {
path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, file);
} else {
path = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, file);
}
rc = pcmk__file_contents(path, contents);
free(path);
return rc;
}
static void
add_schema_file_to_xml(xmlNode *parent, const char *file, GList **already_included)
{
char *contents = NULL;
char *path = NULL;
xmlNode *file_node = NULL;
GList *includes = NULL;
int rc = pcmk_rc_ok;
/* If we already included this file, don't do so again. */
if (g_list_find_custom(*already_included, file, (GCompareFunc) strcmp) != NULL) {
return;
}
/* Ensure whatever file we were given has a suffix we know about. If not,
* just assume it's an RNG file.
*/
if (!pcmk__ends_with(file, ".rng") && !pcmk__ends_with(file, ".xsl")) {
path = crm_strdup_printf("%s.rng", file);
} else {
path = pcmk__str_copy(file);
}
rc = read_file_contents(path, &contents);
if (rc != pcmk_rc_ok || contents == NULL) {
crm_warn("Could not read schema file %s: %s", file, pcmk_rc_str(rc));
free(path);
return;
}
/* Create a new node with the contents of the file
* as a CDATA block underneath it.
*/
file_node = pcmk__xe_create(parent, PCMK_XA_FILE);
crm_xml_add(file_node, PCMK_XA_PATH, path);
*already_included = g_list_prepend(*already_included, path);
xmlAddChild(file_node, xmlNewCDataBlock(parent->doc, (pcmkXmlStr) contents,
strlen(contents)));
/* Scan the file for any or nodes and build up
* a list of the files they reference.
*/
external_refs_in_schema(&includes, contents);
/* For each referenced file, recurse to add it (and potentially anything it
* references, ...) to the XML.
*/
for (GList *iter = includes; iter != NULL; iter = iter->next) {
add_schema_file_to_xml(parent, iter->data, already_included);
}
free(contents);
g_list_free_full(includes, free);
}
/*!
* \internal
* \brief Add an XML schema file and all the files it references as children
* of a given XML node
*
* \param[in,out] parent The parent XML node
* \param[in] name The schema version to compare against
* (for example, "pacemaker-3.1" or "pacemaker-3.1.rng")
* \param[in,out] already_included A list of names that have already been added
* to the parent node.
*
* \note The caller is responsible for freeing both the returned list and
* the elements of the list
*/
void
pcmk__build_schema_xml_node(xmlNode *parent, const char *name, GList **already_included)
{
/* First, create an unattached node to add all the schema files to as children. */
xmlNode *schema_node = pcmk__xe_create(NULL, PCMK__XA_SCHEMA);
crm_xml_add(schema_node, PCMK_XA_VERSION, name);
add_schema_file_to_xml(schema_node, name, already_included);
/* Then, if we actually added any children, attach the node to parent. If
* we did not add any children (for instance, name was invalid), this prevents
* us from returning a document with additional empty children.
*/
if (schema_node->children != NULL) {
xmlAddChild(parent, schema_node);
} else {
free_xml(schema_node);
}
}
/*!
* \internal
* \brief Return the directory containing any extra schema files that a
* Pacemaker Remote node fetched from the cluster
*/
const char *
pcmk__remote_schema_dir(void)
{
const char *dir = pcmk__env_option(PCMK__ENV_REMOTE_SCHEMA_DIRECTORY);
if (pcmk__str_empty(dir)) {
return PCMK__REMOTE_SCHEMA_DIR;
}
return dir;
}
+/*!
+ * \internal
+ * \brief Warn if a given validation schema is deprecated
+ *
+ * \param[in] Schema name to check
+ */
+void
+pcmk__warn_if_schema_deprecated(const char *schema)
+{
+ if ((schema == NULL) ||
+ pcmk__strcase_any_of(schema, "pacemaker-next", PCMK_VALUE_NONE, NULL)) {
+ pcmk__config_warn("Support for " PCMK_XA_VALIDATE_WITH "='%s' is "
+ "deprecated and will be removed in a future release "
+ "without the possibility of upgrades (manually edit "
+ "to use a supported schema)", pcmk__s(schema, ""));
+ }
+}
+
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include
const char *
xml_latest_schema(void)
{
return pcmk__highest_schema_name();
}
const char *
get_schema_name(int version)
{
pcmk__schema_t *schema = g_list_nth_data(known_schemas, version);
return (schema != NULL)? schema->name : "unknown";
}
int
get_schema_version(const char *name)
{
int lpc = 0;
if (name == NULL) {
name = PCMK_VALUE_NONE;
}
for (GList *iter = known_schemas; iter != NULL; iter = iter->next) {
pcmk__schema_t *schema = iter->data;
if (pcmk__str_eq(name, schema->name, pcmk__str_casei)) {
return lpc;
}
lpc++;
}
return -1;
}
int
update_validation(xmlNode **xml, int *best, int max, gboolean transform,
gboolean to_logs)
{
int rc = pcmk__update_schema(xml, get_schema_name(max), transform, to_logs);
if ((best != NULL) && (xml != NULL) && (rc == pcmk_rc_ok)) {
const char *schema_name = crm_element_value(*xml,
PCMK_XA_VALIDATE_WITH);
GList *schema_entry = pcmk__get_schema(schema_name);
if (schema_entry != NULL) {
*best = ((pcmk__schema_t *)(schema_entry->data))->schema_index;
}
}
return pcmk_rc2legacy(rc);
}
gboolean
validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs)
{
bool rc = pcmk__validate_xml(xml_blob, validation,
to_logs? (xmlRelaxNGValidityErrorFunc) xml_log : NULL,
GUINT_TO_POINTER(LOG_ERR));
return rc? TRUE : FALSE;
}
static void
dump_file(const char *filename)
{
FILE *fp = NULL;
int ch, line = 0;
CRM_CHECK(filename != NULL, return);
fp = fopen(filename, "r");
if (fp == NULL) {
crm_perror(LOG_ERR, "Could not open %s for reading", filename);
return;
}
fprintf(stderr, "%4d ", ++line);
do {
ch = getc(fp);
if (ch == EOF) {
putc('\n', stderr);
break;
} else if (ch == '\n') {
fprintf(stderr, "\n%4d ", ++line);
} else {
putc(ch, stderr);
}
} while (1);
fclose(fp);
}
gboolean
validate_xml_verbose(const xmlNode *xml_blob)
{
int fd = 0;
xmlDoc *doc = NULL;
xmlNode *xml = NULL;
gboolean rc = FALSE;
char *filename = NULL;
filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir());
umask(S_IWGRP | S_IWOTH | S_IROTH);
fd = mkstemp(filename);
pcmk__xml_write_fd(xml_blob, filename, fd, false, NULL);
dump_file(filename);
doc = xmlReadFile(filename, NULL, 0);
xml = xmlDocGetRootElement(doc);
rc = pcmk__validate_xml(xml, NULL, NULL, NULL);
free_xml(xml);
unlink(filename);
free(filename);
return rc? TRUE : FALSE;
}
gboolean
cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs)
{
bool rc = pcmk__update_configured_schema(xml, to_logs);
if (best_version != NULL) {
const char *name = crm_element_value(*xml, PCMK_XA_VALIDATE_WITH);
if (name == NULL) {
*best_version = -1;
} else {
GList *entry = pcmk__get_schema(name);
pcmk__schema_t *schema = (entry == NULL)? NULL : entry->data;
*best_version = (schema == NULL)? -1 : schema->schema_index;
}
}
return rc? TRUE: FALSE;
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/tests/schemas/crm_schema_init_test.c b/lib/common/tests/schemas/crm_schema_init_test.c
index 10fc6dab97..8da79cb560 100644
--- a/lib/common/tests/schemas/crm_schema_init_test.c
+++ b/lib/common/tests/schemas/crm_schema_init_test.c
@@ -1,151 +1,152 @@
/*
* Copyright 2023-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include "crmcommon_private.h"
static char *remote_schema_dir = NULL;
static int
symlink_schema(const char *tmpdir, const char *target_file, const char *link_file)
{
int rc = 0;
char *oldpath = NULL;
char *newpath = NULL;
oldpath = crm_strdup_printf("%s/%s", PCMK__TEST_SCHEMA_DIR, target_file);
newpath = crm_strdup_printf("%s/%s", tmpdir, link_file);
rc = symlink(oldpath, newpath);
free(oldpath);
free(newpath);
return rc;
}
static int
rm_files(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb)
{
return remove(pathname);
}
static int
rmtree(const char *dir)
{
return nftw(dir, rm_files, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS);
}
static int
setup(void **state)
{
char *dir = NULL;
/* Create a directory to hold additional schema files. These don't need
* to be anything special - we can just copy existing schemas but give
* them new names.
*/
dir = crm_strdup_printf("%s/test-schemas.XXXXXX", pcmk__get_tmpdir());
remote_schema_dir = mkdtemp(dir);
if (remote_schema_dir == NULL) {
free(dir);
return -1;
}
/* Add new files to simulate a remote node not being up-to-date. We can't
* add a new major version here without also creating an XSL transform, and
* we can't add an older version (like 1.1 or 2.11 or something) because
* remotes will only ever ask for stuff newer than their newest.
*/
if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.1.rng") != 0) {
rmdir(dir);
free(dir);
return -1;
}
if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.2.rng") != 0) {
rmdir(dir);
free(dir);
return -1;
}
setenv("PCMK_remote_schema_directory", remote_schema_dir, 1);
setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
/* Do not call crm_schema_init here because that is the function we're
* testing. It needs to be called in each unit test. However, we can
* call crm_schema_cleanup in teardown().
*/
return 0;
}
static int
teardown(void **state)
{
int rc = 0;
char *f = NULL;
crm_schema_cleanup();
unsetenv("PCMK_remote_schema_directory");
unsetenv("PCMK_schema_directory");
rc = rmtree(remote_schema_dir);
free(remote_schema_dir);
free(f);
return rc;
}
static void
assert_schema(const char *schema_name, int schema_index)
{
GList *entry = NULL;
pcmk__schema_t *schema = NULL;
entry = pcmk__get_schema(schema_name);
assert_non_null(entry);
schema = entry->data;
assert_non_null(schema);
assert_int_equal(schema_index, schema->schema_index);
}
static void
extra_schema_files(void **state)
{
crm_schema_init();
/* Just iterate through the list of schemas and make sure everything
* (including the new schemas we loaded from a second directory) is in
* the right order.
*/
assert_schema("pacemaker-1.0", 0);
assert_schema("pacemaker-1.2", 1);
assert_schema("pacemaker-2.0", 3);
assert_schema("pacemaker-3.0", 14);
assert_schema("pacemaker-3.1", 15);
assert_schema("pacemaker-3.2", 16);
// @COMPAT pacemaker-next is deprecated since 2.1.5
assert_schema("pacemaker-next", 17);
+ // @COMPAT none is deprecated since 2.1.8
assert_schema(PCMK_VALUE_NONE, 18);
}
PCMK__UNIT_TEST(setup, teardown,
cmocka_unit_test(extra_schema_files));
diff --git a/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c
index fce1ebdeb7..19ec743612 100644
--- a/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c
+++ b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c
@@ -1,110 +1,121 @@
/*
* Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include "crmcommon_private.h"
static int
setup(void **state)
{
setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
crm_schema_init();
return 0;
}
static int
teardown(void **state)
{
crm_schema_cleanup();
unsetenv("PCMK_schema_directory");
return 0;
}
// NULL schema name defaults to the "none" schema
+// @COMPAT none is deprecated since 2.1.8
static void
unknown_is_lesser(void **state)
{
assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1",
"pacemaker-0.2") == 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1",
"pacemaker-1.0") < 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0",
"pacemaker-0.1") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.1", NULL) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-0.0") > 0);
+
+ /* @COMPAT pacemaker-next is deprecated since 2.1.5,
+ * and pacemaker-0.6 and pacemaker-0.7 since 2.1.8
+ */
assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.6",
"pacemaker-next") < 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
"pacemaker-0.7") > 0);
- assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.1", NULL) < 0);
- assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-0.0") > 0);
}
+// @COMPAT none is deprecated since 2.1.8
static void
none_is_greater(void **state)
{
assert_true(pcmk__cmp_schemas_by_name(NULL, NULL) == 0);
assert_true(pcmk__cmp_schemas_by_name(NULL, PCMK_VALUE_NONE) == 0);
assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE, NULL) == 0);
assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
PCMK_VALUE_NONE) == 0);
- assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
- PCMK_VALUE_NONE) < 0);
- assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
- "pacemaker-next") > 0);
+
assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0",
PCMK_VALUE_NONE) < 0);
assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
"pacemaker-1.0") > 0);
+
+ // @COMPAT pacemaker-next is deprecated since 2.1.5
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ PCMK_VALUE_NONE) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
+ "pacemaker-next") > 0);
}
+// @COMPAT pacemaker-next is deprecated since 2.1.5
+// @COMPAT none is deprecated since 2.1.8
static void
next_is_before_none(void **state)
{
assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
"pacemaker-next") == 0);
assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-next") > 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", NULL) < 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0",
"pacemaker-next") < 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
"pacemaker-1.0") > 0);
}
static void
known_numeric(void **state)
{
assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0",
"pacemaker-1.0") == 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2",
"pacemaker-1.0") > 0);
assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2",
"pacemaker-2.0") < 0);
}
static void
case_insensitive(void **state)
{
assert_true(pcmk__cmp_schemas_by_name("Pacemaker-1.0",
"pacemaker-1.0") == 0);
assert_true(pcmk__cmp_schemas_by_name("PACEMAKER-1.2",
"pacemaker-1.0") > 0);
assert_true(pcmk__cmp_schemas_by_name("PaceMaker-1.2",
"pacemaker-2.0") < 0);
}
PCMK__UNIT_TEST(setup, teardown,
cmocka_unit_test(unknown_is_lesser),
cmocka_unit_test(none_is_greater),
cmocka_unit_test(next_is_before_none),
cmocka_unit_test(known_numeric),
cmocka_unit_test(case_insensitive));
diff --git a/lib/common/tests/schemas/pcmk__get_schema_test.c b/lib/common/tests/schemas/pcmk__get_schema_test.c
index a44089623b..6513dfccb4 100644
--- a/lib/common/tests/schemas/pcmk__get_schema_test.c
+++ b/lib/common/tests/schemas/pcmk__get_schema_test.c
@@ -1,79 +1,81 @@
/*
* Copyright 2023-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU General Public License version 2
* or later (GPLv2+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include "crmcommon_private.h"
static int
setup(void **state)
{
setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
crm_schema_init();
return 0;
}
static int
teardown(void **state)
{
crm_schema_cleanup();
unsetenv("PCMK_schema_directory");
return 0;
}
static void
assert_schema(const char *name, int expected_index)
{
GList *schema_entry = NULL;
pcmk__schema_t *schema = NULL;
schema_entry = pcmk__get_schema(name);
assert_non_null(schema_entry);
schema = schema_entry->data;
assert_non_null(schema);
assert_int_equal(schema->schema_index, expected_index);
}
static void
unknown_schema(void **state)
{
assert_null(pcmk__get_schema(""));
assert_null(pcmk__get_schema("blahblah"));
assert_null(pcmk__get_schema("pacemaker-2.47"));
assert_null(pcmk__get_schema("pacemaker-47.0"));
}
static void
known_schema(void **state)
{
+ // @COMPAT none is deprecated since 2.1.8
assert_schema(NULL, 16); // defaults to "none"
+
assert_schema("pacemaker-1.0", 0);
assert_schema("pacemaker-1.2", 1);
assert_schema("pacemaker-2.0", 3);
assert_schema("pacemaker-2.5", 8);
assert_schema("pacemaker-3.0", 14);
}
static void
case_insensitive(void **state)
{
assert_schema("PACEMAKER-1.0", 0);
assert_schema("pAcEmAkEr-2.0", 3);
assert_schema("paceMAKER-3.0", 14);
}
PCMK__UNIT_TEST(setup, teardown,
cmocka_unit_test(unknown_schema),
cmocka_unit_test(known_schema),
cmocka_unit_test(case_insensitive));
diff --git a/lib/pacemaker/pcmk_graph_consumer.c b/lib/pacemaker/pcmk_graph_consumer.c
index 4d4f5c9f4e..6f1943a41b 100644
--- a/lib/pacemaker/pcmk_graph_consumer.c
+++ b/lib/pacemaker/pcmk_graph_consumer.c
@@ -1,892 +1,890 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
/*
* Functions for freeing transition graph objects
*/
/*!
* \internal
* \brief Free a transition graph action object
*
* \param[in,out] user_data Action to free
*/
static void
free_graph_action(gpointer user_data)
{
pcmk__graph_action_t *action = user_data;
if (action->timer != 0) {
crm_warn("Cancelling timer for graph action %d", action->id);
g_source_remove(action->timer);
}
if (action->params != NULL) {
g_hash_table_destroy(action->params);
}
free_xml(action->xml);
free(action);
}
/*!
* \internal
* \brief Free a transition graph synapse object
*
* \param[in,out] user_data Synapse to free
*/
static void
free_graph_synapse(gpointer user_data)
{
pcmk__graph_synapse_t *synapse = user_data;
g_list_free_full(synapse->actions, free_graph_action);
g_list_free_full(synapse->inputs, free_graph_action);
free(synapse);
}
/*!
* \internal
* \brief Free a transition graph object
*
* \param[in,out] graph Transition graph to free
*/
void
pcmk__free_graph(pcmk__graph_t *graph)
{
if (graph != NULL) {
g_list_free_full(graph->synapses, free_graph_synapse);
free(graph->source);
free(graph->failed_stop_offset);
free(graph->failed_start_offset);
free(graph);
}
}
/*
* Functions for updating graph
*/
/*!
* \internal
* \brief Update synapse after completed prerequisite
*
* A synapse is ready to be executed once all its prerequisite actions (inputs)
* complete. Given a completed action, check whether it is an input for a given
* synapse, and if so, mark the input as confirmed, and mark the synapse as
* ready if appropriate.
*
* \param[in,out] synapse Transition graph synapse to update
* \param[in] action_id ID of an action that completed
*
* \note The only substantial effect here is confirming synapse inputs.
* should_fire_synapse() will recalculate pcmk__synapse_ready, so the only
* thing that uses the pcmk__synapse_ready from here is
* synapse_state_str().
*/
static void
update_synapse_ready(pcmk__graph_synapse_t *synapse, int action_id)
{
if (pcmk_is_set(synapse->flags, pcmk__synapse_ready)) {
return; // All inputs have already been confirmed
}
// Presume ready until proven otherwise
pcmk__set_synapse_flags(synapse, pcmk__synapse_ready);
for (GList *lpc = synapse->inputs; lpc != NULL; lpc = lpc->next) {
pcmk__graph_action_t *prereq = (pcmk__graph_action_t *) lpc->data;
if (prereq->id == action_id) {
crm_trace("Confirming input %d of synapse %d",
action_id, synapse->id);
pcmk__set_graph_action_flags(prereq, pcmk__graph_action_confirmed);
} else if (!pcmk_is_set(prereq->flags, pcmk__graph_action_confirmed)) {
pcmk__clear_synapse_flags(synapse, pcmk__synapse_ready);
crm_trace("Synapse %d still not ready after action %d",
synapse->id, action_id);
}
}
if (pcmk_is_set(synapse->flags, pcmk__synapse_ready)) {
crm_trace("Synapse %d is now ready to execute", synapse->id);
}
}
/*!
* \internal
* \brief Update action and synapse confirmation after action completion
*
* \param[in,out] synapse Transition graph synapse that action belongs to
* \param[in] action_id ID of action that completed
*/
static void
update_synapse_confirmed(pcmk__graph_synapse_t *synapse, int action_id)
{
bool all_confirmed = true;
for (GList *lpc = synapse->actions; lpc != NULL; lpc = lpc->next) {
pcmk__graph_action_t *action = (pcmk__graph_action_t *) lpc->data;
if (action->id == action_id) {
crm_trace("Confirmed action %d of synapse %d",
action_id, synapse->id);
pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed);
} else if (all_confirmed &&
!pcmk_is_set(action->flags, pcmk__graph_action_confirmed)) {
all_confirmed = false;
crm_trace("Synapse %d still not confirmed after action %d",
synapse->id, action_id);
}
}
if (all_confirmed
&& !pcmk_is_set(synapse->flags, pcmk__synapse_confirmed)) {
crm_trace("Confirmed synapse %d", synapse->id);
pcmk__set_synapse_flags(synapse, pcmk__synapse_confirmed);
}
}
/*!
* \internal
* \brief Update the transition graph with a completed action result
*
* \param[in,out] graph Transition graph to update
* \param[in] action Action that completed
*/
void
pcmk__update_graph(pcmk__graph_t *graph, const pcmk__graph_action_t *action)
{
for (GList *lpc = graph->synapses; lpc != NULL; lpc = lpc->next) {
pcmk__graph_synapse_t *synapse = (pcmk__graph_synapse_t *) lpc->data;
if (pcmk_any_flags_set(synapse->flags,
pcmk__synapse_confirmed|pcmk__synapse_failed)) {
continue; // This synapse already completed
} else if (pcmk_is_set(synapse->flags, pcmk__synapse_executed)) {
update_synapse_confirmed(synapse, action->id);
} else if (!pcmk_is_set(action->flags, pcmk__graph_action_failed)
|| (synapse->priority == PCMK_SCORE_INFINITY)) {
update_synapse_ready(synapse, action->id);
}
}
}
/*
* Functions for executing graph
*/
/* A transition graph consists of various types of actions. The library caller
* registers execution functions for each action type, which will be stored
* here.
*/
static pcmk__graph_functions_t *graph_fns = NULL;
/*!
* \internal
* \brief Set transition graph execution functions
*
* \param[in] Execution functions to use
*/
void
pcmk__set_graph_functions(pcmk__graph_functions_t *fns)
{
crm_debug("Setting custom functions for executing transition graphs");
graph_fns = fns;
CRM_ASSERT(graph_fns != NULL);
CRM_ASSERT(graph_fns->rsc != NULL);
CRM_ASSERT(graph_fns->cluster != NULL);
CRM_ASSERT(graph_fns->pseudo != NULL);
CRM_ASSERT(graph_fns->fence != NULL);
}
/*!
* \internal
* \brief Check whether a graph synapse is ready to be executed
*
* \param[in,out] graph Transition graph that synapse is part of
* \param[in,out] synapse Synapse to check
*
* \return true if synapse is ready, false otherwise
*/
static bool
should_fire_synapse(pcmk__graph_t *graph, pcmk__graph_synapse_t *synapse)
{
GList *lpc = NULL;
pcmk__set_synapse_flags(synapse, pcmk__synapse_ready);
for (lpc = synapse->inputs; lpc != NULL; lpc = lpc->next) {
pcmk__graph_action_t *prereq = (pcmk__graph_action_t *) lpc->data;
if (!(pcmk_is_set(prereq->flags, pcmk__graph_action_confirmed))) {
crm_trace("Input %d for synapse %d not yet confirmed",
prereq->id, synapse->id);
pcmk__clear_synapse_flags(synapse, pcmk__synapse_ready);
break;
} else if (pcmk_is_set(prereq->flags, pcmk__graph_action_failed)
&& !pcmk_is_set(prereq->flags,
pcmk__graph_action_can_fail)) {
crm_trace("Input %d for synapse %d confirmed but failed",
prereq->id, synapse->id);
pcmk__clear_synapse_flags(synapse, pcmk__synapse_ready);
break;
}
}
if (pcmk_is_set(synapse->flags, pcmk__synapse_ready)) {
crm_trace("Synapse %d is ready to execute", synapse->id);
} else {
return false;
}
for (lpc = synapse->actions; lpc != NULL; lpc = lpc->next) {
pcmk__graph_action_t *a = (pcmk__graph_action_t *) lpc->data;
if (a->type == pcmk__pseudo_graph_action) {
/* None of the below applies to pseudo ops */
} else if (synapse->priority < graph->abort_priority) {
crm_trace("Skipping synapse %d: priority %d is less than "
"abort priority %d",
synapse->id, synapse->priority, graph->abort_priority);
graph->skipped++;
return false;
} else if (graph_fns->allowed && !(graph_fns->allowed(graph, a))) {
crm_trace("Deferring synapse %d: not allowed", synapse->id);
return false;
}
}
return true;
}
/*!
* \internal
* \brief Initiate an action from a transition graph
*
* \param[in,out] graph Transition graph containing action
* \param[in,out] action Action to execute
*
* \return Standard Pacemaker return code
*/
static int
initiate_action(pcmk__graph_t *graph, pcmk__graph_action_t *action)
{
const char *id = pcmk__xe_id(action->xml);
CRM_CHECK(id != NULL, return EINVAL);
CRM_CHECK(!pcmk_is_set(action->flags, pcmk__graph_action_executed),
return pcmk_rc_already);
pcmk__set_graph_action_flags(action, pcmk__graph_action_executed);
switch (action->type) {
case pcmk__pseudo_graph_action:
crm_trace("Executing pseudo-action %d (%s)", action->id, id);
return graph_fns->pseudo(graph, action);
case pcmk__rsc_graph_action:
crm_trace("Executing resource action %d (%s)", action->id, id);
return graph_fns->rsc(graph, action);
case pcmk__cluster_graph_action:
if (pcmk__str_eq(crm_element_value(action->xml, PCMK_XA_OPERATION),
PCMK_ACTION_STONITH, pcmk__str_none)) {
crm_trace("Executing fencing action %d (%s)",
action->id, id);
return graph_fns->fence(graph, action);
}
crm_trace("Executing cluster action %d (%s)", action->id, id);
return graph_fns->cluster(graph, action);
default:
crm_err("Unsupported graph action type <%s " PCMK_XA_ID "='%s'> "
"(bug?)",
action->xml->name, id);
return EINVAL;
}
}
/*!
* \internal
* \brief Execute a graph synapse
*
* \param[in,out] graph Transition graph with synapse to execute
* \param[in,out] synapse Synapse to execute
*
* \return Standard Pacemaker return value
*/
static int
fire_synapse(pcmk__graph_t *graph, pcmk__graph_synapse_t *synapse)
{
pcmk__set_synapse_flags(synapse, pcmk__synapse_executed);
for (GList *lpc = synapse->actions; lpc != NULL; lpc = lpc->next) {
pcmk__graph_action_t *action = (pcmk__graph_action_t *) lpc->data;
int rc = initiate_action(graph, action);
if (rc != pcmk_rc_ok) {
crm_err("Failed initiating <%s " PCMK_XA_ID "=%d> in synapse %d: "
"%s",
action->xml->name, action->id, synapse->id,
pcmk_rc_str(rc));
pcmk__set_synapse_flags(synapse, pcmk__synapse_confirmed);
pcmk__set_graph_action_flags(action,
pcmk__graph_action_confirmed
|pcmk__graph_action_failed);
return pcmk_rc_error;
}
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Dummy graph method that can be used with simulations
*
* \param[in,out] graph Transition graph containing action
* \param[in,out] action Graph action to be initiated
*
* \return Standard Pacemaker return code
* \note If the PE_fail environment variable is set to the action ID,
* then the graph action will be marked as failed.
*/
static int
pseudo_action_dummy(pcmk__graph_t *graph, pcmk__graph_action_t *action)
{
static int fail = -1;
if (fail < 0) {
long long fail_ll;
if ((pcmk__scan_ll(getenv("PE_fail"), &fail_ll, 0LL) == pcmk_rc_ok)
&& (fail_ll > 0LL) && (fail_ll <= INT_MAX)) {
fail = (int) fail_ll;
} else {
fail = 0;
}
}
if (action->id == fail) {
crm_err("Dummy event handler: pretending action %d failed", action->id);
pcmk__set_graph_action_flags(action, pcmk__graph_action_failed);
graph->abort_priority = PCMK_SCORE_INFINITY;
} else {
crm_trace("Dummy event handler: action %d initiated", action->id);
}
pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed);
pcmk__update_graph(graph, action);
return pcmk_rc_ok;
}
static pcmk__graph_functions_t default_fns = {
pseudo_action_dummy,
pseudo_action_dummy,
pseudo_action_dummy,
pseudo_action_dummy
};
/*!
* \internal
* \brief Execute all actions in a transition graph
*
* \param[in,out] graph Transition graph to execute
*
* \return Status of transition after execution
*/
enum pcmk__graph_status
pcmk__execute_graph(pcmk__graph_t *graph)
{
GList *lpc = NULL;
int log_level = LOG_DEBUG;
enum pcmk__graph_status pass_result = pcmk__graph_active;
const char *status = "In progress";
if (graph_fns == NULL) {
graph_fns = &default_fns;
}
if (graph == NULL) {
return pcmk__graph_complete;
}
graph->fired = 0;
graph->pending = 0;
graph->skipped = 0;
graph->completed = 0;
graph->incomplete = 0;
// Count completed and in-flight synapses
for (lpc = graph->synapses; lpc != NULL; lpc = lpc->next) {
pcmk__graph_synapse_t *synapse = (pcmk__graph_synapse_t *) lpc->data;
if (pcmk_is_set(synapse->flags, pcmk__synapse_confirmed)) {
graph->completed++;
} else if (!pcmk_is_set(synapse->flags, pcmk__synapse_failed)
&& pcmk_is_set(synapse->flags, pcmk__synapse_executed)) {
graph->pending++;
}
}
crm_trace("Executing graph %d (%d synapses already completed, %d pending)",
graph->id, graph->completed, graph->pending);
// Execute any synapses that are ready
for (lpc = graph->synapses; lpc != NULL; lpc = lpc->next) {
pcmk__graph_synapse_t *synapse = (pcmk__graph_synapse_t *) lpc->data;
if ((graph->batch_limit > 0)
&& (graph->pending >= graph->batch_limit)) {
crm_debug("Throttling graph execution: batch limit (%d) reached",
graph->batch_limit);
break;
} else if (pcmk_is_set(synapse->flags, pcmk__synapse_failed)) {
graph->skipped++;
continue;
} else if (pcmk_any_flags_set(synapse->flags,
pcmk__synapse_confirmed
|pcmk__synapse_executed)) {
continue; // Already handled
} else if (should_fire_synapse(graph, synapse)) {
graph->fired++;
if (fire_synapse(graph, synapse) != pcmk_rc_ok) {
crm_err("Synapse %d failed to fire", synapse->id);
log_level = LOG_ERR;
graph->abort_priority = PCMK_SCORE_INFINITY;
graph->incomplete++;
graph->fired--;
}
if (!(pcmk_is_set(synapse->flags, pcmk__synapse_confirmed))) {
graph->pending++;
}
} else {
crm_trace("Synapse %d cannot fire", synapse->id);
graph->incomplete++;
}
}
if ((graph->pending == 0) && (graph->fired == 0)) {
graph->complete = true;
if ((graph->incomplete != 0) && (graph->abort_priority <= 0)) {
log_level = LOG_WARNING;
pass_result = pcmk__graph_terminated;
status = "Terminated";
} else if (graph->skipped != 0) {
log_level = LOG_NOTICE;
pass_result = pcmk__graph_complete;
status = "Stopped";
} else {
log_level = LOG_NOTICE;
pass_result = pcmk__graph_complete;
status = "Complete";
}
} else if (graph->fired == 0) {
pass_result = pcmk__graph_pending;
}
do_crm_log(log_level,
"Transition %d (Complete=%d, Pending=%d,"
" Fired=%d, Skipped=%d, Incomplete=%d, Source=%s): %s",
graph->id, graph->completed, graph->pending, graph->fired,
graph->skipped, graph->incomplete, graph->source, status);
return pass_result;
}
/*
* Functions for unpacking transition graph XML into structs
*/
/*!
* \internal
* \brief Unpack a transition graph action from XML
*
* \param[in] parent Synapse that action is part of
* \param[in] xml_action Action XML to unparse
*
* \return Newly allocated action on success, or NULL otherwise
*/
static pcmk__graph_action_t *
unpack_action(pcmk__graph_synapse_t *parent, xmlNode *xml_action)
{
enum pcmk__graph_action_type action_type;
pcmk__graph_action_t *action = NULL;
const char *value = pcmk__xe_id(xml_action);
if (value == NULL) {
crm_err("Ignoring transition graph action without " PCMK_XA_ID
" (bug?)");
crm_log_xml_trace(xml_action, "invalid");
return NULL;
}
if (pcmk__xe_is(xml_action, PCMK__XE_RSC_OP)) {
action_type = pcmk__rsc_graph_action;
} else if (pcmk__xe_is(xml_action, PCMK__XE_PSEUDO_EVENT)) {
action_type = pcmk__pseudo_graph_action;
} else if (pcmk__xe_is(xml_action, PCMK__XE_CRM_EVENT)) {
action_type = pcmk__cluster_graph_action;
} else {
crm_err("Ignoring transition graph action of unknown type '%s' (bug?)",
xml_action->name);
crm_log_xml_trace(xml_action, "invalid");
return NULL;
}
action = calloc(1, sizeof(pcmk__graph_action_t));
if (action == NULL) {
crm_perror(LOG_CRIT, "Cannot unpack transition graph action");
crm_log_xml_trace(xml_action, "lost");
return NULL;
}
pcmk__scan_min_int(value, &(action->id), -1);
action->type = pcmk__rsc_graph_action;
action->xml = pcmk__xml_copy(NULL, xml_action);
action->synapse = parent;
action->type = action_type;
action->params = xml2list(action->xml);
value = crm_meta_value(action->params, PCMK_META_TIMEOUT);
pcmk__scan_min_int(value, &(action->timeout), 0);
/* Take PCMK_META_START_DELAY into account for the timeout of the action
* timer
*/
value = crm_meta_value(action->params, PCMK_META_START_DELAY);
{
int start_delay;
pcmk__scan_min_int(value, &start_delay, 0);
action->timeout += start_delay;
}
if (pcmk__guint_from_hash(action->params, CRM_META "_" PCMK_META_INTERVAL,
0, &(action->interval_ms)) != pcmk_rc_ok) {
action->interval_ms = 0;
}
value = crm_meta_value(action->params, PCMK__META_CAN_FAIL);
if (value != NULL) {
int can_fail = 0;
if ((crm_str_to_boolean(value, &can_fail) > 0) && (can_fail > 0)) {
pcmk__set_graph_action_flags(action, pcmk__graph_action_can_fail);
} else {
pcmk__clear_graph_action_flags(action, pcmk__graph_action_can_fail);
}
-#ifndef PCMK__COMPAT_2_0
if (pcmk_is_set(action->flags, pcmk__graph_action_can_fail)) {
crm_warn("Support for the " PCMK__META_CAN_FAIL " meta-attribute "
"is deprecated and will be removed in a future release");
}
-#endif
}
crm_trace("Action %d has timer set to %dms", action->id, action->timeout);
return action;
}
/*!
* \internal
* \brief Unpack transition graph synapse from XML
*
* \param[in,out] new_graph Transition graph that synapse is part of
* \param[in] xml_synapse Synapse XML
*
* \return Newly allocated synapse on success, or NULL otherwise
*/
static pcmk__graph_synapse_t *
unpack_synapse(pcmk__graph_t *new_graph, const xmlNode *xml_synapse)
{
const char *value = NULL;
xmlNode *action_set = NULL;
pcmk__graph_synapse_t *new_synapse = NULL;
crm_trace("Unpacking synapse %s", pcmk__xe_id(xml_synapse));
new_synapse = calloc(1, sizeof(pcmk__graph_synapse_t));
if (new_synapse == NULL) {
return NULL;
}
pcmk__scan_min_int(pcmk__xe_id(xml_synapse), &(new_synapse->id), 0);
value = crm_element_value(xml_synapse, PCMK__XA_PRIORITY);
pcmk__scan_min_int(value, &(new_synapse->priority), 0);
CRM_CHECK(new_synapse->id >= 0,
free_graph_synapse((gpointer) new_synapse); return NULL);
new_graph->num_synapses++;
crm_trace("Unpacking synapse %s action sets",
crm_element_value(xml_synapse, PCMK_XA_ID));
for (action_set = pcmk__xe_first_child(xml_synapse, "action_set", NULL,
NULL);
action_set != NULL; action_set = pcmk__xe_next_same(action_set)) {
for (xmlNode *action = pcmk__xe_first_child(action_set, NULL, NULL,
NULL);
action != NULL; action = pcmk__xe_next(action)) {
pcmk__graph_action_t *new_action = unpack_action(new_synapse,
action);
if (new_action == NULL) {
continue;
}
crm_trace("Adding action %d to synapse %d",
new_action->id, new_synapse->id);
new_graph->num_actions++;
new_synapse->actions = g_list_append(new_synapse->actions,
new_action);
}
}
crm_trace("Unpacking synapse %s inputs", pcmk__xe_id(xml_synapse));
for (xmlNode *inputs = pcmk__xe_first_child(xml_synapse, "inputs", NULL,
NULL);
inputs != NULL; inputs = pcmk__xe_next_same(inputs)) {
for (xmlNode *trigger = pcmk__xe_first_child(inputs, "trigger", NULL,
NULL);
trigger != NULL; trigger = pcmk__xe_next_same(trigger)) {
for (xmlNode *input = pcmk__xe_first_child(trigger, NULL, NULL,
NULL);
input != NULL; input = pcmk__xe_next(input)) {
pcmk__graph_action_t *new_input = unpack_action(new_synapse,
input);
if (new_input == NULL) {
continue;
}
crm_trace("Adding input %d to synapse %d",
new_input->id, new_synapse->id);
new_synapse->inputs = g_list_append(new_synapse->inputs,
new_input);
}
}
}
return new_synapse;
}
/*!
* \internal
* \brief Unpack transition graph XML
*
* \param[in] xml_graph Transition graph XML to unpack
* \param[in] reference Where the XML came from (for logging)
*
* \return Newly allocated transition graph on success, NULL otherwise
* \note The caller is responsible for freeing the return value using
* pcmk__free_graph().
* \note The XML is expected to be structured like:
...
...
*/
pcmk__graph_t *
pcmk__unpack_graph(const xmlNode *xml_graph, const char *reference)
{
pcmk__graph_t *new_graph = NULL;
new_graph = calloc(1, sizeof(pcmk__graph_t));
if (new_graph == NULL) {
return NULL;
}
new_graph->source = strdup(pcmk__s(reference, "unknown"));
if (new_graph->source == NULL) {
pcmk__free_graph(new_graph);
return NULL;
}
new_graph->id = -1;
new_graph->abort_priority = 0;
new_graph->network_delay = 0;
new_graph->stonith_timeout = 0;
new_graph->completion_action = pcmk__graph_done;
// Parse top-level attributes from PCMK__XE_TRANSITION_GRAPH
if (xml_graph != NULL) {
const char *buf = crm_element_value(xml_graph, "transition_id");
CRM_CHECK(buf != NULL,
pcmk__free_graph(new_graph); return NULL);
pcmk__scan_min_int(buf, &(new_graph->id), -1);
buf = crm_element_value(xml_graph, PCMK_OPT_CLUSTER_DELAY);
CRM_CHECK(buf != NULL,
pcmk__free_graph(new_graph); return NULL);
pcmk_parse_interval_spec(buf, &(new_graph->network_delay));
buf = crm_element_value(xml_graph, PCMK_OPT_STONITH_TIMEOUT);
if (buf == NULL) {
new_graph->stonith_timeout = new_graph->network_delay;
} else {
pcmk_parse_interval_spec(buf, &(new_graph->stonith_timeout));
}
// Use 0 (dynamic limit) as default/invalid, -1 (no limit) as minimum
buf = crm_element_value(xml_graph, PCMK_OPT_BATCH_LIMIT);
if ((buf == NULL)
|| (pcmk__scan_min_int(buf, &(new_graph->batch_limit),
-1) != pcmk_rc_ok)) {
new_graph->batch_limit = 0;
}
buf = crm_element_value(xml_graph, PCMK_OPT_MIGRATION_LIMIT);
pcmk__scan_min_int(buf, &(new_graph->migration_limit), -1);
new_graph->failed_stop_offset =
crm_element_value_copy(xml_graph, "failed-stop-offset");
new_graph->failed_start_offset =
crm_element_value_copy(xml_graph, "failed-start-offset");
if (crm_element_value_epoch(xml_graph, "recheck-by",
&(new_graph->recheck_by)) != pcmk_ok) {
new_graph->recheck_by = 0;
}
}
// Unpack each child element
for (const xmlNode *synapse_xml = pcmk__xe_first_child(xml_graph,
"synapse", NULL,
NULL);
synapse_xml != NULL; synapse_xml = pcmk__xe_next_same(synapse_xml)) {
pcmk__graph_synapse_t *new_synapse = unpack_synapse(new_graph,
synapse_xml);
if (new_synapse != NULL) {
new_graph->synapses = g_list_append(new_graph->synapses,
new_synapse);
}
}
crm_debug("Unpacked transition %d from %s: %d actions in %d synapses",
new_graph->id, new_graph->source, new_graph->num_actions,
new_graph->num_synapses);
return new_graph;
}
/*
* Other transition graph utilities
*/
/*!
* \internal
* \brief Synthesize an executor event from a graph action
*
* \param[in] resource If not NULL, use greater call ID than in this XML
* \param[in] action Graph action
* \param[in] status What to use as event execution status
* \param[in] rc What to use as event exit status
* \param[in] exit_reason What to use as event exit reason
*
* \return Newly allocated executor event on success, or NULL otherwise
*/
lrmd_event_data_t *
pcmk__event_from_graph_action(const xmlNode *resource,
const pcmk__graph_action_t *action,
int status, int rc, const char *exit_reason)
{
lrmd_event_data_t *op = NULL;
GHashTableIter iter;
const char *name = NULL;
const char *value = NULL;
xmlNode *action_resource = NULL;
CRM_CHECK(action != NULL, return NULL);
CRM_CHECK(action->type == pcmk__rsc_graph_action, return NULL);
action_resource = pcmk__xe_first_child(action->xml, PCMK_XE_PRIMITIVE, NULL,
NULL);
CRM_CHECK(action_resource != NULL, crm_log_xml_warn(action->xml, "invalid");
return NULL);
op = lrmd_new_event(pcmk__xe_id(action_resource),
crm_element_value(action->xml, PCMK_XA_OPERATION),
action->interval_ms);
lrmd__set_result(op, rc, status, exit_reason);
op->t_run = time(NULL);
op->t_rcchange = op->t_run;
op->params = pcmk__strkey_table(free, free);
g_hash_table_iter_init(&iter, action->params);
while (g_hash_table_iter_next(&iter, (void **)&name, (void **)&value)) {
pcmk__insert_dup(op->params, name, value);
}
for (xmlNode *xop = pcmk__xe_first_child(resource, NULL, NULL, NULL);
xop != NULL; xop = pcmk__xe_next(xop)) {
int tmp = 0;
crm_element_value_int(xop, PCMK__XA_CALL_ID, &tmp);
crm_debug("Got call_id=%d for %s", tmp, pcmk__xe_id(resource));
if (tmp > op->call_id) {
op->call_id = tmp;
}
}
op->call_id++;
return op;
}
diff --git a/lib/pengine/unpack.c b/lib/pengine/unpack.c
index f17dac2c4c..fc733f7536 100644
--- a/lib/pengine/unpack.c
+++ b/lib/pengine/unpack.c
@@ -1,5169 +1,5167 @@
/*
* Copyright 2004-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
* This source code is licensed under the GNU Lesser General Public License
* version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
CRM_TRACE_INIT_DATA(pe_status);
// A (parsed) resource action history entry
struct action_history {
pcmk_resource_t *rsc; // Resource that history is for
pcmk_node_t *node; // Node that history is for
xmlNode *xml; // History entry XML
// Parsed from entry XML
const char *id; // XML ID of history entry
const char *key; // Operation key of action
const char *task; // Action name
const char *exit_reason; // Exit reason given for result
guint interval_ms; // Action interval
int call_id; // Call ID of action
int expected_exit_status; // Expected exit status of action
int exit_status; // Actual exit status of action
int execution_status; // Execution status of action
};
/* This uses pcmk__set_flags_as()/pcmk__clear_flags_as() directly rather than
* use pcmk__set_scheduler_flags()/pcmk__clear_scheduler_flags() so that the
* flag is stringified more readably in log messages.
*/
#define set_config_flag(scheduler, option, flag) do { \
GHashTable *config_hash = (scheduler)->config_hash; \
const char *scf_value = pcmk__cluster_option(config_hash, (option)); \
\
if (scf_value != NULL) { \
if (crm_is_true(scf_value)) { \
(scheduler)->flags = pcmk__set_flags_as(__func__, __LINE__, \
LOG_TRACE, "Scheduler", \
crm_system_name, (scheduler)->flags, \
(flag), #flag); \
} else { \
(scheduler)->flags = pcmk__clear_flags_as(__func__, __LINE__, \
LOG_TRACE, "Scheduler", \
crm_system_name, (scheduler)->flags, \
(flag), #flag); \
} \
} \
} while(0)
static void unpack_rsc_op(pcmk_resource_t *rsc, pcmk_node_t *node,
xmlNode *xml_op, xmlNode **last_failure,
enum action_fail_response *failed);
static void determine_remote_online_status(pcmk_scheduler_t *scheduler,
pcmk_node_t *this_node);
static void add_node_attrs(const xmlNode *xml_obj, pcmk_node_t *node,
bool overwrite, pcmk_scheduler_t *scheduler);
static void determine_online_status(const xmlNode *node_state,
pcmk_node_t *this_node,
pcmk_scheduler_t *scheduler);
static void unpack_node_lrm(pcmk_node_t *node, const xmlNode *xml,
pcmk_scheduler_t *scheduler);
static gboolean
is_dangling_guest_node(pcmk_node_t *node)
{
/* we are looking for a remote-node that was supposed to be mapped to a
* container resource, but all traces of that container have disappeared
* from both the config and the status section. */
if (pcmk__is_pacemaker_remote_node(node)
&& (node->details->remote_rsc != NULL)
&& (node->details->remote_rsc->container == NULL)
&& pcmk_is_set(node->details->remote_rsc->flags,
pcmk_rsc_removed_filler)) {
return TRUE;
}
return FALSE;
}
/*!
* \brief Schedule a fence action for a node
*
* \param[in,out] scheduler Scheduler data
* \param[in,out] node Node to fence
* \param[in] reason Text description of why fencing is needed
* \param[in] priority_delay Whether to consider
* \c PCMK_OPT_PRIORITY_FENCING_DELAY
*/
void
pe_fence_node(pcmk_scheduler_t *scheduler, pcmk_node_t *node,
const char *reason, bool priority_delay)
{
CRM_CHECK(node, return);
/* A guest node is fenced by marking its container as failed */
if (pcmk__is_guest_or_bundle_node(node)) {
pcmk_resource_t *rsc = node->details->remote_rsc->container;
if (!pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
crm_notice("Not fencing guest node %s "
"(otherwise would because %s): "
"its guest resource %s is unmanaged",
pcmk__node_name(node), reason, rsc->id);
} else {
pcmk__sched_warn("Guest node %s will be fenced "
"(by recovering its guest resource %s): %s",
pcmk__node_name(node), rsc->id, reason);
/* We don't mark the node as unclean because that would prevent the
* node from running resources. We want to allow it to run resources
* in this transition if the recovery succeeds.
*/
node->details->remote_requires_reset = TRUE;
pcmk__set_rsc_flags(rsc,
pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
}
}
} else if (is_dangling_guest_node(node)) {
crm_info("Cleaning up dangling connection for guest node %s: "
"fencing was already done because %s, "
"and guest resource no longer exists",
pcmk__node_name(node), reason);
pcmk__set_rsc_flags(node->details->remote_rsc,
pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
} else if (pcmk__is_remote_node(node)) {
pcmk_resource_t *rsc = node->details->remote_rsc;
if ((rsc != NULL) && !pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
crm_notice("Not fencing remote node %s "
"(otherwise would because %s): connection is unmanaged",
pcmk__node_name(node), reason);
} else if(node->details->remote_requires_reset == FALSE) {
node->details->remote_requires_reset = TRUE;
pcmk__sched_warn("Remote node %s %s: %s",
pcmk__node_name(node),
pe_can_fence(scheduler, node)? "will be fenced" : "is unclean",
reason);
}
node->details->unclean = TRUE;
// No need to apply PCMK_OPT_PRIORITY_FENCING_DELAY for remote nodes
pe_fence_op(node, NULL, TRUE, reason, FALSE, scheduler);
} else if (node->details->unclean) {
crm_trace("Cluster node %s %s because %s",
pcmk__node_name(node),
pe_can_fence(scheduler, node)? "would also be fenced" : "also is unclean",
reason);
} else {
pcmk__sched_warn("Cluster node %s %s: %s",
pcmk__node_name(node),
pe_can_fence(scheduler, node)? "will be fenced" : "is unclean",
reason);
node->details->unclean = TRUE;
pe_fence_op(node, NULL, TRUE, reason, priority_delay, scheduler);
}
}
// @TODO xpaths can't handle templates, rules, or id-refs
// nvpair with provides or requires set to unfencing
#define XPATH_UNFENCING_NVPAIR PCMK_XE_NVPAIR \
"[(@" PCMK_XA_NAME "='" PCMK_STONITH_PROVIDES "'" \
"or @" PCMK_XA_NAME "='" PCMK_META_REQUIRES "') " \
"and @" PCMK_XA_VALUE "='" PCMK_VALUE_UNFENCING "']"
// unfencing in rsc_defaults or any resource
#define XPATH_ENABLE_UNFENCING \
"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES \
"//" PCMK_XE_META_ATTRIBUTES "/" XPATH_UNFENCING_NVPAIR \
"|/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RSC_DEFAULTS \
"/" PCMK_XE_META_ATTRIBUTES "/" XPATH_UNFENCING_NVPAIR
static void
set_if_xpath(uint64_t flag, const char *xpath, pcmk_scheduler_t *scheduler)
{
xmlXPathObjectPtr result = NULL;
if (!pcmk_is_set(scheduler->flags, flag)) {
result = xpath_search(scheduler->input, xpath);
if (result && (numXpathResults(result) > 0)) {
pcmk__set_scheduler_flags(scheduler, flag);
}
freeXpathObject(result);
}
}
gboolean
unpack_config(xmlNode *config, pcmk_scheduler_t *scheduler)
{
const char *value = NULL;
guint interval_ms = 0U;
GHashTable *config_hash = pcmk__strkey_table(free, free);
pe_rule_eval_data_t rule_data = {
.node_hash = NULL,
.now = scheduler->now,
.match_data = NULL,
.rsc_data = NULL,
.op_data = NULL
};
scheduler->config_hash = config_hash;
pe__unpack_dataset_nvpairs(config, PCMK_XE_CLUSTER_PROPERTY_SET, &rule_data,
config_hash, PCMK_VALUE_CIB_BOOTSTRAP_OPTIONS,
FALSE, scheduler);
pcmk__validate_cluster_options(config_hash);
set_config_flag(scheduler, PCMK_OPT_ENABLE_STARTUP_PROBES,
pcmk_sched_probe_resources);
if (!pcmk_is_set(scheduler->flags, pcmk_sched_probe_resources)) {
crm_info("Startup probes: disabled (dangerous)");
}
value = pcmk__cluster_option(config_hash, PCMK_OPT_HAVE_WATCHDOG);
if (value && crm_is_true(value)) {
crm_info("Watchdog-based self-fencing will be performed via SBD if "
"fencing is required and " PCMK_OPT_STONITH_WATCHDOG_TIMEOUT
" is nonzero");
pcmk__set_scheduler_flags(scheduler, pcmk_sched_have_fencing);
}
/* Set certain flags via xpath here, so they can be used before the relevant
* configuration sections are unpacked.
*/
set_if_xpath(pcmk_sched_enable_unfencing, XPATH_ENABLE_UNFENCING,
scheduler);
value = pcmk__cluster_option(config_hash, PCMK_OPT_STONITH_TIMEOUT);
pcmk_parse_interval_spec(value, &interval_ms);
if (interval_ms >= INT_MAX) {
scheduler->stonith_timeout = INT_MAX;
} else {
scheduler->stonith_timeout = (int) interval_ms;
}
crm_debug("STONITH timeout: %d", scheduler->stonith_timeout);
set_config_flag(scheduler, PCMK_OPT_STONITH_ENABLED,
pcmk_sched_fencing_enabled);
if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
crm_debug("STONITH of failed nodes is enabled");
} else {
crm_debug("STONITH of failed nodes is disabled");
}
scheduler->stonith_action = pcmk__cluster_option(config_hash,
PCMK_OPT_STONITH_ACTION);
if (!strcmp(scheduler->stonith_action, PCMK__ACTION_POWEROFF)) {
pcmk__warn_once(pcmk__wo_poweroff,
"Support for " PCMK_OPT_STONITH_ACTION " of "
"'" PCMK__ACTION_POWEROFF "' is deprecated and will be "
"removed in a future release "
"(use '" PCMK_ACTION_OFF "' instead)");
scheduler->stonith_action = PCMK_ACTION_OFF;
}
crm_trace("STONITH will %s nodes", scheduler->stonith_action);
set_config_flag(scheduler, PCMK_OPT_CONCURRENT_FENCING,
pcmk_sched_concurrent_fencing);
if (pcmk_is_set(scheduler->flags, pcmk_sched_concurrent_fencing)) {
crm_debug("Concurrent fencing is enabled");
} else {
crm_debug("Concurrent fencing is disabled");
}
value = pcmk__cluster_option(config_hash, PCMK_OPT_PRIORITY_FENCING_DELAY);
if (value) {
pcmk_parse_interval_spec(value, &interval_ms);
scheduler->priority_fencing_delay = (int) (interval_ms / 1000);
crm_trace("Priority fencing delay is %ds",
scheduler->priority_fencing_delay);
}
set_config_flag(scheduler, PCMK_OPT_STOP_ALL_RESOURCES,
pcmk_sched_stop_all);
crm_debug("Stop all active resources: %s",
pcmk__flag_text(scheduler->flags, pcmk_sched_stop_all));
set_config_flag(scheduler, PCMK_OPT_SYMMETRIC_CLUSTER,
pcmk_sched_symmetric_cluster);
if (pcmk_is_set(scheduler->flags, pcmk_sched_symmetric_cluster)) {
crm_debug("Cluster is symmetric" " - resources can run anywhere by default");
}
value = pcmk__cluster_option(config_hash, PCMK_OPT_NO_QUORUM_POLICY);
if (pcmk__str_eq(value, PCMK_VALUE_IGNORE, pcmk__str_casei)) {
scheduler->no_quorum_policy = pcmk_no_quorum_ignore;
} else if (pcmk__str_eq(value, PCMK_VALUE_FREEZE, pcmk__str_casei)) {
scheduler->no_quorum_policy = pcmk_no_quorum_freeze;
} else if (pcmk__str_eq(value, PCMK_VALUE_DEMOTE, pcmk__str_casei)) {
scheduler->no_quorum_policy = pcmk_no_quorum_demote;
} else if (pcmk__str_eq(value, PCMK_VALUE_FENCE_LEGACY, pcmk__str_casei)) {
if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
int do_panic = 0;
crm_element_value_int(scheduler->input, PCMK_XA_NO_QUORUM_PANIC,
&do_panic);
if (do_panic || pcmk_is_set(scheduler->flags, pcmk_sched_quorate)) {
scheduler->no_quorum_policy = pcmk_no_quorum_fence;
} else {
crm_notice("Resetting " PCMK_OPT_NO_QUORUM_POLICY
" to 'stop': cluster has never had quorum");
scheduler->no_quorum_policy = pcmk_no_quorum_stop;
}
} else {
pcmk__config_err("Resetting " PCMK_OPT_NO_QUORUM_POLICY
" to 'stop' because fencing is disabled");
scheduler->no_quorum_policy = pcmk_no_quorum_stop;
}
} else {
scheduler->no_quorum_policy = pcmk_no_quorum_stop;
}
switch (scheduler->no_quorum_policy) {
case pcmk_no_quorum_freeze:
crm_debug("On loss of quorum: Freeze resources");
break;
case pcmk_no_quorum_stop:
crm_debug("On loss of quorum: Stop ALL resources");
break;
case pcmk_no_quorum_demote:
crm_debug("On loss of quorum: "
"Demote promotable resources and stop other resources");
break;
case pcmk_no_quorum_fence:
crm_notice("On loss of quorum: Fence all remaining nodes");
break;
case pcmk_no_quorum_ignore:
crm_notice("On loss of quorum: Ignore");
break;
}
set_config_flag(scheduler, PCMK_OPT_STOP_ORPHAN_RESOURCES,
pcmk_sched_stop_removed_resources);
if (pcmk_is_set(scheduler->flags, pcmk_sched_stop_removed_resources)) {
crm_trace("Orphan resources are stopped");
} else {
crm_trace("Orphan resources are ignored");
}
set_config_flag(scheduler, PCMK_OPT_STOP_ORPHAN_ACTIONS,
pcmk_sched_cancel_removed_actions);
if (pcmk_is_set(scheduler->flags, pcmk_sched_cancel_removed_actions)) {
crm_trace("Orphan resource actions are stopped");
} else {
crm_trace("Orphan resource actions are ignored");
}
value = pcmk__cluster_option(config_hash, PCMK__OPT_REMOVE_AFTER_STOP);
if (value != NULL) {
if (crm_is_true(value)) {
pcmk__set_scheduler_flags(scheduler, pcmk_sched_remove_after_stop);
-#ifndef PCMK__COMPAT_2_0
pcmk__warn_once(pcmk__wo_remove_after,
"Support for the " PCMK__OPT_REMOVE_AFTER_STOP
" cluster property is deprecated and will be "
"removed in a future release");
-#endif
} else {
pcmk__clear_scheduler_flags(scheduler,
pcmk_sched_remove_after_stop);
}
}
set_config_flag(scheduler, PCMK_OPT_MAINTENANCE_MODE,
pcmk_sched_in_maintenance);
crm_trace("Maintenance mode: %s",
pcmk__flag_text(scheduler->flags, pcmk_sched_in_maintenance));
set_config_flag(scheduler, PCMK_OPT_START_FAILURE_IS_FATAL,
pcmk_sched_start_failure_fatal);
if (pcmk_is_set(scheduler->flags, pcmk_sched_start_failure_fatal)) {
crm_trace("Start failures are always fatal");
} else {
crm_trace("Start failures are handled by failcount");
}
if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
set_config_flag(scheduler, PCMK_OPT_STARTUP_FENCING,
pcmk_sched_startup_fencing);
}
if (pcmk_is_set(scheduler->flags, pcmk_sched_startup_fencing)) {
crm_trace("Unseen nodes will be fenced");
} else {
pcmk__warn_once(pcmk__wo_blind,
"Blind faith: not fencing unseen nodes");
}
pe__unpack_node_health_scores(scheduler);
scheduler->placement_strategy =
pcmk__cluster_option(config_hash, PCMK_OPT_PLACEMENT_STRATEGY);
crm_trace("Placement strategy: %s", scheduler->placement_strategy);
set_config_flag(scheduler, PCMK_OPT_SHUTDOWN_LOCK,
pcmk_sched_shutdown_lock);
if (pcmk_is_set(scheduler->flags, pcmk_sched_shutdown_lock)) {
value = pcmk__cluster_option(config_hash, PCMK_OPT_SHUTDOWN_LOCK_LIMIT);
pcmk_parse_interval_spec(value, &(scheduler->shutdown_lock));
scheduler->shutdown_lock /= 1000;
crm_trace("Resources will be locked to nodes that were cleanly "
"shut down (locks expire after %s)",
pcmk__readable_interval(scheduler->shutdown_lock));
} else {
crm_trace("Resources will not be locked to nodes that were cleanly "
"shut down");
}
value = pcmk__cluster_option(config_hash, PCMK_OPT_NODE_PENDING_TIMEOUT);
pcmk_parse_interval_spec(value, &(scheduler->node_pending_timeout));
scheduler->node_pending_timeout /= 1000;
if (scheduler->node_pending_timeout == 0) {
crm_trace("Do not fence pending nodes");
} else {
crm_trace("Fence pending nodes after %s",
pcmk__readable_interval(scheduler->node_pending_timeout
* 1000));
}
return TRUE;
}
pcmk_node_t *
pe_create_node(const char *id, const char *uname, const char *type,
const char *score, pcmk_scheduler_t *scheduler)
{
pcmk_node_t *new_node = NULL;
if (pe_find_node(scheduler->nodes, uname) != NULL) {
pcmk__config_warn("More than one node entry has name '%s'", uname);
}
new_node = calloc(1, sizeof(pcmk_node_t));
if (new_node == NULL) {
pcmk__sched_err("Could not allocate memory for node %s", uname);
return NULL;
}
new_node->weight = char2score(score);
new_node->details = calloc(1, sizeof(struct pe_node_shared_s));
if (new_node->details == NULL) {
free(new_node);
pcmk__sched_err("Could not allocate memory for node %s", uname);
return NULL;
}
crm_trace("Creating node for entry %s/%s", uname, id);
new_node->details->id = id;
new_node->details->uname = uname;
new_node->details->online = FALSE;
new_node->details->shutdown = FALSE;
new_node->details->rsc_discovery_enabled = TRUE;
new_node->details->running_rsc = NULL;
new_node->details->data_set = scheduler;
if (pcmk__str_eq(type, PCMK_VALUE_MEMBER,
pcmk__str_null_matches|pcmk__str_casei)) {
new_node->details->type = pcmk_node_variant_cluster;
} else if (pcmk__str_eq(type, PCMK_VALUE_REMOTE, pcmk__str_casei)) {
new_node->details->type = pcmk_node_variant_remote;
pcmk__set_scheduler_flags(scheduler, pcmk_sched_have_remote_nodes);
} else {
/* @COMPAT 'ping' is the default for backward compatibility, but it
* should be changed to 'member' at a compatibility break
*/
if (!pcmk__str_eq(type, PCMK__VALUE_PING, pcmk__str_casei)) {
pcmk__config_warn("Node %s has unrecognized type '%s', "
"assuming '" PCMK__VALUE_PING "'",
pcmk__s(uname, "without name"), type);
}
pcmk__warn_once(pcmk__wo_ping_node,
"Support for nodes of type '" PCMK__VALUE_PING "' "
"(such as %s) is deprecated and will be removed in a "
"future release",
pcmk__s(uname, "unnamed node"));
new_node->details->type = node_ping;
}
new_node->details->attrs = pcmk__strkey_table(free, free);
if (pcmk__is_pacemaker_remote_node(new_node)) {
pcmk__insert_dup(new_node->details->attrs, CRM_ATTR_KIND, "remote");
} else {
pcmk__insert_dup(new_node->details->attrs, CRM_ATTR_KIND, "cluster");
}
new_node->details->utilization = pcmk__strkey_table(free, free);
new_node->details->digest_cache = pcmk__strkey_table(free,
pe__free_digests);
scheduler->nodes = g_list_insert_sorted(scheduler->nodes, new_node,
pe__cmp_node_name);
return new_node;
}
static const char *
expand_remote_rsc_meta(xmlNode *xml_obj, xmlNode *parent, pcmk_scheduler_t *data)
{
xmlNode *attr_set = NULL;
xmlNode *attr = NULL;
const char *container_id = pcmk__xe_id(xml_obj);
const char *remote_name = NULL;
const char *remote_server = NULL;
const char *remote_port = NULL;
const char *connect_timeout = "60s";
const char *remote_allow_migrate=NULL;
const char *is_managed = NULL;
for (attr_set = pcmk__xe_first_child(xml_obj, NULL, NULL, NULL);
attr_set != NULL; attr_set = pcmk__xe_next(attr_set)) {
if (!pcmk__xe_is(attr_set, PCMK_XE_META_ATTRIBUTES)) {
continue;
}
for (attr = pcmk__xe_first_child(attr_set, NULL, NULL, NULL);
attr != NULL; attr = pcmk__xe_next(attr)) {
const char *value = crm_element_value(attr, PCMK_XA_VALUE);
const char *name = crm_element_value(attr, PCMK_XA_NAME);
if (name == NULL) { // Sanity
continue;
}
if (strcmp(name, PCMK_META_REMOTE_NODE) == 0) {
remote_name = value;
} else if (strcmp(name, PCMK_META_REMOTE_ADDR) == 0) {
remote_server = value;
} else if (strcmp(name, PCMK_META_REMOTE_PORT) == 0) {
remote_port = value;
} else if (strcmp(name, PCMK_META_REMOTE_CONNECT_TIMEOUT) == 0) {
connect_timeout = value;
} else if (strcmp(name, PCMK_META_REMOTE_ALLOW_MIGRATE) == 0) {
remote_allow_migrate = value;
} else if (strcmp(name, PCMK_META_IS_MANAGED) == 0) {
is_managed = value;
}
}
}
if (remote_name == NULL) {
return NULL;
}
if (pe_find_resource(data->resources, remote_name) != NULL) {
return NULL;
}
pe_create_remote_xml(parent, remote_name, container_id,
remote_allow_migrate, is_managed,
connect_timeout, remote_server, remote_port);
return remote_name;
}
static void
handle_startup_fencing(pcmk_scheduler_t *scheduler, pcmk_node_t *new_node)
{
if ((new_node->details->type == pcmk_node_variant_remote)
&& (new_node->details->remote_rsc == NULL)) {
/* Ignore fencing for remote nodes that don't have a connection resource
* associated with them. This happens when remote node entries get left
* in the nodes section after the connection resource is removed.
*/
return;
}
if (pcmk_is_set(scheduler->flags, pcmk_sched_startup_fencing)) {
// All nodes are unclean until we've seen their status entry
new_node->details->unclean = TRUE;
} else {
// Blind faith ...
new_node->details->unclean = FALSE;
}
/* We need to be able to determine if a node's status section
* exists or not separate from whether the node is unclean. */
new_node->details->unseen = TRUE;
}
gboolean
unpack_nodes(xmlNode *xml_nodes, pcmk_scheduler_t *scheduler)
{
xmlNode *xml_obj = NULL;
pcmk_node_t *new_node = NULL;
const char *id = NULL;
const char *uname = NULL;
const char *type = NULL;
const char *score = NULL;
for (xml_obj = pcmk__xe_first_child(xml_nodes, NULL, NULL, NULL);
xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj)) {
if (pcmk__xe_is(xml_obj, PCMK_XE_NODE)) {
new_node = NULL;
id = crm_element_value(xml_obj, PCMK_XA_ID);
uname = crm_element_value(xml_obj, PCMK_XA_UNAME);
type = crm_element_value(xml_obj, PCMK_XA_TYPE);
score = crm_element_value(xml_obj, PCMK_XA_SCORE);
crm_trace("Processing node %s/%s", uname, id);
if (id == NULL) {
pcmk__config_err("Ignoring <" PCMK_XE_NODE
"> entry in configuration without id");
continue;
}
new_node = pe_create_node(id, uname, type, score, scheduler);
if (new_node == NULL) {
return FALSE;
}
handle_startup_fencing(scheduler, new_node);
add_node_attrs(xml_obj, new_node, FALSE, scheduler);
crm_trace("Done with node %s",
crm_element_value(xml_obj, PCMK_XA_UNAME));
}
}
if (scheduler->localhost
&& (pe_find_node(scheduler->nodes, scheduler->localhost) == NULL)) {
crm_info("Creating a fake local node");
pe_create_node(scheduler->localhost, scheduler->localhost, NULL, 0,
scheduler);
}
return TRUE;
}
static void
setup_container(pcmk_resource_t *rsc, pcmk_scheduler_t *scheduler)
{
const char *container_id = NULL;
if (rsc->children) {
g_list_foreach(rsc->children, (GFunc) setup_container, scheduler);
return;
}
container_id = g_hash_table_lookup(rsc->meta, PCMK__META_CONTAINER);
if (container_id && !pcmk__str_eq(container_id, rsc->id, pcmk__str_casei)) {
pcmk_resource_t *container = pe_find_resource(scheduler->resources,
container_id);
if (container) {
rsc->container = container;
pcmk__set_rsc_flags(container, pcmk_rsc_has_filler);
container->fillers = g_list_append(container->fillers, rsc);
pcmk__rsc_trace(rsc, "Resource %s's container is %s",
rsc->id, container_id);
} else {
pcmk__config_err("Resource %s: Unknown resource container (%s)",
rsc->id, container_id);
}
}
}
gboolean
unpack_remote_nodes(xmlNode *xml_resources, pcmk_scheduler_t *scheduler)
{
xmlNode *xml_obj = NULL;
/* Create remote nodes and guest nodes from the resource configuration
* before unpacking resources.
*/
for (xml_obj = pcmk__xe_first_child(xml_resources, NULL, NULL, NULL);
xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj)) {
const char *new_node_id = NULL;
/* Check for remote nodes, which are defined by ocf:pacemaker:remote
* primitives.
*/
if (xml_contains_remote_node(xml_obj)) {
new_node_id = pcmk__xe_id(xml_obj);
/* The "pe_find_node" check is here to make sure we don't iterate over
* an expanded node that has already been added to the node list. */
if (new_node_id
&& (pe_find_node(scheduler->nodes, new_node_id) == NULL)) {
crm_trace("Found remote node %s defined by resource %s",
new_node_id, pcmk__xe_id(xml_obj));
pe_create_node(new_node_id, new_node_id, PCMK_VALUE_REMOTE,
NULL, scheduler);
}
continue;
}
/* Check for guest nodes, which are defined by special meta-attributes
* of a primitive of any type (for example, VirtualDomain or Xen).
*/
if (pcmk__xe_is(xml_obj, PCMK_XE_PRIMITIVE)) {
/* This will add an ocf:pacemaker:remote primitive to the
* configuration for the guest node's connection, to be unpacked
* later.
*/
new_node_id = expand_remote_rsc_meta(xml_obj, xml_resources,
scheduler);
if (new_node_id
&& (pe_find_node(scheduler->nodes, new_node_id) == NULL)) {
crm_trace("Found guest node %s in resource %s",
new_node_id, pcmk__xe_id(xml_obj));
pe_create_node(new_node_id, new_node_id, PCMK_VALUE_REMOTE,
NULL, scheduler);
}
continue;
}
/* Check for guest nodes inside a group. Clones are currently not
* supported as guest nodes.
*/
if (pcmk__xe_is(xml_obj, PCMK_XE_GROUP)) {
xmlNode *xml_obj2 = NULL;
for (xml_obj2 = pcmk__xe_first_child(xml_obj, NULL, NULL, NULL);
xml_obj2 != NULL; xml_obj2 = pcmk__xe_next(xml_obj2)) {
new_node_id = expand_remote_rsc_meta(xml_obj2, xml_resources,
scheduler);
if (new_node_id
&& (pe_find_node(scheduler->nodes, new_node_id) == NULL)) {
crm_trace("Found guest node %s in resource %s inside group %s",
new_node_id, pcmk__xe_id(xml_obj2),
pcmk__xe_id(xml_obj));
pe_create_node(new_node_id, new_node_id, PCMK_VALUE_REMOTE,
NULL, scheduler);
}
}
}
}
return TRUE;
}
/* Call this after all the nodes and resources have been
* unpacked, but before the status section is read.
*
* A remote node's online status is reflected by the state
* of the remote node's connection resource. We need to link
* the remote node to this connection resource so we can have
* easy access to the connection resource during the scheduler calculations.
*/
static void
link_rsc2remotenode(pcmk_scheduler_t *scheduler, pcmk_resource_t *new_rsc)
{
pcmk_node_t *remote_node = NULL;
if (new_rsc->is_remote_node == FALSE) {
return;
}
if (pcmk_is_set(scheduler->flags, pcmk_sched_location_only)) {
/* remote_nodes and remote_resources are not linked in quick location calculations */
return;
}
remote_node = pe_find_node(scheduler->nodes, new_rsc->id);
CRM_CHECK(remote_node != NULL, return);
pcmk__rsc_trace(new_rsc, "Linking remote connection resource %s to %s",
new_rsc->id, pcmk__node_name(remote_node));
remote_node->details->remote_rsc = new_rsc;
if (new_rsc->container == NULL) {
/* Handle start-up fencing for remote nodes (as opposed to guest nodes)
* the same as is done for cluster nodes.
*/
handle_startup_fencing(scheduler, remote_node);
} else {
/* pe_create_node() marks the new node as "remote" or "cluster"; now
* that we know the node is a guest node, update it correctly.
*/
pcmk__insert_dup(remote_node->details->attrs,
CRM_ATTR_KIND, "container");
}
}
static void
destroy_tag(gpointer data)
{
pcmk_tag_t *tag = data;
if (tag) {
free(tag->id);
g_list_free_full(tag->refs, free);
free(tag);
}
}
/*!
* \internal
* \brief Parse configuration XML for resource information
*
* \param[in] xml_resources Top of resource configuration XML
* \param[in,out] scheduler Scheduler data
*
* \return TRUE
*
* \note unpack_remote_nodes() MUST be called before this, so that the nodes can
* be used when pe__unpack_resource() calls resource_location()
*/
gboolean
unpack_resources(const xmlNode *xml_resources, pcmk_scheduler_t *scheduler)
{
xmlNode *xml_obj = NULL;
GList *gIter = NULL;
scheduler->template_rsc_sets = pcmk__strkey_table(free, destroy_tag);
for (xml_obj = pcmk__xe_first_child(xml_resources, NULL, NULL, NULL);
xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj)) {
pcmk_resource_t *new_rsc = NULL;
const char *id = pcmk__xe_id(xml_obj);
if (pcmk__str_empty(id)) {
pcmk__config_err("Ignoring <%s> resource without ID",
xml_obj->name);
continue;
}
if (pcmk__xe_is(xml_obj, PCMK_XE_TEMPLATE)) {
if (g_hash_table_lookup_extended(scheduler->template_rsc_sets, id,
NULL, NULL) == FALSE) {
/* Record the template's ID for the knowledge of its existence anyway. */
pcmk__insert_dup(scheduler->template_rsc_sets, id, NULL);
}
continue;
}
crm_trace("Unpacking <%s " PCMK_XA_ID "='%s'>", xml_obj->name, id);
if (pe__unpack_resource(xml_obj, &new_rsc, NULL,
scheduler) == pcmk_rc_ok) {
scheduler->resources = g_list_append(scheduler->resources, new_rsc);
pcmk__rsc_trace(new_rsc, "Added resource %s", new_rsc->id);
} else {
pcmk__config_err("Ignoring <%s> resource '%s' "
"because configuration is invalid",
xml_obj->name, id);
}
}
for (gIter = scheduler->resources; gIter != NULL; gIter = gIter->next) {
pcmk_resource_t *rsc = (pcmk_resource_t *) gIter->data;
setup_container(rsc, scheduler);
link_rsc2remotenode(scheduler, rsc);
}
scheduler->resources = g_list_sort(scheduler->resources,
pe__cmp_rsc_priority);
if (pcmk_is_set(scheduler->flags, pcmk_sched_location_only)) {
/* Ignore */
} else if (pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)
&& !pcmk_is_set(scheduler->flags, pcmk_sched_have_fencing)) {
pcmk__config_err("Resource start-up disabled since no STONITH resources have been defined");
pcmk__config_err("Either configure some or disable STONITH with the "
PCMK_OPT_STONITH_ENABLED " option");
pcmk__config_err("NOTE: Clusters with shared data need STONITH to ensure data integrity");
}
return TRUE;
}
gboolean
unpack_tags(xmlNode *xml_tags, pcmk_scheduler_t *scheduler)
{
xmlNode *xml_tag = NULL;
scheduler->tags = pcmk__strkey_table(free, destroy_tag);
for (xml_tag = pcmk__xe_first_child(xml_tags, NULL, NULL, NULL);
xml_tag != NULL; xml_tag = pcmk__xe_next(xml_tag)) {
xmlNode *xml_obj_ref = NULL;
const char *tag_id = pcmk__xe_id(xml_tag);
if (!pcmk__xe_is(xml_tag, PCMK_XE_TAG)) {
continue;
}
if (tag_id == NULL) {
pcmk__config_err("Ignoring <%s> without " PCMK_XA_ID,
(const char *) xml_tag->name);
continue;
}
for (xml_obj_ref = pcmk__xe_first_child(xml_tag, NULL, NULL, NULL);
xml_obj_ref != NULL; xml_obj_ref = pcmk__xe_next(xml_obj_ref)) {
const char *obj_ref = pcmk__xe_id(xml_obj_ref);
if (!pcmk__xe_is(xml_obj_ref, PCMK_XE_OBJ_REF)) {
continue;
}
if (obj_ref == NULL) {
pcmk__config_err("Ignoring <%s> for tag '%s' without " PCMK_XA_ID,
xml_obj_ref->name, tag_id);
continue;
}
if (add_tag_ref(scheduler->tags, tag_id, obj_ref) == FALSE) {
return FALSE;
}
}
}
return TRUE;
}
/* The ticket state section:
* "/cib/status/tickets/ticket_state" */
static gboolean
unpack_ticket_state(xmlNode *xml_ticket, pcmk_scheduler_t *scheduler)
{
const char *ticket_id = NULL;
const char *granted = NULL;
const char *last_granted = NULL;
const char *standby = NULL;
xmlAttrPtr xIter = NULL;
pcmk_ticket_t *ticket = NULL;
ticket_id = pcmk__xe_id(xml_ticket);
if (pcmk__str_empty(ticket_id)) {
return FALSE;
}
crm_trace("Processing ticket state for %s", ticket_id);
ticket = g_hash_table_lookup(scheduler->tickets, ticket_id);
if (ticket == NULL) {
ticket = ticket_new(ticket_id, scheduler);
if (ticket == NULL) {
return FALSE;
}
}
for (xIter = xml_ticket->properties; xIter; xIter = xIter->next) {
const char *prop_name = (const char *)xIter->name;
const char *prop_value = pcmk__xml_attr_value(xIter);
if (pcmk__str_eq(prop_name, PCMK_XA_ID, pcmk__str_none)) {
continue;
}
pcmk__insert_dup(ticket->state, prop_name, prop_value);
}
granted = g_hash_table_lookup(ticket->state, PCMK__XA_GRANTED);
if (granted && crm_is_true(granted)) {
ticket->granted = TRUE;
crm_info("We have ticket '%s'", ticket->id);
} else {
ticket->granted = FALSE;
crm_info("We do not have ticket '%s'", ticket->id);
}
last_granted = g_hash_table_lookup(ticket->state, PCMK_XA_LAST_GRANTED);
if (last_granted) {
long long last_granted_ll;
pcmk__scan_ll(last_granted, &last_granted_ll, 0LL);
ticket->last_granted = (time_t) last_granted_ll;
}
standby = g_hash_table_lookup(ticket->state, PCMK_XA_STANDBY);
if (standby && crm_is_true(standby)) {
ticket->standby = TRUE;
if (ticket->granted) {
crm_info("Granted ticket '%s' is in standby-mode", ticket->id);
}
} else {
ticket->standby = FALSE;
}
crm_trace("Done with ticket state for %s", ticket_id);
return TRUE;
}
static gboolean
unpack_tickets_state(xmlNode *xml_tickets, pcmk_scheduler_t *scheduler)
{
xmlNode *xml_obj = NULL;
for (xml_obj = pcmk__xe_first_child(xml_tickets, NULL, NULL, NULL);
xml_obj != NULL; xml_obj = pcmk__xe_next(xml_obj)) {
if (!pcmk__xe_is(xml_obj, PCMK__XE_TICKET_STATE)) {
continue;
}
unpack_ticket_state(xml_obj, scheduler);
}
return TRUE;
}
static void
unpack_handle_remote_attrs(pcmk_node_t *this_node, const xmlNode *state,
pcmk_scheduler_t *scheduler)
{
const char *discovery = NULL;
const xmlNode *attrs = NULL;
pcmk_resource_t *rsc = NULL;
if (!pcmk__xe_is(state, PCMK__XE_NODE_STATE)) {
return;
}
if ((this_node == NULL) || !pcmk__is_pacemaker_remote_node(this_node)) {
return;
}
crm_trace("Processing Pacemaker Remote node %s",
pcmk__node_name(this_node));
pcmk__scan_min_int(crm_element_value(state, PCMK__XA_NODE_IN_MAINTENANCE),
&(this_node->details->remote_maintenance), 0);
rsc = this_node->details->remote_rsc;
if (this_node->details->remote_requires_reset == FALSE) {
this_node->details->unclean = FALSE;
this_node->details->unseen = FALSE;
}
attrs = pcmk__xe_first_child(state, PCMK__XE_TRANSIENT_ATTRIBUTES, NULL,
NULL);
add_node_attrs(attrs, this_node, TRUE, scheduler);
if (pe__shutdown_requested(this_node)) {
crm_info("%s is shutting down", pcmk__node_name(this_node));
this_node->details->shutdown = TRUE;
}
if (crm_is_true(pcmk__node_attr(this_node, PCMK_NODE_ATTR_STANDBY, NULL,
pcmk__rsc_node_current))) {
crm_info("%s is in standby mode", pcmk__node_name(this_node));
this_node->details->standby = TRUE;
}
if (crm_is_true(pcmk__node_attr(this_node, PCMK_NODE_ATTR_MAINTENANCE, NULL,
pcmk__rsc_node_current))
|| ((rsc != NULL) && !pcmk_is_set(rsc->flags, pcmk_rsc_managed))) {
crm_info("%s is in maintenance mode", pcmk__node_name(this_node));
this_node->details->maintenance = TRUE;
}
discovery = pcmk__node_attr(this_node,
PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED,
NULL, pcmk__rsc_node_current);
if ((discovery != NULL) && !crm_is_true(discovery)) {
pcmk__warn_once(pcmk__wo_rdisc_enabled,
"Support for the "
PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED
" node attribute is deprecated and will be removed"
" (and behave as 'true') in a future release.");
if (pcmk__is_remote_node(this_node)
&& !pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
pcmk__config_warn("Ignoring "
PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED
" attribute on Pacemaker Remote node %s"
" because fencing is disabled",
pcmk__node_name(this_node));
} else {
/* This is either a remote node with fencing enabled, or a guest
* node. We don't care whether fencing is enabled when fencing guest
* nodes, because they are "fenced" by recovering their containing
* resource.
*/
crm_info("%s has resource discovery disabled",
pcmk__node_name(this_node));
this_node->details->rsc_discovery_enabled = FALSE;
}
}
}
/*!
* \internal
* \brief Unpack a cluster node's transient attributes
*
* \param[in] state CIB node state XML
* \param[in,out] node Cluster node whose attributes are being unpacked
* \param[in,out] scheduler Scheduler data
*/
static void
unpack_transient_attributes(const xmlNode *state, pcmk_node_t *node,
pcmk_scheduler_t *scheduler)
{
const char *discovery = NULL;
const xmlNode *attrs = pcmk__xe_first_child(state,
PCMK__XE_TRANSIENT_ATTRIBUTES,
NULL, NULL);
add_node_attrs(attrs, node, TRUE, scheduler);
if (crm_is_true(pcmk__node_attr(node, PCMK_NODE_ATTR_STANDBY, NULL,
pcmk__rsc_node_current))) {
crm_info("%s is in standby mode", pcmk__node_name(node));
node->details->standby = TRUE;
}
if (crm_is_true(pcmk__node_attr(node, PCMK_NODE_ATTR_MAINTENANCE, NULL,
pcmk__rsc_node_current))) {
crm_info("%s is in maintenance mode", pcmk__node_name(node));
node->details->maintenance = TRUE;
}
discovery = pcmk__node_attr(node,
PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED,
NULL, pcmk__rsc_node_current);
if ((discovery != NULL) && !crm_is_true(discovery)) {
pcmk__config_warn("Ignoring "
PCMK__NODE_ATTR_RESOURCE_DISCOVERY_ENABLED
" attribute for %s because disabling resource"
" discovery is not allowed for cluster nodes",
pcmk__node_name(node));
}
}
/*!
* \internal
* \brief Unpack a node state entry (first pass)
*
* Unpack one node state entry from status. This unpacks information from the
* \C PCMK__XE_NODE_STATE element itself and node attributes inside it, but not
* the resource history inside it. Multiple passes through the status are needed
* to fully unpack everything.
*
* \param[in] state CIB node state XML
* \param[in,out] scheduler Scheduler data
*/
static void
unpack_node_state(const xmlNode *state, pcmk_scheduler_t *scheduler)
{
const char *id = NULL;
const char *uname = NULL;
pcmk_node_t *this_node = NULL;
id = crm_element_value(state, PCMK_XA_ID);
if (id == NULL) {
pcmk__config_err("Ignoring invalid " PCMK__XE_NODE_STATE " entry without "
PCMK_XA_ID);
crm_log_xml_info(state, "missing-id");
return;
}
uname = crm_element_value(state, PCMK_XA_UNAME);
if (uname == NULL) {
/* If a joining peer makes the cluster acquire the quorum from corosync
* meanwhile it has not joined CPG membership of pacemaker-controld yet,
* it's possible that the created PCMK__XE_NODE_STATE entry doesn't have
* a PCMK_XA_UNAME yet. We should recognize the node as `pending` and
* wait for it to join CPG.
*/
crm_trace("Handling " PCMK__XE_NODE_STATE " entry with id=\"%s\" "
"without " PCMK_XA_UNAME,
id);
}
this_node = pe_find_node_any(scheduler->nodes, id, uname);
if (this_node == NULL) {
crm_notice("Ignoring recorded state for removed node with name %s and "
PCMK_XA_ID " %s", pcmk__s(uname, "unknown"), id);
return;
}
if (pcmk__is_pacemaker_remote_node(this_node)) {
/* We can't determine the online status of Pacemaker Remote nodes until
* after all resource history has been unpacked. In this first pass, we
* do need to mark whether the node has been fenced, as this plays a
* role during unpacking cluster node resource state.
*/
pcmk__scan_min_int(crm_element_value(state, PCMK__XA_NODE_FENCED),
&(this_node->details->remote_was_fenced), 0);
return;
}
unpack_transient_attributes(state, this_node, scheduler);
/* Provisionally mark this cluster node as clean. We have at least seen it
* in the current cluster's lifetime.
*/
this_node->details->unclean = FALSE;
this_node->details->unseen = FALSE;
crm_trace("Determining online status of cluster node %s (id %s)",
pcmk__node_name(this_node), id);
determine_online_status(state, this_node, scheduler);
if (!pcmk_is_set(scheduler->flags, pcmk_sched_quorate)
&& this_node->details->online
&& (scheduler->no_quorum_policy == pcmk_no_quorum_fence)) {
/* Everything else should flow from this automatically
* (at least until the scheduler becomes able to migrate off
* healthy resources)
*/
pe_fence_node(scheduler, this_node, "cluster does not have quorum",
FALSE);
}
}
/*!
* \internal
* \brief Unpack nodes' resource history as much as possible
*
* Unpack as many nodes' resource history as possible in one pass through the
* status. We need to process Pacemaker Remote nodes' connections/containers
* before unpacking their history; the connection/container history will be
* in another node's history, so it might take multiple passes to unpack
* everything.
*
* \param[in] status CIB XML status section
* \param[in] fence If true, treat any not-yet-unpacked nodes as unseen
* \param[in,out] scheduler Scheduler data
*
* \return Standard Pacemaker return code (specifically pcmk_rc_ok if done,
* or EAGAIN if more unpacking remains to be done)
*/
static int
unpack_node_history(const xmlNode *status, bool fence,
pcmk_scheduler_t *scheduler)
{
int rc = pcmk_rc_ok;
// Loop through all PCMK__XE_NODE_STATE entries in CIB status
for (const xmlNode *state = pcmk__xe_first_child(status,
PCMK__XE_NODE_STATE, NULL,
NULL);
state != NULL; state = pcmk__xe_next_same(state)) {
const char *id = pcmk__xe_id(state);
const char *uname = crm_element_value(state, PCMK_XA_UNAME);
pcmk_node_t *this_node = NULL;
if ((id == NULL) || (uname == NULL)) {
// Warning already logged in first pass through status section
crm_trace("Not unpacking resource history from malformed "
PCMK__XE_NODE_STATE " without id and/or uname");
continue;
}
this_node = pe_find_node_any(scheduler->nodes, id, uname);
if (this_node == NULL) {
// Warning already logged in first pass through status section
crm_trace("Not unpacking resource history for node %s because "
"no longer in configuration", id);
continue;
}
if (this_node->details->unpacked) {
crm_trace("Not unpacking resource history for node %s because "
"already unpacked", id);
continue;
}
if (fence) {
// We're processing all remaining nodes
} else if (pcmk__is_guest_or_bundle_node(this_node)) {
/* We can unpack a guest node's history only after we've unpacked
* other resource history to the point that we know that the node's
* connection and containing resource are both up.
*/
pcmk_resource_t *rsc = this_node->details->remote_rsc;
if ((rsc == NULL) || (rsc->role != pcmk_role_started)
|| (rsc->container->role != pcmk_role_started)) {
crm_trace("Not unpacking resource history for guest node %s "
"because container and connection are not known to "
"be up", id);
continue;
}
} else if (pcmk__is_remote_node(this_node)) {
/* We can unpack a remote node's history only after we've unpacked
* other resource history to the point that we know that the node's
* connection is up, with the exception of when shutdown locks are
* in use.
*/
pcmk_resource_t *rsc = this_node->details->remote_rsc;
if ((rsc == NULL)
|| (!pcmk_is_set(scheduler->flags, pcmk_sched_shutdown_lock)
&& (rsc->role != pcmk_role_started))) {
crm_trace("Not unpacking resource history for remote node %s "
"because connection is not known to be up", id);
continue;
}
/* If fencing and shutdown locks are disabled and we're not processing
* unseen nodes, then we don't want to unpack offline nodes until online
* nodes have been unpacked. This allows us to number active clone
* instances first.
*/
} else if (!pcmk_any_flags_set(scheduler->flags,
pcmk_sched_fencing_enabled
|pcmk_sched_shutdown_lock)
&& !this_node->details->online) {
crm_trace("Not unpacking resource history for offline "
"cluster node %s", id);
continue;
}
if (pcmk__is_pacemaker_remote_node(this_node)) {
determine_remote_online_status(scheduler, this_node);
unpack_handle_remote_attrs(this_node, state, scheduler);
}
crm_trace("Unpacking resource history for %snode %s",
(fence? "unseen " : ""), id);
this_node->details->unpacked = TRUE;
unpack_node_lrm(this_node, state, scheduler);
rc = EAGAIN; // Other node histories might depend on this one
}
return rc;
}
/* remove nodes that are down, stopping */
/* create positive rsc_to_node constraints between resources and the nodes they are running on */
/* anything else? */
gboolean
unpack_status(xmlNode *status, pcmk_scheduler_t *scheduler)
{
xmlNode *state = NULL;
crm_trace("Beginning unpack");
if (scheduler->tickets == NULL) {
scheduler->tickets = pcmk__strkey_table(free, destroy_ticket);
}
for (state = pcmk__xe_first_child(status, NULL, NULL, NULL); state != NULL;
state = pcmk__xe_next(state)) {
if (pcmk__xe_is(state, PCMK_XE_TICKETS)) {
unpack_tickets_state((xmlNode *) state, scheduler);
} else if (pcmk__xe_is(state, PCMK__XE_NODE_STATE)) {
unpack_node_state(state, scheduler);
}
}
while (unpack_node_history(status, FALSE, scheduler) == EAGAIN) {
crm_trace("Another pass through node resource histories is needed");
}
// Now catch any nodes we didn't see
unpack_node_history(status,
pcmk_is_set(scheduler->flags,
pcmk_sched_fencing_enabled),
scheduler);
/* Now that we know where resources are, we can schedule stops of containers
* with failed bundle connections
*/
if (scheduler->stop_needed != NULL) {
for (GList *item = scheduler->stop_needed; item; item = item->next) {
pcmk_resource_t *container = item->data;
pcmk_node_t *node = pcmk__current_node(container);
if (node) {
stop_action(container, node, FALSE);
}
}
g_list_free(scheduler->stop_needed);
scheduler->stop_needed = NULL;
}
/* Now that we know status of all Pacemaker Remote connections and nodes,
* we can stop connections for node shutdowns, and check the online status
* of remote/guest nodes that didn't have any node history to unpack.
*/
for (GList *gIter = scheduler->nodes; gIter != NULL; gIter = gIter->next) {
pcmk_node_t *this_node = gIter->data;
if (!pcmk__is_pacemaker_remote_node(this_node)) {
continue;
}
if (this_node->details->shutdown
&& (this_node->details->remote_rsc != NULL)) {
pe__set_next_role(this_node->details->remote_rsc, pcmk_role_stopped,
"remote shutdown");
}
if (!this_node->details->unpacked) {
determine_remote_online_status(scheduler, this_node);
}
}
return TRUE;
}
/*!
* \internal
* \brief Unpack node's time when it became a member at the cluster layer
*
* \param[in] node_state Node's \c PCMK__XE_NODE_STATE entry
* \param[in,out] scheduler Scheduler data
*
* \return Epoch time when node became a cluster member
* (or scheduler effective time for legacy entries) if a member,
* 0 if not a member, or -1 if no valid information available
*/
static long long
unpack_node_member(const xmlNode *node_state, pcmk_scheduler_t *scheduler)
{
const char *member_time = crm_element_value(node_state, PCMK__XA_IN_CCM);
int member = 0;
if (member_time == NULL) {
return -1LL;
} else if (crm_str_to_boolean(member_time, &member) == 1) {
/* If in_ccm=0, we'll return 0 here. If in_ccm=1, either the entry was
* recorded as a boolean for a DC < 2.1.7, or the node is pending
* shutdown and has left the CPG, in which case it was set to 1 to avoid
* fencing for PCMK_OPT_NODE_PENDING_TIMEOUT.
*
* We return the effective time for in_ccm=1 because what's important to
* avoid fencing is that effective time minus this value is less than
* the pending node timeout.
*/
return member? (long long) get_effective_time(scheduler) : 0LL;
} else {
long long when_member = 0LL;
if ((pcmk__scan_ll(member_time, &when_member,
0LL) != pcmk_rc_ok) || (when_member < 0LL)) {
crm_warn("Unrecognized value '%s' for " PCMK__XA_IN_CCM
" in " PCMK__XE_NODE_STATE " entry", member_time);
return -1LL;
}
return when_member;
}
}
/*!
* \internal
* \brief Unpack node's time when it became online in process group
*
* \param[in] node_state Node's \c PCMK__XE_NODE_STATE entry
*
* \return Epoch time when node became online in process group (or 0 if not
* online, or 1 for legacy online entries)
*/
static long long
unpack_node_online(const xmlNode *node_state)
{
const char *peer_time = crm_element_value(node_state, PCMK_XA_CRMD);
// @COMPAT Entries recorded for DCs < 2.1.7 have "online" or "offline"
if (pcmk__str_eq(peer_time, PCMK_VALUE_OFFLINE,
pcmk__str_casei|pcmk__str_null_matches)) {
return 0LL;
} else if (pcmk__str_eq(peer_time, PCMK_VALUE_ONLINE, pcmk__str_casei)) {
return 1LL;
} else {
long long when_online = 0LL;
if ((pcmk__scan_ll(peer_time, &when_online, 0LL) != pcmk_rc_ok)
|| (when_online < 0)) {
crm_warn("Unrecognized value '%s' for " PCMK_XA_CRMD " in "
PCMK__XE_NODE_STATE " entry, assuming offline", peer_time);
return 0LL;
}
return when_online;
}
}
/*!
* \internal
* \brief Unpack node attribute for user-requested fencing
*
* \param[in] node Node to check
* \param[in] node_state Node's \c PCMK__XE_NODE_STATE entry in CIB status
*
* \return \c true if fencing has been requested for \p node, otherwise \c false
*/
static bool
unpack_node_terminate(const pcmk_node_t *node, const xmlNode *node_state)
{
long long value = 0LL;
int value_i = 0;
const char *value_s = pcmk__node_attr(node, PCMK_NODE_ATTR_TERMINATE,
NULL, pcmk__rsc_node_current);
// Value may be boolean or an epoch time
if (crm_str_to_boolean(value_s, &value_i) == 1) {
return (value_i != 0);
}
if (pcmk__scan_ll(value_s, &value, 0LL) == pcmk_rc_ok) {
return (value > 0);
}
crm_warn("Ignoring unrecognized value '%s' for " PCMK_NODE_ATTR_TERMINATE
"node attribute for %s", value_s, pcmk__node_name(node));
return false;
}
static gboolean
determine_online_status_no_fencing(pcmk_scheduler_t *scheduler,
const xmlNode *node_state,
pcmk_node_t *this_node)
{
gboolean online = FALSE;
const char *join = crm_element_value(node_state, PCMK__XA_JOIN);
const char *exp_state = crm_element_value(node_state, PCMK_XA_EXPECTED);
long long when_member = unpack_node_member(node_state, scheduler);
long long when_online = unpack_node_online(node_state);
if (when_member <= 0) {
crm_trace("Node %s is %sdown", pcmk__node_name(this_node),
((when_member < 0)? "presumed " : ""));
} else if (when_online > 0) {
if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) {
online = TRUE;
} else {
crm_debug("Node %s is not ready to run resources: %s",
pcmk__node_name(this_node), join);
}
} else if (this_node->details->expected_up == FALSE) {
crm_trace("Node %s controller is down: "
"member@%lld online@%lld join=%s expected=%s",
pcmk__node_name(this_node), when_member, when_online,
pcmk__s(join, ""), pcmk__s(exp_state, ""));
} else {
/* mark it unclean */
pe_fence_node(scheduler, this_node, "peer is unexpectedly down", FALSE);
crm_info("Node %s member@%lld online@%lld join=%s expected=%s",
pcmk__node_name(this_node), when_member, when_online,
pcmk__s(join, ""), pcmk__s(exp_state, ""));
}
return online;
}
/*!
* \internal
* \brief Check whether a node has taken too long to join controller group
*
* \param[in,out] scheduler Scheduler data
* \param[in] node Node to check
* \param[in] when_member Epoch time when node became a cluster member
* \param[in] when_online Epoch time when node joined controller group
*
* \return true if node has been pending (on the way up) longer than
* \c PCMK_OPT_NODE_PENDING_TIMEOUT, otherwise false
* \note This will also update the cluster's recheck time if appropriate.
*/
static inline bool
pending_too_long(pcmk_scheduler_t *scheduler, const pcmk_node_t *node,
long long when_member, long long when_online)
{
if ((scheduler->node_pending_timeout > 0)
&& (when_member > 0) && (when_online <= 0)) {
// There is a timeout on pending nodes, and node is pending
time_t timeout = when_member + scheduler->node_pending_timeout;
if (get_effective_time(node->details->data_set) >= timeout) {
return true; // Node has timed out
}
// Node is pending, but still has time
pe__update_recheck_time(timeout, scheduler, "pending node timeout");
}
return false;
}
static bool
determine_online_status_fencing(pcmk_scheduler_t *scheduler,
const xmlNode *node_state,
pcmk_node_t *this_node)
{
bool termination_requested = unpack_node_terminate(this_node, node_state);
const char *join = crm_element_value(node_state, PCMK__XA_JOIN);
const char *exp_state = crm_element_value(node_state, PCMK_XA_EXPECTED);
long long when_member = unpack_node_member(node_state, scheduler);
long long when_online = unpack_node_online(node_state);
/*
- PCMK__XA_JOIN ::= member|down|pending|banned
- PCMK_XA_EXPECTED ::= member|down
@COMPAT with entries recorded for DCs < 2.1.7
- PCMK__XA_IN_CCM ::= true|false
- PCMK_XA_CRMD ::= online|offline
Since crm_feature_set 3.18.0 (pacemaker-2.1.7):
- PCMK__XA_IN_CCM ::= |0
Since when node has been a cluster member. A value 0 of means the node is not
a cluster member.
- PCMK_XA_CRMD ::= |0
Since when peer has been online in CPG. A value 0 means the peer is offline
in CPG.
*/
crm_trace("Node %s member@%lld online@%lld join=%s expected=%s%s",
pcmk__node_name(this_node), when_member, when_online,
pcmk__s(join, ""), pcmk__s(exp_state, ""),
(termination_requested? " (termination requested)" : ""));
if (this_node->details->shutdown) {
crm_debug("%s is shutting down", pcmk__node_name(this_node));
/* Slightly different criteria since we can't shut down a dead peer */
return (when_online > 0);
}
if (when_member < 0) {
pe_fence_node(scheduler, this_node,
"peer has not been seen by the cluster", FALSE);
return false;
}
if (pcmk__str_eq(join, CRMD_JOINSTATE_NACK, pcmk__str_none)) {
pe_fence_node(scheduler, this_node,
"peer failed Pacemaker membership criteria", FALSE);
} else if (termination_requested) {
if ((when_member <= 0) && (when_online <= 0)
&& pcmk__str_eq(join, CRMD_JOINSTATE_DOWN, pcmk__str_none)) {
crm_info("%s was fenced as requested", pcmk__node_name(this_node));
return false;
}
pe_fence_node(scheduler, this_node, "fencing was requested", false);
} else if (pcmk__str_eq(exp_state, CRMD_JOINSTATE_DOWN,
pcmk__str_null_matches)) {
if (pending_too_long(scheduler, this_node, when_member, when_online)) {
pe_fence_node(scheduler, this_node,
"peer pending timed out on joining the process group",
FALSE);
} else if ((when_member > 0) || (when_online > 0)) {
crm_info("- %s is not ready to run resources",
pcmk__node_name(this_node));
this_node->details->standby = TRUE;
this_node->details->pending = TRUE;
} else {
crm_trace("%s is down or still coming up",
pcmk__node_name(this_node));
}
} else if (when_member <= 0) {
// Consider PCMK_OPT_PRIORITY_FENCING_DELAY for lost nodes
pe_fence_node(scheduler, this_node,
"peer is no longer part of the cluster", TRUE);
} else if (when_online <= 0) {
pe_fence_node(scheduler, this_node,
"peer process is no longer available", FALSE);
/* Everything is running at this point, now check join state */
} else if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_none)) {
crm_info("%s is active", pcmk__node_name(this_node));
} else if (pcmk__str_any_of(join, CRMD_JOINSTATE_PENDING,
CRMD_JOINSTATE_DOWN, NULL)) {
crm_info("%s is not ready to run resources",
pcmk__node_name(this_node));
this_node->details->standby = TRUE;
this_node->details->pending = TRUE;
} else {
pe_fence_node(scheduler, this_node, "peer was in an unknown state",
FALSE);
}
return (when_member > 0);
}
static void
determine_remote_online_status(pcmk_scheduler_t *scheduler,
pcmk_node_t *this_node)
{
pcmk_resource_t *rsc = this_node->details->remote_rsc;
pcmk_resource_t *container = NULL;
pcmk_node_t *host = NULL;
/* If there is a node state entry for a (former) Pacemaker Remote node
* but no resource creating that node, the node's connection resource will
* be NULL. Consider it an offline remote node in that case.
*/
if (rsc == NULL) {
this_node->details->online = FALSE;
goto remote_online_done;
}
container = rsc->container;
if (container && pcmk__list_of_1(rsc->running_on)) {
host = rsc->running_on->data;
}
/* If the resource is currently started, mark it online. */
if (rsc->role == pcmk_role_started) {
crm_trace("%s node %s presumed ONLINE because connection resource is started",
(container? "Guest" : "Remote"), this_node->details->id);
this_node->details->online = TRUE;
}
/* consider this node shutting down if transitioning start->stop */
if ((rsc->role == pcmk_role_started)
&& (rsc->next_role == pcmk_role_stopped)) {
crm_trace("%s node %s shutting down because connection resource is stopping",
(container? "Guest" : "Remote"), this_node->details->id);
this_node->details->shutdown = TRUE;
}
/* Now check all the failure conditions. */
if(container && pcmk_is_set(container->flags, pcmk_rsc_failed)) {
crm_trace("Guest node %s UNCLEAN because guest resource failed",
this_node->details->id);
this_node->details->online = FALSE;
this_node->details->remote_requires_reset = TRUE;
} else if (pcmk_is_set(rsc->flags, pcmk_rsc_failed)) {
crm_trace("%s node %s OFFLINE because connection resource failed",
(container? "Guest" : "Remote"), this_node->details->id);
this_node->details->online = FALSE;
} else if ((rsc->role == pcmk_role_stopped)
|| ((container != NULL)
&& (container->role == pcmk_role_stopped))) {
crm_trace("%s node %s OFFLINE because its resource is stopped",
(container? "Guest" : "Remote"), this_node->details->id);
this_node->details->online = FALSE;
this_node->details->remote_requires_reset = FALSE;
} else if (host && (host->details->online == FALSE)
&& host->details->unclean) {
crm_trace("Guest node %s UNCLEAN because host is unclean",
this_node->details->id);
this_node->details->online = FALSE;
this_node->details->remote_requires_reset = TRUE;
}
remote_online_done:
crm_trace("Remote node %s online=%s",
this_node->details->id, this_node->details->online ? "TRUE" : "FALSE");
}
static void
determine_online_status(const xmlNode *node_state, pcmk_node_t *this_node,
pcmk_scheduler_t *scheduler)
{
gboolean online = FALSE;
const char *exp_state = crm_element_value(node_state, PCMK_XA_EXPECTED);
CRM_CHECK(this_node != NULL, return);
this_node->details->shutdown = FALSE;
this_node->details->expected_up = FALSE;
if (pe__shutdown_requested(this_node)) {
this_node->details->shutdown = TRUE;
} else if (pcmk__str_eq(exp_state, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) {
this_node->details->expected_up = TRUE;
}
if (this_node->details->type == node_ping) {
this_node->details->unclean = FALSE;
online = FALSE; /* As far as resource management is concerned,
* the node is safely offline.
* Anyone caught abusing this logic will be shot
*/
} else if (!pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
online = determine_online_status_no_fencing(scheduler, node_state,
this_node);
} else {
online = determine_online_status_fencing(scheduler, node_state,
this_node);
}
if (online) {
this_node->details->online = TRUE;
} else {
/* remove node from contention */
this_node->fixed = TRUE; // @COMPAT deprecated and unused
this_node->weight = -PCMK_SCORE_INFINITY;
}
if (online && this_node->details->shutdown) {
/* don't run resources here */
this_node->fixed = TRUE; // @COMPAT deprecated and unused
this_node->weight = -PCMK_SCORE_INFINITY;
}
if (this_node->details->type == node_ping) {
crm_info("%s is not a Pacemaker node", pcmk__node_name(this_node));
} else if (this_node->details->unclean) {
pcmk__sched_warn("%s is unclean", pcmk__node_name(this_node));
} else if (this_node->details->online) {
crm_info("%s is %s", pcmk__node_name(this_node),
this_node->details->shutdown ? "shutting down" :
this_node->details->pending ? "pending" :
this_node->details->standby ? "standby" :
this_node->details->maintenance ? "maintenance" : "online");
} else {
crm_trace("%s is offline", pcmk__node_name(this_node));
}
}
/*!
* \internal
* \brief Find the end of a resource's name, excluding any clone suffix
*
* \param[in] id Resource ID to check
*
* \return Pointer to last character of resource's base name
*/
const char *
pe_base_name_end(const char *id)
{
if (!pcmk__str_empty(id)) {
const char *end = id + strlen(id) - 1;
for (const char *s = end; s > id; --s) {
switch (*s) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
break;
case ':':
return (s == end)? s : (s - 1);
default:
return end;
}
}
return end;
}
return NULL;
}
/*!
* \internal
* \brief Get a resource name excluding any clone suffix
*
* \param[in] last_rsc_id Resource ID to check
*
* \return Pointer to newly allocated string with resource's base name
* \note It is the caller's responsibility to free() the result.
* This asserts on error, so callers can assume result is not NULL.
*/
char *
clone_strip(const char *last_rsc_id)
{
const char *end = pe_base_name_end(last_rsc_id);
char *basename = NULL;
CRM_ASSERT(end);
basename = strndup(last_rsc_id, end - last_rsc_id + 1);
CRM_ASSERT(basename);
return basename;
}
/*!
* \internal
* \brief Get the name of the first instance of a cloned resource
*
* \param[in] last_rsc_id Resource ID to check
*
* \return Pointer to newly allocated string with resource's base name plus :0
* \note It is the caller's responsibility to free() the result.
* This asserts on error, so callers can assume result is not NULL.
*/
char *
clone_zero(const char *last_rsc_id)
{
const char *end = pe_base_name_end(last_rsc_id);
size_t base_name_len = end - last_rsc_id + 1;
char *zero = NULL;
CRM_ASSERT(end);
zero = pcmk__assert_alloc(base_name_len + 3, sizeof(char));
memcpy(zero, last_rsc_id, base_name_len);
zero[base_name_len] = ':';
zero[base_name_len + 1] = '0';
return zero;
}
static pcmk_resource_t *
create_fake_resource(const char *rsc_id, const xmlNode *rsc_entry,
pcmk_scheduler_t *scheduler)
{
pcmk_resource_t *rsc = NULL;
xmlNode *xml_rsc = pcmk__xe_create(NULL, PCMK_XE_PRIMITIVE);
pcmk__xe_copy_attrs(xml_rsc, rsc_entry, pcmk__xaf_none);
crm_xml_add(xml_rsc, PCMK_XA_ID, rsc_id);
crm_log_xml_debug(xml_rsc, "Orphan resource");
if (pe__unpack_resource(xml_rsc, &rsc, NULL, scheduler) != pcmk_rc_ok) {
return NULL;
}
if (xml_contains_remote_node(xml_rsc)) {
pcmk_node_t *node;
crm_debug("Detected orphaned remote node %s", rsc_id);
node = pe_find_node(scheduler->nodes, rsc_id);
if (node == NULL) {
node = pe_create_node(rsc_id, rsc_id, PCMK_VALUE_REMOTE, NULL,
scheduler);
}
link_rsc2remotenode(scheduler, rsc);
if (node) {
crm_trace("Setting node %s as shutting down due to orphaned connection resource", rsc_id);
node->details->shutdown = TRUE;
}
}
if (crm_element_value(rsc_entry, PCMK__META_CONTAINER)) {
/* This orphaned rsc needs to be mapped to a container. */
crm_trace("Detected orphaned container filler %s", rsc_id);
pcmk__set_rsc_flags(rsc, pcmk_rsc_removed_filler);
}
pcmk__set_rsc_flags(rsc, pcmk_rsc_removed);
scheduler->resources = g_list_append(scheduler->resources, rsc);
return rsc;
}
/*!
* \internal
* \brief Create orphan instance for anonymous clone resource history
*
* \param[in,out] parent Clone resource that orphan will be added to
* \param[in] rsc_id Orphan's resource ID
* \param[in] node Where orphan is active (for logging only)
* \param[in,out] scheduler Scheduler data
*
* \return Newly added orphaned instance of \p parent
*/
static pcmk_resource_t *
create_anonymous_orphan(pcmk_resource_t *parent, const char *rsc_id,
const pcmk_node_t *node, pcmk_scheduler_t *scheduler)
{
pcmk_resource_t *top = pe__create_clone_child(parent, scheduler);
// find_rsc() because we might be a cloned group
pcmk_resource_t *orphan = top->fns->find_rsc(top, rsc_id, NULL,
pcmk_rsc_match_clone_only);
pcmk__rsc_debug(parent, "Created orphan %s for %s: %s on %s",
top->id, parent->id, rsc_id, pcmk__node_name(node));
return orphan;
}
/*!
* \internal
* \brief Check a node for an instance of an anonymous clone
*
* Return a child instance of the specified anonymous clone, in order of
* preference: (1) the instance running on the specified node, if any;
* (2) an inactive instance (i.e. within the total of \c PCMK_META_CLONE_MAX
* instances); (3) a newly created orphan (that is, \c PCMK_META_CLONE_MAX
* instances are already active).
*
* \param[in,out] scheduler Scheduler data
* \param[in] node Node on which to check for instance
* \param[in,out] parent Clone to check
* \param[in] rsc_id Name of cloned resource in history (no instance)
*/
static pcmk_resource_t *
find_anonymous_clone(pcmk_scheduler_t *scheduler, const pcmk_node_t *node,
pcmk_resource_t *parent, const char *rsc_id)
{
GList *rIter = NULL;
pcmk_resource_t *rsc = NULL;
pcmk_resource_t *inactive_instance = NULL;
gboolean skip_inactive = FALSE;
CRM_ASSERT(parent != NULL);
CRM_ASSERT(pcmk__is_clone(parent));
CRM_ASSERT(!pcmk_is_set(parent->flags, pcmk_rsc_unique));
// Check for active (or partially active, for cloned groups) instance
pcmk__rsc_trace(parent, "Looking for %s on %s in %s",
rsc_id, pcmk__node_name(node), parent->id);
for (rIter = parent->children; rsc == NULL && rIter; rIter = rIter->next) {
GList *locations = NULL;
pcmk_resource_t *child = rIter->data;
/* Check whether this instance is already known to be active or pending
* anywhere, at this stage of unpacking. Because this function is called
* for a resource before the resource's individual operation history
* entries are unpacked, locations will generally not contain the
* desired node.
*
* However, there are three exceptions:
* (1) when child is a cloned group and we have already unpacked the
* history of another member of the group on the same node;
* (2) when we've already unpacked the history of another numbered
* instance on the same node (which can happen if
* PCMK_META_GLOBALLY_UNIQUE was flipped from true to false); and
* (3) when we re-run calculations on the same scheduler data as part of
* a simulation.
*/
child->fns->location(child, &locations, 2);
if (locations) {
/* We should never associate the same numbered anonymous clone
* instance with multiple nodes, and clone instances can't migrate,
* so there must be only one location, regardless of history.
*/
CRM_LOG_ASSERT(locations->next == NULL);
if (pcmk__same_node((pcmk_node_t *) locations->data, node)) {
/* This child instance is active on the requested node, so check
* for a corresponding configured resource. We use find_rsc()
* instead of child because child may be a cloned group, and we
* need the particular member corresponding to rsc_id.
*
* If the history entry is orphaned, rsc will be NULL.
*/
rsc = parent->fns->find_rsc(child, rsc_id, NULL,
pcmk_rsc_match_clone_only);
if (rsc) {
/* If there are multiple instance history entries for an
* anonymous clone in a single node's history (which can
* happen if PCMK_META_GLOBALLY_UNIQUE is switched from true
* to false), we want to consider the instances beyond the
* first as orphans, even if there are inactive instance
* numbers available.
*/
if (rsc->running_on) {
crm_notice("Active (now-)anonymous clone %s has "
"multiple (orphan) instance histories on %s",
parent->id, pcmk__node_name(node));
skip_inactive = TRUE;
rsc = NULL;
} else {
pcmk__rsc_trace(parent, "Resource %s, active", rsc->id);
}
}
}
g_list_free(locations);
} else {
pcmk__rsc_trace(parent, "Resource %s, skip inactive", child->id);
if (!skip_inactive && !inactive_instance
&& !pcmk_is_set(child->flags, pcmk_rsc_blocked)) {
// Remember one inactive instance in case we don't find active
inactive_instance = parent->fns->find_rsc(child, rsc_id, NULL,
pcmk_rsc_match_clone_only);
/* ... but don't use it if it was already associated with a
* pending action on another node
*/
if ((inactive_instance != NULL) &&
(inactive_instance->pending_node != NULL) &&
!pcmk__same_node(inactive_instance->pending_node, node)) {
inactive_instance = NULL;
}
}
}
}
if ((rsc == NULL) && !skip_inactive && (inactive_instance != NULL)) {
pcmk__rsc_trace(parent, "Resource %s, empty slot",
inactive_instance->id);
rsc = inactive_instance;
}
/* If the resource has PCMK_META_REQUIRES set to PCMK_VALUE_QUORUM or
* PCMK_VALUE_NOTHING, and we don't have a clone instance for every node, we
* don't want to consume a valid instance number for unclean nodes. Such
* instances may appear to be active according to the history, but should be
* considered inactive, so we can start an instance elsewhere. Treat such
* instances as orphans.
*
* An exception is instances running on guest nodes -- since guest node
* "fencing" is actually just a resource stop, requires shouldn't apply.
*
* @TODO Ideally, we'd use an inactive instance number if it is not needed
* for any clean instances. However, we don't know that at this point.
*/
if ((rsc != NULL) && !pcmk_is_set(rsc->flags, pcmk_rsc_needs_fencing)
&& (!node->details->online || node->details->unclean)
&& !pcmk__is_guest_or_bundle_node(node)
&& !pe__is_universal_clone(parent, scheduler)) {
rsc = NULL;
}
if (rsc == NULL) {
rsc = create_anonymous_orphan(parent, rsc_id, node, scheduler);
pcmk__rsc_trace(parent, "Resource %s, orphan", rsc->id);
}
return rsc;
}
static pcmk_resource_t *
unpack_find_resource(pcmk_scheduler_t *scheduler, const pcmk_node_t *node,
const char *rsc_id)
{
pcmk_resource_t *rsc = NULL;
pcmk_resource_t *parent = NULL;
crm_trace("looking for %s", rsc_id);
rsc = pe_find_resource(scheduler->resources, rsc_id);
if (rsc == NULL) {
/* If we didn't find the resource by its name in the operation history,
* check it again as a clone instance. Even when PCMK_META_CLONE_MAX=0,
* we create a single :0 orphan to match against here.
*/
char *clone0_id = clone_zero(rsc_id);
pcmk_resource_t *clone0 = pe_find_resource(scheduler->resources,
clone0_id);
if (clone0 && !pcmk_is_set(clone0->flags, pcmk_rsc_unique)) {
rsc = clone0;
parent = uber_parent(clone0);
crm_trace("%s found as %s (%s)", rsc_id, clone0_id, parent->id);
} else {
crm_trace("%s is not known as %s either (orphan)",
rsc_id, clone0_id);
}
free(clone0_id);
} else if (rsc->variant > pcmk_rsc_variant_primitive) {
crm_trace("Resource history for %s is orphaned because it is no longer primitive",
rsc_id);
return NULL;
} else {
parent = uber_parent(rsc);
}
if (pcmk__is_anonymous_clone(parent)) {
if (pcmk__is_bundled(parent)) {
rsc = pe__find_bundle_replica(parent->parent, node);
} else {
char *base = clone_strip(rsc_id);
rsc = find_anonymous_clone(scheduler, node, parent, base);
free(base);
CRM_ASSERT(rsc != NULL);
}
}
if (rsc && !pcmk__str_eq(rsc_id, rsc->id, pcmk__str_casei)
&& !pcmk__str_eq(rsc_id, rsc->clone_name, pcmk__str_casei)) {
pcmk__str_update(&rsc->clone_name, rsc_id);
pcmk__rsc_debug(rsc, "Internally renamed %s on %s to %s%s",
rsc_id, pcmk__node_name(node), rsc->id,
pcmk_is_set(rsc->flags, pcmk_rsc_removed)? " (ORPHAN)" : "");
}
return rsc;
}
static pcmk_resource_t *
process_orphan_resource(const xmlNode *rsc_entry, const pcmk_node_t *node,
pcmk_scheduler_t *scheduler)
{
pcmk_resource_t *rsc = NULL;
const char *rsc_id = crm_element_value(rsc_entry, PCMK_XA_ID);
crm_debug("Detected orphan resource %s on %s",
rsc_id, pcmk__node_name(node));
rsc = create_fake_resource(rsc_id, rsc_entry, scheduler);
if (rsc == NULL) {
return NULL;
}
if (!pcmk_is_set(scheduler->flags, pcmk_sched_stop_removed_resources)) {
pcmk__clear_rsc_flags(rsc, pcmk_rsc_managed);
} else {
CRM_CHECK(rsc != NULL, return NULL);
pcmk__rsc_trace(rsc, "Added orphan %s", rsc->id);
resource_location(rsc, NULL, -PCMK_SCORE_INFINITY,
"__orphan_do_not_run__", scheduler);
}
return rsc;
}
static void
process_rsc_state(pcmk_resource_t *rsc, pcmk_node_t *node,
enum action_fail_response on_fail)
{
pcmk_node_t *tmpnode = NULL;
char *reason = NULL;
enum action_fail_response save_on_fail = pcmk_on_fail_ignore;
CRM_ASSERT(rsc);
pcmk__rsc_trace(rsc, "Resource %s is %s on %s: on_fail=%s",
rsc->id, pcmk_role_text(rsc->role), pcmk__node_name(node),
pcmk_on_fail_text(on_fail));
/* process current state */
if (rsc->role != pcmk_role_unknown) {
pcmk_resource_t *iter = rsc;
while (iter) {
if (g_hash_table_lookup(iter->known_on, node->details->id) == NULL) {
pcmk_node_t *n = pe__copy_node(node);
pcmk__rsc_trace(rsc, "%s%s%s known on %s",
rsc->id,
((rsc->clone_name == NULL)? "" : " also known as "),
((rsc->clone_name == NULL)? "" : rsc->clone_name),
pcmk__node_name(n));
g_hash_table_insert(iter->known_on, (gpointer) n->details->id, n);
}
if (pcmk_is_set(iter->flags, pcmk_rsc_unique)) {
break;
}
iter = iter->parent;
}
}
/* If a managed resource is believed to be running, but node is down ... */
if ((rsc->role > pcmk_role_stopped)
&& node->details->online == FALSE
&& node->details->maintenance == FALSE
&& pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
gboolean should_fence = FALSE;
/* If this is a guest node, fence it (regardless of whether fencing is
* enabled, because guest node fencing is done by recovery of the
* container resource rather than by the fencer). Mark the resource
* we're processing as failed. When the guest comes back up, its
* operation history in the CIB will be cleared, freeing the affected
* resource to run again once we are sure we know its state.
*/
if (pcmk__is_guest_or_bundle_node(node)) {
pcmk__set_rsc_flags(rsc, pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
should_fence = TRUE;
} else if (pcmk_is_set(rsc->cluster->flags,
pcmk_sched_fencing_enabled)) {
if (pcmk__is_remote_node(node)
&& (node->details->remote_rsc != NULL)
&& !pcmk_is_set(node->details->remote_rsc->flags,
pcmk_rsc_failed)) {
/* Setting unseen means that fencing of the remote node will
* occur only if the connection resource is not going to start
* somewhere. This allows connection resources on a failed
* cluster node to move to another node without requiring the
* remote nodes to be fenced as well.
*/
node->details->unseen = TRUE;
reason = crm_strdup_printf("%s is active there (fencing will be"
" revoked if remote connection can "
"be re-established elsewhere)",
rsc->id);
}
should_fence = TRUE;
}
if (should_fence) {
if (reason == NULL) {
reason = crm_strdup_printf("%s is thought to be active there", rsc->id);
}
pe_fence_node(rsc->cluster, node, reason, FALSE);
}
free(reason);
}
/* In order to calculate priority_fencing_delay correctly, save the failure information and pass it to native_add_running(). */
save_on_fail = on_fail;
if (node->details->unclean) {
/* No extra processing needed
* Also allows resources to be started again after a node is shot
*/
on_fail = pcmk_on_fail_ignore;
}
switch (on_fail) {
case pcmk_on_fail_ignore:
/* nothing to do */
break;
case pcmk_on_fail_demote:
pcmk__set_rsc_flags(rsc, pcmk_rsc_failed);
demote_action(rsc, node, FALSE);
break;
case pcmk_on_fail_fence_node:
/* treat it as if it is still running
* but also mark the node as unclean
*/
reason = crm_strdup_printf("%s failed there", rsc->id);
pe_fence_node(rsc->cluster, node, reason, FALSE);
free(reason);
break;
case pcmk_on_fail_standby_node:
node->details->standby = TRUE;
node->details->standby_onfail = TRUE;
break;
case pcmk_on_fail_block:
/* is_managed == FALSE will prevent any
* actions being sent for the resource
*/
pcmk__clear_rsc_flags(rsc, pcmk_rsc_managed);
pcmk__set_rsc_flags(rsc, pcmk_rsc_blocked);
break;
case pcmk_on_fail_ban:
/* make sure it comes up somewhere else
* or not at all
*/
resource_location(rsc, node, -PCMK_SCORE_INFINITY,
"__action_migration_auto__", rsc->cluster);
break;
case pcmk_on_fail_stop:
pe__set_next_role(rsc, pcmk_role_stopped,
PCMK_META_ON_FAIL "=" PCMK_VALUE_STOP);
break;
case pcmk_on_fail_restart:
if ((rsc->role != pcmk_role_stopped)
&& (rsc->role != pcmk_role_unknown)) {
pcmk__set_rsc_flags(rsc,
pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
stop_action(rsc, node, FALSE);
}
break;
case pcmk_on_fail_restart_container:
pcmk__set_rsc_flags(rsc, pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
if ((rsc->container != NULL) && pcmk__is_bundled(rsc)) {
/* A bundle's remote connection can run on a different node than
* the bundle's container. We don't necessarily know where the
* container is running yet, so remember it and add a stop
* action for it later.
*/
rsc->cluster->stop_needed =
g_list_prepend(rsc->cluster->stop_needed, rsc->container);
} else if (rsc->container) {
stop_action(rsc->container, node, FALSE);
} else if ((rsc->role != pcmk_role_stopped)
&& (rsc->role != pcmk_role_unknown)) {
stop_action(rsc, node, FALSE);
}
break;
case pcmk_on_fail_reset_remote:
pcmk__set_rsc_flags(rsc, pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
if (pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)) {
tmpnode = NULL;
if (rsc->is_remote_node) {
tmpnode = pe_find_node(rsc->cluster->nodes, rsc->id);
}
if (pcmk__is_remote_node(tmpnode)
&& !(tmpnode->details->remote_was_fenced)) {
/* The remote connection resource failed in a way that
* should result in fencing the remote node.
*/
pe_fence_node(rsc->cluster, tmpnode,
"remote connection is unrecoverable", FALSE);
}
}
/* require the stop action regardless if fencing is occurring or not. */
if (rsc->role > pcmk_role_stopped) {
stop_action(rsc, node, FALSE);
}
/* if reconnect delay is in use, prevent the connection from exiting the
* "STOPPED" role until the failure is cleared by the delay timeout. */
if (rsc->remote_reconnect_ms) {
pe__set_next_role(rsc, pcmk_role_stopped, "remote reset");
}
break;
}
/* ensure a remote-node connection failure forces an unclean remote-node
* to be fenced. By setting unseen = FALSE, the remote-node failure will
* result in a fencing operation regardless if we're going to attempt to
* reconnect to the remote-node in this transition or not. */
if (pcmk_is_set(rsc->flags, pcmk_rsc_failed) && rsc->is_remote_node) {
tmpnode = pe_find_node(rsc->cluster->nodes, rsc->id);
if (tmpnode && tmpnode->details->unclean) {
tmpnode->details->unseen = FALSE;
}
}
if ((rsc->role != pcmk_role_stopped)
&& (rsc->role != pcmk_role_unknown)) {
if (pcmk_is_set(rsc->flags, pcmk_rsc_removed)) {
if (pcmk_is_set(rsc->flags, pcmk_rsc_managed)) {
crm_notice("Removed resource %s is active on %s and will be "
"stopped when possible",
rsc->id, pcmk__node_name(node));
} else {
crm_notice("Removed resource %s must be stopped manually on %s "
"because " PCMK_OPT_STOP_ORPHAN_RESOURCES
" is set to false", rsc->id, pcmk__node_name(node));
}
}
native_add_running(rsc, node, rsc->cluster,
(save_on_fail != pcmk_on_fail_ignore));
switch (on_fail) {
case pcmk_on_fail_ignore:
break;
case pcmk_on_fail_demote:
case pcmk_on_fail_block:
pcmk__set_rsc_flags(rsc, pcmk_rsc_failed);
break;
default:
pcmk__set_rsc_flags(rsc,
pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
break;
}
} else if (rsc->clone_name && strchr(rsc->clone_name, ':') != NULL) {
/* Only do this for older status sections that included instance numbers
* Otherwise stopped instances will appear as orphans
*/
pcmk__rsc_trace(rsc, "Resetting clone_name %s for %s (stopped)",
rsc->clone_name, rsc->id);
free(rsc->clone_name);
rsc->clone_name = NULL;
} else {
GList *possible_matches = pe__resource_actions(rsc, node,
PCMK_ACTION_STOP, FALSE);
GList *gIter = possible_matches;
for (; gIter != NULL; gIter = gIter->next) {
pcmk_action_t *stop = (pcmk_action_t *) gIter->data;
pcmk__set_action_flags(stop, pcmk_action_optional);
}
g_list_free(possible_matches);
}
/* A successful stop after migrate_to on the migration source doesn't make
* the partially migrated resource stopped on the migration target.
*/
if ((rsc->role == pcmk_role_stopped)
&& rsc->partial_migration_source
&& rsc->partial_migration_source->details == node->details
&& rsc->partial_migration_target
&& rsc->running_on) {
rsc->role = pcmk_role_started;
}
}
/* create active recurring operations as optional */
static void
process_recurring(pcmk_node_t *node, pcmk_resource_t *rsc,
int start_index, int stop_index,
GList *sorted_op_list, pcmk_scheduler_t *scheduler)
{
int counter = -1;
const char *task = NULL;
const char *status = NULL;
GList *gIter = sorted_op_list;
CRM_ASSERT(rsc);
pcmk__rsc_trace(rsc, "%s: Start index %d, stop index = %d",
rsc->id, start_index, stop_index);
for (; gIter != NULL; gIter = gIter->next) {
xmlNode *rsc_op = (xmlNode *) gIter->data;
guint interval_ms = 0;
char *key = NULL;
const char *id = pcmk__xe_id(rsc_op);
counter++;
if (node->details->online == FALSE) {
pcmk__rsc_trace(rsc, "Skipping %s on %s: node is offline",
rsc->id, pcmk__node_name(node));
break;
/* Need to check if there's a monitor for role="Stopped" */
} else if (start_index < stop_index && counter <= stop_index) {
pcmk__rsc_trace(rsc, "Skipping %s on %s: resource is not active",
id, pcmk__node_name(node));
continue;
} else if (counter < start_index) {
pcmk__rsc_trace(rsc, "Skipping %s on %s: old %d",
id, pcmk__node_name(node), counter);
continue;
}
crm_element_value_ms(rsc_op, PCMK_META_INTERVAL, &interval_ms);
if (interval_ms == 0) {
pcmk__rsc_trace(rsc, "Skipping %s on %s: non-recurring",
id, pcmk__node_name(node));
continue;
}
status = crm_element_value(rsc_op, PCMK__XA_OP_STATUS);
if (pcmk__str_eq(status, "-1", pcmk__str_casei)) {
pcmk__rsc_trace(rsc, "Skipping %s on %s: status",
id, pcmk__node_name(node));
continue;
}
task = crm_element_value(rsc_op, PCMK_XA_OPERATION);
/* create the action */
key = pcmk__op_key(rsc->id, task, interval_ms);
pcmk__rsc_trace(rsc, "Creating %s on %s", key, pcmk__node_name(node));
custom_action(rsc, key, task, node, TRUE, scheduler);
}
}
void
calculate_active_ops(const GList *sorted_op_list, int *start_index,
int *stop_index)
{
int counter = -1;
int implied_monitor_start = -1;
int implied_clone_start = -1;
const char *task = NULL;
const char *status = NULL;
*stop_index = -1;
*start_index = -1;
for (const GList *iter = sorted_op_list; iter != NULL; iter = iter->next) {
const xmlNode *rsc_op = (const xmlNode *) iter->data;
counter++;
task = crm_element_value(rsc_op, PCMK_XA_OPERATION);
status = crm_element_value(rsc_op, PCMK__XA_OP_STATUS);
if (pcmk__str_eq(task, PCMK_ACTION_STOP, pcmk__str_casei)
&& pcmk__str_eq(status, "0", pcmk__str_casei)) {
*stop_index = counter;
} else if (pcmk__strcase_any_of(task, PCMK_ACTION_START,
PCMK_ACTION_MIGRATE_FROM, NULL)) {
*start_index = counter;
} else if ((implied_monitor_start <= *stop_index)
&& pcmk__str_eq(task, PCMK_ACTION_MONITOR,
pcmk__str_casei)) {
const char *rc = crm_element_value(rsc_op, PCMK__XA_RC_CODE);
if (pcmk__strcase_any_of(rc, "0", "8", NULL)) {
implied_monitor_start = counter;
}
} else if (pcmk__strcase_any_of(task, PCMK_ACTION_PROMOTE,
PCMK_ACTION_DEMOTE, NULL)) {
implied_clone_start = counter;
}
}
if (*start_index == -1) {
if (implied_clone_start != -1) {
*start_index = implied_clone_start;
} else if (implied_monitor_start != -1) {
*start_index = implied_monitor_start;
}
}
}
// If resource history entry has shutdown lock, remember lock node and time
static void
unpack_shutdown_lock(const xmlNode *rsc_entry, pcmk_resource_t *rsc,
const pcmk_node_t *node, pcmk_scheduler_t *scheduler)
{
time_t lock_time = 0; // When lock started (i.e. node shutdown time)
if ((crm_element_value_epoch(rsc_entry, PCMK_OPT_SHUTDOWN_LOCK,
&lock_time) == pcmk_ok) && (lock_time != 0)) {
if ((scheduler->shutdown_lock > 0)
&& (get_effective_time(scheduler)
> (lock_time + scheduler->shutdown_lock))) {
pcmk__rsc_info(rsc, "Shutdown lock for %s on %s expired",
rsc->id, pcmk__node_name(node));
pe__clear_resource_history(rsc, node);
} else {
/* @COMPAT I don't like breaking const signatures, but
* rsc->lock_node should really be const -- we just can't change it
* until the next API compatibility break.
*/
rsc->lock_node = (pcmk_node_t *) node;
rsc->lock_time = lock_time;
}
}
}
/*!
* \internal
* \brief Unpack one \c PCMK__XE_LRM_RESOURCE entry from a node's CIB status
*
* \param[in,out] node Node whose status is being unpacked
* \param[in] rsc_entry \c PCMK__XE_LRM_RESOURCE XML being unpacked
* \param[in,out] scheduler Scheduler data
*
* \return Resource corresponding to the entry, or NULL if no operation history
*/
static pcmk_resource_t *
unpack_lrm_resource(pcmk_node_t *node, const xmlNode *lrm_resource,
pcmk_scheduler_t *scheduler)
{
GList *gIter = NULL;
int stop_index = -1;
int start_index = -1;
enum rsc_role_e req_role = pcmk_role_unknown;
const char *rsc_id = pcmk__xe_id(lrm_resource);
pcmk_resource_t *rsc = NULL;
GList *op_list = NULL;
GList *sorted_op_list = NULL;
xmlNode *rsc_op = NULL;
xmlNode *last_failure = NULL;
enum action_fail_response on_fail = pcmk_on_fail_ignore;
enum rsc_role_e saved_role = pcmk_role_unknown;
if (rsc_id == NULL) {
pcmk__config_err("Ignoring invalid " PCMK__XE_LRM_RESOURCE
" entry: No " PCMK_XA_ID);
crm_log_xml_info(lrm_resource, "missing-id");
return NULL;
}
crm_trace("Unpacking " PCMK__XE_LRM_RESOURCE " for %s on %s",
rsc_id, pcmk__node_name(node));
/* Build a list of individual PCMK__XE_LRM_RSC_OP entries, so we can sort
* them
*/
for (rsc_op = pcmk__xe_first_child(lrm_resource, PCMK__XE_LRM_RSC_OP, NULL,
NULL);
rsc_op != NULL; rsc_op = pcmk__xe_next_same(rsc_op)) {
op_list = g_list_prepend(op_list, rsc_op);
}
if (!pcmk_is_set(scheduler->flags, pcmk_sched_shutdown_lock)) {
if (op_list == NULL) {
// If there are no operations, there is nothing to do
return NULL;
}
}
/* find the resource */
rsc = unpack_find_resource(scheduler, node, rsc_id);
if (rsc == NULL) {
if (op_list == NULL) {
// If there are no operations, there is nothing to do
return NULL;
} else {
rsc = process_orphan_resource(lrm_resource, node, scheduler);
}
}
CRM_ASSERT(rsc != NULL);
// Check whether the resource is "shutdown-locked" to this node
if (pcmk_is_set(scheduler->flags, pcmk_sched_shutdown_lock)) {
unpack_shutdown_lock(lrm_resource, rsc, node, scheduler);
}
/* process operations */
saved_role = rsc->role;
rsc->role = pcmk_role_unknown;
sorted_op_list = g_list_sort(op_list, sort_op_by_callid);
for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) {
xmlNode *rsc_op = (xmlNode *) gIter->data;
unpack_rsc_op(rsc, node, rsc_op, &last_failure, &on_fail);
}
/* create active recurring operations as optional */
calculate_active_ops(sorted_op_list, &start_index, &stop_index);
process_recurring(node, rsc, start_index, stop_index, sorted_op_list,
scheduler);
/* no need to free the contents */
g_list_free(sorted_op_list);
process_rsc_state(rsc, node, on_fail);
if (get_target_role(rsc, &req_role)) {
if ((rsc->next_role == pcmk_role_unknown)
|| (req_role < rsc->next_role)) {
pe__set_next_role(rsc, req_role, PCMK_META_TARGET_ROLE);
} else if (req_role > rsc->next_role) {
pcmk__rsc_info(rsc,
"%s: Not overwriting calculated next role %s"
" with requested next role %s",
rsc->id, pcmk_role_text(rsc->next_role),
pcmk_role_text(req_role));
}
}
if (saved_role > rsc->role) {
rsc->role = saved_role;
}
return rsc;
}
static void
handle_orphaned_container_fillers(const xmlNode *lrm_rsc_list,
pcmk_scheduler_t *scheduler)
{
for (const xmlNode *rsc_entry = pcmk__xe_first_child(lrm_rsc_list, NULL,
NULL, NULL);
rsc_entry != NULL; rsc_entry = pcmk__xe_next(rsc_entry)) {
pcmk_resource_t *rsc;
pcmk_resource_t *container;
const char *rsc_id;
const char *container_id;
if (!pcmk__xe_is(rsc_entry, PCMK__XE_LRM_RESOURCE)) {
continue;
}
container_id = crm_element_value(rsc_entry, PCMK__META_CONTAINER);
rsc_id = crm_element_value(rsc_entry, PCMK_XA_ID);
if (container_id == NULL || rsc_id == NULL) {
continue;
}
container = pe_find_resource(scheduler->resources, container_id);
if (container == NULL) {
continue;
}
rsc = pe_find_resource(scheduler->resources, rsc_id);
if ((rsc == NULL) || (rsc->container != NULL)
|| !pcmk_is_set(rsc->flags, pcmk_rsc_removed_filler)) {
continue;
}
pcmk__rsc_trace(rsc, "Mapped container of orphaned resource %s to %s",
rsc->id, container_id);
rsc->container = container;
container->fillers = g_list_append(container->fillers, rsc);
}
}
/*!
* \internal
* \brief Unpack one node's lrm status section
*
* \param[in,out] node Node whose status is being unpacked
* \param[in] xml CIB node state XML
* \param[in,out] scheduler Scheduler data
*/
static void
unpack_node_lrm(pcmk_node_t *node, const xmlNode *xml,
pcmk_scheduler_t *scheduler)
{
bool found_orphaned_container_filler = false;
// Drill down to PCMK__XE_LRM_RESOURCES section
xml = pcmk__xe_first_child(xml, PCMK__XE_LRM, NULL, NULL);
if (xml == NULL) {
return;
}
xml = pcmk__xe_first_child(xml, PCMK__XE_LRM_RESOURCES, NULL, NULL);
if (xml == NULL) {
return;
}
// Unpack each PCMK__XE_LRM_RESOURCE entry
for (const xmlNode *rsc_entry = pcmk__xe_first_child(xml,
PCMK__XE_LRM_RESOURCE,
NULL, NULL);
rsc_entry != NULL; rsc_entry = pcmk__xe_next_same(rsc_entry)) {
pcmk_resource_t *rsc = unpack_lrm_resource(node, rsc_entry, scheduler);
if ((rsc != NULL)
&& pcmk_is_set(rsc->flags, pcmk_rsc_removed_filler)) {
found_orphaned_container_filler = true;
}
}
/* Now that all resource state has been unpacked for this node, map any
* orphaned container fillers to their container resource.
*/
if (found_orphaned_container_filler) {
handle_orphaned_container_fillers(xml, scheduler);
}
}
static void
set_active(pcmk_resource_t *rsc)
{
const pcmk_resource_t *top = pe__const_top_resource(rsc, false);
if (top && pcmk_is_set(top->flags, pcmk_rsc_promotable)) {
rsc->role = pcmk_role_unpromoted;
} else {
rsc->role = pcmk_role_started;
}
}
static void
set_node_score(gpointer key, gpointer value, gpointer user_data)
{
pcmk_node_t *node = value;
int *score = user_data;
node->weight = *score;
}
#define XPATH_NODE_STATE "/" PCMK_XE_CIB "/" PCMK_XE_STATUS \
"/" PCMK__XE_NODE_STATE
#define SUB_XPATH_LRM_RESOURCE "/" PCMK__XE_LRM \
"/" PCMK__XE_LRM_RESOURCES \
"/" PCMK__XE_LRM_RESOURCE
#define SUB_XPATH_LRM_RSC_OP "/" PCMK__XE_LRM_RSC_OP
static xmlNode *
find_lrm_op(const char *resource, const char *op, const char *node, const char *source,
int target_rc, pcmk_scheduler_t *scheduler)
{
GString *xpath = NULL;
xmlNode *xml = NULL;
CRM_CHECK((resource != NULL) && (op != NULL) && (node != NULL),
return NULL);
xpath = g_string_sized_new(256);
pcmk__g_strcat(xpath,
XPATH_NODE_STATE "[@" PCMK_XA_UNAME "='", node, "']"
SUB_XPATH_LRM_RESOURCE "[@" PCMK_XA_ID "='", resource, "']"
SUB_XPATH_LRM_RSC_OP "[@" PCMK_XA_OPERATION "='", op, "'",
NULL);
/* Need to check against transition_magic too? */
if ((source != NULL) && (strcmp(op, PCMK_ACTION_MIGRATE_TO) == 0)) {
pcmk__g_strcat(xpath,
" and @" PCMK__META_MIGRATE_TARGET "='", source, "']",
NULL);
} else if ((source != NULL)
&& (strcmp(op, PCMK_ACTION_MIGRATE_FROM) == 0)) {
pcmk__g_strcat(xpath,
" and @" PCMK__META_MIGRATE_SOURCE "='", source, "']",
NULL);
} else {
g_string_append_c(xpath, ']');
}
xml = get_xpath_object((const char *) xpath->str, scheduler->input,
LOG_DEBUG);
g_string_free(xpath, TRUE);
if (xml && target_rc >= 0) {
int rc = PCMK_OCF_UNKNOWN_ERROR;
int status = PCMK_EXEC_ERROR;
crm_element_value_int(xml, PCMK__XA_RC_CODE, &rc);
crm_element_value_int(xml, PCMK__XA_OP_STATUS, &status);
if ((rc != target_rc) || (status != PCMK_EXEC_DONE)) {
return NULL;
}
}
return xml;
}
static xmlNode *
find_lrm_resource(const char *rsc_id, const char *node_name,
pcmk_scheduler_t *scheduler)
{
GString *xpath = NULL;
xmlNode *xml = NULL;
CRM_CHECK((rsc_id != NULL) && (node_name != NULL), return NULL);
xpath = g_string_sized_new(256);
pcmk__g_strcat(xpath,
XPATH_NODE_STATE "[@" PCMK_XA_UNAME "='", node_name, "']"
SUB_XPATH_LRM_RESOURCE "[@" PCMK_XA_ID "='", rsc_id, "']",
NULL);
xml = get_xpath_object((const char *) xpath->str, scheduler->input,
LOG_DEBUG);
g_string_free(xpath, TRUE);
return xml;
}
/*!
* \internal
* \brief Check whether a resource has no completed action history on a node
*
* \param[in,out] rsc Resource to check
* \param[in] node_name Node to check
*
* \return true if \p rsc_id is unknown on \p node_name, otherwise false
*/
static bool
unknown_on_node(pcmk_resource_t *rsc, const char *node_name)
{
bool result = false;
xmlXPathObjectPtr search;
char *xpath = NULL;
xpath = crm_strdup_printf(XPATH_NODE_STATE "[@" PCMK_XA_UNAME "='%s']"
SUB_XPATH_LRM_RESOURCE "[@" PCMK_XA_ID "='%s']"
SUB_XPATH_LRM_RSC_OP
"[@" PCMK__XA_RC_CODE "!='%d']",
node_name, rsc->id, PCMK_OCF_UNKNOWN);
search = xpath_search(rsc->cluster->input, xpath);
result = (numXpathResults(search) == 0);
freeXpathObject(search);
free(xpath);
return result;
}
/*!
* \brief Check whether a probe/monitor indicating the resource was not running
* on a node happened after some event
*
* \param[in] rsc_id Resource being checked
* \param[in] node_name Node being checked
* \param[in] xml_op Event that monitor is being compared to
* \param[in] same_node Whether the operations are on the same node
* \param[in,out] scheduler Scheduler data
*
* \return true if such a monitor happened after event, false otherwise
*/
static bool
monitor_not_running_after(const char *rsc_id, const char *node_name,
const xmlNode *xml_op, bool same_node,
pcmk_scheduler_t *scheduler)
{
/* Any probe/monitor operation on the node indicating it was not running
* there
*/
xmlNode *monitor = find_lrm_op(rsc_id, PCMK_ACTION_MONITOR, node_name,
NULL, PCMK_OCF_NOT_RUNNING, scheduler);
return (monitor && pe__is_newer_op(monitor, xml_op, same_node) > 0);
}
/*!
* \brief Check whether any non-monitor operation on a node happened after some
* event
*
* \param[in] rsc_id Resource being checked
* \param[in] node_name Node being checked
* \param[in] xml_op Event that non-monitor is being compared to
* \param[in] same_node Whether the operations are on the same node
* \param[in,out] scheduler Scheduler data
*
* \return true if such a operation happened after event, false otherwise
*/
static bool
non_monitor_after(const char *rsc_id, const char *node_name,
const xmlNode *xml_op, bool same_node,
pcmk_scheduler_t *scheduler)
{
xmlNode *lrm_resource = NULL;
lrm_resource = find_lrm_resource(rsc_id, node_name, scheduler);
if (lrm_resource == NULL) {
return false;
}
for (xmlNode *op = pcmk__xe_first_child(lrm_resource, PCMK__XE_LRM_RSC_OP,
NULL, NULL);
op != NULL; op = pcmk__xe_next_same(op)) {
const char * task = NULL;
if (op == xml_op) {
continue;
}
task = crm_element_value(op, PCMK_XA_OPERATION);
if (pcmk__str_any_of(task, PCMK_ACTION_START, PCMK_ACTION_STOP,
PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM,
NULL)
&& pe__is_newer_op(op, xml_op, same_node) > 0) {
return true;
}
}
return false;
}
/*!
* \brief Check whether the resource has newer state on a node after a migration
* attempt
*
* \param[in] rsc_id Resource being checked
* \param[in] node_name Node being checked
* \param[in] migrate_to Any migrate_to event that is being compared to
* \param[in] migrate_from Any migrate_from event that is being compared to
* \param[in,out] scheduler Scheduler data
*
* \return true if such a operation happened after event, false otherwise
*/
static bool
newer_state_after_migrate(const char *rsc_id, const char *node_name,
const xmlNode *migrate_to,
const xmlNode *migrate_from,
pcmk_scheduler_t *scheduler)
{
const xmlNode *xml_op = migrate_to;
const char *source = NULL;
const char *target = NULL;
bool same_node = false;
if (migrate_from) {
xml_op = migrate_from;
}
source = crm_element_value(xml_op, PCMK__META_MIGRATE_SOURCE);
target = crm_element_value(xml_op, PCMK__META_MIGRATE_TARGET);
/* It's preferred to compare to the migrate event on the same node if
* existing, since call ids are more reliable.
*/
if (pcmk__str_eq(node_name, target, pcmk__str_casei)) {
if (migrate_from) {
xml_op = migrate_from;
same_node = true;
} else {
xml_op = migrate_to;
}
} else if (pcmk__str_eq(node_name, source, pcmk__str_casei)) {
if (migrate_to) {
xml_op = migrate_to;
same_node = true;
} else {
xml_op = migrate_from;
}
}
/* If there's any newer non-monitor operation on the node, or any newer
* probe/monitor operation on the node indicating it was not running there,
* the migration events potentially no longer matter for the node.
*/
return non_monitor_after(rsc_id, node_name, xml_op, same_node, scheduler)
|| monitor_not_running_after(rsc_id, node_name, xml_op, same_node,
scheduler);
}
/*!
* \internal
* \brief Parse migration source and target node names from history entry
*
* \param[in] entry Resource history entry for a migration action
* \param[in] source_node If not NULL, source must match this node
* \param[in] target_node If not NULL, target must match this node
* \param[out] source_name Where to store migration source node name
* \param[out] target_name Where to store migration target node name
*
* \return Standard Pacemaker return code
*/
static int
get_migration_node_names(const xmlNode *entry, const pcmk_node_t *source_node,
const pcmk_node_t *target_node,
const char **source_name, const char **target_name)
{
*source_name = crm_element_value(entry, PCMK__META_MIGRATE_SOURCE);
*target_name = crm_element_value(entry, PCMK__META_MIGRATE_TARGET);
if ((*source_name == NULL) || (*target_name == NULL)) {
pcmk__config_err("Ignoring resource history entry %s without "
PCMK__META_MIGRATE_SOURCE " and "
PCMK__META_MIGRATE_TARGET, pcmk__xe_id(entry));
return pcmk_rc_unpack_error;
}
if ((source_node != NULL)
&& !pcmk__str_eq(*source_name, source_node->details->uname,
pcmk__str_casei|pcmk__str_null_matches)) {
pcmk__config_err("Ignoring resource history entry %s because "
PCMK__META_MIGRATE_SOURCE "='%s' does not match %s",
pcmk__xe_id(entry), *source_name,
pcmk__node_name(source_node));
return pcmk_rc_unpack_error;
}
if ((target_node != NULL)
&& !pcmk__str_eq(*target_name, target_node->details->uname,
pcmk__str_casei|pcmk__str_null_matches)) {
pcmk__config_err("Ignoring resource history entry %s because "
PCMK__META_MIGRATE_TARGET "='%s' does not match %s",
pcmk__xe_id(entry), *target_name,
pcmk__node_name(target_node));
return pcmk_rc_unpack_error;
}
return pcmk_rc_ok;
}
/*
* \internal
* \brief Add a migration source to a resource's list of dangling migrations
*
* If the migrate_to and migrate_from actions in a live migration both
* succeeded, but there is no stop on the source, the migration is considered
* "dangling." Add the source to the resource's dangling migration list, which
* will be used to schedule a stop on the source without affecting the target.
*
* \param[in,out] rsc Resource involved in migration
* \param[in] node Migration source
*/
static void
add_dangling_migration(pcmk_resource_t *rsc, const pcmk_node_t *node)
{
pcmk__rsc_trace(rsc, "Dangling migration of %s requires stop on %s",
rsc->id, pcmk__node_name(node));
rsc->role = pcmk_role_stopped;
rsc->dangling_migrations = g_list_prepend(rsc->dangling_migrations,
(gpointer) node);
}
/*!
* \internal
* \brief Update resource role etc. after a successful migrate_to action
*
* \param[in,out] history Parsed action result history
*/
static void
unpack_migrate_to_success(struct action_history *history)
{
/* A complete migration sequence is:
* 1. migrate_to on source node (which succeeded if we get to this function)
* 2. migrate_from on target node
* 3. stop on source node
*
* If no migrate_from has happened, the migration is considered to be
* "partial". If the migrate_from succeeded but no stop has happened, the
* migration is considered to be "dangling".
*
* If a successful migrate_to and stop have happened on the source node, we
* still need to check for a partial migration, due to scenarios (easier to
* produce with batch-limit=1) like:
*
* - A resource is migrating from node1 to node2, and a migrate_to is
* initiated for it on node1.
*
* - node2 goes into standby mode while the migrate_to is pending, which
* aborts the transition.
*
* - Upon completion of the migrate_to, a new transition schedules a stop
* on both nodes and a start on node1.
*
* - If the new transition is aborted for any reason while the resource is
* stopping on node1, the transition after that stop completes will see
* the migrate_to and stop on the source, but it's still a partial
* migration, and the resource must be stopped on node2 because it is
* potentially active there due to the migrate_to.
*
* We also need to take into account that either node's history may be
* cleared at any point in the migration process.
*/
int from_rc = PCMK_OCF_OK;
int from_status = PCMK_EXEC_PENDING;
pcmk_node_t *target_node = NULL;
xmlNode *migrate_from = NULL;
const char *source = NULL;
const char *target = NULL;
bool source_newer_op = false;
bool target_newer_state = false;
bool active_on_target = false;
// Get source and target node names from XML
if (get_migration_node_names(history->xml, history->node, NULL, &source,
&target) != pcmk_rc_ok) {
return;
}
// Check for newer state on the source
source_newer_op = non_monitor_after(history->rsc->id, source, history->xml,
true, history->rsc->cluster);
// Check for a migrate_from action from this source on the target
migrate_from = find_lrm_op(history->rsc->id, PCMK_ACTION_MIGRATE_FROM,
target, source, -1, history->rsc->cluster);
if (migrate_from != NULL) {
if (source_newer_op) {
/* There's a newer non-monitor operation on the source and a
* migrate_from on the target, so this migrate_to is irrelevant to
* the resource's state.
*/
return;
}
crm_element_value_int(migrate_from, PCMK__XA_RC_CODE, &from_rc);
crm_element_value_int(migrate_from, PCMK__XA_OP_STATUS, &from_status);
}
/* If the resource has newer state on both the source and target after the
* migration events, this migrate_to is irrelevant to the resource's state.
*/
target_newer_state = newer_state_after_migrate(history->rsc->id, target,
history->xml, migrate_from,
history->rsc->cluster);
if (source_newer_op && target_newer_state) {
return;
}
/* Check for dangling migration (migrate_from succeeded but stop not done).
* We know there's no stop because we already returned if the target has a
* migrate_from and the source has any newer non-monitor operation.
*/
if ((from_rc == PCMK_OCF_OK) && (from_status == PCMK_EXEC_DONE)) {
add_dangling_migration(history->rsc, history->node);
return;
}
/* Without newer state, this migrate_to implies the resource is active.
* (Clones are not allowed to migrate, so role can't be promoted.)
*/
history->rsc->role = pcmk_role_started;
target_node = pe_find_node(history->rsc->cluster->nodes, target);
active_on_target = !target_newer_state && (target_node != NULL)
&& target_node->details->online;
if (from_status != PCMK_EXEC_PENDING) { // migrate_from failed on target
if (active_on_target) {
native_add_running(history->rsc, target_node, history->rsc->cluster,
TRUE);
} else {
// Mark resource as failed, require recovery, and prevent migration
pcmk__set_rsc_flags(history->rsc,
pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
pcmk__clear_rsc_flags(history->rsc, pcmk_rsc_migratable);
}
return;
}
// The migrate_from is pending, complete but erased, or to be scheduled
/* If there is no history at all for the resource on an online target, then
* it was likely cleaned. Just return, and we'll schedule a probe. Once we
* have the probe result, it will be reflected in target_newer_state.
*/
if ((target_node != NULL) && target_node->details->online
&& unknown_on_node(history->rsc, target)) {
return;
}
if (active_on_target) {
pcmk_node_t *source_node = pe_find_node(history->rsc->cluster->nodes,
source);
native_add_running(history->rsc, target_node, history->rsc->cluster,
FALSE);
if ((source_node != NULL) && source_node->details->online) {
/* This is a partial migration: the migrate_to completed
* successfully on the source, but the migrate_from has not
* completed. Remember the source and target; if the newly
* chosen target remains the same when we schedule actions
* later, we may continue with the migration.
*/
history->rsc->partial_migration_target = target_node;
history->rsc->partial_migration_source = source_node;
}
} else if (!source_newer_op) {
// Mark resource as failed, require recovery, and prevent migration
pcmk__set_rsc_flags(history->rsc,
pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
pcmk__clear_rsc_flags(history->rsc, pcmk_rsc_migratable);
}
}
/*!
* \internal
* \brief Update resource role etc. after a failed migrate_to action
*
* \param[in,out] history Parsed action result history
*/
static void
unpack_migrate_to_failure(struct action_history *history)
{
xmlNode *target_migrate_from = NULL;
const char *source = NULL;
const char *target = NULL;
// Get source and target node names from XML
if (get_migration_node_names(history->xml, history->node, NULL, &source,
&target) != pcmk_rc_ok) {
return;
}
/* If a migration failed, we have to assume the resource is active. Clones
* are not allowed to migrate, so role can't be promoted.
*/
history->rsc->role = pcmk_role_started;
// Check for migrate_from on the target
target_migrate_from = find_lrm_op(history->rsc->id,
PCMK_ACTION_MIGRATE_FROM, target, source,
PCMK_OCF_OK, history->rsc->cluster);
if (/* If the resource state is unknown on the target, it will likely be
* probed there.
* Don't just consider it running there. We will get back here anyway in
* case the probe detects it's running there.
*/
!unknown_on_node(history->rsc, target)
/* If the resource has newer state on the target after the migration
* events, this migrate_to no longer matters for the target.
*/
&& !newer_state_after_migrate(history->rsc->id, target, history->xml,
target_migrate_from,
history->rsc->cluster)) {
/* The resource has no newer state on the target, so assume it's still
* active there.
* (if it is up).
*/
pcmk_node_t *target_node = pe_find_node(history->rsc->cluster->nodes,
target);
if (target_node && target_node->details->online) {
native_add_running(history->rsc, target_node, history->rsc->cluster,
FALSE);
}
} else if (!non_monitor_after(history->rsc->id, source, history->xml, true,
history->rsc->cluster)) {
/* We know the resource has newer state on the target, but this
* migrate_to still matters for the source as long as there's no newer
* non-monitor operation there.
*/
// Mark node as having dangling migration so we can force a stop later
history->rsc->dangling_migrations =
g_list_prepend(history->rsc->dangling_migrations,
(gpointer) history->node);
}
}
/*!
* \internal
* \brief Update resource role etc. after a failed migrate_from action
*
* \param[in,out] history Parsed action result history
*/
static void
unpack_migrate_from_failure(struct action_history *history)
{
xmlNode *source_migrate_to = NULL;
const char *source = NULL;
const char *target = NULL;
// Get source and target node names from XML
if (get_migration_node_names(history->xml, NULL, history->node, &source,
&target) != pcmk_rc_ok) {
return;
}
/* If a migration failed, we have to assume the resource is active. Clones
* are not allowed to migrate, so role can't be promoted.
*/
history->rsc->role = pcmk_role_started;
// Check for a migrate_to on the source
source_migrate_to = find_lrm_op(history->rsc->id, PCMK_ACTION_MIGRATE_TO,
source, target, PCMK_OCF_OK,
history->rsc->cluster);
if (/* If the resource state is unknown on the source, it will likely be
* probed there.
* Don't just consider it running there. We will get back here anyway in
* case the probe detects it's running there.
*/
!unknown_on_node(history->rsc, source)
/* If the resource has newer state on the source after the migration
* events, this migrate_from no longer matters for the source.
*/
&& !newer_state_after_migrate(history->rsc->id, source,
source_migrate_to, history->xml,
history->rsc->cluster)) {
/* The resource has no newer state on the source, so assume it's still
* active there (if it is up).
*/
pcmk_node_t *source_node = pe_find_node(history->rsc->cluster->nodes,
source);
if (source_node && source_node->details->online) {
native_add_running(history->rsc, source_node, history->rsc->cluster,
TRUE);
}
}
}
/*!
* \internal
* \brief Add an action to cluster's list of failed actions
*
* \param[in,out] history Parsed action result history
*/
static void
record_failed_op(struct action_history *history)
{
if (!(history->node->details->online)) {
return;
}
for (const xmlNode *xIter = history->rsc->cluster->failed->children;
xIter != NULL; xIter = xIter->next) {
const char *key = pcmk__xe_history_key(xIter);
const char *uname = crm_element_value(xIter, PCMK_XA_UNAME);
if (pcmk__str_eq(history->key, key, pcmk__str_none)
&& pcmk__str_eq(uname, history->node->details->uname,
pcmk__str_casei)) {
crm_trace("Skipping duplicate entry %s on %s",
history->key, pcmk__node_name(history->node));
return;
}
}
crm_trace("Adding entry for %s on %s to failed action list",
history->key, pcmk__node_name(history->node));
crm_xml_add(history->xml, PCMK_XA_UNAME, history->node->details->uname);
crm_xml_add(history->xml, PCMK__XA_RSC_ID, history->rsc->id);
pcmk__xml_copy(history->rsc->cluster->failed, history->xml);
}
static char *
last_change_str(const xmlNode *xml_op)
{
time_t when;
char *result = NULL;
if (crm_element_value_epoch(xml_op, PCMK_XA_LAST_RC_CHANGE,
&when) == pcmk_ok) {
char *when_s = pcmk__epoch2str(&when, 0);
const char *p = strchr(when_s, ' ');
// Skip day of week to make message shorter
if ((p != NULL) && (*(++p) != '\0')) {
result = pcmk__str_copy(p);
}
free(when_s);
}
if (result == NULL) {
result = pcmk__str_copy("unknown_time");
}
return result;
}
/*!
* \internal
* \brief Compare two on-fail values
*
* \param[in] first One on-fail value to compare
* \param[in] second The other on-fail value to compare
*
* \return A negative number if second is more severe than first, zero if they
* are equal, or a positive number if first is more severe than second.
* \note This is only needed until the action_fail_response values can be
* renumbered at the next API compatibility break.
*/
static int
cmp_on_fail(enum action_fail_response first, enum action_fail_response second)
{
switch (first) {
case pcmk_on_fail_demote:
switch (second) {
case pcmk_on_fail_ignore:
return 1;
case pcmk_on_fail_demote:
return 0;
default:
return -1;
}
break;
case pcmk_on_fail_reset_remote:
switch (second) {
case pcmk_on_fail_ignore:
case pcmk_on_fail_demote:
case pcmk_on_fail_restart:
return 1;
case pcmk_on_fail_reset_remote:
return 0;
default:
return -1;
}
break;
case pcmk_on_fail_restart_container:
switch (second) {
case pcmk_on_fail_ignore:
case pcmk_on_fail_demote:
case pcmk_on_fail_restart:
case pcmk_on_fail_reset_remote:
return 1;
case pcmk_on_fail_restart_container:
return 0;
default:
return -1;
}
break;
default:
break;
}
switch (second) {
case pcmk_on_fail_demote:
return (first == pcmk_on_fail_ignore)? -1 : 1;
case pcmk_on_fail_reset_remote:
switch (first) {
case pcmk_on_fail_ignore:
case pcmk_on_fail_demote:
case pcmk_on_fail_restart:
return -1;
default:
return 1;
}
break;
case pcmk_on_fail_restart_container:
switch (first) {
case pcmk_on_fail_ignore:
case pcmk_on_fail_demote:
case pcmk_on_fail_restart:
case pcmk_on_fail_reset_remote:
return -1;
default:
return 1;
}
break;
default:
break;
}
return first - second;
}
/*!
* \internal
* \brief Ban a resource (or its clone if an anonymous instance) from all nodes
*
* \param[in,out] rsc Resource to ban
*/
static void
ban_from_all_nodes(pcmk_resource_t *rsc)
{
int score = -PCMK_SCORE_INFINITY;
pcmk_resource_t *fail_rsc = rsc;
if (fail_rsc->parent != NULL) {
pcmk_resource_t *parent = uber_parent(fail_rsc);
if (pcmk__is_anonymous_clone(parent)) {
/* For anonymous clones, if an operation with
* PCMK_META_ON_FAIL=PCMK_VALUE_STOP fails for any instance, the
* entire clone must stop.
*/
fail_rsc = parent;
}
}
// Ban the resource from all nodes
crm_notice("%s will not be started under current conditions", fail_rsc->id);
if (fail_rsc->allowed_nodes != NULL) {
g_hash_table_destroy(fail_rsc->allowed_nodes);
}
fail_rsc->allowed_nodes = pe__node_list2table(rsc->cluster->nodes);
g_hash_table_foreach(fail_rsc->allowed_nodes, set_node_score, &score);
}
/*!
* \internal
* \brief Get configured failure handling and role after failure for an action
*
* \param[in,out] history Unpacked action history entry
* \param[out] on_fail Where to set configured failure handling
* \param[out] fail_role Where to set to role after failure
*/
static void
unpack_failure_handling(struct action_history *history,
enum action_fail_response *on_fail,
enum rsc_role_e *fail_role)
{
xmlNode *config = pcmk__find_action_config(history->rsc, history->task,
history->interval_ms, true);
GHashTable *meta = pcmk__unpack_action_meta(history->rsc, history->node,
history->task,
history->interval_ms, config);
const char *on_fail_str = g_hash_table_lookup(meta, PCMK_META_ON_FAIL);
*on_fail = pcmk__parse_on_fail(history->rsc, history->task,
history->interval_ms, on_fail_str);
*fail_role = pcmk__role_after_failure(history->rsc, history->task, *on_fail,
meta);
g_hash_table_destroy(meta);
}
/*!
* \internal
* \brief Update resource role, failure handling, etc., after a failed action
*
* \param[in,out] history Parsed action result history
* \param[in] config_on_fail Action failure handling from configuration
* \param[in] fail_role Resource's role after failure of this action
* \param[out] last_failure This will be set to the history XML
* \param[in,out] on_fail Actual handling of action result
*/
static void
unpack_rsc_op_failure(struct action_history *history,
enum action_fail_response config_on_fail,
enum rsc_role_e fail_role, xmlNode **last_failure,
enum action_fail_response *on_fail)
{
bool is_probe = false;
char *last_change_s = NULL;
*last_failure = history->xml;
is_probe = pcmk_xe_is_probe(history->xml);
last_change_s = last_change_str(history->xml);
if (!pcmk_is_set(history->rsc->cluster->flags, pcmk_sched_symmetric_cluster)
&& (history->exit_status == PCMK_OCF_NOT_INSTALLED)) {
crm_trace("Unexpected result (%s%s%s) was recorded for "
"%s of %s on %s at %s " CRM_XS " exit-status=%d id=%s",
services_ocf_exitcode_str(history->exit_status),
(pcmk__str_empty(history->exit_reason)? "" : ": "),
pcmk__s(history->exit_reason, ""),
(is_probe? "probe" : history->task), history->rsc->id,
pcmk__node_name(history->node), last_change_s,
history->exit_status, history->id);
} else {
pcmk__sched_warn("Unexpected result (%s%s%s) was recorded for %s of "
"%s on %s at %s " CRM_XS " exit-status=%d id=%s",
services_ocf_exitcode_str(history->exit_status),
(pcmk__str_empty(history->exit_reason)? "" : ": "),
pcmk__s(history->exit_reason, ""),
(is_probe? "probe" : history->task), history->rsc->id,
pcmk__node_name(history->node), last_change_s,
history->exit_status, history->id);
if (is_probe && (history->exit_status != PCMK_OCF_OK)
&& (history->exit_status != PCMK_OCF_NOT_RUNNING)
&& (history->exit_status != PCMK_OCF_RUNNING_PROMOTED)) {
/* A failed (not just unexpected) probe result could mean the user
* didn't know resources will be probed even where they can't run.
*/
crm_notice("If it is not possible for %s to run on %s, see "
"the " PCMK_XA_RESOURCE_DISCOVERY " option for location "
"constraints",
history->rsc->id, pcmk__node_name(history->node));
}
record_failed_op(history);
}
free(last_change_s);
if (cmp_on_fail(*on_fail, config_on_fail) < 0) {
pcmk__rsc_trace(history->rsc, "on-fail %s -> %s for %s",
pcmk_on_fail_text(*on_fail),
pcmk_on_fail_text(config_on_fail), history->key);
*on_fail = config_on_fail;
}
if (strcmp(history->task, PCMK_ACTION_STOP) == 0) {
resource_location(history->rsc, history->node, -PCMK_SCORE_INFINITY,
"__stop_fail__", history->rsc->cluster);
} else if (strcmp(history->task, PCMK_ACTION_MIGRATE_TO) == 0) {
unpack_migrate_to_failure(history);
} else if (strcmp(history->task, PCMK_ACTION_MIGRATE_FROM) == 0) {
unpack_migrate_from_failure(history);
} else if (strcmp(history->task, PCMK_ACTION_PROMOTE) == 0) {
history->rsc->role = pcmk_role_promoted;
} else if (strcmp(history->task, PCMK_ACTION_DEMOTE) == 0) {
if (config_on_fail == pcmk_on_fail_block) {
history->rsc->role = pcmk_role_promoted;
pe__set_next_role(history->rsc, pcmk_role_stopped,
"demote with " PCMK_META_ON_FAIL "=block");
} else if (history->exit_status == PCMK_OCF_NOT_RUNNING) {
history->rsc->role = pcmk_role_stopped;
} else {
/* Staying in the promoted role would put the scheduler and
* controller into a loop. Setting the role to unpromoted is not
* dangerous because the resource will be stopped as part of
* recovery, and any promotion will be ordered after that stop.
*/
history->rsc->role = pcmk_role_unpromoted;
}
}
if (is_probe && (history->exit_status == PCMK_OCF_NOT_INSTALLED)) {
/* leave stopped */
pcmk__rsc_trace(history->rsc, "Leaving %s stopped", history->rsc->id);
history->rsc->role = pcmk_role_stopped;
} else if (history->rsc->role < pcmk_role_started) {
pcmk__rsc_trace(history->rsc, "Setting %s active", history->rsc->id);
set_active(history->rsc);
}
pcmk__rsc_trace(history->rsc,
"Resource %s: role=%s unclean=%s on_fail=%s fail_role=%s",
history->rsc->id, pcmk_role_text(history->rsc->role),
pcmk__btoa(history->node->details->unclean),
pcmk_on_fail_text(config_on_fail),
pcmk_role_text(fail_role));
if ((fail_role != pcmk_role_started)
&& (history->rsc->next_role < fail_role)) {
pe__set_next_role(history->rsc, fail_role, "failure");
}
if (fail_role == pcmk_role_stopped) {
ban_from_all_nodes(history->rsc);
}
}
/*!
* \internal
* \brief Block a resource with a failed action if it cannot be recovered
*
* If resource action is a failed stop and fencing is not possible, mark the
* resource as unmanaged and blocked, since recovery cannot be done.
*
* \param[in,out] history Parsed action history entry
*/
static void
block_if_unrecoverable(struct action_history *history)
{
char *last_change_s = NULL;
if (strcmp(history->task, PCMK_ACTION_STOP) != 0) {
return; // All actions besides stop are always recoverable
}
if (pe_can_fence(history->node->details->data_set, history->node)) {
return; // Failed stops are recoverable via fencing
}
last_change_s = last_change_str(history->xml);
pcmk__sched_err("No further recovery can be attempted for %s "
"because %s on %s failed (%s%s%s) at %s "
CRM_XS " rc=%d id=%s",
history->rsc->id, history->task,
pcmk__node_name(history->node),
services_ocf_exitcode_str(history->exit_status),
(pcmk__str_empty(history->exit_reason)? "" : ": "),
pcmk__s(history->exit_reason, ""),
last_change_s, history->exit_status, history->id);
free(last_change_s);
pcmk__clear_rsc_flags(history->rsc, pcmk_rsc_managed);
pcmk__set_rsc_flags(history->rsc, pcmk_rsc_blocked);
}
/*!
* \internal
* \brief Update action history's execution status and why
*
* \param[in,out] history Parsed action history entry
* \param[out] why Where to store reason for update
* \param[in] value New value
* \param[in] reason Description of why value was changed
*/
static inline void
remap_because(struct action_history *history, const char **why, int value,
const char *reason)
{
if (history->execution_status != value) {
history->execution_status = value;
*why = reason;
}
}
/*!
* \internal
* \brief Remap informational monitor results and operation status
*
* For the monitor results, certain OCF codes are for providing extended information
* to the user about services that aren't yet failed but not entirely healthy either.
* These must be treated as the "normal" result by Pacemaker.
*
* For operation status, the action result can be used to determine an appropriate
* status for the purposes of responding to the action. The status provided by the
* executor is not directly usable since the executor does not know what was expected.
*
* \param[in,out] history Parsed action history entry
* \param[in,out] on_fail What should be done about the result
* \param[in] expired Whether result is expired
*
* \note If the result is remapped and the node is not shutting down or failed,
* the operation will be recorded in the scheduler data's list of failed
* operations to highlight it for the user.
*
* \note This may update the resource's current and next role.
*/
static void
remap_operation(struct action_history *history,
enum action_fail_response *on_fail, bool expired)
{
bool is_probe = false;
int orig_exit_status = history->exit_status;
int orig_exec_status = history->execution_status;
const char *why = NULL;
const char *task = history->task;
// Remap degraded results to their successful counterparts
history->exit_status = pcmk__effective_rc(history->exit_status);
if (history->exit_status != orig_exit_status) {
why = "degraded result";
if (!expired && (!history->node->details->shutdown
|| history->node->details->online)) {
record_failed_op(history);
}
}
if (!pcmk__is_bundled(history->rsc)
&& pcmk_xe_mask_probe_failure(history->xml)
&& ((history->execution_status != PCMK_EXEC_DONE)
|| (history->exit_status != PCMK_OCF_NOT_RUNNING))) {
history->execution_status = PCMK_EXEC_DONE;
history->exit_status = PCMK_OCF_NOT_RUNNING;
why = "equivalent probe result";
}
/* If the executor reported an execution status of anything but done or
* error, consider that final. But for done or error, we know better whether
* it should be treated as a failure or not, because we know the expected
* result.
*/
switch (history->execution_status) {
case PCMK_EXEC_DONE:
case PCMK_EXEC_ERROR:
break;
// These should be treated as node-fatal
case PCMK_EXEC_NO_FENCE_DEVICE:
case PCMK_EXEC_NO_SECRETS:
remap_because(history, &why, PCMK_EXEC_ERROR_HARD,
"node-fatal error");
goto remap_done;
default:
goto remap_done;
}
is_probe = pcmk_xe_is_probe(history->xml);
if (is_probe) {
task = "probe";
}
if (history->expected_exit_status < 0) {
/* Pre-1.0 Pacemaker versions, and Pacemaker 1.1.6 or earlier with
* Heartbeat 2.0.7 or earlier as the cluster layer, did not include the
* expected exit status in the transition key, which (along with the
* similar case of a corrupted transition key in the CIB) will be
* reported to this function as -1. Pacemaker 2.0+ does not support
* rolling upgrades from those versions or processing of saved CIB files
* from those versions, so we do not need to care much about this case.
*/
remap_because(history, &why, PCMK_EXEC_ERROR,
"obsolete history format");
pcmk__config_warn("Expected result not found for %s on %s "
"(corrupt or obsolete CIB?)",
history->key, pcmk__node_name(history->node));
} else if (history->exit_status == history->expected_exit_status) {
remap_because(history, &why, PCMK_EXEC_DONE, "expected result");
} else {
remap_because(history, &why, PCMK_EXEC_ERROR, "unexpected result");
pcmk__rsc_debug(history->rsc,
"%s on %s: expected %d (%s), got %d (%s%s%s)",
history->key, pcmk__node_name(history->node),
history->expected_exit_status,
services_ocf_exitcode_str(history->expected_exit_status),
history->exit_status,
services_ocf_exitcode_str(history->exit_status),
(pcmk__str_empty(history->exit_reason)? "" : ": "),
pcmk__s(history->exit_reason, ""));
}
switch (history->exit_status) {
case PCMK_OCF_OK:
if (is_probe
&& (history->expected_exit_status == PCMK_OCF_NOT_RUNNING)) {
char *last_change_s = last_change_str(history->xml);
remap_because(history, &why, PCMK_EXEC_DONE, "probe");
pcmk__rsc_info(history->rsc,
"Probe found %s active on %s at %s",
history->rsc->id, pcmk__node_name(history->node),
last_change_s);
free(last_change_s);
}
break;
case PCMK_OCF_NOT_RUNNING:
if (is_probe
|| (history->expected_exit_status == history->exit_status)
|| !pcmk_is_set(history->rsc->flags, pcmk_rsc_managed)) {
/* For probes, recurring monitors for the Stopped role, and
* unmanaged resources, "not running" is not considered a
* failure.
*/
remap_because(history, &why, PCMK_EXEC_DONE, "exit status");
history->rsc->role = pcmk_role_stopped;
*on_fail = pcmk_on_fail_ignore;
pe__set_next_role(history->rsc, pcmk_role_unknown,
"not running");
}
break;
case PCMK_OCF_RUNNING_PROMOTED:
if (is_probe
&& (history->exit_status != history->expected_exit_status)) {
char *last_change_s = last_change_str(history->xml);
remap_because(history, &why, PCMK_EXEC_DONE, "probe");
pcmk__rsc_info(history->rsc,
"Probe found %s active and promoted on %s at %s",
history->rsc->id,
pcmk__node_name(history->node), last_change_s);
free(last_change_s);
}
if (!expired
|| (history->exit_status == history->expected_exit_status)) {
history->rsc->role = pcmk_role_promoted;
}
break;
case PCMK_OCF_FAILED_PROMOTED:
if (!expired) {
history->rsc->role = pcmk_role_promoted;
}
remap_because(history, &why, PCMK_EXEC_ERROR, "exit status");
break;
case PCMK_OCF_NOT_CONFIGURED:
remap_because(history, &why, PCMK_EXEC_ERROR_FATAL, "exit status");
break;
case PCMK_OCF_UNIMPLEMENT_FEATURE:
{
guint interval_ms = 0;
crm_element_value_ms(history->xml, PCMK_META_INTERVAL,
&interval_ms);
if (interval_ms == 0) {
if (!expired) {
block_if_unrecoverable(history);
}
remap_because(history, &why, PCMK_EXEC_ERROR_HARD,
"exit status");
} else {
remap_because(history, &why, PCMK_EXEC_NOT_SUPPORTED,
"exit status");
}
}
break;
case PCMK_OCF_NOT_INSTALLED:
case PCMK_OCF_INVALID_PARAM:
case PCMK_OCF_INSUFFICIENT_PRIV:
if (!expired) {
block_if_unrecoverable(history);
}
remap_because(history, &why, PCMK_EXEC_ERROR_HARD, "exit status");
break;
default:
if (history->execution_status == PCMK_EXEC_DONE) {
char *last_change_s = last_change_str(history->xml);
crm_info("Treating unknown exit status %d from %s of %s "
"on %s at %s as failure",
history->exit_status, task, history->rsc->id,
pcmk__node_name(history->node), last_change_s);
remap_because(history, &why, PCMK_EXEC_ERROR,
"unknown exit status");
free(last_change_s);
}
break;
}
remap_done:
if (why != NULL) {
pcmk__rsc_trace(history->rsc,
"Remapped %s result from [%s: %s] to [%s: %s] "
"because of %s",
history->key, pcmk_exec_status_str(orig_exec_status),
crm_exit_str(orig_exit_status),
pcmk_exec_status_str(history->execution_status),
crm_exit_str(history->exit_status), why);
}
}
// return TRUE if start or monitor last failure but parameters changed
static bool
should_clear_for_param_change(const xmlNode *xml_op, const char *task,
pcmk_resource_t *rsc, pcmk_node_t *node)
{
if (pcmk__str_any_of(task, PCMK_ACTION_START, PCMK_ACTION_MONITOR, NULL)) {
if (pe__bundle_needs_remote_name(rsc)) {
/* We haven't allocated resources yet, so we can't reliably
* substitute addr parameters for the REMOTE_CONTAINER_HACK.
* When that's needed, defer the check until later.
*/
pe__add_param_check(xml_op, rsc, node, pcmk__check_last_failure,
rsc->cluster);
} else {
pcmk__op_digest_t *digest_data = NULL;
digest_data = rsc_action_digest_cmp(rsc, xml_op, node,
rsc->cluster);
switch (digest_data->rc) {
case pcmk__digest_unknown:
crm_trace("Resource %s history entry %s on %s"
" has no digest to compare",
rsc->id, pcmk__xe_history_key(xml_op),
node->details->id);
break;
case pcmk__digest_match:
break;
default:
return TRUE;
}
}
}
return FALSE;
}
// Order action after fencing of remote node, given connection rsc
static void
order_after_remote_fencing(pcmk_action_t *action, pcmk_resource_t *remote_conn,
pcmk_scheduler_t *scheduler)
{
pcmk_node_t *remote_node = pe_find_node(scheduler->nodes, remote_conn->id);
if (remote_node) {
pcmk_action_t *fence = pe_fence_op(remote_node, NULL, TRUE, NULL,
FALSE, scheduler);
order_actions(fence, action, pcmk__ar_first_implies_then);
}
}
static bool
should_ignore_failure_timeout(const pcmk_resource_t *rsc, const char *task,
guint interval_ms, bool is_last_failure)
{
/* Clearing failures of recurring monitors has special concerns. The
* executor reports only changes in the monitor result, so if the
* monitor is still active and still getting the same failure result,
* that will go undetected after the failure is cleared.
*
* Also, the operation history will have the time when the recurring
* monitor result changed to the given code, not the time when the
* result last happened.
*
* @TODO We probably should clear such failures only when the failure
* timeout has passed since the last occurrence of the failed result.
* However we don't record that information. We could maybe approximate
* that by clearing only if there is a more recent successful monitor or
* stop result, but we don't even have that information at this point
* since we are still unpacking the resource's operation history.
*
* This is especially important for remote connection resources with a
* reconnect interval, so in that case, we skip clearing failures
* if the remote node hasn't been fenced.
*/
if (rsc->remote_reconnect_ms
&& pcmk_is_set(rsc->cluster->flags, pcmk_sched_fencing_enabled)
&& (interval_ms != 0)
&& pcmk__str_eq(task, PCMK_ACTION_MONITOR, pcmk__str_casei)) {
pcmk_node_t *remote_node = pe_find_node(rsc->cluster->nodes, rsc->id);
if (remote_node && !remote_node->details->remote_was_fenced) {
if (is_last_failure) {
crm_info("Waiting to clear monitor failure for remote node %s"
" until fencing has occurred", rsc->id);
}
return TRUE;
}
}
return FALSE;
}
/*!
* \internal
* \brief Check operation age and schedule failure clearing when appropriate
*
* This function has two distinct purposes. The first is to check whether an
* operation history entry is expired (i.e. the resource has a failure timeout,
* the entry is older than the timeout, and the resource either has no fail
* count or its fail count is entirely older than the timeout). The second is to
* schedule fail count clearing when appropriate (i.e. the operation is expired
* and either the resource has an expired fail count or the operation is a
* last_failure for a remote connection resource with a reconnect interval,
* or the operation is a last_failure for a start or monitor operation and the
* resource's parameters have changed since the operation).
*
* \param[in,out] history Parsed action result history
*
* \return true if operation history entry is expired, otherwise false
*/
static bool
check_operation_expiry(struct action_history *history)
{
bool expired = false;
bool is_last_failure = pcmk__ends_with(history->id, "_last_failure_0");
time_t last_run = 0;
int unexpired_fail_count = 0;
const char *clear_reason = NULL;
if (history->execution_status == PCMK_EXEC_NOT_INSTALLED) {
pcmk__rsc_trace(history->rsc,
"Resource history entry %s on %s is not expired: "
"Not Installed does not expire",
history->id, pcmk__node_name(history->node));
return false; // "Not installed" must always be cleared manually
}
if ((history->rsc->failure_timeout > 0)
&& (crm_element_value_epoch(history->xml, PCMK_XA_LAST_RC_CHANGE,
&last_run) == 0)) {
/* Resource has a PCMK_META_FAILURE_TIMEOUT and history entry has a
* timestamp
*/
time_t now = get_effective_time(history->rsc->cluster);
time_t last_failure = 0;
// Is this particular operation history older than the failure timeout?
if ((now >= (last_run + history->rsc->failure_timeout))
&& !should_ignore_failure_timeout(history->rsc, history->task,
history->interval_ms,
is_last_failure)) {
expired = true;
}
// Does the resource as a whole have an unexpired fail count?
unexpired_fail_count = pe_get_failcount(history->node, history->rsc,
&last_failure,
pcmk__fc_effective,
history->xml);
// Update scheduler recheck time according to *last* failure
crm_trace("%s@%lld is %sexpired @%lld with unexpired_failures=%d timeout=%ds"
" last-failure@%lld",
history->id, (long long) last_run, (expired? "" : "not "),
(long long) now, unexpired_fail_count,
history->rsc->failure_timeout, (long long) last_failure);
last_failure += history->rsc->failure_timeout + 1;
if (unexpired_fail_count && (now < last_failure)) {
pe__update_recheck_time(last_failure, history->rsc->cluster,
"fail count expiration");
}
}
if (expired) {
if (pe_get_failcount(history->node, history->rsc, NULL,
pcmk__fc_default, history->xml)) {
// There is a fail count ignoring timeout
if (unexpired_fail_count == 0) {
// There is no fail count considering timeout
clear_reason = "it expired";
} else {
/* This operation is old, but there is an unexpired fail count.
* In a properly functioning cluster, this should only be
* possible if this operation is not a failure (otherwise the
* fail count should be expired too), so this is really just a
* failsafe.
*/
pcmk__rsc_trace(history->rsc,
"Resource history entry %s on %s is not "
"expired: Unexpired fail count",
history->id, pcmk__node_name(history->node));
expired = false;
}
} else if (is_last_failure
&& (history->rsc->remote_reconnect_ms != 0)) {
/* Clear any expired last failure when reconnect interval is set,
* even if there is no fail count.
*/
clear_reason = "reconnect interval is set";
}
}
if (!expired && is_last_failure
&& should_clear_for_param_change(history->xml, history->task,
history->rsc, history->node)) {
clear_reason = "resource parameters have changed";
}
if (clear_reason != NULL) {
pcmk_action_t *clear_op = NULL;
// Schedule clearing of the fail count
clear_op = pe__clear_failcount(history->rsc, history->node,
clear_reason, history->rsc->cluster);
if (pcmk_is_set(history->rsc->cluster->flags,
pcmk_sched_fencing_enabled)
&& (history->rsc->remote_reconnect_ms != 0)) {
/* If we're clearing a remote connection due to a reconnect
* interval, we want to wait until any scheduled fencing
* completes.
*
* We could limit this to remote_node->details->unclean, but at
* this point, that's always true (it won't be reliable until
* after unpack_node_history() is done).
*/
crm_info("Clearing %s failure will wait until any scheduled "
"fencing of %s completes",
history->task, history->rsc->id);
order_after_remote_fencing(clear_op, history->rsc,
history->rsc->cluster);
}
}
if (expired && (history->interval_ms == 0)
&& pcmk__str_eq(history->task, PCMK_ACTION_MONITOR, pcmk__str_none)) {
switch (history->exit_status) {
case PCMK_OCF_OK:
case PCMK_OCF_NOT_RUNNING:
case PCMK_OCF_RUNNING_PROMOTED:
case PCMK_OCF_DEGRADED:
case PCMK_OCF_DEGRADED_PROMOTED:
// Don't expire probes that return these values
pcmk__rsc_trace(history->rsc,
"Resource history entry %s on %s is not "
"expired: Probe result",
history->id, pcmk__node_name(history->node));
expired = false;
break;
}
}
return expired;
}
int
pe__target_rc_from_xml(const xmlNode *xml_op)
{
int target_rc = 0;
const char *key = crm_element_value(xml_op, PCMK__XA_TRANSITION_KEY);
if (key == NULL) {
return -1;
}
decode_transition_key(key, NULL, NULL, NULL, &target_rc);
return target_rc;
}
/*!
* \internal
* \brief Update a resource's state for an action result
*
* \param[in,out] history Parsed action history entry
* \param[in] exit_status Exit status to base new state on
* \param[in] last_failure Resource's last_failure entry, if known
* \param[in,out] on_fail Resource's current failure handling
*/
static void
update_resource_state(struct action_history *history, int exit_status,
const xmlNode *last_failure,
enum action_fail_response *on_fail)
{
bool clear_past_failure = false;
if ((exit_status == PCMK_OCF_NOT_INSTALLED)
|| (!pcmk__is_bundled(history->rsc)
&& pcmk_xe_mask_probe_failure(history->xml))) {
history->rsc->role = pcmk_role_stopped;
} else if (exit_status == PCMK_OCF_NOT_RUNNING) {
clear_past_failure = true;
} else if (pcmk__str_eq(history->task, PCMK_ACTION_MONITOR,
pcmk__str_none)) {
if ((last_failure != NULL)
&& pcmk__str_eq(history->key, pcmk__xe_history_key(last_failure),
pcmk__str_none)) {
clear_past_failure = true;
}
if (history->rsc->role < pcmk_role_started) {
set_active(history->rsc);
}
} else if (pcmk__str_eq(history->task, PCMK_ACTION_START, pcmk__str_none)) {
history->rsc->role = pcmk_role_started;
clear_past_failure = true;
} else if (pcmk__str_eq(history->task, PCMK_ACTION_STOP, pcmk__str_none)) {
history->rsc->role = pcmk_role_stopped;
clear_past_failure = true;
} else if (pcmk__str_eq(history->task, PCMK_ACTION_PROMOTE,
pcmk__str_none)) {
history->rsc->role = pcmk_role_promoted;
clear_past_failure = true;
} else if (pcmk__str_eq(history->task, PCMK_ACTION_DEMOTE,
pcmk__str_none)) {
if (*on_fail == pcmk_on_fail_demote) {
/* Demote clears an error only if
* PCMK_META_ON_FAIL=PCMK_VALUE_DEMOTE
*/
clear_past_failure = true;
}
history->rsc->role = pcmk_role_unpromoted;
} else if (pcmk__str_eq(history->task, PCMK_ACTION_MIGRATE_FROM,
pcmk__str_none)) {
history->rsc->role = pcmk_role_started;
clear_past_failure = true;
} else if (pcmk__str_eq(history->task, PCMK_ACTION_MIGRATE_TO,
pcmk__str_none)) {
unpack_migrate_to_success(history);
} else if (history->rsc->role < pcmk_role_started) {
pcmk__rsc_trace(history->rsc, "%s active on %s",
history->rsc->id, pcmk__node_name(history->node));
set_active(history->rsc);
}
if (!clear_past_failure) {
return;
}
switch (*on_fail) {
case pcmk_on_fail_stop:
case pcmk_on_fail_ban:
case pcmk_on_fail_standby_node:
case pcmk_on_fail_fence_node:
pcmk__rsc_trace(history->rsc,
"%s (%s) is not cleared by a completed %s",
history->rsc->id, pcmk_on_fail_text(*on_fail),
history->task);
break;
case pcmk_on_fail_block:
case pcmk_on_fail_ignore:
case pcmk_on_fail_demote:
case pcmk_on_fail_restart:
case pcmk_on_fail_restart_container:
*on_fail = pcmk_on_fail_ignore;
pe__set_next_role(history->rsc, pcmk_role_unknown,
"clear past failures");
break;
case pcmk_on_fail_reset_remote:
if (history->rsc->remote_reconnect_ms == 0) {
/* With no reconnect interval, the connection is allowed to
* start again after the remote node is fenced and
* completely stopped. (With a reconnect interval, we wait
* for the failure to be cleared entirely before attempting
* to reconnect.)
*/
*on_fail = pcmk_on_fail_ignore;
pe__set_next_role(history->rsc, pcmk_role_unknown,
"clear past failures and reset remote");
}
break;
}
}
/*!
* \internal
* \brief Check whether a given history entry matters for resource state
*
* \param[in] history Parsed action history entry
*
* \return true if action can affect resource state, otherwise false
*/
static inline bool
can_affect_state(struct action_history *history)
{
#if 0
/* @COMPAT It might be better to parse only actions we know we're interested
* in, rather than exclude a couple we don't. However that would be a
* behavioral change that should be done at a major or minor series release.
* Currently, unknown operations can affect whether a resource is considered
* active and/or failed.
*/
return pcmk__str_any_of(history->task, PCMK_ACTION_MONITOR,
PCMK_ACTION_START, PCMK_ACTION_STOP,
PCMK_ACTION_PROMOTE, PCMK_ACTION_DEMOTE,
PCMK_ACTION_MIGRATE_TO, PCMK_ACTION_MIGRATE_FROM,
"asyncmon", NULL);
#else
return !pcmk__str_any_of(history->task, PCMK_ACTION_NOTIFY,
PCMK_ACTION_META_DATA, NULL);
#endif
}
/*!
* \internal
* \brief Unpack execution/exit status and exit reason from a history entry
*
* \param[in,out] history Action history entry to unpack
*
* \return Standard Pacemaker return code
*/
static int
unpack_action_result(struct action_history *history)
{
if ((crm_element_value_int(history->xml, PCMK__XA_OP_STATUS,
&(history->execution_status)) < 0)
|| (history->execution_status < PCMK_EXEC_PENDING)
|| (history->execution_status > PCMK_EXEC_MAX)
|| (history->execution_status == PCMK_EXEC_CANCELLED)) {
pcmk__config_err("Ignoring resource history entry %s for %s on %s "
"with invalid " PCMK__XA_OP_STATUS " '%s'",
history->id, history->rsc->id,
pcmk__node_name(history->node),
pcmk__s(crm_element_value(history->xml,
PCMK__XA_OP_STATUS),
""));
return pcmk_rc_unpack_error;
}
if ((crm_element_value_int(history->xml, PCMK__XA_RC_CODE,
&(history->exit_status)) < 0)
|| (history->exit_status < 0) || (history->exit_status > CRM_EX_MAX)) {
#if 0
/* @COMPAT We should ignore malformed entries, but since that would
* change behavior, it should be done at a major or minor series
* release.
*/
pcmk__config_err("Ignoring resource history entry %s for %s on %s "
"with invalid " PCMK__XA_RC_CODE " '%s'",
history->id, history->rsc->id,
pcmk__node_name(history->node),
pcmk__s(crm_element_value(history->xml,
PCMK__XA_RC_CODE),
""));
return pcmk_rc_unpack_error;
#else
history->exit_status = CRM_EX_ERROR;
#endif
}
history->exit_reason = crm_element_value(history->xml, PCMK_XA_EXIT_REASON);
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Process an action history entry whose result expired
*
* \param[in,out] history Parsed action history entry
* \param[in] orig_exit_status Action exit status before remapping
*
* \return Standard Pacemaker return code (in particular, pcmk_rc_ok means the
* entry needs no further processing)
*/
static int
process_expired_result(struct action_history *history, int orig_exit_status)
{
if (!pcmk__is_bundled(history->rsc)
&& pcmk_xe_mask_probe_failure(history->xml)
&& (orig_exit_status != history->expected_exit_status)) {
if (history->rsc->role <= pcmk_role_stopped) {
history->rsc->role = pcmk_role_unknown;
}
crm_trace("Ignoring resource history entry %s for probe of %s on %s: "
"Masked failure expired",
history->id, history->rsc->id,
pcmk__node_name(history->node));
return pcmk_rc_ok;
}
if (history->exit_status == history->expected_exit_status) {
return pcmk_rc_undetermined; // Only failures expire
}
if (history->interval_ms == 0) {
crm_notice("Ignoring resource history entry %s for %s of %s on %s: "
"Expired failure",
history->id, history->task, history->rsc->id,
pcmk__node_name(history->node));
return pcmk_rc_ok;
}
if (history->node->details->online && !history->node->details->unclean) {
/* Reschedule the recurring action. schedule_cancel() won't work at
* this stage, so as a hacky workaround, forcibly change the restart
* digest so pcmk__check_action_config() does what we want later.
*
* @TODO We should skip this if there is a newer successful monitor.
* Also, this causes rescheduling only if the history entry
* has a PCMK__XA_OP_DIGEST (which the expire-non-blocked-failure
* scheduler regression test doesn't, but that may not be a
* realistic scenario in production).
*/
crm_notice("Rescheduling %s-interval %s of %s on %s "
"after failure expired",
pcmk__readable_interval(history->interval_ms), history->task,
history->rsc->id, pcmk__node_name(history->node));
crm_xml_add(history->xml, PCMK__XA_OP_RESTART_DIGEST,
"calculated-failure-timeout");
return pcmk_rc_ok;
}
return pcmk_rc_undetermined;
}
/*!
* \internal
* \brief Process a masked probe failure
*
* \param[in,out] history Parsed action history entry
* \param[in] orig_exit_status Action exit status before remapping
* \param[in] last_failure Resource's last_failure entry, if known
* \param[in,out] on_fail Resource's current failure handling
*/
static void
mask_probe_failure(struct action_history *history, int orig_exit_status,
const xmlNode *last_failure,
enum action_fail_response *on_fail)
{
pcmk_resource_t *ban_rsc = history->rsc;
if (!pcmk_is_set(history->rsc->flags, pcmk_rsc_unique)) {
ban_rsc = uber_parent(history->rsc);
}
crm_notice("Treating probe result '%s' for %s on %s as 'not running'",
services_ocf_exitcode_str(orig_exit_status), history->rsc->id,
pcmk__node_name(history->node));
update_resource_state(history, history->expected_exit_status, last_failure,
on_fail);
crm_xml_add(history->xml, PCMK_XA_UNAME, history->node->details->uname);
record_failed_op(history);
resource_location(ban_rsc, history->node, -PCMK_SCORE_INFINITY,
"masked-probe-failure", history->rsc->cluster);
}
/*!
* \internal Check whether a given failure is for a given pending action
*
* \param[in] history Parsed history entry for pending action
* \param[in] last_failure Resource's last_failure entry, if known
*
* \return true if \p last_failure is failure of pending action in \p history,
* otherwise false
* \note Both \p history and \p last_failure must come from the same
* \c PCMK__XE_LRM_RESOURCE block, as node and resource are assumed to be
* the same.
*/
static bool
failure_is_newer(const struct action_history *history,
const xmlNode *last_failure)
{
guint failure_interval_ms = 0U;
long long failure_change = 0LL;
long long this_change = 0LL;
if (last_failure == NULL) {
return false; // Resource has no last_failure entry
}
if (!pcmk__str_eq(history->task,
crm_element_value(last_failure, PCMK_XA_OPERATION),
pcmk__str_none)) {
return false; // last_failure is for different action
}
if ((crm_element_value_ms(last_failure, PCMK_META_INTERVAL,
&failure_interval_ms) != pcmk_ok)
|| (history->interval_ms != failure_interval_ms)) {
return false; // last_failure is for action with different interval
}
if ((pcmk__scan_ll(crm_element_value(history->xml, PCMK_XA_LAST_RC_CHANGE),
&this_change, 0LL) != pcmk_rc_ok)
|| (pcmk__scan_ll(crm_element_value(last_failure,
PCMK_XA_LAST_RC_CHANGE),
&failure_change, 0LL) != pcmk_rc_ok)
|| (failure_change < this_change)) {
return false; // Failure is not known to be newer
}
return true;
}
/*!
* \internal
* \brief Update a resource's role etc. for a pending action
*
* \param[in,out] history Parsed history entry for pending action
* \param[in] last_failure Resource's last_failure entry, if known
*/
static void
process_pending_action(struct action_history *history,
const xmlNode *last_failure)
{
/* For recurring monitors, a failure is recorded only in RSC_last_failure_0,
* and there might be a RSC_monitor_INTERVAL entry with the last successful
* or pending result.
*
* If last_failure contains the failure of the pending recurring monitor
* we're processing here, and is newer, the action is no longer pending.
* (Pending results have call ID -1, which sorts last, so the last failure
* if any should be known.)
*/
if (failure_is_newer(history, last_failure)) {
return;
}
if (strcmp(history->task, PCMK_ACTION_START) == 0) {
pcmk__set_rsc_flags(history->rsc, pcmk_rsc_start_pending);
set_active(history->rsc);
} else if (strcmp(history->task, PCMK_ACTION_PROMOTE) == 0) {
history->rsc->role = pcmk_role_promoted;
} else if ((strcmp(history->task, PCMK_ACTION_MIGRATE_TO) == 0)
&& history->node->details->unclean) {
/* A migrate_to action is pending on a unclean source, so force a stop
* on the target.
*/
const char *migrate_target = NULL;
pcmk_node_t *target = NULL;
migrate_target = crm_element_value(history->xml,
PCMK__META_MIGRATE_TARGET);
target = pe_find_node(history->rsc->cluster->nodes, migrate_target);
if (target != NULL) {
stop_action(history->rsc, target, FALSE);
}
}
if (history->rsc->pending_task != NULL) {
/* There should never be multiple pending actions, but as a failsafe,
* just remember the first one processed for display purposes.
*/
return;
}
if (pcmk_is_probe(history->task, history->interval_ms)) {
/* Pending probes are currently never displayed, even if pending
* operations are requested. If we ever want to change that,
* enable the below and the corresponding part of
* native.c:native_pending_task().
*/
#if 0
history->rsc->pending_task = strdup("probe");
history->rsc->pending_node = history->node;
#endif
} else {
history->rsc->pending_task = strdup(history->task);
history->rsc->pending_node = history->node;
}
}
static void
unpack_rsc_op(pcmk_resource_t *rsc, pcmk_node_t *node, xmlNode *xml_op,
xmlNode **last_failure, enum action_fail_response *on_fail)
{
int old_rc = 0;
bool expired = false;
pcmk_resource_t *parent = rsc;
enum rsc_role_e fail_role = pcmk_role_unknown;
enum action_fail_response failure_strategy = pcmk_on_fail_restart;
struct action_history history = {
.rsc = rsc,
.node = node,
.xml = xml_op,
.execution_status = PCMK_EXEC_UNKNOWN,
};
CRM_CHECK(rsc && node && xml_op, return);
history.id = pcmk__xe_id(xml_op);
if (history.id == NULL) {
pcmk__config_err("Ignoring resource history entry for %s on %s "
"without ID", rsc->id, pcmk__node_name(node));
return;
}
// Task and interval
history.task = crm_element_value(xml_op, PCMK_XA_OPERATION);
if (history.task == NULL) {
pcmk__config_err("Ignoring resource history entry %s for %s on %s "
"without " PCMK_XA_OPERATION,
history.id, rsc->id, pcmk__node_name(node));
return;
}
crm_element_value_ms(xml_op, PCMK_META_INTERVAL, &(history.interval_ms));
if (!can_affect_state(&history)) {
pcmk__rsc_trace(rsc,
"Ignoring resource history entry %s for %s on %s "
"with irrelevant action '%s'",
history.id, rsc->id, pcmk__node_name(node),
history.task);
return;
}
if (unpack_action_result(&history) != pcmk_rc_ok) {
return; // Error already logged
}
history.expected_exit_status = pe__target_rc_from_xml(xml_op);
history.key = pcmk__xe_history_key(xml_op);
crm_element_value_int(xml_op, PCMK__XA_CALL_ID, &(history.call_id));
pcmk__rsc_trace(rsc, "Unpacking %s (%s call %d on %s): %s (%s)",
history.id, history.task, history.call_id,
pcmk__node_name(node),
pcmk_exec_status_str(history.execution_status),
crm_exit_str(history.exit_status));
if (node->details->unclean) {
pcmk__rsc_trace(rsc,
"%s is running on %s, which is unclean (further action "
"depends on value of stop's on-fail attribute)",
rsc->id, pcmk__node_name(node));
}
expired = check_operation_expiry(&history);
old_rc = history.exit_status;
remap_operation(&history, on_fail, expired);
if (expired && (process_expired_result(&history, old_rc) == pcmk_rc_ok)) {
goto done;
}
if (!pcmk__is_bundled(rsc) && pcmk_xe_mask_probe_failure(xml_op)) {
mask_probe_failure(&history, old_rc, *last_failure, on_fail);
goto done;
}
if (!pcmk_is_set(rsc->flags, pcmk_rsc_unique)) {
parent = uber_parent(rsc);
}
switch (history.execution_status) {
case PCMK_EXEC_PENDING:
process_pending_action(&history, *last_failure);
goto done;
case PCMK_EXEC_DONE:
update_resource_state(&history, history.exit_status, *last_failure,
on_fail);
goto done;
case PCMK_EXEC_NOT_INSTALLED:
unpack_failure_handling(&history, &failure_strategy, &fail_role);
if (failure_strategy == pcmk_on_fail_ignore) {
crm_warn("Cannot ignore failed %s of %s on %s: "
"Resource agent doesn't exist "
CRM_XS " status=%d rc=%d id=%s",
history.task, rsc->id, pcmk__node_name(node),
history.execution_status, history.exit_status,
history.id);
/* Also for printing it as "FAILED" by marking it as
* pcmk_rsc_failed later
*/
*on_fail = pcmk_on_fail_ban;
}
resource_location(parent, node, -PCMK_SCORE_INFINITY,
"hard-error", rsc->cluster);
unpack_rsc_op_failure(&history, failure_strategy, fail_role,
last_failure, on_fail);
goto done;
case PCMK_EXEC_NOT_CONNECTED:
if (pcmk__is_pacemaker_remote_node(node)
&& pcmk_is_set(node->details->remote_rsc->flags,
pcmk_rsc_managed)) {
/* We should never get into a situation where a managed remote
* connection resource is considered OK but a resource action
* behind the connection gets a "not connected" status. But as a
* fail-safe in case a bug or unusual circumstances do lead to
* that, ensure the remote connection is considered failed.
*/
pcmk__set_rsc_flags(node->details->remote_rsc,
pcmk_rsc_failed|pcmk_rsc_stop_if_failed);
}
break; // Not done, do error handling
case PCMK_EXEC_ERROR:
case PCMK_EXEC_ERROR_HARD:
case PCMK_EXEC_ERROR_FATAL:
case PCMK_EXEC_TIMEOUT:
case PCMK_EXEC_NOT_SUPPORTED:
case PCMK_EXEC_INVALID:
break; // Not done, do error handling
default: // No other value should be possible at this point
break;
}
unpack_failure_handling(&history, &failure_strategy, &fail_role);
if ((failure_strategy == pcmk_on_fail_ignore)
|| ((failure_strategy == pcmk_on_fail_restart_container)
&& (strcmp(history.task, PCMK_ACTION_STOP) == 0))) {
char *last_change_s = last_change_str(xml_op);
crm_warn("Pretending failed %s (%s%s%s) of %s on %s at %s succeeded "
CRM_XS " %s",
history.task, services_ocf_exitcode_str(history.exit_status),
(pcmk__str_empty(history.exit_reason)? "" : ": "),
pcmk__s(history.exit_reason, ""), rsc->id,
pcmk__node_name(node), last_change_s, history.id);
free(last_change_s);
update_resource_state(&history, history.expected_exit_status,
*last_failure, on_fail);
crm_xml_add(xml_op, PCMK_XA_UNAME, node->details->uname);
pcmk__set_rsc_flags(rsc, pcmk_rsc_ignore_failure);
record_failed_op(&history);
if ((failure_strategy == pcmk_on_fail_restart_container)
&& cmp_on_fail(*on_fail, pcmk_on_fail_restart) <= 0) {
*on_fail = failure_strategy;
}
} else {
unpack_rsc_op_failure(&history, failure_strategy, fail_role,
last_failure, on_fail);
if (history.execution_status == PCMK_EXEC_ERROR_HARD) {
uint8_t log_level = LOG_ERR;
if (history.exit_status == PCMK_OCF_NOT_INSTALLED) {
log_level = LOG_NOTICE;
}
do_crm_log(log_level,
"Preventing %s from restarting on %s because "
"of hard failure (%s%s%s) " CRM_XS " %s",
parent->id, pcmk__node_name(node),
services_ocf_exitcode_str(history.exit_status),
(pcmk__str_empty(history.exit_reason)? "" : ": "),
pcmk__s(history.exit_reason, ""), history.id);
resource_location(parent, node, -PCMK_SCORE_INFINITY,
"hard-error", rsc->cluster);
} else if (history.execution_status == PCMK_EXEC_ERROR_FATAL) {
pcmk__sched_err("Preventing %s from restarting anywhere because "
"of fatal failure (%s%s%s) " CRM_XS " %s",
parent->id,
services_ocf_exitcode_str(history.exit_status),
(pcmk__str_empty(history.exit_reason)? "" : ": "),
pcmk__s(history.exit_reason, ""), history.id);
resource_location(parent, NULL, -PCMK_SCORE_INFINITY,
"fatal-error", rsc->cluster);
}
}
done:
pcmk__rsc_trace(rsc, "%s role on %s after %s is %s (next %s)",
rsc->id, pcmk__node_name(node), history.id,
pcmk_role_text(rsc->role),
pcmk_role_text(rsc->next_role));
}
static void
add_node_attrs(const xmlNode *xml_obj, pcmk_node_t *node, bool overwrite,
pcmk_scheduler_t *scheduler)
{
const char *cluster_name = NULL;
pe_rule_eval_data_t rule_data = {
.node_hash = NULL,
.now = scheduler->now,
.match_data = NULL,
.rsc_data = NULL,
.op_data = NULL
};
pcmk__insert_dup(node->details->attrs,
CRM_ATTR_UNAME, node->details->uname);
pcmk__insert_dup(node->details->attrs, CRM_ATTR_ID, node->details->id);
if (pcmk__str_eq(node->details->id, scheduler->dc_uuid, pcmk__str_casei)) {
scheduler->dc_node = node;
node->details->is_dc = TRUE;
pcmk__insert_dup(node->details->attrs,
CRM_ATTR_IS_DC, PCMK_VALUE_TRUE);
} else {
pcmk__insert_dup(node->details->attrs,
CRM_ATTR_IS_DC, PCMK_VALUE_FALSE);
}
cluster_name = g_hash_table_lookup(scheduler->config_hash,
PCMK_OPT_CLUSTER_NAME);
if (cluster_name) {
pcmk__insert_dup(node->details->attrs, CRM_ATTR_CLUSTER_NAME,
cluster_name);
}
pe__unpack_dataset_nvpairs(xml_obj, PCMK_XE_INSTANCE_ATTRIBUTES, &rule_data,
node->details->attrs, NULL, overwrite,
scheduler);
pe__unpack_dataset_nvpairs(xml_obj, PCMK_XE_UTILIZATION, &rule_data,
node->details->utilization, NULL,
FALSE, scheduler);
if (pcmk__node_attr(node, CRM_ATTR_SITE_NAME, NULL,
pcmk__rsc_node_current) == NULL) {
const char *site_name = pcmk__node_attr(node, "site-name", NULL,
pcmk__rsc_node_current);
if (site_name) {
pcmk__insert_dup(node->details->attrs,
CRM_ATTR_SITE_NAME, site_name);
} else if (cluster_name) {
/* Default to cluster-name if unset */
pcmk__insert_dup(node->details->attrs,
CRM_ATTR_SITE_NAME, cluster_name);
}
}
}
static GList *
extract_operations(const char *node, const char *rsc, xmlNode * rsc_entry, gboolean active_filter)
{
int counter = -1;
int stop_index = -1;
int start_index = -1;
xmlNode *rsc_op = NULL;
GList *gIter = NULL;
GList *op_list = NULL;
GList *sorted_op_list = NULL;
/* extract operations */
op_list = NULL;
sorted_op_list = NULL;
for (rsc_op = pcmk__xe_first_child(rsc_entry, NULL, NULL, NULL);
rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op)) {
if (pcmk__xe_is(rsc_op, PCMK__XE_LRM_RSC_OP)) {
crm_xml_add(rsc_op, PCMK_XA_RESOURCE, rsc);
crm_xml_add(rsc_op, PCMK_XA_UNAME, node);
op_list = g_list_prepend(op_list, rsc_op);
}
}
if (op_list == NULL) {
/* if there are no operations, there is nothing to do */
return NULL;
}
sorted_op_list = g_list_sort(op_list, sort_op_by_callid);
/* create active recurring operations as optional */
if (active_filter == FALSE) {
return sorted_op_list;
}
op_list = NULL;
calculate_active_ops(sorted_op_list, &start_index, &stop_index);
for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) {
xmlNode *rsc_op = (xmlNode *) gIter->data;
counter++;
if (start_index < stop_index) {
crm_trace("Skipping %s: not active", pcmk__xe_id(rsc_entry));
break;
} else if (counter < start_index) {
crm_trace("Skipping %s: old", pcmk__xe_id(rsc_op));
continue;
}
op_list = g_list_append(op_list, rsc_op);
}
g_list_free(sorted_op_list);
return op_list;
}
GList *
find_operations(const char *rsc, const char *node, gboolean active_filter,
pcmk_scheduler_t *scheduler)
{
GList *output = NULL;
GList *intermediate = NULL;
xmlNode *tmp = NULL;
xmlNode *status = pcmk__xe_first_child(scheduler->input, PCMK_XE_STATUS,
NULL, NULL);
pcmk_node_t *this_node = NULL;
xmlNode *node_state = NULL;
CRM_CHECK(status != NULL, return NULL);
for (node_state = pcmk__xe_first_child(status, NULL, NULL, NULL);
node_state != NULL; node_state = pcmk__xe_next(node_state)) {
if (pcmk__xe_is(node_state, PCMK__XE_NODE_STATE)) {
const char *uname = crm_element_value(node_state, PCMK_XA_UNAME);
if (node != NULL && !pcmk__str_eq(uname, node, pcmk__str_casei)) {
continue;
}
this_node = pe_find_node(scheduler->nodes, uname);
if(this_node == NULL) {
CRM_LOG_ASSERT(this_node != NULL);
continue;
} else if (pcmk__is_pacemaker_remote_node(this_node)) {
determine_remote_online_status(scheduler, this_node);
} else {
determine_online_status(node_state, this_node, scheduler);
}
if (this_node->details->online
|| pcmk_is_set(scheduler->flags, pcmk_sched_fencing_enabled)) {
/* offline nodes run no resources...
* unless stonith is enabled in which case we need to
* make sure rsc start events happen after the stonith
*/
xmlNode *lrm_rsc = NULL;
tmp = pcmk__xe_first_child(node_state, PCMK__XE_LRM, NULL,
NULL);
tmp = pcmk__xe_first_child(tmp, PCMK__XE_LRM_RESOURCES, NULL,
NULL);
for (lrm_rsc = pcmk__xe_first_child(tmp, NULL, NULL, NULL);
lrm_rsc != NULL; lrm_rsc = pcmk__xe_next(lrm_rsc)) {
if (pcmk__xe_is(lrm_rsc, PCMK__XE_LRM_RESOURCE)) {
const char *rsc_id = crm_element_value(lrm_rsc,
PCMK_XA_ID);
if (rsc != NULL && !pcmk__str_eq(rsc_id, rsc, pcmk__str_casei)) {
continue;
}
intermediate = extract_operations(uname, rsc_id, lrm_rsc, active_filter);
output = g_list_concat(output, intermediate);
}
}
}
}
}
return output;
}
diff --git a/xml/Makefile.am b/xml/Makefile.am
index 0120e67945..2afffa0c7f 100644
--- a/xml/Makefile.am
+++ b/xml/Makefile.am
@@ -1,311 +1,315 @@
#
# Copyright 2004-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
include $(top_srcdir)/mk/common.mk
noarch_pkgconfig_DATA = $(builddir)/pacemaker-schemas.pc
# Pacemaker has 3 schemas: the CIB schema, the API schema (for command-line
# tool XML output), and a legacy schema for crm_mon --as-xml.
#
# See README.md for details on updating CIB schema files (API is similar)
# The CIB and crm_mon schemas are installed directly in CRM_SCHEMA_DIRECTORY
# for historical reasons, while the API schema is installed in a subdirectory.
APIdir = $(CRM_SCHEMA_DIRECTORY)/api
CIBdir = $(CRM_SCHEMA_DIRECTORY)
MONdir = $(CRM_SCHEMA_DIRECTORY)
basexsltdir = $(CRM_SCHEMA_DIRECTORY)/base
dist_basexslt_DATA = $(srcdir)/base/access-render-2.xsl
# Extract a sorted list of available numeric schema versions
# from filenames like NAME-MAJOR[.MINOR][.MINOR-MINOR].rng
numeric_versions = $(shell ls -1 $(1) \
| sed -n -e 's/^.*-\([0-9][0-9.]*\).rng$$/\1/p' \
| sort -u -t. -k 1,1n -k 2,2n -k 3,3n)
# @COMPAT: pacemaker-next is deprecated since 2.1.5
version_pairs = $(join \
$(1),$(addprefix \
-,$(wordlist \
2,$(words $(1)),$(1) \
) next \
) \
)
version_pairs_last = $(wordlist \
$(words \
$(wordlist \
2,$(1),$(2) \
) \
),$(1),$(2) \
)
# NOTE: All files in API_request_base, CIB_cfg_base, API_base, and CIB_base
# need to start with a unique prefix. These variables all get iterated over
# and globbed, and two files starting with the same prefix will cause
# problems.
# Names of API schemas that form the choices for pacemaker-result content
API_request_base = command-output \
crm_attribute \
crm_error \
crm_mon \
crm_node \
crm_resource \
crm_rule \
crm_shadow \
crm_simulate \
crm_ticket \
crmadmin \
digests \
pacemakerd \
stonith_admin \
version
# Names of CIB schemas that form the choices for cib/configuration content
CIB_cfg_base = options \
nodes \
resources \
constraints \
fencing \
acls \
tags \
alerts
# Names of all schemas (including top level and those included by others)
API_base = $(API_request_base) \
any-element \
failure \
fence-event \
generic-list \
instruction \
item \
node-attrs \
node-history \
nodes \
ocf-ra \
options \
patchset \
resources \
status \
subprocess-output \
ticket
CIB_base = cib \
$(CIB_cfg_base) \
status \
score \
rule \
nvset
# Static schema files and transforms (only CIB has transforms)
#
# This is more complicated than it should be due to the need to support
# VPATH builds and "make distcheck". We need the absolute paths for reliable
# substitution back and forth, and relative paths for distributed files.
API_abs_files = $(foreach base,$(API_base),$(wildcard $(abs_srcdir)/api/$(base)-*.rng))
CIB_abs_files = $(foreach base,$(CIB_base),$(wildcard $(abs_srcdir)/$(base).rng $(abs_srcdir)/$(base)-*.rng))
CIB_abs_xsl = $(abs_srcdir)/upgrade-1.3.xsl \
$(abs_srcdir)/upgrade-2.10.xsl \
$(wildcard $(abs_srcdir)/upgrade-*enter.xsl) \
$(wildcard $(abs_srcdir)/upgrade-*leave.xsl)
MON_abs_files = $(abs_srcdir)/crm_mon.rng
API_files = $(foreach base,$(API_base),$(wildcard $(srcdir)/api/$(base)-*.rng))
CIB_files = $(foreach base,$(CIB_base),$(wildcard $(srcdir)/$(base).rng $(srcdir)/$(base)-*.rng))
CIB_xsl = $(srcdir)/upgrade-1.3.xsl \
$(srcdir)/upgrade-2.10.xsl \
$(wildcard $(srcdir)/upgrade-*enter.xsl) \
$(wildcard $(srcdir)/upgrade-*leave.xsl)
MON_files = $(srcdir)/crm_mon.rng
# Sorted lists of all numeric schema versions
API_numeric_versions = $(call numeric_versions,${API_files})
CIB_numeric_versions = $(call numeric_versions,${CIB_files})
MON_numeric_versions = $(call numeric_versions,$(wildcard $(srcdir)/api/crm_mon*.rng))
# The highest numeric schema version
API_max ?= $(lastword $(API_numeric_versions))
CIB_max ?= $(lastword $(CIB_numeric_versions))
MON_max ?= $(lastword $(MON_numeric_versions))
# Sorted lists of all schema versions (including "next")
# @COMPAT: pacemaker-next is deprecated since 2.1.5
API_versions = next $(API_numeric_versions)
CIB_versions = next $(CIB_numeric_versions)
# Build tree locations of static schema files and transforms (for VPATH builds)
API_build_copies = $(foreach f,$(API_abs_files),$(subst $(abs_srcdir),$(abs_builddir),$(f)))
CIB_build_copies = $(foreach f,$(CIB_abs_files) $(CIB_abs_xsl),$(subst $(abs_srcdir),$(abs_builddir),$(f)))
MON_build_copies = $(foreach f,$(MON_abs_files),$(subst $(abs_srcdir),$(abs_builddir),$(f)))
# Dynamically generated schema files
API_generated = api/api-result.rng $(foreach base,$(API_versions),api/api-result-$(base).rng)
CIB_generated = pacemaker.rng \
$(foreach base,$(CIB_versions),pacemaker-$(base).rng) \
versions.rng
MON_generated = crm_mon.rng
CIB_version_pairs = $(call version_pairs,${CIB_numeric_versions})
CIB_version_pairs_cnt = $(words ${CIB_version_pairs})
CIB_version_pairs_last = $(call version_pairs_last,${CIB_version_pairs_cnt},${CIB_version_pairs})
dist_API_DATA = $(API_files)
dist_CIB_DATA = $(CIB_files) \
$(CIB_xsl)
nodist_API_DATA = $(API_generated)
nodist_CIB_DATA = $(CIB_generated)
nodist_MON_DATA = $(MON_generated)
EXTRA_DIST = README.md \
best-match.sh \
cibtr-2.rng \
context-of.xsl \
ocf-meta2man.xsl \
regression.sh \
upgrade-2.10-roundtrip.xsl \
upgrade-detail.xsl \
xslt_cibtr-2.rng \
assets \
test-2 \
test-2-enter \
test-2-leave \
test-2-roundtrip
.PHONY: cib-versions
cib-versions:
@echo "Max: $(CIB_max)"
@echo "Available: $(CIB_versions)"
.PHONY: api-versions
api-versions:
@echo "Max: $(API_max)"
@echo "Available: $(API_versions)"
# Dynamically generated top-level API schema
api/api-result.rng: api/api-result-$(API_max).rng
$(AM_V_at)$(MKDIR_P) api # might not exist in VPATH build
$(AM_V_SCHEMA)cp $(top_builddir)/xml/$< $@
api/api-result-%.rng: $(API_build_copies) best-match.sh Makefile.am
$(AM_V_at)echo '' > $@
$(AM_V_at)echo '' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)for rng in $(API_request_base); do $(srcdir)/best-match.sh api/$$rng $(*) $(@) " " || :; done
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)$(srcdir)/best-match.sh api/status $(*) $(@) " " || :
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_SCHEMA)echo '' >> $@
crm_mon.rng: api/crm_mon-$(MON_max).rng
$(AM_V_at)echo '' > $@
$(AM_V_at)echo '> $@
$(AM_V_at)echo ' datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_SCHEMA)echo '' >> $@
# Dynamically generated top-level CIB schema
pacemaker.rng: pacemaker-$(CIB_max).rng
$(AM_V_SCHEMA)cp $(top_builddir)/xml/$< $@
pacemaker-%.rng: $(CIB_build_copies) best-match.sh Makefile.am
$(AM_V_at)echo '' > $@
$(AM_V_at)echo '' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)$(srcdir)/best-match.sh cib $(*) $(@) " "
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)for rng in $(CIB_cfg_base); do $(srcdir)/best-match.sh $$rng $(*) $(@) " " || :; done
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)$(srcdir)/best-match.sh status $(*) $(@) " "
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_SCHEMA)echo '' >> $@
# Dynamically generated CIB schema listing all pacemaker versions
+#
+# @COMPAT none, pacemaker-0.6, pacemaker-0.7, pacemaker-1.1, and
+# transitional-0.6 are deprecated since 2.1.8, as is validate-with being
+# optional
versions.rng: pacemaker-$(CIB_max).rng Makefile.am
$(AM_V_at)echo '' > $@
$(AM_V_at)echo '' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' none' >> $@
$(AM_V_at)echo ' pacemaker-0.6' >> $@
$(AM_V_at)echo ' transitional-0.6' >> $@
$(AM_V_at)echo ' pacemaker-0.7' >> $@
$(AM_V_at)echo ' pacemaker-1.1' >> $@
$(AM_V_at)for rng in $(CIB_versions); do echo " pacemaker-$$rng" >> $@; done
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_SCHEMA)echo '' >> $@
.PHONY: diff
diff: best-match.sh
@echo "# Comparing changes in + since $(CIB_max)"
@./version-diff.sh ${CIB_version_pairs_last}
.PHONY: fulldiff
fulldiff: best-match.sh
@echo "# Comparing all changes across all the subsequent increments"
@./version-diff.sh ${CIB_version_pairs}
CLEANFILES = $(API_generated) \
$(CIB_generated) \
$(MON_generated)
# Remove pacemaker schema files generated by *any* source version. This allows
# "make -C xml clean" to have the desired effect when checking out an earlier
# revision in a source tree.
.PHONY: clean-local
clean-local:
if [ "x$(srcdir)" != "x$(builddir)" ]; then \
rm -f $(API_build_copies) $(CIB_build_copies) $(MON_build_copies); \
fi
rm -f $(builddir)/pacemaker-[0-9]*.[0-9]*.rng
# Enable ability to use $@ in prerequisite
.SECONDEXPANSION:
# For VPATH builds, copy the static schema files into the build tree
$(API_build_copies) $(CIB_build_copies) $(MON_build_copies): $$(subst $(abs_builddir),$(srcdir),$$(@))
$(AM_V_GEN)if [ "x$(srcdir)" != "x$(builddir)" ]; then \
$(MKDIR_P) "$(dir $(@))"; \
cp "$(<)" "$(@)"; \
fi