diff --git a/cts/cli/regression.validity.exp b/cts/cli/regression.validity.exp index 763f577296..6a56f9d212 100644 --- a/cts/cli/regression.validity.exp +++ b/cts/cli/regression.validity.exp @@ -1,470 +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) =#=#=#= - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 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 pacemaker-3.10 +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) =#=#=#= - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 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 unknown to pacemaker-3.10 +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) =#=#=#= - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10 - 11 - 12 - 13 - 14 - 15 - 16 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) 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) 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/daemons/based/based_io.c b/daemons/based/based_io.c index 0f8f03fd33..f22582de40 100644 --- a/daemons/based/based_io.c +++ b/daemons/based/based_io.c @@ -1,477 +1,477 @@ /* * 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 (validate_xml(root, NULL, TRUE) == FALSE) { + 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; } 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/daemons/based/based_messages.c b/daemons/based/based_messages.c index 58de8abd59..9c17938960 100644 --- a/daemons/based/based_messages.c +++ b/daemons/based/based_messages.c @@ -1,539 +1,539 @@ /* * 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 /* Maximum number of diffs to ignore while waiting for a resync */ #define MAX_DIFF_RETRY 5 bool based_is_primary = false; xmlNode *the_cib = NULL; int cib_process_shutdown_req(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { const char *host = crm_element_value(req, PCMK__XA_SRC); *answer = NULL; if (crm_element_value(req, PCMK__XA_CIB_ISREPLYTO) == NULL) { crm_info("Peer %s is requesting to shut down", host); return pcmk_ok; } if (cib_shutdown_flag == FALSE) { crm_err("Peer %s mistakenly thinks we wanted to shut down", host); return -EINVAL; } crm_info("Peer %s has acknowledged our shutdown request", host); terminate_cib(__func__, 0); return pcmk_ok; } // @COMPAT: Remove when PCMK__CIB_REQUEST_NOOP is removed int cib_process_noop(const char *op, int options, const char *section, xmlNode *req, xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, xmlNode **answer) { crm_trace("Processing \"%s\" event", op); *answer = NULL; return pcmk_ok; } int cib_process_readwrite(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { int result = pcmk_ok; crm_trace("Processing \"%s\" event", op); if (pcmk__str_eq(op, PCMK__CIB_REQUEST_IS_PRIMARY, pcmk__str_none)) { if (based_is_primary) { result = pcmk_ok; } else { result = -EPERM; } return result; } if (pcmk__str_eq(op, PCMK__CIB_REQUEST_PRIMARY, pcmk__str_none)) { if (!based_is_primary) { crm_info("We are now in R/W mode"); based_is_primary = true; } else { crm_debug("We are still in R/W mode"); } } else if (based_is_primary) { crm_info("We are now in R/O mode"); based_is_primary = false; } return result; } /* Set to 1 when a sync is requested, incremented when a diff is ignored, * reset to 0 when a sync is received */ static int sync_in_progress = 0; void send_sync_request(const char *host) { xmlNode *sync_me = pcmk__xe_create(NULL, "sync-me"); crm_node_t *peer = NULL; crm_info("Requesting re-sync from %s", (host? host : "all peers")); sync_in_progress = 1; crm_xml_add(sync_me, PCMK__XA_T, PCMK__VALUE_CIB); crm_xml_add(sync_me, PCMK__XA_CIB_OP, PCMK__CIB_REQUEST_SYNC_TO_ONE); crm_xml_add(sync_me, PCMK__XA_CIB_DELEGATED_FROM, stand_alone? "localhost" : crm_cluster->uname); if (host != NULL) { peer = pcmk__get_node(0, host, NULL, pcmk__node_search_cluster); } send_cluster_message(peer, crm_msg_cib, sync_me, FALSE); free_xml(sync_me); } int cib_process_ping(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { const char *host = crm_element_value(req, PCMK__XA_SRC); const char *seq = crm_element_value(req, PCMK__XA_CIB_PING_ID); char *digest = calculate_xml_versioned_digest(the_cib, FALSE, TRUE, CRM_FEATURE_SET); xmlNode *wrapper = NULL; crm_trace("Processing \"%s\" event %s from %s", op, seq, host); *answer = pcmk__xe_create(NULL, PCMK__XE_PING_RESPONSE); crm_xml_add(*answer, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); crm_xml_add(*answer, PCMK__XA_DIGEST, digest); crm_xml_add(*answer, PCMK__XA_CIB_PING_ID, seq); wrapper = pcmk__xe_create(*answer, PCMK__XE_CIB_CALLDATA); if (the_cib != NULL) { pcmk__if_tracing( { /* Append additional detail so the receiver can log the * differences */ pcmk__xml_copy(wrapper, the_cib); }, { // Always include at least the version details const char *name = (const char *) the_cib->name; xmlNode *shallow = pcmk__xe_create(wrapper, name); copy_in_properties(shallow, the_cib); } ); } crm_info("Reporting our current digest to %s: %s for %s.%s.%s", host, digest, crm_element_value(existing_cib, PCMK_XA_ADMIN_EPOCH), crm_element_value(existing_cib, PCMK_XA_EPOCH), crm_element_value(existing_cib, PCMK_XA_NUM_UPDATES)); free(digest); return pcmk_ok; } int cib_process_sync(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { return sync_our_cib(req, TRUE); } int cib_process_upgrade_server(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { int rc = pcmk_ok; *answer = NULL; if (crm_element_value(req, PCMK__XA_CIB_SCHEMA_MAX) != NULL) { /* The originator of an upgrade request sends it to the DC, without * PCMK__XA_CIB_SCHEMA_MAX. If an upgrade is needed, the DC * re-broadcasts the request with PCMK__XA_CIB_SCHEMA_MAX, and each node * performs the upgrade (and notifies its local clients) here. */ return cib_process_upgrade( op, options, section, req, input, existing_cib, result_cib, answer); } else { xmlNode *scratch = pcmk__xml_copy(NULL, existing_cib); const char *host = crm_element_value(req, PCMK__XA_SRC); const char *original_schema = NULL; const char *new_schema = NULL; const char *client_id = crm_element_value(req, PCMK__XA_CIB_CLIENTID); const char *call_opts = crm_element_value(req, PCMK__XA_CIB_CALLOPT); const char *call_id = crm_element_value(req, PCMK__XA_CIB_CALLID); crm_trace("Processing \"%s\" event", op); original_schema = crm_element_value(existing_cib, PCMK_XA_VALIDATE_WITH); rc = pcmk__update_schema(&scratch, NULL, true, true); rc = pcmk_rc2legacy(rc); new_schema = crm_element_value(scratch, PCMK_XA_VALIDATE_WITH); if (pcmk__cmp_schemas_by_name(new_schema, original_schema) > 0) { xmlNode *up = pcmk__xe_create(NULL, __func__); rc = pcmk_ok; crm_notice("Upgrade request from %s verified", host); crm_xml_add(up, PCMK__XA_T, PCMK__VALUE_CIB); crm_xml_add(up, PCMK__XA_CIB_OP, PCMK__CIB_REQUEST_UPGRADE); crm_xml_add(up, PCMK__XA_CIB_SCHEMA_MAX, new_schema); crm_xml_add(up, PCMK__XA_CIB_DELEGATED_FROM, host); crm_xml_add(up, PCMK__XA_CIB_CLIENTID, client_id); crm_xml_add(up, PCMK__XA_CIB_CALLOPT, call_opts); crm_xml_add(up, PCMK__XA_CIB_CALLID, call_id); if (cib_legacy_mode() && based_is_primary) { rc = cib_process_upgrade( op, options, section, up, input, existing_cib, result_cib, answer); } else { send_cluster_message(NULL, crm_msg_cib, up, FALSE); } free_xml(up); } else if(rc == pcmk_ok) { rc = -pcmk_err_schema_unchanged; } if (rc != pcmk_ok) { // Notify originating peer so it can notify its local clients crm_node_t *origin = NULL; origin = pcmk__search_node_caches(0, host, pcmk__node_search_cluster); crm_info("Rejecting upgrade request from %s: %s " CRM_XS " rc=%d peer=%s", host, pcmk_strerror(rc), rc, (origin? origin->uname : "lost")); if (origin) { xmlNode *up = pcmk__xe_create(NULL, __func__); crm_xml_add(up, PCMK__XA_T, PCMK__VALUE_CIB); crm_xml_add(up, PCMK__XA_CIB_OP, PCMK__CIB_REQUEST_UPGRADE); crm_xml_add(up, PCMK__XA_CIB_DELEGATED_FROM, host); crm_xml_add(up, PCMK__XA_CIB_ISREPLYTO, host); crm_xml_add(up, PCMK__XA_CIB_CLIENTID, client_id); crm_xml_add(up, PCMK__XA_CIB_CALLOPT, call_opts); crm_xml_add(up, PCMK__XA_CIB_CALLID, call_id); crm_xml_add_int(up, PCMK__XA_CIB_UPGRADE_RC, rc); if (send_cluster_message(origin, crm_msg_cib, up, TRUE) == FALSE) { crm_warn("Could not send CIB upgrade result to %s", host); } free_xml(up); } } free_xml(scratch); } return rc; } int cib_process_sync_one(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { return sync_our_cib(req, FALSE); } int cib_server_process_diff(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { int rc = pcmk_ok; if (sync_in_progress > MAX_DIFF_RETRY) { /* Don't ignore diffs forever; the last request may have been lost. * If the diff fails, we'll ask for another full resync. */ sync_in_progress = 0; } // The primary instance should never ignore a diff if (sync_in_progress && !based_is_primary) { int diff_add_updates = 0; int diff_add_epoch = 0; int diff_add_admin_epoch = 0; int diff_del_updates = 0; int diff_del_epoch = 0; int diff_del_admin_epoch = 0; cib_diff_version_details(input, &diff_add_admin_epoch, &diff_add_epoch, &diff_add_updates, &diff_del_admin_epoch, &diff_del_epoch, &diff_del_updates); sync_in_progress++; crm_notice("Not applying diff %d.%d.%d -> %d.%d.%d (sync in progress)", diff_del_admin_epoch, diff_del_epoch, diff_del_updates, diff_add_admin_epoch, diff_add_epoch, diff_add_updates); return -pcmk_err_diff_resync; } rc = cib_process_diff(op, options, section, req, input, existing_cib, result_cib, answer); crm_trace("result: %s (%d), %s", pcmk_strerror(rc), rc, (based_is_primary? "primary": "secondary")); if ((rc == -pcmk_err_diff_resync) && !based_is_primary) { free_xml(*result_cib); *result_cib = NULL; send_sync_request(NULL); } else if (rc == -pcmk_err_diff_resync) { rc = -pcmk_err_diff_failed; if (options & cib_force_diff) { crm_warn("Not requesting full refresh in R/W mode"); } } else if ((rc != pcmk_ok) && !based_is_primary && cib_legacy_mode()) { crm_warn("Requesting full CIB refresh because update failed: %s" CRM_XS " rc=%d", pcmk_strerror(rc), rc); pcmk__log_xml_patchset(LOG_INFO, input); free_xml(*result_cib); *result_cib = NULL; send_sync_request(NULL); } return rc; } int cib_process_replace_svr(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { int rc = cib_process_replace(op, options, section, req, input, existing_cib, result_cib, answer); if ((rc == pcmk_ok) && pcmk__xe_is(input, PCMK_XE_CIB)) { sync_in_progress = 0; } return rc; } // @COMPAT: Remove when PCMK__CIB_REQUEST_ABS_DELETE is removed int cib_process_delete_absolute(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) { return -EINVAL; } static xmlNode * cib_msg_copy(xmlNode *msg) { static const char *field_list[] = { PCMK__XA_T, PCMK__XA_CIB_CLIENTID, PCMK__XA_CIB_CALLOPT, PCMK__XA_CIB_CALLID, PCMK__XA_CIB_OP, PCMK__XA_CIB_ISREPLYTO, PCMK__XA_CIB_SECTION, PCMK__XA_CIB_HOST, PCMK__XA_CIB_RC, PCMK__XA_CIB_DELEGATED_FROM, PCMK__XA_CIB_OBJECT, PCMK__XA_CIB_OBJECT_TYPE, PCMK__XA_CIB_UPDATE, PCMK__XA_CIB_CLIENTNAME, PCMK__XA_CIB_USER, PCMK__XA_CIB_NOTIFY_TYPE, PCMK__XA_CIB_NOTIFY_ACTIVATE, }; xmlNode *copy = pcmk__xe_create(NULL, PCMK__XE_COPY); for (int lpc = 0; lpc < PCMK__NELEM(field_list); lpc++) { const char *field = field_list[lpc]; const char *value = crm_element_value(msg, field); if (value != NULL) { crm_xml_add(copy, field, value); } } return copy; } int sync_our_cib(xmlNode * request, gboolean all) { int result = pcmk_ok; char *digest = NULL; const char *host = crm_element_value(request, PCMK__XA_SRC); const char *op = crm_element_value(request, PCMK__XA_CIB_OP); crm_node_t *peer = NULL; xmlNode *replace_request = NULL; xmlNode *wrapper = NULL; CRM_CHECK(the_cib != NULL, return -EINVAL); CRM_CHECK(all || (host != NULL), return -EINVAL); crm_debug("Syncing CIB to %s", all ? "all peers" : host); replace_request = cib_msg_copy(request); if (host != NULL) { crm_xml_add(replace_request, PCMK__XA_CIB_ISREPLYTO, host); } if (all) { pcmk__xe_remove_attr(replace_request, PCMK__XA_CIB_HOST); } crm_xml_add(replace_request, PCMK__XA_CIB_OP, PCMK__CIB_REQUEST_REPLACE); // @TODO Keep for tracing, or drop? crm_xml_add(replace_request, PCMK__XA_ORIGINAL_CIB_OP, op); pcmk__xe_set_bool_attr(replace_request, PCMK__XA_CIB_UPDATE, true); crm_xml_add(replace_request, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); digest = calculate_xml_versioned_digest(the_cib, FALSE, TRUE, CRM_FEATURE_SET); crm_xml_add(replace_request, PCMK__XA_DIGEST, digest); wrapper = pcmk__xe_create(replace_request, PCMK__XE_CIB_CALLDATA); pcmk__xml_copy(wrapper, the_cib); if (!all) { peer = pcmk__get_node(0, host, NULL, pcmk__node_search_cluster); } if (!send_cluster_message(peer, crm_msg_cib, replace_request, FALSE)) { result = -ENOTCONN; } free_xml(replace_request); free(digest); return result; } int cib_process_commit_transaction(const char *op, int options, const char *section, xmlNode *req, xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, xmlNode **answer) { /* On success, our caller will activate *result_cib locally, trigger a * replace notification if appropriate, and sync *result_cib to all nodes. * On failure, our caller will free *result_cib. */ int rc = pcmk_rc_ok; const char *client_id = crm_element_value(req, PCMK__XA_CIB_CLIENTID); const char *origin = crm_element_value(req, PCMK__XA_SRC); pcmk__client_t *client = pcmk__find_client_by_id(client_id); rc = based_commit_transaction(input, client, origin, result_cib); if (rc != pcmk_rc_ok) { char *source = based_transaction_source_str(client, origin); crm_err("Could not commit transaction for %s: %s", source, pcmk_rc_str(rc)); free(source); } return pcmk_rc2legacy(rc); } int cib_process_schemas(const char *op, int options, const char *section, xmlNode *req, xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, xmlNode **answer) { xmlNode *wrapper = NULL; xmlNode *data = NULL; const char *after_ver = NULL; GList *schemas = NULL; GList *already_included = NULL; *answer = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS); wrapper = pcmk__xe_first_child(req, PCMK__XE_CIB_CALLDATA, NULL, NULL); data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); if (data == NULL) { crm_warn("No data specified in request"); return -EPROTO; } after_ver = crm_element_value(data, PCMK_XA_VERSION); if (after_ver == NULL) { crm_warn("No version specified in request"); return -EPROTO; } /* The client requested all schemas after the latest one we know about, which * means the client is fully up-to-date. Return a properly formatted reply * with no schemas. */ - if (pcmk__str_eq(after_ver, xml_latest_schema(), pcmk__str_none)) { + if (pcmk__str_eq(after_ver, pcmk__highest_schema_name(), pcmk__str_none)) { return pcmk_ok; } schemas = pcmk__schema_files_later_than(after_ver); for (GList *iter = schemas; iter != NULL; iter = iter->next) { pcmk__build_schema_xml_node(*answer, iter->data, &already_included); } g_list_free_full(schemas, free); g_list_free_full(already_included, free); return pcmk_ok; } diff --git a/daemons/execd/cts-exec-helper.c b/daemons/execd/cts-exec-helper.c index 54756cc395..a81b15fd84 100644 --- a/daemons/execd/cts-exec-helper.c +++ b/daemons/execd/cts-exec-helper.c @@ -1,626 +1,626 @@ /* * Copyright 2012-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 #define SUMMARY "cts-exec-helper - inject commands into the Pacemaker executor and watch for events" static int exec_call_id = 0; static gboolean start_test(gpointer user_data); static void try_connect(void); static char *key = NULL; static char *val = NULL; static struct { int verbose; int quiet; guint interval_ms; int timeout; int start_delay; int cancel_call_id; gboolean no_wait; gboolean is_running; gboolean no_connect; int exec_call_opts; const char *api_call; const char *rsc_id; const char *provider; const char *class; const char *type; const char *action; const char *listen; gboolean use_tls; lrmd_key_value_t *params; } options; static gboolean interval_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { return pcmk_parse_interval_spec(optarg, &options.interval_ms) == pcmk_rc_ok; } static gboolean notify_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "--notify-orig", "-n", NULL)) { options.exec_call_opts = lrmd_opt_notify_orig_only; } else if (pcmk__str_any_of(option_name, "--notify-changes", "-o", NULL)) { options.exec_call_opts = lrmd_opt_notify_changes_only; } return TRUE; } static gboolean param_key_val_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "--param-key", "-k", NULL)) { pcmk__str_update(&key, optarg); } else if (pcmk__str_any_of(option_name, "--param-val", "-v", NULL)) { pcmk__str_update(&val, optarg); } if (key != NULL && val != NULL) { options.params = lrmd_key_value_add(options.params, key, val); pcmk__str_update(&key, NULL); pcmk__str_update(&val, NULL); } return TRUE; } static GOptionEntry basic_entries[] = { { "api-call", 'c', 0, G_OPTION_ARG_STRING, &options.api_call, "Directly relates to executor API functions", NULL }, { "is-running", 'R', 0, G_OPTION_ARG_NONE, &options.is_running, "Determine if a resource is registered and running", NULL }, { "listen", 'l', 0, G_OPTION_ARG_STRING, &options.listen, "Listen for a specific event string", NULL }, { "no-wait", 'w', 0, G_OPTION_ARG_NONE, &options.no_wait, "Make api call and do not wait for result", NULL }, { "notify-changes", 'o', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, notify_cb, "Only notify client changes to recurring operations", NULL }, { "notify-orig", 'n', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, notify_cb, "Only notify this client of the results of an API action", NULL }, { "tls", 'S', 0, G_OPTION_ARG_NONE, &options.use_tls, "Use TLS backend for local connection", NULL }, { NULL } }; static GOptionEntry api_call_entries[] = { { "action", 'a', 0, G_OPTION_ARG_STRING, &options.action, NULL, NULL }, { "cancel-call-id", 'x', 0, G_OPTION_ARG_INT, &options.cancel_call_id, NULL, NULL }, { "class", 'C', 0, G_OPTION_ARG_STRING, &options.class, NULL, NULL }, { "interval", 'i', 0, G_OPTION_ARG_CALLBACK, interval_cb, NULL, NULL }, { "param-key", 'k', 0, G_OPTION_ARG_CALLBACK, param_key_val_cb, NULL, NULL }, { "param-val", 'v', 0, G_OPTION_ARG_CALLBACK, param_key_val_cb, NULL, NULL }, { "provider", 'P', 0, G_OPTION_ARG_STRING, &options.provider, NULL, NULL }, { "rsc-id", 'r', 0, G_OPTION_ARG_STRING, &options.rsc_id, NULL, NULL }, { "start-delay", 's', 0, G_OPTION_ARG_INT, &options.start_delay, NULL, NULL }, { "timeout", 't', 0, G_OPTION_ARG_INT, &options.timeout, NULL, NULL }, { "type", 'T', 0, G_OPTION_ARG_STRING, &options.type, NULL, NULL }, { NULL } }; static GMainLoop *mainloop = NULL; static lrmd_t *lrmd_conn = NULL; static char event_buf_v0[1024]; static crm_exit_t test_exit(crm_exit_t exit_code) { lrmd_api_delete(lrmd_conn); return crm_exit(exit_code); } #define print_result(fmt, args...) \ if (!options.quiet) { \ printf(fmt "\n" , ##args); \ } #define report_event(event) \ snprintf(event_buf_v0, sizeof(event_buf_v0), "NEW_EVENT event_type:%s rsc_id:%s action:%s rc:%s op_status:%s", \ lrmd_event_type2str(event->type), \ event->rsc_id, \ event->op_type ? event->op_type : "none", \ services_ocf_exitcode_str(event->rc), \ pcmk_exec_status_str(event->op_status)); \ crm_info("%s", event_buf_v0); static void test_shutdown(int nsig) { lrmd_api_delete(lrmd_conn); lrmd_conn = NULL; } static void read_events(lrmd_event_data_t * event) { report_event(event); if (options.listen) { if (pcmk__str_eq(options.listen, event_buf_v0, pcmk__str_casei)) { print_result("LISTEN EVENT SUCCESSFUL"); test_exit(CRM_EX_OK); } } if (exec_call_id && (event->call_id == exec_call_id)) { if (event->op_status == 0 && event->rc == 0) { print_result("API-CALL SUCCESSFUL for 'exec'"); } else { print_result("API-CALL FAILURE for 'exec', rc:%d lrmd_op_status:%s", event->rc, pcmk_exec_status_str(event->op_status)); test_exit(CRM_EX_ERROR); } if (!options.listen) { test_exit(CRM_EX_OK); } } } static gboolean timeout_err(gpointer data) { print_result("LISTEN EVENT FAILURE - timeout occurred, never found"); test_exit(CRM_EX_TIMEOUT); return FALSE; } static void connection_events(lrmd_event_data_t * event) { int rc = event->connection_rc; if (event->type != lrmd_event_connect) { /* ignore */ return; } if (!rc) { crm_info("Executor client connection established"); start_test(NULL); return; } else { sleep(1); try_connect(); crm_notice("Executor client connection failed"); } } static void try_connect(void) { int tries = 10; static int num_tries = 0; int rc = 0; lrmd_conn->cmds->set_callback(lrmd_conn, connection_events); for (; num_tries < tries; num_tries++) { rc = lrmd_conn->cmds->connect_async(lrmd_conn, crm_system_name, 3000); if (!rc) { return; /* we'll hear back in async callback */ } sleep(1); } print_result("API CONNECTION FAILURE"); test_exit(CRM_EX_ERROR); } static gboolean start_test(gpointer user_data) { int rc = 0; if (!options.no_connect) { if (!lrmd_conn->cmds->is_connected(lrmd_conn)) { try_connect(); /* async connect -- this function will get called back into */ return 0; } } lrmd_conn->cmds->set_callback(lrmd_conn, read_events); if (options.timeout) { g_timeout_add(options.timeout, timeout_err, NULL); } if (!options.api_call) { return 0; } if (pcmk__str_eq(options.api_call, "exec", pcmk__str_casei)) { rc = lrmd_conn->cmds->exec(lrmd_conn, options.rsc_id, options.action, NULL, options.interval_ms, options.timeout, options.start_delay, options.exec_call_opts, options.params); if (rc > 0) { exec_call_id = rc; print_result("API-CALL 'exec' action pending, waiting on response"); } } else if (pcmk__str_eq(options.api_call, "register_rsc", pcmk__str_casei)) { rc = lrmd_conn->cmds->register_rsc(lrmd_conn, options.rsc_id, options.class, options.provider, options.type, 0); } else if (pcmk__str_eq(options.api_call, "get_rsc_info", pcmk__str_casei)) { lrmd_rsc_info_t *rsc_info; rsc_info = lrmd_conn->cmds->get_rsc_info(lrmd_conn, options.rsc_id, 0); if (rsc_info) { print_result("RSC_INFO: id:%s class:%s provider:%s type:%s", rsc_info->id, rsc_info->standard, (rsc_info->provider? rsc_info->provider : ""), rsc_info->type); lrmd_free_rsc_info(rsc_info); rc = pcmk_ok; } else { rc = -1; } } else if (pcmk__str_eq(options.api_call, "unregister_rsc", pcmk__str_casei)) { rc = lrmd_conn->cmds->unregister_rsc(lrmd_conn, options.rsc_id, 0); } else if (pcmk__str_eq(options.api_call, "cancel", pcmk__str_casei)) { rc = lrmd_conn->cmds->cancel(lrmd_conn, options.rsc_id, options.action, options.interval_ms); } else if (pcmk__str_eq(options.api_call, "metadata", pcmk__str_casei)) { char *output = NULL; rc = lrmd_conn->cmds->get_metadata(lrmd_conn, options.class, options.provider, options.type, &output, 0); if (rc == pcmk_ok) { print_result("%s", output); free(output); } } else if (pcmk__str_eq(options.api_call, "list_agents", pcmk__str_casei)) { lrmd_list_t *list = NULL; lrmd_list_t *iter = NULL; rc = lrmd_conn->cmds->list_agents(lrmd_conn, &list, options.class, options.provider); if (rc > 0) { print_result("%d agents found", rc); for (iter = list; iter != NULL; iter = iter->next) { print_result("%s", iter->val); } lrmd_list_freeall(list); rc = 0; } else { print_result("API_CALL FAILURE - no agents found"); rc = -1; } } else if (pcmk__str_eq(options.api_call, "list_ocf_providers", pcmk__str_casei)) { lrmd_list_t *list = NULL; lrmd_list_t *iter = NULL; rc = lrmd_conn->cmds->list_ocf_providers(lrmd_conn, options.type, &list); if (rc > 0) { print_result("%d providers found", rc); for (iter = list; iter != NULL; iter = iter->next) { print_result("%s", iter->val); } lrmd_list_freeall(list); rc = 0; } else { print_result("API_CALL FAILURE - no providers found"); rc = -1; } } else if (pcmk__str_eq(options.api_call, "list_standards", pcmk__str_casei)) { lrmd_list_t *list = NULL; lrmd_list_t *iter = NULL; rc = lrmd_conn->cmds->list_standards(lrmd_conn, &list); if (rc > 0) { print_result("%d standards found", rc); for (iter = list; iter != NULL; iter = iter->next) { print_result("%s", iter->val); } lrmd_list_freeall(list); rc = 0; } else { print_result("API_CALL FAILURE - no providers found"); rc = -1; } } else if (pcmk__str_eq(options.api_call, "get_recurring_ops", pcmk__str_casei)) { GList *op_list = NULL; GList *op_item = NULL; rc = lrmd_conn->cmds->get_recurring_ops(lrmd_conn, options.rsc_id, 0, 0, &op_list); for (op_item = op_list; op_item != NULL; op_item = op_item->next) { lrmd_op_info_t *op_info = op_item->data; print_result("RECURRING_OP: %s_%s_%s timeout=%sms", op_info->rsc_id, op_info->action, op_info->interval_ms_s, op_info->timeout_ms_s); lrmd_free_op_info(op_info); } g_list_free(op_list); } else if (options.api_call) { print_result("API-CALL FAILURE unknown action '%s'", options.action); test_exit(CRM_EX_ERROR); } if (rc < 0) { print_result("API-CALL FAILURE for '%s' api_rc:%d", options.api_call, rc); test_exit(CRM_EX_ERROR); } if (options.api_call && rc == pcmk_ok) { print_result("API-CALL SUCCESSFUL for '%s'", options.api_call); if (!options.listen) { test_exit(CRM_EX_OK); } } if (options.no_wait) { /* just make the call and exit regardless of anything else. */ test_exit(CRM_EX_OK); } return 0; } /*! * \internal * \brief Generate resource parameters from CIB if none explicitly given * * \return Standard Pacemaker return code */ static int generate_params(void) { int rc = pcmk_rc_ok; pcmk_scheduler_t *scheduler = NULL; xmlNode *cib_xml_copy = NULL; pcmk_resource_t *rsc = NULL; GHashTable *params = NULL; GHashTable *meta = NULL; GHashTableIter iter; char *key = NULL; char *value = NULL; if (options.params != NULL) { return pcmk_rc_ok; // User specified parameters explicitly } // Retrieve and update CIB rc = cib__signon_query(NULL, NULL, &cib_xml_copy); if (rc != pcmk_rc_ok) { return rc; } - if (!cli_config_update(&cib_xml_copy, NULL, FALSE)) { + if (!pcmk__update_configured_schema(&cib_xml_copy, false)) { crm_err("Could not update CIB"); return pcmk_rc_cib_corrupt; } // Calculate cluster status scheduler = pe_new_working_set(); if (scheduler == NULL) { crm_crit("Could not allocate scheduler data"); return ENOMEM; } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts|pcmk_sched_no_compat); scheduler->input = cib_xml_copy; scheduler->now = crm_time_new(NULL); cluster_status(scheduler); // Find resource in CIB rsc = pe_find_resource_with_flags(scheduler->resources, options.rsc_id, pcmk_rsc_match_history |pcmk_rsc_match_basename); if (rsc == NULL) { crm_err("Resource does not exist in config"); pe_free_working_set(scheduler); return EINVAL; } // Add resource instance parameters to options.params params = pe_rsc_params(rsc, NULL, scheduler); if (params != NULL) { g_hash_table_iter_init(&iter, params); while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) { options.params = lrmd_key_value_add(options.params, key, value); } } // Add resource meta-attributes to options.params meta = pcmk__strkey_table(free, free); get_meta_attributes(meta, rsc, NULL, scheduler); g_hash_table_iter_init(&iter, meta); while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) { char *crm_name = crm_meta_name(key); options.params = lrmd_key_value_add(options.params, crm_name, value); free(crm_name); } g_hash_table_destroy(meta); pe_free_working_set(scheduler); return rc; } static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; context = pcmk__build_arg_context(args, NULL, group, NULL); pcmk__add_main_args(context, basic_entries); pcmk__add_arg_group(context, "api-call", "API Call Options:", "Parameters for api-call option", api_call_entries); return context; } int main(int argc, char **argv) { GError *error = NULL; crm_exit_t exit_code = CRM_EX_OK; crm_trigger_t *trig = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); /* Typically we'd pass all the single character options that take an argument * as the second parameter here (and there's a bunch of those in this tool). * However, we control how this program is called so we can just not call it * in a way where the preprocessing ever matters. */ gchar **processed_args = pcmk__cmdline_preproc(argv, NULL); GOptionContext *context = build_arg_context(args, NULL); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } /* We have to use crm_log_init here to set up the logging because there's * different handling for daemons vs. command line programs, and * pcmk__cli_init_logging is set up to only handle the latter. */ crm_log_init(NULL, LOG_INFO, TRUE, (args->verbosity? TRUE : FALSE), argc, argv, FALSE); for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } if (!options.listen && pcmk__strcase_any_of(options.api_call, "metadata", "list_agents", "list_standards", "list_ocf_providers", NULL)) { options.no_connect = TRUE; } if (options.is_running) { int rc = pcmk_rc_ok; if (options.rsc_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "--is-running requires --rsc-id"); goto done; } options.interval_ms = 0; if (options.timeout == 0) { options.timeout = 30000; } rc = generate_params(); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Can not determine resource status: " "unable to get parameters from CIB"); goto done; } options.api_call = "exec"; options.action = PCMK_ACTION_MONITOR; options.exec_call_opts = lrmd_opt_notify_orig_only; } if (!options.api_call && !options.listen) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must specify at least one of --api-call, --listen, " "or --is-running"); goto done; } if (options.use_tls) { lrmd_conn = lrmd_remote_api_new(NULL, "localhost", 0); } else { lrmd_conn = lrmd_api_new(); } trig = mainloop_add_trigger(G_PRIORITY_HIGH, start_test, NULL); mainloop_set_trigger(trig); mainloop_add_signal(SIGTERM, test_shutdown); crm_info("Starting"); mainloop = g_main_loop_new(NULL, FALSE); g_main_loop_run(mainloop); done: g_strfreev(processed_args); pcmk__free_arg_context(context); free(key); free(val); pcmk__output_and_clear_error(&error, NULL); return test_exit(exit_code); } diff --git a/daemons/execd/remoted_schemas.c b/daemons/execd/remoted_schemas.c index 1f31fae184..2cb8f323bc 100644 --- a/daemons/execd/remoted_schemas.c +++ b/daemons/execd/remoted_schemas.c @@ -1,284 +1,285 @@ /* * 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 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 "pacemaker-execd.h" static pid_t schema_fetch_pid = 0; static int rm_files(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb) { /* Don't delete PCMK__REMOTE_SCHEMA_DIR . */ if (ftwb->level == 0) { return 0; } if (remove(pathname) != 0) { int rc = errno; crm_err("Could not remove %s: %s", pathname, pcmk_rc_str(rc)); return -1; } return 0; } static void clean_up_extra_schema_files(void) { const char *remote_schema_dir = pcmk__remote_schema_dir(); struct stat sb; int rc; rc = stat(remote_schema_dir, &sb); if (rc == -1) { if (errno == ENOENT) { /* If the directory doesn't exist, try to make it first. */ if (mkdir(remote_schema_dir, 0755) != 0) { rc = errno; crm_err("Could not create directory for schemas: %s", pcmk_rc_str(rc)); } } else { rc = errno; crm_err("Could not create directory for schemas: %s", pcmk_rc_str(rc)); } } else if (!S_ISDIR(sb.st_mode)) { /* If something exists with the same name that's not a directory, that's * an error. */ crm_err("%s already exists but is not a directory", remote_schema_dir); } else { /* It's a directory - clear it out so we can download potentially new * schema files. */ rc = nftw(remote_schema_dir, rm_files, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS); if (rc != 0) { crm_err("Could not remove %s: %s", remote_schema_dir, pcmk_rc_str(rc)); } } } static void write_extra_schema_file(xmlNode *xml, void *user_data) { const char *remote_schema_dir = pcmk__remote_schema_dir(); const char *file = NULL; char *path = NULL; int rc; file = crm_element_value(xml, PCMK_XA_PATH); if (file == NULL) { crm_warn("No destination path given in schema request"); return; } path = crm_strdup_printf("%s/%s", remote_schema_dir, file); /* The schema is a CDATA node, which is a child of the node. Traverse * all children and look for the first CDATA child. There can't be more than * one because we only have one file attribute on the parent. */ for (xmlNode *child = xml->children; child != NULL; child = child->next) { FILE *stream = NULL; if (child->type != XML_CDATA_SECTION_NODE) { continue; } stream = fopen(path, "w+"); if (stream == NULL) { crm_warn("Could not write schema file %s: %s", path, strerror(errno)); } else { rc = fprintf(stream, "%s", child->content); if (rc < 0) { crm_warn("Could not write schema file %s: %s", path, strerror(errno)); } fclose(stream); } break; } free(path); } static void get_schema_files(void) { int rc = pcmk_rc_ok; cib_t *cib = NULL; xmlNode *reply; cib = cib_new(); if (cib == NULL) { _exit(ENOTCONN); } rc = cib->cmds->signon(cib, crm_system_name, cib_query); if (rc != pcmk_ok) { crm_err("Could not connect to the CIB manager: %s", pcmk_strerror(rc)); _exit(pcmk_rc2exitc(rc)); } - rc = cib->cmds->fetch_schemas(cib, &reply, xml_latest_schema(), cib_sync_call); + rc = cib->cmds->fetch_schemas(cib, &reply, pcmk__highest_schema_name(), + cib_sync_call); if (rc != pcmk_ok) { crm_err("Could not get schema files: %s", pcmk_strerror(rc)); rc = pcmk_legacy2rc(rc); } else if (reply->children != NULL) { /* The returned document looks something like this: * * * * * * * * * ... * * * * ... * * * * * All the and tags are really just there for organizing * the XML a little better. What we really care about are the nodes, * and specifically the path attributes and the CDATA children (not shown) * of each. We can use an xpath query to reach down and get all the * nodes at once. * * If we already have the latest schema version, or we asked for one later * than what the cluster supports, we'll get back an empty node, * so all this will continue to work. It just won't do anything. */ crm_foreach_xpath_result(reply, "//" PCMK_XA_FILE, write_extra_schema_file, NULL); } cib__clean_up_connection(&cib); _exit(pcmk_rc2exitc(rc)); } /* Load any additional schema files when the child is finished fetching and * saving them to disk. */ static void get_schema_files_complete(mainloop_child_t *p, pid_t pid, int core, int signo, int exitcode) { const char *errmsg = "Could not load additional schema files"; if ((signo == 0) && (exitcode == 0)) { const char *remote_schema_dir = pcmk__remote_schema_dir(); /* Don't just crm_schema_init here because that will load the base * schemas again too. Instead just load the things we fetched. */ pcmk__load_schemas_from_dir(remote_schema_dir); pcmk__sort_schemas(); crm_info("Fetching extra schema files completed successfully"); } 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" : "")); } /* Clean up any incomplete schema data we might have been downloading when * the process timed out or crashed. We don't need to do any extra cleanup * because we never loaded the extra schemas, and we don't need to call * crm_schema_init because that was called in remoted_request_cib_schema_files * before this function. */ clean_up_extra_schema_files(); } } void remoted_request_cib_schema_files(void) { pid_t pid; int rc; /* If a previous schema-fetch process is still running when we're called * again, it's hung. Attempt to kill it before cleaning up the extra * directory. */ if (schema_fetch_pid != 0) { if (mainloop_child_kill(schema_fetch_pid) == FALSE) { crm_warn("Unable to kill pre-existing schema-fetch process"); return; } schema_fetch_pid = 0; } /* Clean up any extra schema files we downloaded from a previous cluster * connection. After the files are gone, we need to wipe them from * known_schemas, but there's no opposite operation for add_schema(). * * Instead, unload all the schemas. This means we'll also forget about all - * the installed schemas as well, which means that xml_latest_schema() will - * fail. So we need to load the base schemas right now. + * installed schemas as well, which means that pcmk__highest_schema_name() + * would fail. So we need to load the base schemas right now. */ clean_up_extra_schema_files(); crm_schema_cleanup(); crm_schema_init(); crm_info("Fetching extra schema files from cluster"); pid = fork(); switch (pid) { case -1: { rc = errno; crm_warn("Could not spawn process to get schema files: %s", pcmk_rc_str(rc)); break; } case 0: /* child */ get_schema_files(); break; default: /* parent */ schema_fetch_pid = pid; mainloop_child_add_with_flags(pid, 5 * 60 * 1000, "schema-fetch", NULL, mainloop_leave_pid_group, get_schema_files_complete); break; } } diff --git a/daemons/schedulerd/schedulerd_messages.c b/daemons/schedulerd/schedulerd_messages.c index c28550aa9c..3bbd3e8bf9 100644 --- a/daemons/schedulerd/schedulerd_messages.c +++ b/daemons/schedulerd/schedulerd_messages.c @@ -1,340 +1,340 @@ /* * 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 "pacemaker-schedulerd.h" static GHashTable *schedulerd_handlers = NULL; static pcmk_scheduler_t * init_working_set(void) { pcmk_scheduler_t *scheduler = pe_new_working_set(); pcmk__mem_assert(scheduler); crm_config_error = FALSE; crm_config_warning = FALSE; was_processing_error = FALSE; was_processing_warning = FALSE; scheduler->priv = logger_out; return scheduler; } static xmlNode * handle_pecalc_request(pcmk__request_t *request) { static struct series_s { const char *name; const char *param; /* Maximum number of inputs of this kind to save to disk. * If -1, save all; if 0, save none. */ int wrap; } series[] = { { "pe-error", PCMK_OPT_PE_ERROR_SERIES_MAX, -1 }, { "pe-warn", PCMK_OPT_PE_WARN_SERIES_MAX, 5000 }, { "pe-input", PCMK_OPT_PE_INPUT_SERIES_MAX, 4000 }, }; xmlNode *msg = request->xml; xmlNode *wrapper = pcmk__xe_first_child(msg, PCMK__XE_CRM_XML, NULL, NULL); xmlNode *xml_data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); static char *last_digest = NULL; static char *filename = NULL; unsigned int seq; int series_id = 0; int series_wrap = 0; char *digest = NULL; const char *value = NULL; time_t execution_date = time(NULL); xmlNode *converted = NULL; xmlNode *reply = NULL; bool is_repoke = false; bool process = true; pcmk_scheduler_t *scheduler = init_working_set(); pcmk__ipc_send_ack(request->ipc_client, request->ipc_id, request->ipc_flags, PCMK__XE_ACK, NULL, CRM_EX_INDETERMINATE); digest = calculate_xml_versioned_digest(xml_data, FALSE, FALSE, CRM_FEATURE_SET); converted = pcmk__xml_copy(NULL, xml_data); - if (!cli_config_update(&converted, NULL, TRUE)) { + if (!pcmk__update_configured_schema(&converted, true)) { scheduler->graph = pcmk__xe_create(NULL, PCMK__XE_TRANSITION_GRAPH); crm_xml_add_int(scheduler->graph, "transition_id", 0); crm_xml_add_int(scheduler->graph, PCMK_OPT_CLUSTER_DELAY, 0); process = false; free(digest); } else if (pcmk__str_eq(digest, last_digest, pcmk__str_casei)) { is_repoke = true; free(digest); } else { free(last_digest); last_digest = digest; } if (process) { pcmk__schedule_actions(converted, pcmk_sched_no_counts |pcmk_sched_no_compat |pcmk_sched_show_utilization, scheduler); } // Get appropriate index into series[] array if (was_processing_error || crm_config_error) { series_id = 0; } else if (was_processing_warning || crm_config_warning) { series_id = 1; } else { series_id = 2; } value = pcmk__cluster_option(scheduler->config_hash, series[series_id].param); if ((value == NULL) || (pcmk__scan_min_int(value, &series_wrap, -1) != pcmk_rc_ok)) { series_wrap = series[series_id].wrap; } if (pcmk__read_series_sequence(PE_STATE_DIR, series[series_id].name, &seq) != pcmk_rc_ok) { // @TODO maybe handle errors better ... seq = 0; } crm_trace("Series %s: wrap=%d, seq=%u, pref=%s", series[series_id].name, series_wrap, seq, value); scheduler->input = NULL; reply = create_reply(msg, scheduler->graph); if (reply == NULL) { pcmk__format_result(&request->result, CRM_EX_ERROR, PCMK_EXEC_ERROR, "Failed building ping reply for client %s", pcmk__client_name(request->ipc_client)); goto done; } if (series_wrap == 0) { // Don't save any inputs of this kind free(filename); filename = NULL; } else if (!is_repoke) { // Input changed, save to disk free(filename); filename = pcmk__series_filename(PE_STATE_DIR, series[series_id].name, seq, true); } crm_xml_add(reply, PCMK__XA_CRM_TGRAPH_IN, filename); crm_xml_add_int(reply, PCMK__XA_GRAPH_ERRORS, was_processing_error); crm_xml_add_int(reply, PCMK__XA_GRAPH_WARNINGS, was_processing_warning); crm_xml_add_int(reply, PCMK__XA_CONFIG_ERRORS, crm_config_error); crm_xml_add_int(reply, PCMK__XA_CONFIG_WARNINGS, crm_config_warning); pcmk__log_transition_summary(filename); if (series_wrap == 0) { crm_debug("Not saving input to disk (disabled by configuration)"); } else if (is_repoke) { crm_info("Input has not changed since last time, not saving to disk"); } else { unlink(filename); crm_xml_add_ll(xml_data, PCMK_XA_EXECUTION_DATE, (long long) execution_date); pcmk__xml_write_file(xml_data, filename, true, NULL); pcmk__write_series_sequence(PE_STATE_DIR, series[series_id].name, ++seq, series_wrap); } pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); done: free_xml(converted); pe_free_working_set(scheduler); return reply; } static xmlNode * handle_unknown_request(pcmk__request_t *request) { pcmk__ipc_send_ack(request->ipc_client, request->ipc_id, request->ipc_flags, PCMK__XE_ACK, NULL, CRM_EX_INVALID_PARAM); pcmk__format_result(&request->result, CRM_EX_PROTOCOL, PCMK_EXEC_INVALID, "Unknown IPC request type '%s' (bug?)", pcmk__client_name(request->ipc_client)); return NULL; } static xmlNode * handle_hello_request(pcmk__request_t *request) { pcmk__ipc_send_ack(request->ipc_client, request->ipc_id, request->ipc_flags, PCMK__XE_ACK, NULL, CRM_EX_INDETERMINATE); crm_trace("Received IPC hello from %s", pcmk__client_name(request->ipc_client)); pcmk__set_result(&request->result, CRM_EX_OK, PCMK_EXEC_DONE, NULL); return NULL; } static void schedulerd_register_handlers(void) { pcmk__server_command_t handlers[] = { { CRM_OP_HELLO, handle_hello_request }, { CRM_OP_PECALC, handle_pecalc_request }, { NULL, handle_unknown_request }, }; schedulerd_handlers = pcmk__register_handlers(handlers); } static int32_t pe_ipc_accept(qb_ipcs_connection_t * c, uid_t uid, gid_t gid) { crm_trace("Connection %p", c); if (pcmk__new_client(c, uid, gid) == NULL) { return -ENOMEM; } return 0; } static int32_t pe_ipc_dispatch(qb_ipcs_connection_t * qbc, void *data, size_t size) { uint32_t id = 0; uint32_t flags = 0; xmlNode *msg = NULL; pcmk__client_t *c = pcmk__find_client(qbc); const char *sys_to = NULL; CRM_CHECK(c != NULL, return 0); if (schedulerd_handlers == NULL) { schedulerd_register_handlers(); } msg = pcmk__client_data2xml(c, data, &id, &flags); if (msg == NULL) { pcmk__ipc_send_ack(c, id, flags, PCMK__XE_ACK, NULL, CRM_EX_PROTOCOL); return 0; } sys_to = crm_element_value(msg, PCMK__XA_CRM_SYS_TO); if (pcmk__str_eq(crm_element_value(msg, PCMK__XA_SUBT), PCMK__VALUE_RESPONSE, pcmk__str_none)) { pcmk__ipc_send_ack(c, id, flags, PCMK__XE_ACK, NULL, CRM_EX_INDETERMINATE); crm_info("Ignoring IPC reply from %s", pcmk__client_name(c)); } else if (!pcmk__str_eq(sys_to, CRM_SYSTEM_PENGINE, pcmk__str_none)) { pcmk__ipc_send_ack(c, id, flags, PCMK__XE_ACK, NULL, CRM_EX_INDETERMINATE); crm_info("Ignoring invalid IPC message: to '%s' not " CRM_SYSTEM_PENGINE, pcmk__s(sys_to, "")); } else { char *log_msg = NULL; const char *reason = NULL; xmlNode *reply = NULL; pcmk__request_t request = { .ipc_client = c, .ipc_id = id, .ipc_flags = flags, .peer = NULL, .xml = msg, .call_options = 0, .result = PCMK__UNKNOWN_RESULT, }; request.op = crm_element_value_copy(request.xml, PCMK__XA_CRM_TASK); CRM_CHECK(request.op != NULL, return 0); reply = pcmk__process_request(&request, schedulerd_handlers); if (reply != NULL) { pcmk__ipc_send_xml(c, id, reply, crm_ipc_server_event); free_xml(reply); } reason = request.result.exit_reason; log_msg = crm_strdup_printf("Processed %s request from %s %s: %s%s%s%s", request.op, pcmk__request_origin_type(&request), pcmk__request_origin(&request), pcmk_exec_status_str(request.result.execution_status), (reason == NULL)? "" : " (", (reason == NULL)? "" : reason, (reason == NULL)? "" : ")"); if (!pcmk__result_ok(&request.result)) { crm_warn("%s", log_msg); } else { crm_debug("%s", log_msg); } free(log_msg); pcmk__reset_request(&request); } free_xml(msg); return 0; } /* Error code means? */ static int32_t pe_ipc_closed(qb_ipcs_connection_t * c) { pcmk__client_t *client = pcmk__find_client(c); if (client == NULL) { return 0; } crm_trace("Connection %p", c); pcmk__free_client(client); return 0; } static void pe_ipc_destroy(qb_ipcs_connection_t * c) { crm_trace("Connection %p", c); pe_ipc_closed(c); } struct qb_ipcs_service_handlers ipc_callbacks = { .connection_accept = pe_ipc_accept, .connection_created = NULL, .msg_process = pe_ipc_dispatch, .connection_closed = pe_ipc_closed, .connection_destroyed = pe_ipc_destroy }; diff --git a/include/crm/common/Makefile.am b/include/crm/common/Makefile.am index 64adbf526a..f47503611a 100644 --- a/include/crm/common/Makefile.am +++ b/include/crm/common/Makefile.am @@ -1,54 +1,52 @@ # # 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. # MAINTAINERCLEANFILES = Makefile.in headerdir=$(pkgincludedir)/crm/common header_HEADERS = acl.h \ actions.h \ agents.h \ agents_compat.h \ cib.h \ ipc.h \ ipc_controld.h \ ipc_pacemakerd.h \ ipc_schedulerd.h \ iso8601.h \ logging.h \ logging_compat.h \ mainloop.h \ mainloop_compat.h \ nodes.h \ nodes_internal.h \ nvpair.h \ options.h \ output.h \ resources.h \ results.h \ results_compat.h \ roles.h \ rules.h \ scheduler.h \ scheduler_types.h \ - schemas.h \ - schemas_compat.h \ scores.h \ scores_compat.h \ tags.h \ tickets.h \ util.h \ util_compat.h \ xml.h \ xml_compat.h \ xml_io.h \ xml_io_compat.h \ xml_names.h noinst_HEADERS = $(wildcard *internal.h) diff --git a/include/crm/common/schemas.h b/include/crm/common/schemas.h deleted file mode 100644 index ade23829ad..0000000000 --- a/include/crm/common/schemas.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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. - */ - -#ifndef PCMK__CRM_COMMON_SCHEMAS__H -# define PCMK__CRM_COMMON_SCHEMAS__H - -#include // gboolean -#include // xmlNode - -gboolean validate_xml(xmlNode *xml_blob, const char *validation, - gboolean to_logs); -gboolean validate_xml_verbose(const xmlNode *xml_blob); - -int get_schema_version(const char *name); -const char *get_schema_name(int version); -const char *xml_latest_schema(void); -gboolean cli_config_update(xmlNode ** xml, int *best_version, gboolean to_logs); - -#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) -#include -#endif - -#ifdef __cplusplus -} -#endif - -#endif // PCMK__CRM_COMMON_SCHEMAS__H diff --git a/include/crm/common/schemas_compat.h b/include/crm/common/schemas_compat.h deleted file mode 100644 index 39ff71cd89..0000000000 --- a/include/crm/common/schemas_compat.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 Lesser General Public License - * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. - */ - -#ifndef PCMK__CRM_COMMON_SCHEMAS_COMPAT__H -#define PCMK__CRM_COMMON_SCHEMAS_COMPAT__H - -#include // xmlNode -#include // gboolean - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * \file - * \brief Deprecated Pacemaker XML schemas API - * \ingroup core - * \deprecated Do not include this header directly. The APIs in this header, and - * the header itself, will be removed in a future release. - */ - -//! \deprecated Do not use -int update_validation(xmlNode **xml_blob, int *best, int max, - gboolean transform, gboolean to_logs); - -#ifdef __cplusplus -} -#endif - -#endif // PCMK__CRM_COMMON_SCHEMAS_COMPAT__H diff --git a/include/crm/common/schemas_internal.h b/include/crm/common/schemas_internal.h index 4c398f7dcb..425b539b0f 100644 --- a/include/crm/common/schemas_internal.h +++ b/include/crm/common/schemas_internal.h @@ -1,34 +1,37 @@ /* * 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); -gboolean pcmk__validate_xml(xmlNode *xml_blob, const char *validation, - xmlRelaxNGValidityErrorFunc error_handler, - void *error_handler_context); +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); #endif // PCMK__SCHEMAS_INTERNAL__H diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h index 66c4682b95..28b349455f 100644 --- a/include/crm/common/xml.h +++ b/include/crm/common/xml.h @@ -1,130 +1,129 @@ /* * 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. */ #ifndef PCMK__CRM_COMMON_XML__H # define PCMK__CRM_COMMON_XML__H # include # include # include # include # include # include # include # include # include # include -# include # include # include #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Wrappers for and extensions to libxml2 * \ingroup core */ typedef const xmlChar *pcmkXmlStr; /* * \brief xmlCopyPropList ACLs-sensitive replacement expading i++ notation * * The gist is the same as with \c{xmlCopyPropList(target, src->properties)}. * The function exits prematurely when any attribute cannot be copied for * ACLs violation. Even without bailing out, the result can possibly be * incosistent with expectations in that case, hence the caller shall, * aposteriori, verify that no document-level-tracked denial was indicated * with \c{xml_acl_denied(target)} and drop whole such intermediate object. * * \param[in,out] target Element to receive attributes from #src element * \param[in] src Element carrying attributes to copy over to #target * * \note Original commit 1c632c506 sadly haven't stated which otherwise * assumed behaviours of xmlCopyPropList were missing beyond otherwise * custom extensions like said ACLs and "atomic increment" (that landed * later on, anyway). */ void copy_in_properties(xmlNode *target, const xmlNode *src); void expand_plus_plus(xmlNode * target, const char *name, const char *value); /* * Searching & Modifying */ xmlNode *get_xpath_object(const char *xpath, xmlNode * xml_obj, int error_level); char *calculate_on_disk_digest(xmlNode * local_cib); char *calculate_operation_digest(xmlNode * local_cib, const char *version); char *calculate_xml_versioned_digest(xmlNode * input, gboolean sort, gboolean do_filter, const char *version); /*! * \brief Initialize the CRM XML subsystem * * This method sets global XML settings and loads pacemaker schemas into the cache. */ void crm_xml_init(void); void crm_xml_cleanup(void); void pcmk_free_xml_subtree(xmlNode *xml); void free_xml(xmlNode * child); xmlNode *sorted_xml(xmlNode * input, xmlNode * parent, gboolean recursive); xmlXPathObjectPtr xpath_search(const xmlNode *xml_top, const char *path); void crm_foreach_xpath_result(xmlNode *xml, const char *xpath, void (*helper)(xmlNode*, void*), void *user_data); xmlNode *expand_idref(xmlNode * input, xmlNode * top); void freeXpathObject(xmlXPathObjectPtr xpathObj); xmlNode *getXpathResult(xmlXPathObjectPtr xpathObj, int index); void dedupXpathResults(xmlXPathObjectPtr xpathObj); static inline int numXpathResults(xmlXPathObjectPtr xpathObj) { if(xpathObj == NULL || xpathObj->nodesetval == NULL) { return 0; } return xpathObj->nodesetval->nodeNr; } bool xml_tracking_changes(xmlNode * xml); bool xml_document_dirty(xmlNode *xml); void xml_track_changes(xmlNode * xml, const char *user, xmlNode *acl_source, bool enforce_acls); void xml_calculate_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_calculate_significant_changes(xmlNode *old_xml, xmlNode *new_xml); void xml_accept_changes(xmlNode * xml); bool xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]); xmlNode *xml_create_patchset( int format, xmlNode *source, xmlNode *target, bool *config, bool manage_version); int xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version); void patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, bool with_digest); void crm_xml_sanitize_id(char *id); void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3); #if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1) #include #endif #ifdef __cplusplus } #endif #endif diff --git a/include/crm/common/xml_compat.h b/include/crm/common/xml_compat.h index d5d5779fa3..f657ea9af7 100644 --- a/include/crm/common/xml_compat.h +++ b/include/crm/common/xml_compat.h @@ -1,167 +1,190 @@ /* * 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. */ #ifndef PCMK__CRM_COMMON_XML_COMPAT__H # define PCMK__CRM_COMMON_XML_COMPAT__H #include // gboolean #include // xmlNode #include // crm_xml_add() #include // PCMK_XE_CLONE #ifdef __cplusplus extern "C" { #endif /** * \file * \brief Deprecated Pacemaker XML API * \ingroup core * \deprecated Do not include this header directly. The XML APIs in this * header, and the header itself, will be removed in a future * release. */ //! \deprecated Do not use (will be removed in a future release) #define XML_PARANOIA_CHECKS 0 //! \deprecated This function will be removed in a future release xmlDoc *getDocPtr(xmlNode *node); //! \deprecated This function will be removed in a future release int add_node_nocopy(xmlNode * parent, const char *name, xmlNode * child); //! \deprecated This function will be removed in a future release xmlNode *find_entity(xmlNode *parent, const char *node_name, const char *id); //! \deprecated This function will be removed in a future release char *xml_get_path(const xmlNode *xml); //! \deprecated This function will be removed in a future release void xml_log_changes(uint8_t level, const char *function, const xmlNode *xml); //! \deprecated This function will be removed in a future release void xml_log_patchset(uint8_t level, const char *function, const xmlNode *xml); //! \deprecated Use xml_apply_patchset() instead gboolean apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml); //! \deprecated Do not use (will be removed in a future release) void crm_destroy_xml(gpointer data); //! \deprecated Check children member directly gboolean xml_has_children(const xmlNode *root); //! \deprecated Use crm_xml_add() with "true" or "false" instead static inline const char * crm_xml_add_boolean(xmlNode *node, const char *name, gboolean value) { return crm_xml_add(node, name, (value? "true" : "false")); } //! \deprecated Use name member directly static inline const char * crm_element_name(const xmlNode *xml) { return (xml == NULL)? NULL : (const char *) xml->name; } //! \deprecated Do not use char *crm_xml_escape(const char *text); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *copy_xml(xmlNode *src_node); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *add_node_copy(xmlNode *new_parent, xmlNode *xml_node); //! \deprecated Do not use void purge_diff_markers(xmlNode *a_node); //! \deprecated Do not use xmlNode *diff_xml_object(xmlNode *left, xmlNode *right, gboolean suppress); //! \deprecated Do not use xmlNode *subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, gboolean full, gboolean *changed, const char *marker); //! \deprecated Do not use gboolean can_prune_leaf(xmlNode *xml_node); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *create_xml_node(xmlNode *parent, const char *name); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *pcmk_create_xml_text_node(xmlNode *parent, const char *name, const char *content); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *pcmk_create_html_node(xmlNode *parent, const char *element_name, const char *id, const char *class_name, const char *text); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *first_named_child(const xmlNode *parent, const char *name); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *find_xml_node(const xmlNode *root, const char *search_path, gboolean must_find); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *crm_next_same_xml(const xmlNode *sibling); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation void xml_remove_prop(xmlNode *obj, const char *name); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation gboolean replace_xml_child(xmlNode *parent, xmlNode *child, xmlNode *update, gboolean delete_only); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation gboolean update_xml_child(xmlNode *child, xmlNode *to_update); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation int find_xml_children(xmlNode **children, xmlNode *root, const char *tag, const char *field, const char *value, gboolean search_matches); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *get_xpath_object_relative(const char *xpath, xmlNode *xml_obj, int error_level); //! \deprecated Do not use void fix_plus_plus_recursive(xmlNode *target); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation gboolean add_message_xml(xmlNode *msg, const char *field, xmlNode *xml); //! \deprecated Do not use Pacemaker for general-purpose XML manipulation xmlNode *get_message_xml(const xmlNode *msg, const char *field); +//! \deprecated Do not use +const char *xml_latest_schema(void); + +//! \deprecated Do not use +const char *get_schema_name(int version); + +//! \deprecated Do not use +int get_schema_version(const char *name); + +//! \deprecated Do not use +int update_validation(xmlNode **xml_blob, int *best, int max, + gboolean transform, gboolean to_logs); + +//! \deprecated Do not use +gboolean validate_xml(xmlNode *xml_blob, const char *validation, + gboolean to_logs); + +//! \deprecated Do not use +gboolean validate_xml_verbose(const xmlNode *xml_blob); + +//! \deprecated Do not use +gboolean cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs); + //! \deprecated Do not use static inline const char * crm_map_element_name(const xmlNode *xml) { if (xml == NULL) { return NULL; } else if (strcmp((const char *) xml->name, "master") == 0) { // Can't use PCMK__XE_PROMOTABLE_LEGACY because it's internal return PCMK_XE_CLONE; } else { return (const char *) xml->name; } } #ifdef __cplusplus } #endif #endif // PCMK__CRM_COMMON_XML_COMPAT__H diff --git a/lib/cib/cib_file.c b/lib/cib/cib_file.c index 4f6cdc2f79..24bd029406 100644 --- a/lib/cib/cib_file.c +++ b/lib/cib/cib_file.c @@ -1,1175 +1,1176 @@ /* * 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 #include #include #include #define CIB_SERIES "cib" #define CIB_SERIES_MAX 100 #define CIB_SERIES_BZIP FALSE /* Must be false because archived copies are created with hard links */ #define CIB_LIVE_NAME CIB_SERIES ".xml" // key: client ID (const char *) -> value: client (cib_t *) static GHashTable *client_table = NULL; enum cib_file_flags { cib_file_flag_dirty = (1 << 0), cib_file_flag_live = (1 << 1), }; typedef struct cib_file_opaque_s { char *id; char *filename; uint32_t flags; // Group of enum cib_file_flags xmlNode *cib_xml; } cib_file_opaque_t; static int cib_file_process_commit_transaction(const char *op, int options, const char *section, xmlNode *req, xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, xmlNode **answer); /*! * \internal * \brief Add a CIB file client to client table * * \param[in] cib CIB client */ static void register_client(const cib_t *cib) { cib_file_opaque_t *private = cib->variant_opaque; if (client_table == NULL) { client_table = pcmk__strkey_table(NULL, NULL); } g_hash_table_insert(client_table, private->id, (gpointer) cib); } /*! * \internal * \brief Remove a CIB file client from client table * * \param[in] cib CIB client */ static void unregister_client(const cib_t *cib) { cib_file_opaque_t *private = cib->variant_opaque; if (client_table == NULL) { return; } g_hash_table_remove(client_table, private->id); /* @COMPAT: Add to crm_exit() when libcib and libcrmcommon are merged, * instead of destroying the client table when there are no more clients. */ if (g_hash_table_size(client_table) == 0) { g_hash_table_destroy(client_table); client_table = NULL; } } /*! * \internal * \brief Look up a CIB file client by its ID * * \param[in] client_id CIB client ID * * \return CIB client with matching ID if found, or \p NULL otherwise */ static cib_t * get_client(const char *client_id) { if (client_table == NULL) { return NULL; } return g_hash_table_lookup(client_table, (gpointer) client_id); } static const cib__op_fn_t cib_op_functions[] = { [cib__op_apply_patch] = cib_process_diff, [cib__op_bump] = cib_process_bump, [cib__op_commit_transact] = cib_file_process_commit_transaction, [cib__op_create] = cib_process_create, [cib__op_delete] = cib_process_delete, [cib__op_erase] = cib_process_erase, [cib__op_modify] = cib_process_modify, [cib__op_query] = cib_process_query, [cib__op_replace] = cib_process_replace, [cib__op_upgrade] = cib_process_upgrade, }; /* cib_file_backup() and cib_file_write_with_digest() need to chown the * written files only in limited circumstances, so these variables allow * that to be indicated without affecting external callers */ static uid_t cib_file_owner = 0; static uid_t cib_file_group = 0; static gboolean cib_do_chown = FALSE; #define cib_set_file_flags(cibfile, flags_to_set) do { \ (cibfile)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_TRACE, "CIB file", \ cibfile->filename, \ (cibfile)->flags, \ (flags_to_set), \ #flags_to_set); \ } while (0) #define cib_clear_file_flags(cibfile, flags_to_clear) do { \ (cibfile)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_TRACE, "CIB file", \ cibfile->filename, \ (cibfile)->flags, \ (flags_to_clear), \ #flags_to_clear); \ } while (0) /*! * \internal * \brief Get the function that performs a given CIB file operation * * \param[in] operation Operation whose function to look up * * \return Function that performs \p operation for a CIB file client */ static cib__op_fn_t file_get_op_function(const cib__operation_t *operation) { enum cib__op_type type = operation->type; CRM_ASSERT(type >= 0); if (type >= PCMK__NELEM(cib_op_functions)) { return NULL; } return cib_op_functions[type]; } /*! * \internal * \brief Check whether a file is the live CIB * * \param[in] filename Name of file to check * * \return TRUE if file exists and its real path is same as live CIB's */ static gboolean cib_file_is_live(const char *filename) { gboolean same = FALSE; if (filename != NULL) { // Canonicalize file names for true comparison char *real_filename = NULL; if (pcmk__real_path(filename, &real_filename) == pcmk_rc_ok) { char *real_livename = NULL; if (pcmk__real_path(CRM_CONFIG_DIR "/" CIB_LIVE_NAME, &real_livename) == pcmk_rc_ok) { same = !strcmp(real_filename, real_livename); free(real_livename); } free(real_filename); } } return same; } static int cib_file_process_request(cib_t *cib, xmlNode *request, xmlNode **output) { int rc = pcmk_ok; const cib__operation_t *operation = NULL; cib__op_fn_t op_function = NULL; int call_id = 0; int call_options = cib_none; const char *op = crm_element_value(request, PCMK__XA_CIB_OP); const char *section = crm_element_value(request, PCMK__XA_CIB_SECTION); xmlNode *wrapper = pcmk__xe_first_child(request, PCMK__XE_CIB_CALLDATA, NULL, NULL); xmlNode *data = pcmk__xe_first_child(wrapper, NULL, NULL, NULL); bool changed = false; bool read_only = false; xmlNode *result_cib = NULL; xmlNode *cib_diff = NULL; cib_file_opaque_t *private = cib->variant_opaque; // We error checked these in callers cib__get_operation(op, &operation); op_function = file_get_op_function(operation); crm_element_value_int(request, PCMK__XA_CIB_CALLID, &call_id); crm_element_value_int(request, PCMK__XA_CIB_CALLOPT, &call_options); read_only = !pcmk_is_set(operation->flags, cib__op_attr_modifies); // Mirror the logic in prepare_input() in pacemaker-based if ((section != NULL) && pcmk__xe_is(data, PCMK_XE_CIB)) { data = pcmk_find_cib_element(data, section); } rc = cib_perform_op(cib, op, call_options, op_function, read_only, section, request, data, true, &changed, &private->cib_xml, &result_cib, &cib_diff, output); if (pcmk_is_set(call_options, cib_transaction)) { /* The rest of the logic applies only to the transaction as a whole, not * to individual requests. */ goto done; } if (rc == -pcmk_err_schema_validation) { - validate_xml_verbose(result_cib); + // Show validation errors to stderr + pcmk__validate_xml(result_cib, NULL, NULL, NULL); } else if ((rc == pcmk_ok) && !read_only) { pcmk__log_xml_patchset(LOG_DEBUG, cib_diff); if (result_cib != private->cib_xml) { free_xml(private->cib_xml); private->cib_xml = result_cib; } cib_set_file_flags(private, cib_file_flag_dirty); } // Global operation callback (deprecated) if (cib->op_callback != NULL) { cib->op_callback(NULL, call_id, rc, *output); } done: if ((result_cib != private->cib_xml) && (result_cib != *output)) { free_xml(result_cib); } free_xml(cib_diff); return rc; } static int cib_file_perform_op_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) { int rc = pcmk_ok; xmlNode *request = NULL; xmlNode *output = NULL; cib_file_opaque_t *private = cib->variant_opaque; const cib__operation_t *operation = NULL; crm_info("Handling %s operation for %s as %s", pcmk__s(op, "invalid"), pcmk__s(section, "entire CIB"), pcmk__s(user_name, "default user")); if (output_data != NULL) { *output_data = NULL; } if (cib->state == cib_disconnected) { return -ENOTCONN; } rc = cib__get_operation(op, &operation); rc = pcmk_rc2legacy(rc); if (rc != pcmk_ok) { // @COMPAT: At compatibility break, use rc directly return -EPROTONOSUPPORT; } if (file_get_op_function(operation) == NULL) { // @COMPAT: At compatibility break, use EOPNOTSUPP crm_err("Operation %s is not supported by CIB file clients", op); return -EPROTONOSUPPORT; } cib__set_call_options(call_options, "file operation", cib_no_mtime); rc = cib__create_op(cib, op, host, section, data, call_options, user_name, NULL, &request); if (rc != pcmk_ok) { return rc; } crm_xml_add(request, PCMK_XE_ACL_TARGET, user_name); crm_xml_add(request, PCMK__XA_CIB_CLIENTID, private->id); if (pcmk_is_set(call_options, cib_transaction)) { rc = cib__extend_transaction(cib, request); goto done; } rc = cib_file_process_request(cib, request, &output); if ((output_data != NULL) && (output != NULL)) { if (output->doc == private->cib_xml->doc) { *output_data = pcmk__xml_copy(NULL, output); } else { *output_data = output; } } done: if ((output != NULL) && (output->doc != private->cib_xml->doc) && ((output_data == NULL) || (output != *output_data))) { free_xml(output); } free_xml(request); return rc; } /*! * \internal * \brief Read CIB from disk and validate it against XML schema * * \param[in] filename Name of file to read CIB from * \param[out] output Where to store the read CIB XML * * \return pcmk_ok on success, * -ENXIO if file does not exist (or stat() otherwise fails), or * -pcmk_err_schema_validation if XML doesn't parse or validate * \note If filename is the live CIB, this will *not* verify its digest, * though that functionality would be trivial to add here. * Also, this will *not* verify that the file is writable, * because some callers might not need to write. */ static int load_file_cib(const char *filename, xmlNode **output) { struct stat buf; xmlNode *root = NULL; /* Ensure file is readable */ if (strcmp(filename, "-") && (stat(filename, &buf) < 0)) { return -ENXIO; } /* Parse XML from file */ root = pcmk__xml_read(filename); if (root == NULL) { return -pcmk_err_schema_validation; } /* Add a status section if not already present */ if (pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL) == NULL) { pcmk__xe_create(root, PCMK_XE_STATUS); } /* Validate XML against its specified schema */ - if (validate_xml(root, NULL, TRUE) == FALSE) { + if (!pcmk__configured_schema_validates(root)) { const char *schema = crm_element_value(root, PCMK_XA_VALIDATE_WITH); crm_err("CIB does not validate against %s, or that schema is unknown", schema); free_xml(root); return -pcmk_err_schema_validation; } /* Remember the parsed XML for later use */ *output = root; return pcmk_ok; } static int cib_file_signon(cib_t *cib, const char *name, enum cib_conn_type type) { int rc = pcmk_ok; cib_file_opaque_t *private = cib->variant_opaque; if (private->filename == NULL) { rc = -EINVAL; } else { rc = load_file_cib(private->filename, &private->cib_xml); } if (rc == pcmk_ok) { crm_debug("Opened connection to local file '%s' for %s", private->filename, name); cib->state = cib_connected_command; cib->type = cib_command; register_client(cib); } else { crm_info("Connection to local file '%s' for %s (client %s) failed: %s", private->filename, name, private->id, pcmk_strerror(rc)); } return rc; } /*! * \internal * \brief Write out the in-memory CIB to a live CIB file * * \param[in] cib_root Root of XML tree to write * \param[in,out] path Full path to file to write * * \return 0 on success, -1 on failure */ static int cib_file_write_live(xmlNode *cib_root, char *path) { uid_t uid = geteuid(); struct passwd *daemon_pwent; char *sep = strrchr(path, '/'); const char *cib_dirname, *cib_filename; int rc = 0; /* Get the desired uid/gid */ errno = 0; daemon_pwent = getpwnam(CRM_DAEMON_USER); if (daemon_pwent == NULL) { crm_perror(LOG_ERR, "Could not find %s user", CRM_DAEMON_USER); return -1; } /* If we're root, we can change the ownership; * if we're daemon, anything we create will be OK; * otherwise, block access so we don't create wrong owner */ if ((uid != 0) && (uid != daemon_pwent->pw_uid)) { crm_perror(LOG_ERR, "Must be root or %s to modify live CIB", CRM_DAEMON_USER); return 0; } /* fancy footwork to separate dirname from filename * (we know the canonical name maps to the live CIB, * but the given name might be relative, or symlinked) */ if (sep == NULL) { /* no directory component specified */ cib_dirname = "./"; cib_filename = path; } else if (sep == path) { /* given name is in / */ cib_dirname = "/"; cib_filename = path + 1; } else { /* typical case; split given name into parts */ *sep = '\0'; cib_dirname = path; cib_filename = sep + 1; } /* if we're root, we want to update the file ownership */ if (uid == 0) { cib_file_owner = daemon_pwent->pw_uid; cib_file_group = daemon_pwent->pw_gid; cib_do_chown = TRUE; } /* write the file */ if (cib_file_write_with_digest(cib_root, cib_dirname, cib_filename) != pcmk_ok) { rc = -1; } /* turn off file ownership changes, for other callers */ if (uid == 0) { cib_do_chown = FALSE; } /* undo fancy stuff */ if ((sep != NULL) && (*sep == '\0')) { *sep = '/'; } return rc; } /*! * \internal * \brief Sign-off method for CIB file variants * * This will write the file to disk if needed, and free the in-memory CIB. If * the file is the live CIB, it will compute and write a signature as well. * * \param[in,out] cib CIB object to sign off * * \return pcmk_ok on success, pcmk_err_generic on failure * \todo This method should refuse to write the live CIB if the CIB manager is * running. */ static int cib_file_signoff(cib_t *cib) { int rc = pcmk_ok; cib_file_opaque_t *private = cib->variant_opaque; crm_debug("Disconnecting from the CIB manager"); cib->state = cib_disconnected; cib->type = cib_no_connection; unregister_client(cib); cib->cmds->end_transaction(cib, false, cib_none); /* If the in-memory CIB has been changed, write it to disk */ if (pcmk_is_set(private->flags, cib_file_flag_dirty)) { /* If this is the live CIB, write it out with a digest */ if (pcmk_is_set(private->flags, cib_file_flag_live)) { if (cib_file_write_live(private->cib_xml, private->filename) < 0) { rc = pcmk_err_generic; } /* Otherwise, it's a simple write */ } else { bool compress = pcmk__ends_with_ext(private->filename, ".bz2"); if (pcmk__xml_write_file(private->cib_xml, private->filename, compress, NULL) != pcmk_rc_ok) { rc = pcmk_err_generic; } } if (rc == pcmk_ok) { crm_info("Wrote CIB to %s", private->filename); cib_clear_file_flags(private, cib_file_flag_dirty); } else { crm_err("Could not write CIB to %s", private->filename); } } /* Free the in-memory CIB */ free_xml(private->cib_xml); private->cib_xml = NULL; return rc; } static int cib_file_free(cib_t *cib) { int rc = pcmk_ok; if (cib->state != cib_disconnected) { rc = cib_file_signoff(cib); } if (rc == pcmk_ok) { cib_file_opaque_t *private = cib->variant_opaque; free(private->id); free(private->filename); free(private); free(cib->cmds); free(cib->user); free(cib); } else { fprintf(stderr, "Couldn't sign off: %d\n", rc); } return rc; } static int cib_file_inputfd(cib_t *cib) { return -EPROTONOSUPPORT; } static int cib_file_register_notification(cib_t *cib, const char *callback, int enabled) { return -EPROTONOSUPPORT; } static int cib_file_set_connection_dnotify(cib_t *cib, void (*dnotify) (gpointer user_data)) { return -EPROTONOSUPPORT; } /*! * \internal * \brief Get the given CIB connection's unique client identifier * * \param[in] cib CIB connection * \param[out] async_id If not \p NULL, where to store asynchronous client ID * \param[out] sync_id If not \p NULL, where to store synchronous client ID * * \return Legacy Pacemaker return code * * \note This is the \p cib_file variant implementation of * \p cib_api_operations_t:client_id(). */ static int cib_file_client_id(const cib_t *cib, const char **async_id, const char **sync_id) { cib_file_opaque_t *private = cib->variant_opaque; if (async_id != NULL) { *async_id = private->id; } if (sync_id != NULL) { *sync_id = private->id; } return pcmk_ok; } cib_t * cib_file_new(const char *cib_location) { cib_file_opaque_t *private = NULL; cib_t *cib = cib_new_variant(); if (cib == NULL) { return NULL; } private = calloc(1, sizeof(cib_file_opaque_t)); if (private == NULL) { free(cib); return NULL; } private->id = crm_generate_uuid(); cib->variant = cib_file; cib->variant_opaque = private; if (cib_location == NULL) { cib_location = getenv("CIB_file"); CRM_CHECK(cib_location != NULL, return NULL); // Shouldn't be possible } private->flags = 0; if (cib_file_is_live(cib_location)) { cib_set_file_flags(private, cib_file_flag_live); crm_trace("File %s detected as live CIB", cib_location); } private->filename = strdup(cib_location); /* assign variant specific ops */ cib->delegate_fn = cib_file_perform_op_delegate; cib->cmds->signon = cib_file_signon; cib->cmds->signoff = cib_file_signoff; cib->cmds->free = cib_file_free; cib->cmds->inputfd = cib_file_inputfd; // Deprecated method cib->cmds->register_notification = cib_file_register_notification; cib->cmds->set_connection_dnotify = cib_file_set_connection_dnotify; cib->cmds->client_id = cib_file_client_id; return cib; } /*! * \internal * \brief Compare the calculated digest of an XML tree against a signature file * * \param[in] root Root of XML tree to compare * \param[in] sigfile Name of signature file containing digest to compare * * \return TRUE if digests match or signature file does not exist, else FALSE */ static gboolean cib_file_verify_digest(xmlNode *root, const char *sigfile) { gboolean passed = FALSE; char *expected; int rc = pcmk__file_contents(sigfile, &expected); switch (rc) { case pcmk_rc_ok: if (expected == NULL) { crm_err("On-disk digest at %s is empty", sigfile); return FALSE; } break; case ENOENT: crm_warn("No on-disk digest present at %s", sigfile); return TRUE; default: crm_err("Could not read on-disk digest from %s: %s", sigfile, pcmk_rc_str(rc)); return FALSE; } passed = pcmk__verify_digest(root, expected); free(expected); return passed; } /*! * \internal * \brief Read an XML tree from a file and verify its digest * * \param[in] filename Name of XML file to read * \param[in] sigfile Name of signature file containing digest to compare * \param[out] root If non-NULL, will be set to pointer to parsed XML tree * * \return 0 if file was successfully read, parsed and verified, otherwise: * -errno on stat() failure, * -pcmk_err_cib_corrupt if file size is 0 or XML is not parseable, or * -pcmk_err_cib_modified if digests do not match * \note If root is non-NULL, it is the caller's responsibility to free *root on * successful return. */ int cib_file_read_and_verify(const char *filename, const char *sigfile, xmlNode **root) { int s_res; struct stat buf; char *local_sigfile = NULL; xmlNode *local_root = NULL; CRM_ASSERT(filename != NULL); if (root) { *root = NULL; } /* Verify that file exists and its size is nonzero */ s_res = stat(filename, &buf); if (s_res < 0) { crm_perror(LOG_WARNING, "Could not verify cluster configuration file %s", filename); return -errno; } else if (buf.st_size == 0) { crm_warn("Cluster configuration file %s is corrupt (size is zero)", filename); return -pcmk_err_cib_corrupt; } /* Parse XML */ local_root = pcmk__xml_read(filename); if (local_root == NULL) { crm_warn("Cluster configuration file %s is corrupt (unparseable as XML)", filename); return -pcmk_err_cib_corrupt; } /* If sigfile is not specified, use original file name plus .sig */ if (sigfile == NULL) { sigfile = local_sigfile = crm_strdup_printf("%s.sig", filename); } /* Verify that digests match */ if (cib_file_verify_digest(local_root, sigfile) == FALSE) { free(local_sigfile); free_xml(local_root); return -pcmk_err_cib_modified; } free(local_sigfile); if (root) { *root = local_root; } else { free_xml(local_root); } return pcmk_ok; } /*! * \internal * \brief Back up a CIB * * \param[in] cib_dirname Directory containing CIB file and backups * \param[in] cib_filename Name (relative to cib_dirname) of CIB file to back up * * \return 0 on success, -1 on error */ static int cib_file_backup(const char *cib_dirname, const char *cib_filename) { int rc = 0; unsigned int seq; char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename); char *cib_digest = crm_strdup_printf("%s.sig", cib_path); char *backup_path; char *backup_digest; // Determine backup and digest file names if (pcmk__read_series_sequence(cib_dirname, CIB_SERIES, &seq) != pcmk_rc_ok) { // @TODO maybe handle errors better ... seq = 0; } backup_path = pcmk__series_filename(cib_dirname, CIB_SERIES, seq, CIB_SERIES_BZIP); backup_digest = crm_strdup_printf("%s.sig", backup_path); /* Remove the old backups if they exist */ unlink(backup_path); unlink(backup_digest); /* Back up the CIB, by hard-linking it to the backup name */ if ((link(cib_path, backup_path) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not archive %s by linking to %s", cib_path, backup_path); rc = -1; /* Back up the CIB signature similarly */ } else if ((link(cib_digest, backup_digest) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not archive %s by linking to %s", cib_digest, backup_digest); rc = -1; /* Update the last counter and ensure everything is sync'd to media */ } else { pcmk__write_series_sequence(cib_dirname, CIB_SERIES, ++seq, CIB_SERIES_MAX); if (cib_do_chown) { int rc2; if ((chown(backup_path, cib_file_owner, cib_file_group) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not set owner of %s", backup_path); rc = -1; } if ((chown(backup_digest, cib_file_owner, cib_file_group) < 0) && (errno != ENOENT)) { crm_perror(LOG_ERR, "Could not set owner of %s", backup_digest); rc = -1; } rc2 = pcmk__chown_series_sequence(cib_dirname, CIB_SERIES, cib_file_owner, cib_file_group); if (rc2 != pcmk_rc_ok) { crm_err("Could not set owner of sequence file in %s: %s", cib_dirname, pcmk_rc_str(rc2)); rc = -1; } } pcmk__sync_directory(cib_dirname); crm_info("Archived previous version as %s", backup_path); } free(cib_path); free(cib_digest); free(backup_path); free(backup_digest); return rc; } /*! * \internal * \brief Prepare CIB XML to be written to disk * * Set \c PCMK_XA_NUM_UPDATES to 0, set \c PCMK_XA_CIB_LAST_WRITTEN to the * current timestamp, and strip out the status section. * * \param[in,out] root Root of CIB XML tree * * \return void */ static void cib_file_prepare_xml(xmlNode *root) { xmlNode *cib_status_root = NULL; /* Always write out with num_updates=0 and current last-written timestamp */ crm_xml_add(root, PCMK_XA_NUM_UPDATES, "0"); pcmk__xe_add_last_written(root); /* Delete status section before writing to file, because * we discard it on startup anyway, and users get confused by it */ cib_status_root = pcmk__xe_first_child(root, PCMK_XE_STATUS, NULL, NULL); CRM_CHECK(cib_status_root != NULL, return); free_xml(cib_status_root); } /*! * \internal * \brief Write CIB to disk, along with a signature file containing its digest * * \param[in,out] cib_root Root of XML tree to write * \param[in] cib_dirname Directory containing CIB and signature files * \param[in] cib_filename Name (relative to cib_dirname) of file to write * * \return pcmk_ok on success, * pcmk_err_cib_modified if existing cib_filename doesn't match digest, * pcmk_err_cib_backup if existing cib_filename couldn't be backed up, * or pcmk_err_cib_save if new cib_filename couldn't be saved */ int cib_file_write_with_digest(xmlNode *cib_root, const char *cib_dirname, const char *cib_filename) { int exit_rc = pcmk_ok; int rc, fd; char *digest = NULL; /* Detect CIB version for diagnostic purposes */ const char *epoch = crm_element_value(cib_root, PCMK_XA_EPOCH); const char *admin_epoch = crm_element_value(cib_root, PCMK_XA_ADMIN_EPOCH); /* Determine full CIB and signature pathnames */ char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename); char *digest_path = crm_strdup_printf("%s.sig", cib_path); /* Create temporary file name patterns for writing out CIB and signature */ char *tmp_cib = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname); char *tmp_digest = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname); /* Ensure the admin didn't modify the existing CIB underneath us */ crm_trace("Reading cluster configuration file %s", cib_path); rc = cib_file_read_and_verify(cib_path, NULL, NULL); if ((rc != pcmk_ok) && (rc != -ENOENT)) { crm_err("%s was manually modified while the cluster was active!", cib_path); exit_rc = pcmk_err_cib_modified; goto cleanup; } /* Back up the existing CIB */ if (cib_file_backup(cib_dirname, cib_filename) < 0) { exit_rc = pcmk_err_cib_backup; goto cleanup; } crm_debug("Writing CIB to disk"); umask(S_IWGRP | S_IWOTH | S_IROTH); cib_file_prepare_xml(cib_root); /* Write the CIB to a temporary file, so we can deploy (near) atomically */ fd = mkstemp(tmp_cib); if (fd < 0) { crm_perror(LOG_ERR, "Couldn't open temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } /* Protect the temporary file */ if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) { crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) { crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } /* Write out the CIB */ if (pcmk__xml_write_fd(cib_root, tmp_cib, fd, false, NULL) != pcmk_rc_ok) { crm_err("Changes couldn't be written to %s", tmp_cib); exit_rc = pcmk_err_cib_save; goto cleanup; } /* Calculate CIB digest */ digest = calculate_on_disk_digest(cib_root); CRM_ASSERT(digest != NULL); crm_info("Wrote version %s.%s.0 of the CIB to disk (digest: %s)", (admin_epoch ? admin_epoch : "0"), (epoch ? epoch : "0"), digest); /* Write the CIB digest to a temporary file */ fd = mkstemp(tmp_digest); if (fd < 0) { crm_perror(LOG_ERR, "Could not create temporary file for CIB digest"); exit_rc = pcmk_err_cib_save; goto cleanup; } if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) { crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", tmp_cib); exit_rc = pcmk_err_cib_save; close(fd); goto cleanup; } rc = pcmk__write_sync(fd, digest); if (rc != pcmk_rc_ok) { crm_err("Could not write digest to %s: %s", tmp_digest, pcmk_rc_str(rc)); exit_rc = pcmk_err_cib_save; close(fd); goto cleanup; } close(fd); crm_debug("Wrote digest %s to disk", digest); /* Verify that what we wrote is sane */ crm_info("Reading cluster configuration file %s (digest: %s)", tmp_cib, tmp_digest); rc = cib_file_read_and_verify(tmp_cib, tmp_digest, NULL); CRM_ASSERT(rc == 0); /* Rename temporary files to live, and sync directory changes to media */ crm_debug("Activating %s", tmp_cib); if (rename(tmp_cib, cib_path) < 0) { crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_cib, cib_path); exit_rc = pcmk_err_cib_save; } if (rename(tmp_digest, digest_path) < 0) { crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_digest, digest_path); exit_rc = pcmk_err_cib_save; } pcmk__sync_directory(cib_dirname); cleanup: free(cib_path); free(digest_path); free(digest); free(tmp_digest); free(tmp_cib); return exit_rc; } /*! * \internal * \brief Process requests in a CIB transaction * * Stop when a request fails or when all requests have been processed. * * \param[in,out] cib CIB client * \param[in,out] transaction CIB transaction * * \return Standard Pacemaker return code */ static int cib_file_process_transaction_requests(cib_t *cib, xmlNode *transaction) { cib_file_opaque_t *private = cib->variant_opaque; for (xmlNode *request = pcmk__xe_first_child(transaction, PCMK__XE_CIB_COMMAND, NULL, NULL); request != NULL; request = pcmk__xe_next_same(request)) { xmlNode *output = NULL; const char *op = crm_element_value(request, PCMK__XA_CIB_OP); int rc = cib_file_process_request(cib, request, &output); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { crm_err("Aborting transaction for CIB file client (%s) on file " "'%s' due to failed %s request: %s", private->id, private->filename, op, pcmk_rc_str(rc)); crm_log_xml_info(request, "Failed request"); return rc; } crm_trace("Applied %s request to transaction working CIB for CIB file " "client (%s) on file '%s'", op, private->id, private->filename); crm_log_xml_trace(request, "Successful request"); } return pcmk_rc_ok; } /*! * \internal * \brief Commit a given CIB file client's transaction to a working CIB copy * * \param[in,out] cib CIB file client * \param[in] transaction CIB transaction * \param[in,out] result_cib Where to store result CIB * * \return Standard Pacemaker return code * * \note The caller is responsible for replacing the \p cib argument's * \p private->cib_xml with \p result_cib on success, and for freeing * \p result_cib using \p free_xml() on failure. */ static int cib_file_commit_transaction(cib_t *cib, xmlNode *transaction, xmlNode **result_cib) { int rc = pcmk_rc_ok; cib_file_opaque_t *private = cib->variant_opaque; xmlNode *saved_cib = private->cib_xml; CRM_CHECK(pcmk__xe_is(transaction, PCMK__XE_CIB_TRANSACTION), return pcmk_rc_no_transaction); /* *result_cib should be a copy of private->cib_xml (created by * cib_perform_op()). If not, make a copy now. Change tracking isn't * strictly required here because: * * Each request in the transaction will have changes tracked and ACLs * checked if appropriate. * * cib_perform_op() will infer changes for the commit request at the end. */ CRM_CHECK((*result_cib != NULL) && (*result_cib != private->cib_xml), *result_cib = pcmk__xml_copy(NULL, private->cib_xml)); crm_trace("Committing transaction for CIB file client (%s) on file '%s' to " "working CIB", private->id, private->filename); // Apply all changes to a working copy of the CIB private->cib_xml = *result_cib; rc = cib_file_process_transaction_requests(cib, transaction); crm_trace("Transaction commit %s for CIB file client (%s) on file '%s'", ((rc == pcmk_rc_ok)? "succeeded" : "failed"), private->id, private->filename); /* Some request types (for example, erase) may have freed private->cib_xml * (the working copy) and pointed it at a new XML object. In that case, it * follows that *result_cib (the working copy) was freed. * * Point *result_cib at the updated working copy stored in private->cib_xml. */ *result_cib = private->cib_xml; // Point private->cib_xml back to the unchanged original copy private->cib_xml = saved_cib; return rc; } static int cib_file_process_commit_transaction(const char *op, int options, const char *section, xmlNode *req, xmlNode *input, xmlNode *existing_cib, xmlNode **result_cib, xmlNode **answer) { int rc = pcmk_rc_ok; const char *client_id = crm_element_value(req, PCMK__XA_CIB_CLIENTID); cib_t *cib = NULL; CRM_CHECK(client_id != NULL, return -EINVAL); cib = get_client(client_id); CRM_CHECK(cib != NULL, return -EINVAL); rc = cib_file_commit_transaction(cib, input, result_cib); if (rc != pcmk_rc_ok) { cib_file_opaque_t *private = cib->variant_opaque; crm_err("Could not commit transaction for CIB file client (%s) on " "file '%s': %s", private->id, private->filename, pcmk_rc_str(rc)); } return pcmk_rc2legacy(rc); } diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c index 44077e5ae4..81aa12e6f2 100644 --- a/lib/cib/cib_utils.c +++ b/lib/cib/cib_utils.c @@ -1,1097 +1,1098 @@ /* * 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, xml_latest_schema()); + 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); copy_in_properties(top, scratch); 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); /* 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 && !validate_xml(scratch, NULL, true)) { + 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) { copy_in_properties(generation, the_cib); 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/crmcommon_private.h b/lib/common/crmcommon_private.h index e052599543..2036554fb3 100644 --- a/lib/common/crmcommon_private.h +++ b/lib/common/crmcommon_private.h @@ -1,419 +1,419 @@ /* * Copyright 2018-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 CRMCOMMON_PRIVATE__H # define CRMCOMMON_PRIVATE__H /* This header is for the sole use of libcrmcommon, so that functions can be * declared with G_GNUC_INTERNAL for efficiency. */ #include // uint8_t, uint32_t #include // bool #include // size_t #include // gchar, GList #include // xmlNode, xmlAttr #include // struct qb_ipc_response_header // Decent chunk size for processing large amounts of data #define PCMK__BUFFER_SIZE 4096 #if defined(PCMK__UNIT_TESTING) #undef G_GNUC_INTERNAL #define G_GNUC_INTERNAL #endif /* When deleting portions of an XML tree, we keep a record so we can know later * (e.g. when checking differences) that something was deleted. */ typedef struct pcmk__deleted_xml_s { gchar *path; int position; } pcmk__deleted_xml_t; typedef struct xml_node_private_s { uint32_t check; uint32_t flags; } xml_node_private_t; typedef struct xml_doc_private_s { uint32_t check; uint32_t flags; char *user; GList *acls; GList *deleted_objs; // List of pcmk__deleted_xml_t } xml_doc_private_t; // XML entity references #define PCMK__XML_ENTITY_AMP "&" #define PCMK__XML_ENTITY_GT ">" #define PCMK__XML_ENTITY_LT "<" #define PCMK__XML_ENTITY_QUOT """ //! libxml2 supports only XML version 1.0, at least as of libxml2-2.12.5 #define PCMK__XML_VERSION ((pcmkXmlStr) "1.0") #define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_set), #flags_to_set); \ } while (0) #define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ (flags_to_clear), #flags_to_clear); \ } while (0) G_GNUC_INTERNAL bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); G_GNUC_INTERNAL void pcmk__xml_mark_created(xmlNode *xml); G_GNUC_INTERNAL int pcmk__xml_position(const xmlNode *xml, enum xml_private_flags ignore_if_set); G_GNUC_INTERNAL xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, bool exact); G_GNUC_INTERNAL void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, bool as_diff); G_GNUC_INTERNAL xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, bool exact); G_GNUC_INTERNAL void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); G_GNUC_INTERNAL void pcmk__free_acls(GList *acls); G_GNUC_INTERNAL void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); G_GNUC_INTERNAL bool pcmk__is_user_in_group(const char *user, const char *group); G_GNUC_INTERNAL void pcmk__apply_acl(xmlNode *xml); G_GNUC_INTERNAL void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); G_GNUC_INTERNAL void pcmk__mark_xml_attr_dirty(xmlAttr *a); G_GNUC_INTERNAL bool pcmk__xa_filterable(const char *name); G_GNUC_INTERNAL void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) G_GNUC_PRINTF(2, 3); G_GNUC_INTERNAL void pcmk__mark_xml_node_dirty(xmlNode *xml); G_GNUC_INTERNAL bool pcmk__marked_as_deleted(xmlAttrPtr a, void *user_data); G_GNUC_INTERNAL void pcmk__dump_xml_attr(const xmlAttr *attr, GString *buffer); /* * Date/times */ // For use with pcmk__add_time_from_xml() enum pcmk__time_component { pcmk__time_unknown, pcmk__time_years, pcmk__time_months, pcmk__time_weeks, pcmk__time_days, pcmk__time_hours, pcmk__time_minutes, pcmk__time_seconds, }; G_GNUC_INTERNAL const char *pcmk__time_component_attr(enum pcmk__time_component component); G_GNUC_INTERNAL int pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component, const xmlNode *xml); G_GNUC_INTERNAL void pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source); /* * IPC */ #define PCMK__IPC_VERSION 1 #define PCMK__CONTROLD_API_MAJOR "1" #define PCMK__CONTROLD_API_MINOR "0" // IPC behavior that varies by daemon typedef struct pcmk__ipc_methods_s { /*! * \internal * \brief Allocate any private data needed by daemon IPC * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*new_data)(pcmk_ipc_api_t *api); /*! * \internal * \brief Free any private data used by daemon IPC * * \param[in,out] api_data Data allocated by new_data() method */ void (*free_data)(void *api_data); /*! * \internal * \brief Perform daemon-specific handling after successful connection * * Some daemons require clients to register before sending any other * commands. The controller requires a CRM_OP_HELLO (with no reply), and * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a * reply). Ideally this would be consistent across all daemons, but for now * this allows each to do its own authorization. * * \param[in,out] api IPC API connection * * \return Standard Pacemaker return code */ int (*post_connect)(pcmk_ipc_api_t *api); /*! * \internal * \brief Check whether an IPC request results in a reply * * \param[in,out] api IPC API connection * \param[in] request IPC request XML * * \return true if request would result in an IPC reply, false otherwise */ bool (*reply_expected)(pcmk_ipc_api_t *api, const xmlNode *request); /*! * \internal * \brief Perform daemon-specific handling of an IPC message * * \param[in,out] api IPC API connection * \param[in,out] msg Message read from IPC connection * * \return true if more IPC reply messages should be expected */ bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); /*! * \internal * \brief Perform daemon-specific handling of an IPC disconnect * * \param[in,out] api IPC API connection */ void (*post_disconnect)(pcmk_ipc_api_t *api); } pcmk__ipc_methods_t; // Implementation of pcmk_ipc_api_t struct pcmk_ipc_api_s { enum pcmk_ipc_server server; // Daemon this IPC API instance is for enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched size_t ipc_size_max; // maximum IPC buffer size crm_ipc_t *ipc; // IPC connection mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC bool free_on_disconnect; // Whether disconnect should free object pcmk_ipc_callback_t cb; // Caller-registered callback (if any) void *user_data; // Caller-registered data (if any) void *api_data; // For daemon-specific use pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon }; typedef struct pcmk__ipc_header_s { struct qb_ipc_response_header qb; uint32_t size_uncompressed; uint32_t size_compressed; uint32_t flags; uint8_t version; } pcmk__ipc_header_t; G_GNUC_INTERNAL int pcmk__send_ipc_request(pcmk_ipc_api_t *api, const xmlNode *request); G_GNUC_INTERNAL void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, crm_exit_t status, void *event_data); G_GNUC_INTERNAL unsigned int pcmk__ipc_buffer_size(unsigned int max); G_GNUC_INTERNAL bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__attrd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__controld_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); G_GNUC_INTERNAL pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); /* * Logging */ //! XML is newly created #define PCMK__XML_PREFIX_CREATED "++" //! XML has been deleted #define PCMK__XML_PREFIX_DELETED "--" //! XML has been modified #define PCMK__XML_PREFIX_MODIFIED "+ " //! XML has been moved #define PCMK__XML_PREFIX_MOVED "+~" /* * Output */ G_GNUC_INTERNAL int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, const char *filename, char **argv); G_GNUC_INTERNAL void pcmk__register_option_messages(pcmk__output_t *out); G_GNUC_INTERNAL void pcmk__register_patchset_messages(pcmk__output_t *out); G_GNUC_INTERNAL bool pcmk__output_text_get_fancy(pcmk__output_t *out); /* * Rules */ // How node attribute values may be compared in rules enum pcmk__comparison { pcmk__comparison_unknown, pcmk__comparison_defined, pcmk__comparison_undefined, pcmk__comparison_eq, pcmk__comparison_ne, pcmk__comparison_lt, pcmk__comparison_lte, pcmk__comparison_gt, pcmk__comparison_gte, }; // How node attribute values may be parsed in rules enum pcmk__type { pcmk__type_unknown, pcmk__type_string, pcmk__type_integer, pcmk__type_number, pcmk__type_version, }; // Where to obtain reference value for a node attribute comparison enum pcmk__reference_source { pcmk__source_unknown, pcmk__source_literal, pcmk__source_instance_attrs, pcmk__source_meta_attrs, }; G_GNUC_INTERNAL enum pcmk__comparison pcmk__parse_comparison(const char *op); G_GNUC_INTERNAL enum pcmk__type pcmk__parse_type(const char *type, enum pcmk__comparison op, const char *value1, const char *value2); G_GNUC_INTERNAL enum pcmk__reference_source pcmk__parse_source(const char *source); G_GNUC_INTERNAL int pcmk__cmp_by_type(const char *value1, const char *value2, enum pcmk__type type); G_GNUC_INTERNAL int pcmk__unpack_duration(const xmlNode *duration, const crm_time_t *start, crm_time_t **end); G_GNUC_INTERNAL int pcmk__evaluate_date_spec(const xmlNode *date_spec, const crm_time_t *now); G_GNUC_INTERNAL int pcmk__evaluate_attr_expression(const xmlNode *expression, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_rsc_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); G_GNUC_INTERNAL int pcmk__evaluate_op_expression(const xmlNode *expr, const pcmk_rule_input_t *rule_input); /* * Utils */ #define PCMK__PW_BUFFER_LEN 500 /* * Schemas */ typedef struct { unsigned char v[2]; } pcmk__schema_version_t; enum pcmk__schema_validator { pcmk__schema_validator_none, pcmk__schema_validator_rng }; typedef struct { int schema_index; char *name; char *transform; void *cache; enum pcmk__schema_validator validator; pcmk__schema_version_t version; char *transform_enter; bool transform_onleave; } pcmk__schema_t; G_GNUC_INTERNAL -int pcmk__find_x_0_schema_index(void); +GList *pcmk__find_x_0_schema(void); #endif // CRMCOMMON_PRIVATE__H diff --git a/lib/common/schemas.c b/lib/common/schemas.c index a75f719e96..c5a515e728 100644 --- a/lib/common/schemas.c +++ b/lib/common/schemas.c @@ -1,1654 +1,1717 @@ /* * 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 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. */ return g_list_length(known_schemas) - 3; } -/* Return the index of the most recent X.0 schema. */ -int -pcmk__find_x_0_schema_index(void) +/*! + * \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) { - /* We can't just use best to determine whether we've found the index - * or not. What if we have a very long list of schemas all in the - * same major version series? We'd return 0 for that, which means - * we would still run this function every time. + /* 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, these can't be static because they'll stick - * around from one test run to the next. They need to be cleared out + /* 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. */ - bool found = false; - int best = 0; + GList *x_0_entry = NULL; #else - static bool found = false; - static int best = 0; + static GList *x_0_entry = NULL; #endif - int i; - GList *best_node = NULL; - pcmk__schema_t *best_schema = NULL; - if (found) { - return best; - } + pcmk__schema_t *highest_schema = NULL; - CRM_ASSERT(known_schemas != NULL); - - /* Get the most recent schema so we can look at its version number. */ - best = xml_latest_schema_index(); - best_node = g_list_nth(known_schemas, best); - best_schema = best_node->data; - - /* The "pacemaker-next" and "none" schemas are added to the real schemas, - * so a list of length three actually only has one useful schema. - * - * @COMPAT pacemaker-next is deprecated since 2.1.5. - * Update this when we drop that schema. - */ - if (g_list_length(known_schemas) == 3) { - goto done; + if (x_0_entry != NULL) { + return x_0_entry; } + x_0_entry = get_highest_schema(); + highest_schema = x_0_entry->data; - /* Start comparing the list from the node before the best schema (there's - * no point in comparing something to itself). Then, 'i' is an index - * starting at the best schema and will always point at the node after - * 'iter'. This makes it the value we want to return when we find what - * we're looking for. - */ - i = best; - - for (GList *iter = best_node->prev; iter != NULL; iter = iter->prev) { + 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 best schema. + * the highest schema. */ - if (schema->version.v[0] < best_schema->version.v[0]) { - best = i; - goto done; + 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 index 0. + * one major version series, so return the first schema entry. */ - } else if (iter->prev == NULL) { - best = 0; - goto done; + if (iter->prev == NULL) { + x_0_entry = known_schemas->data; + break; } - - i--; } - -done: - found = true; - return best; -} - -const char * -xml_latest_schema(void) -{ - return get_schema_name(xml_latest_schema_index()); + 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; 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) { 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; 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); 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 gboolean -validate_with_relaxng(xmlDocPtr doc, xmlRelaxNGValidityErrorFunc error_handler, void *error_handler_context, const char *relaxng_file, +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; - gboolean valid = TRUE; + bool valid = true; relaxng_ctx_cache_t *ctx = NULL; - CRM_CHECK(doc != NULL, return FALSE); - CRM_CHECK(relaxng_file != NULL, return FALSE); + 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; + 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; 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) { 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 gboolean -validate_with(xmlNode *xml, pcmk__schema_t *schema, xmlRelaxNGValidityErrorFunc error_handler, void* error_handler_context) +static bool +validate_with(xmlNode *xml, pcmk__schema_t *schema, + xmlRelaxNGValidityErrorFunc error_handler, + void *error_handler_context) { - gboolean valid = FALSE; + bool valid = false; char *file = NULL; relaxng_ctx_cache_t **cache = NULL; if (schema == NULL) { - return FALSE; + return false; } if (schema->validator == pcmk__schema_validator_none) { - return TRUE; + 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; } -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 = validate_xml(xml, NULL, FALSE); - free_xml(xml); - - unlink(filename); - free(filename); - - return rc; -} - -gboolean -validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs) -{ - return pcmk__validate_xml(xml_blob, validation, to_logs ? (xmlRelaxNGValidityErrorFunc) xml_log : NULL, GUINT_TO_POINTER(LOG_ERR)); -} - -gboolean +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); + CRM_CHECK((xml_blob != NULL) && (xml_blob->doc != NULL), return false); if (validation == NULL) { validation = crm_element_value(xml_blob, PCMK_XA_VALIDATE_WITH); } if (validation == NULL) { - bool valid = FALSE; + 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; + 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); } crm_err("Unknown validator: %s", validation); - return FALSE; + return false; +} + +/*! + * \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; } -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; -} - /*! * \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); 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) { 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; } -gboolean -cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) +/*! + * \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) { - gboolean rc = TRUE; + bool rc = true; char *original_schema_name = NULL; - int version = 0; - int orig_version = 0; - int min_version = pcmk__find_x_0_schema_index(); + 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); - version = get_schema_version(original_schema_name); - orig_version = version; - if (version < min_version) { + 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); - version = get_schema_version(new_schema_name); - } else { - version = 0; + entry = pcmk__get_schema(new_schema_name); } + schema = (entry == NULL)? NULL : entry->data; - if (version < min_version) { + if ((schema == NULL) + || (schema->schema_index < x_0_schema->schema_index)) { // Updated configuration schema is still not acceptable - if (version < orig_version || orig_version == -1) { + 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 %s", + "%s to the latest", pcmk__s(original_schema_name, "no"), - get_schema_name(min_version), - get_schema_name(orig_version), - xml_latest_schema()); + 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 %s\n", + "%s to the latest\n", pcmk__s(original_schema_name, "no"), - get_schema_name(min_version), - get_schema_name(orig_version), - xml_latest_schema()); + 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"), - get_schema_name(min_version), + 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"), - get_schema_name(min_version), + x_0_schema->name, pcmk__s(new_schema_name, "unspecified version")); } } free_xml(converted); converted = NULL; - rc = FALSE; + rc = false; } else { // Updated configuration schema is acceptable free_xml(*xml); *xml = converted; - if (version < xml_latest_schema_index()) { + 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"), - get_schema_name(version)); - } - } else { - if (to_logs) { - crm_info("Configuration with %s schema was internally " - "upgraded to latest version %s", - pcmk__s(original_schema_name, "no"), - get_schema_name(version)); + 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 if (version >= get_schema_version(PCMK_VALUE_NONE)) { - // Schema validation is disabled - if (to_logs) { - pcmk__config_warn("Schema validation of configuration is disabled " - "(enabling is encouraged and prevents common " - "misconfigurations)"); + } else { + pcmk__schema_t *none_schema = NULL; - } else { - fprintf(stderr, "Schema validation of configuration is disabled " - "(enabling is encouraged and prevents common " - "misconfigurations)\n"); - } - } + 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)"); - if (best_version) { - *best_version = version; + } else { + fprintf(stderr, "Schema validation of configuration is " + "disabled (enabling is encouraged and " + "prevents common misconfigurations)\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; } // Deprecated functions kept only for backward API compatibility // LCOV_EXCL_START -#include +#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/Makefile.am b/lib/common/tests/schemas/Makefile.am index d13349568d..ba0f8054bc 100644 --- a/lib/common/tests/schemas/Makefile.am +++ b/lib/common/tests/schemas/Makefile.am @@ -1,90 +1,88 @@ # # 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 $(top_srcdir)/mk/tap.mk include $(top_srcdir)/mk/unittest.mk CFLAGS += -DPCMK__TEST_SCHEMA_DIR='"$(abs_builddir)/schemas"' # Add "_test" to the end of all test program names to simplify .gitignore. # These tests share a schema subdirectory SHARED_SCHEMA_TESTS = pcmk__cmp_schemas_by_name_test \ crm_schema_init_test \ - get_schema_name_test \ - get_schema_version_test \ pcmk__build_schema_xml_node_test \ pcmk__get_schema_test \ pcmk__schema_files_later_than_test # This test has its own schema directory -FIND_X_0_SCHEMA_TEST = pcmk__find_x_0_schema_index_test +FIND_X_0_SCHEMA_TEST = pcmk__find_x_0_schema_test check_PROGRAMS = $(SHARED_SCHEMA_TESTS) $(FIND_X_0_SCHEMA_TEST) TESTS = $(check_PROGRAMS) $(SHARED_SCHEMA_TESTS): setup-schema-dir $(FIND_X_0_SCHEMA_TEST): setup-find_x_0-schema-dir # Set up a temporary schemas/ directory containing only some of the full set of # pacemaker schema files. This lets us know exactly how many schemas are present, # allowing us to write tests without having to make changes when new schemas are # added. # # This directory contains the following: # # * pacemaker-next.rng - Used to verify that this sorts before all versions # * upgrade-*.xsl - Required by various schema versions # * pacemaker-[0-9]*.rng - We're only pulling in 15 schemas, which is enough # to get everything through pacemaker-3.0.rng. This # includes 2.10, needed so we can check that versions # are compared as numbers instead of strings. # * other RNG files - This catches everything except the pacemaker-*rng # files. These files are included by the top-level # pacemaker-*rng files, so we need them for tests. # This will glob more than we need, but the extra ones # won't get in the way. LINK_FILES = $(abs_top_builddir)/xml/pacemaker-next.rng \ $(abs_top_builddir)/xml/upgrade-*.xsl ROOT_RNGS = $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15) INCLUDED_RNGS = $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker-[0-9]) # Most tests share a common, read-only schema directory .PHONY: setup-schema-dir setup-schema-dir: $(MKDIR_P) schemas ( cd schemas ; \ ln -sf $(LINK_FILES) . ; \ for f in $(ROOT_RNGS); do \ ln -sf $$f $$(basename $$f); \ done ; \ for f in $(INCLUDED_RNGS); do \ ln -sf ../$$f $$(basename $$f); \ done ) -# pcmk__find_x_0_schema_index_test moves schema files around, so it needs its +# pcmk__find_x_0_schema_test moves schema files around, so it needs its # own directory, otherwise other tests run in parallel could fail. .PHONY: setup-find_x_0-schema-dir setup-find_x_0-schema-dir: $(MKDIR_P) schemas/find_x_0 ( cd schemas/find_x_0 ; \ ln -sf $(LINK_FILES) . ; \ for f in $(ROOT_RNGS); do \ ln -sf $$f $$(basename $$f); \ done ; \ for f in $(INCLUDED_RNGS); do \ ln -sf ../$$f $$(basename $$f); \ done ) .PHONY: clean-local clean-local: -rm -rf schemas diff --git a/lib/common/tests/schemas/crm_schema_init_test.c b/lib/common/tests/schemas/crm_schema_init_test.c index f0a7054652..10fc6dab97 100644 --- a/lib/common/tests/schemas/crm_schema_init_test.c +++ b/lib/common/tests/schemas/crm_schema_init_test.c @@ -1,135 +1,151 @@ /* * 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_string_equal("pacemaker-1.0", get_schema_name(0)); - assert_string_equal("pacemaker-1.2", get_schema_name(1)); - assert_string_equal("pacemaker-2.0", get_schema_name(3)); - assert_string_equal("pacemaker-3.0", get_schema_name(14)); - assert_string_equal("pacemaker-3.1", get_schema_name(15)); - assert_string_equal("pacemaker-3.2", get_schema_name(16)); + 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_string_equal("pacemaker-next", get_schema_name(17)); + assert_schema("pacemaker-next", 17); - assert_string_equal(PCMK_VALUE_NONE, get_schema_name(18)); + assert_schema(PCMK_VALUE_NONE, 18); } PCMK__UNIT_TEST(setup, teardown, cmocka_unit_test(extra_schema_files)); diff --git a/lib/common/tests/schemas/get_schema_name_test.c b/lib/common/tests/schemas/get_schema_name_test.c deleted file mode 100644 index f1eb9fe0ef..0000000000 --- a/lib/common/tests/schemas/get_schema_name_test.c +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 - -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 -bad_input(void **state) -{ - assert_string_equal("unknown", get_schema_name(-1)); - assert_string_equal("unknown", get_schema_name(47000)); -} - -static void -typical_usage(void **state) -{ - assert_string_equal("pacemaker-1.0", get_schema_name(0)); - assert_string_equal("pacemaker-1.2", get_schema_name(1)); - assert_string_equal("pacemaker-2.0", get_schema_name(3)); - assert_string_equal("pacemaker-2.5", get_schema_name(8)); - assert_string_equal("pacemaker-3.0", get_schema_name(14)); - - // @COMPAT pacemaker-next is deprecated since 2.1.5 - assert_string_equal("pacemaker-next", get_schema_name(15)); - - assert_string_equal(PCMK_VALUE_NONE, get_schema_name(16)); -} - -PCMK__UNIT_TEST(setup, teardown, - cmocka_unit_test(bad_input), - cmocka_unit_test(typical_usage)); diff --git a/lib/common/tests/schemas/get_schema_version_test.c b/lib/common/tests/schemas/get_schema_version_test.c deleted file mode 100644 index 863c6687ae..0000000000 --- a/lib/common/tests/schemas/get_schema_version_test.c +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 - -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 -bad_input(void **state) -{ - assert_int_equal(-1, get_schema_version("")); - assert_int_equal(-1, get_schema_version("blahblah")); - assert_int_equal(-1, get_schema_version("pacemaker-2.47")); - assert_int_equal(-1, get_schema_version("pacemaker-47.0")); -} - -static void -typical_usage(void **state) -{ - assert_int_equal(0, get_schema_version("pacemaker-1.0")); - assert_int_equal(0, get_schema_version("PACEMAKER-1.0")); - assert_int_equal(1, get_schema_version("pacemaker-1.2")); - assert_int_equal(3, get_schema_version("pacemaker-2.0")); - assert_int_equal(3, get_schema_version("pAcEmAkEr-2.0")); - assert_int_equal(8, get_schema_version("pacemaker-2.5")); - assert_int_equal(14, get_schema_version("pacemaker-3.0")); - assert_int_equal(14, get_schema_version("paceMAKER-3.0")); - - // @COMPAT pacemaker-next is deprecated since 2.1.5 - assert_int_equal(15, get_schema_version("pacemaker-next")); - - assert_int_equal(16, get_schema_version("none")); - assert_int_equal(16, get_schema_version("NONE")); - assert_int_equal(16, get_schema_version(NULL)); // defaults to "none" -} - -PCMK__UNIT_TEST(setup, teardown, - cmocka_unit_test(bad_input), - cmocka_unit_test(typical_usage)); diff --git a/lib/common/tests/schemas/pcmk__find_x_0_schema_index_test.c b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c similarity index 82% rename from lib/common/tests/schemas/pcmk__find_x_0_schema_index_test.c rename to lib/common/tests/schemas/pcmk__find_x_0_schema_test.c index 4d094f39f3..25ba0f3314 100644 --- a/lib/common/tests/schemas/pcmk__find_x_0_schema_index_test.c +++ b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c @@ -1,93 +1,100 @@ /* * 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 // NULL, rename() #include // setenv(), unsetenv() #include #include #include "crmcommon_private.h" #define SCHEMA_PREFIX PCMK__TEST_SCHEMA_DIR "/find_x_0/pacemaker-" static int setup(void **state) { // Use a unique schema directory so we can move files around setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR "/find_x_0", 1); return 0; } static int teardown(void **state) { unsetenv("PCMK_schema_directory"); return 0; } +static void +assert_schema_0(int schema_index, const char *schema_name) +{ + GList *entry = NULL; + pcmk__schema_t *schema = NULL; + + entry = pcmk__find_x_0_schema(); + assert_non_null(entry); + + schema = entry->data; + assert_non_null(schema); + + assert_int_equal(schema->schema_index, schema_index); + assert_string_equal(schema->name, schema_name); +} + static void last_is_0(void **state) { /* This loads all the schemas normally linked for unit testing, so we have * many 1.x and 2.x schemas and a single pacemaker-3.0 schema at index 14. */ crm_schema_init(); - - assert_int_equal(14, pcmk__find_x_0_schema_index()); - assert_string_equal(get_schema_name(14), "pacemaker-3.0"); - + assert_schema_0(14, "pacemaker-3.0"); crm_schema_cleanup(); } static void last_is_not_0(void **state) { /* Disable the pacemaker-3.0 schema, so we now should get pacemaker-2.0 at * index 3. */ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng", SCHEMA_PREFIX "3.0.bak")); crm_schema_init(); - - assert_int_equal(3, pcmk__find_x_0_schema_index()); - assert_string_equal(get_schema_name(3), "pacemaker-2.0"); - + assert_schema_0(3, "pacemaker-2.0"); assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak", SCHEMA_PREFIX "3.0.rng")); crm_schema_cleanup(); } static void schema_0_missing(void **state) { /* Disable the pacemaker-3.0 and pacemaker-2.0 schemas, so we now should get * pacemaker-2.1 at index 3. */ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng", SCHEMA_PREFIX "3.0.bak")); assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.rng", SCHEMA_PREFIX "2.0.bak")); crm_schema_init(); - - assert_int_equal(3, pcmk__find_x_0_schema_index()); - assert_string_equal(get_schema_name(3), "pacemaker-2.1"); - + assert_schema_0(3, "pacemaker-2.1"); assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.bak", SCHEMA_PREFIX "2.0.rng")); assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak", SCHEMA_PREFIX "3.0.rng")); crm_schema_cleanup(); } PCMK__UNIT_TEST(setup, teardown, cmocka_unit_test(last_is_0), cmocka_unit_test(last_is_not_0), cmocka_unit_test(schema_0_missing)) diff --git a/lib/pacemaker/pcmk_simulate.c b/lib/pacemaker/pcmk_simulate.c index 88f86fb835..74dcbde7b6 100644 --- a/lib/pacemaker/pcmk_simulate.c +++ b/lib/pacemaker/pcmk_simulate.c @@ -1,1013 +1,1013 @@ /* * Copyright 2021-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 "libpacemaker_private.h" static pcmk__output_t *out = NULL; static cib_t *fake_cib = NULL; static GList *fake_resource_list = NULL; static const GList *fake_op_fail_list = NULL; static void set_effective_date(pcmk_scheduler_t *scheduler, bool print_original, const char *use_date); /*! * \internal * \brief Create an action name for use in a dot graph * * \param[in] action Action to create name for * \param[in] verbose If true, add action ID to name * * \return Newly allocated string with action name * \note It is the caller's responsibility to free the result. */ static char * create_action_name(const pcmk_action_t *action, bool verbose) { char *action_name = NULL; const char *prefix = ""; const char *action_host = NULL; const char *clone_name = NULL; const char *task = action->task; if (action->node != NULL) { action_host = action->node->details->uname; } else if (!pcmk_is_set(action->flags, pcmk_action_pseudo)) { action_host = ""; } if (pcmk__str_eq(action->task, PCMK_ACTION_CANCEL, pcmk__str_none)) { prefix = "Cancel "; task = action->cancel_task; } if (action->rsc != NULL) { clone_name = action->rsc->clone_name; } if (clone_name != NULL) { char *key = NULL; guint interval_ms = 0; if (pcmk__guint_from_hash(action->meta, PCMK_META_INTERVAL, 0, &interval_ms) != pcmk_rc_ok) { interval_ms = 0; } if (pcmk__strcase_any_of(action->task, PCMK_ACTION_NOTIFY, PCMK_ACTION_NOTIFIED, NULL)) { const char *n_type = g_hash_table_lookup(action->meta, "notify_key_type"); const char *n_task = g_hash_table_lookup(action->meta, "notify_key_operation"); CRM_ASSERT(n_type != NULL); CRM_ASSERT(n_task != NULL); key = pcmk__notify_key(clone_name, n_type, n_task); } else { key = pcmk__op_key(clone_name, task, interval_ms); } if (action_host != NULL) { action_name = crm_strdup_printf("%s%s %s", prefix, key, action_host); } else { action_name = crm_strdup_printf("%s%s", prefix, key); } free(key); } else if (pcmk__str_eq(action->task, PCMK_ACTION_STONITH, pcmk__str_none)) { const char *op = g_hash_table_lookup(action->meta, PCMK__META_STONITH_ACTION); action_name = crm_strdup_printf("%s%s '%s' %s", prefix, action->task, op, action_host); } else if (action->rsc && action_host) { action_name = crm_strdup_printf("%s%s %s", prefix, action->uuid, action_host); } else if (action_host) { action_name = crm_strdup_printf("%s%s %s", prefix, action->task, action_host); } else { action_name = crm_strdup_printf("%s", action->uuid); } if (verbose) { char *with_id = crm_strdup_printf("%s (%d)", action_name, action->id); free(action_name); action_name = with_id; } return action_name; } /*! * \internal * \brief Display the status of a cluster * * \param[in,out] scheduler Scheduler data * \param[in] show_opts How to modify display (as pcmk_show_opt_e flags) * \param[in] section_opts Sections to display (as pcmk_section_e flags) * \param[in] title What to use as list title * \param[in] print_spacer Whether to display a spacer first */ static void print_cluster_status(pcmk_scheduler_t *scheduler, uint32_t show_opts, uint32_t section_opts, const char *title, bool print_spacer) { pcmk__output_t *out = scheduler->priv; GList *all = NULL; crm_exit_t stonith_rc = 0; enum pcmk_pacemakerd_state state = pcmk_pacemakerd_state_invalid; section_opts |= pcmk_section_nodes | pcmk_section_resources; show_opts |= pcmk_show_inactive_rscs | pcmk_show_failed_detail; all = g_list_prepend(all, (gpointer) "*"); PCMK__OUTPUT_SPACER_IF(out, print_spacer); out->begin_list(out, NULL, NULL, "%s", title); out->message(out, "cluster-status", scheduler, state, stonith_rc, NULL, pcmk__fence_history_none, section_opts, show_opts, NULL, all, all); out->end_list(out); g_list_free(all); } /*! * \internal * \brief Display a summary of all actions scheduled in a transition * * \param[in,out] scheduler Scheduler data (fully scheduled) * \param[in] print_spacer Whether to display a spacer first */ static void print_transition_summary(pcmk_scheduler_t *scheduler, bool print_spacer) { pcmk__output_t *out = scheduler->priv; PCMK__OUTPUT_SPACER_IF(out, print_spacer); out->begin_list(out, NULL, NULL, "Transition Summary"); pcmk__output_actions(scheduler); out->end_list(out); } /*! * \internal * \brief Reset scheduler input, output, date, and flags * * \param[in,out] scheduler Scheduler data * \param[in] input What to set as cluster input * \param[in] out What to set as cluster output object * \param[in] use_date What to set as cluster's current timestamp * \param[in] flags Group of enum pcmk_scheduler_flags to set */ static void reset(pcmk_scheduler_t *scheduler, xmlNodePtr input, pcmk__output_t *out, const char *use_date, unsigned int flags) { scheduler->input = input; scheduler->priv = out; set_effective_date(scheduler, true, use_date); if (pcmk_is_set(flags, pcmk_sim_sanitized)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_sanitized); } if (pcmk_is_set(flags, pcmk_sim_show_scores)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_output_scores); } if (pcmk_is_set(flags, pcmk_sim_show_utilization)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_show_utilization); } } /*! * \brief Write out a file in dot(1) format describing the actions that will * be taken by the scheduler in response to an input CIB file. * * \param[in,out] scheduler Scheduler data * \param[in] dot_file The filename to write * \param[in] all_actions Write all actions, even those that are optional * or are on unmanaged resources * \param[in] verbose Add extra information, such as action IDs, to the * output * * \return Standard Pacemaker return code */ static int write_sim_dotfile(pcmk_scheduler_t *scheduler, const char *dot_file, bool all_actions, bool verbose) { GList *iter = NULL; FILE *dot_strm = fopen(dot_file, "w"); if (dot_strm == NULL) { return errno; } fprintf(dot_strm, " digraph \"g\" {\n"); for (iter = scheduler->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = (pcmk_action_t *) iter->data; const char *style = "dashed"; const char *font = "black"; const char *color = "black"; char *action_name = create_action_name(action, verbose); if (pcmk_is_set(action->flags, pcmk_action_pseudo)) { font = "orange"; } if (pcmk_is_set(action->flags, pcmk_action_added_to_graph)) { style = PCMK__VALUE_BOLD; color = "green"; } else if ((action->rsc != NULL) && !pcmk_is_set(action->rsc->flags, pcmk_rsc_managed)) { color = "red"; font = "purple"; if (!all_actions) { goto do_not_write; } } else if (pcmk_is_set(action->flags, pcmk_action_optional)) { color = "blue"; if (!all_actions) { goto do_not_write; } } else { color = "red"; CRM_LOG_ASSERT(!pcmk_is_set(action->flags, pcmk_action_runnable)); } pcmk__set_action_flags(action, pcmk_action_added_to_graph); fprintf(dot_strm, "\"%s\" [ style=%s color=\"%s\" fontcolor=\"%s\"]\n", action_name, style, color, font); do_not_write: free(action_name); } for (iter = scheduler->actions; iter != NULL; iter = iter->next) { pcmk_action_t *action = (pcmk_action_t *) iter->data; for (GList *before_iter = action->actions_before; before_iter != NULL; before_iter = before_iter->next) { pcmk__related_action_t *before = before_iter->data; char *before_name = NULL; char *after_name = NULL; const char *style = "dashed"; bool optional = true; if (before->state == pe_link_dumped) { optional = false; style = PCMK__VALUE_BOLD; } else if ((uint32_t) before->type == pcmk__ar_none) { continue; } else if (pcmk_is_set(before->action->flags, pcmk_action_added_to_graph) && pcmk_is_set(action->flags, pcmk_action_added_to_graph) && (uint32_t) before->type != pcmk__ar_if_on_same_node_or_target) { optional = false; } if (all_actions || !optional) { before_name = create_action_name(before->action, verbose); after_name = create_action_name(action, verbose); fprintf(dot_strm, "\"%s\" -> \"%s\" [ style = %s]\n", before_name, after_name, style); free(before_name); free(after_name); } } } fprintf(dot_strm, "}\n"); fflush(dot_strm); fclose(dot_strm); return pcmk_rc_ok; } /*! * \brief Profile the configuration updates and scheduler actions in a single * CIB file, printing the profiling timings. * * \note \p scheduler->priv must have been set to a valid \p pcmk__output_t * object before this function is called. * * \param[in] xml_file The CIB file to profile * \param[in] repeat Number of times to run * \param[in,out] scheduler Scheduler data * \param[in] use_date The date to set the cluster's time to (may be NULL) */ static void profile_file(const char *xml_file, long long repeat, pcmk_scheduler_t *scheduler, const char *use_date) { pcmk__output_t *out = scheduler->priv; xmlNode *cib_object = NULL; clock_t start = 0; clock_t end; unsigned long long scheduler_flags = pcmk_sched_no_compat; CRM_ASSERT(out != NULL); cib_object = pcmk__xml_read(xml_file); start = clock(); if (pcmk_find_cib_element(cib_object, PCMK_XE_STATUS) == NULL) { pcmk__xe_create(cib_object, PCMK_XE_STATUS); } - if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) { + if (!pcmk__update_configured_schema(&cib_object, false)) { free_xml(cib_object); return; } - if (validate_xml(cib_object, NULL, FALSE) != TRUE) { + if (!pcmk__validate_xml(cib_object, NULL, NULL, NULL)) { free_xml(cib_object); return; } if (pcmk_is_set(scheduler->flags, pcmk_sched_output_scores)) { scheduler_flags |= pcmk_sched_output_scores; } if (pcmk_is_set(scheduler->flags, pcmk_sched_show_utilization)) { scheduler_flags |= pcmk_sched_show_utilization; } for (int i = 0; i < repeat; ++i) { xmlNode *input = cib_object; if (repeat > 1) { input = pcmk__xml_copy(NULL, cib_object); } scheduler->input = input; set_effective_date(scheduler, false, use_date); pcmk__schedule_actions(input, scheduler_flags, scheduler); pe_reset_working_set(scheduler); } end = clock(); out->message(out, "profile", xml_file, start, end); } void pcmk__profile_dir(const char *dir, long long repeat, pcmk_scheduler_t *scheduler, const char *use_date) { pcmk__output_t *out = scheduler->priv; struct dirent **namelist; int file_num = scandir(dir, &namelist, 0, alphasort); CRM_ASSERT(out != NULL); if (file_num > 0) { struct stat prop; char buffer[FILENAME_MAX]; out->begin_list(out, NULL, NULL, "Timings"); while (file_num--) { if ('.' == namelist[file_num]->d_name[0]) { free(namelist[file_num]); continue; } else if (!pcmk__ends_with_ext(namelist[file_num]->d_name, ".xml")) { free(namelist[file_num]); continue; } snprintf(buffer, sizeof(buffer), "%s/%s", dir, namelist[file_num]->d_name); if (stat(buffer, &prop) == 0 && S_ISREG(prop.st_mode)) { profile_file(buffer, repeat, scheduler, use_date); } free(namelist[file_num]); } free(namelist); out->end_list(out); } } /*! * \brief Set the date of the cluster, either to the value given by * \p use_date, or to the \c PCMK_XA_EXECUTION_DATE value in the CIB. * * \note \p scheduler->priv must have been set to a valid \p pcmk__output_t * object before this function is called. * * \param[in,out] scheduler Scheduler data * \param[in] print_original If \p true, the \c PCMK_XA_EXECUTION_DATE * should also be printed * \param[in] use_date The date to set the cluster's time to * (may be NULL) */ static void set_effective_date(pcmk_scheduler_t *scheduler, bool print_original, const char *use_date) { pcmk__output_t *out = scheduler->priv; time_t original_date = 0; CRM_ASSERT(out != NULL); crm_element_value_epoch(scheduler->input, PCMK_XA_EXECUTION_DATE, &original_date); if (use_date) { scheduler->now = crm_time_new(use_date); out->info(out, "Setting effective cluster time: %s", use_date); crm_time_log(LOG_NOTICE, "Pretending 'now' is", scheduler->now, crm_time_log_date | crm_time_log_timeofday); } else if (original_date != 0) { scheduler->now = pcmk__copy_timet(original_date); if (print_original) { char *when = crm_time_as_string(scheduler->now, crm_time_log_date|crm_time_log_timeofday); out->info(out, "Using the original execution date of: %s", when); free(when); } } } /*! * \internal * \brief Simulate successfully executing a pseudo-action in a graph * * \param[in,out] graph Graph to update with pseudo-action result * \param[in,out] action Pseudo-action to simulate executing * * \return Standard Pacemaker return code */ static int simulate_pseudo_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { const char *node = crm_element_value(action->xml, PCMK__META_ON_NODE); const char *task = crm_element_value(action->xml, PCMK__XA_OPERATION_KEY); pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); out->message(out, "inject-pseudo-action", node, task); pcmk__update_graph(graph, action); return pcmk_rc_ok; } /*! * \internal * \brief Simulate executing a resource action in a graph * * \param[in,out] graph Graph to update with resource action result * \param[in,out] action Resource action to simulate executing * * \return Standard Pacemaker return code */ static int simulate_resource_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { int rc; lrmd_event_data_t *op = NULL; int target_outcome = PCMK_OCF_OK; const char *rtype = NULL; const char *rclass = NULL; const char *resource = NULL; const char *rprovider = NULL; const char *resource_config_name = NULL; const char *operation = crm_element_value(action->xml, PCMK_XA_OPERATION); const char *target_rc_s = crm_meta_value(action->params, PCMK__META_OP_TARGET_RC); xmlNode *cib_node = NULL; xmlNode *cib_resource = NULL; xmlNode *action_rsc = pcmk__xe_first_child(action->xml, PCMK_XE_PRIMITIVE, NULL, NULL); char *node = crm_element_value_copy(action->xml, PCMK__META_ON_NODE); char *uuid = NULL; const char *router_node = crm_element_value(action->xml, PCMK__XA_ROUTER_NODE); // Certain actions don't need to be displayed or history entries if (pcmk__str_eq(operation, CRM_OP_REPROBE, pcmk__str_none)) { crm_debug("No history injection for %s op on %s", operation, node); goto done; // Confirm action and update graph } if (action_rsc == NULL) { // Shouldn't be possible crm_log_xml_err(action->xml, "Bad"); free(node); return EPROTO; } /* A resource might be known by different names in the configuration and in * the action (for example, a clone instance). Grab the configuration name * (which is preferred when writing history), and if necessary, the instance * name. */ resource_config_name = crm_element_value(action_rsc, PCMK_XA_ID); if (resource_config_name == NULL) { // Shouldn't be possible crm_log_xml_err(action->xml, "No ID"); free(node); return EPROTO; } resource = resource_config_name; if (pe_find_resource(fake_resource_list, resource) == NULL) { const char *longname = crm_element_value(action_rsc, PCMK__XA_LONG_ID); if ((longname != NULL) && (pe_find_resource(fake_resource_list, longname) != NULL)) { resource = longname; } } // Certain actions need to be displayed but don't need history entries if (pcmk__strcase_any_of(operation, PCMK_ACTION_DELETE, PCMK_ACTION_META_DATA, NULL)) { out->message(out, "inject-rsc-action", resource, operation, node, (guint) 0); goto done; // Confirm action and update graph } rclass = crm_element_value(action_rsc, PCMK_XA_CLASS); rtype = crm_element_value(action_rsc, PCMK_XA_TYPE); rprovider = crm_element_value(action_rsc, PCMK_XA_PROVIDER); pcmk__scan_min_int(target_rc_s, &target_outcome, 0); CRM_ASSERT(fake_cib->cmds->query(fake_cib, NULL, NULL, cib_sync_call|cib_scope_local) == pcmk_ok); // Ensure the action node is in the CIB uuid = crm_element_value_copy(action->xml, PCMK__META_ON_NODE_UUID); cib_node = pcmk__inject_node(fake_cib, node, ((router_node == NULL)? uuid: node)); free(uuid); CRM_ASSERT(cib_node != NULL); // Add a history entry for the action cib_resource = pcmk__inject_resource_history(out, cib_node, resource, resource_config_name, rclass, rtype, rprovider); if (cib_resource == NULL) { crm_err("Could not simulate action %d history for resource %s", action->id, resource); free(node); free_xml(cib_node); return EINVAL; } // Simulate and display an executor event for the action result op = pcmk__event_from_graph_action(cib_resource, action, PCMK_EXEC_DONE, target_outcome, "User-injected result"); out->message(out, "inject-rsc-action", resource, op->op_type, node, op->interval_ms); // Check whether action is in a list of desired simulated failures for (const GList *iter = fake_op_fail_list; iter != NULL; iter = iter->next) { const char *spec = (const char *) iter->data; char *key = NULL; const char *match_name = NULL; // Allow user to specify anonymous clone with or without instance number key = crm_strdup_printf(PCMK__OP_FMT "@%s=", resource, op->op_type, op->interval_ms, node); if (strncasecmp(key, spec, strlen(key)) == 0) { match_name = resource; } free(key); // If not found, try the resource's name in the configuration if ((match_name == NULL) && (strcmp(resource, resource_config_name) != 0)) { key = crm_strdup_printf(PCMK__OP_FMT "@%s=", resource_config_name, op->op_type, op->interval_ms, node); if (strncasecmp(key, spec, strlen(key)) == 0) { match_name = resource_config_name; } free(key); } if (match_name == NULL) { continue; // This failed action entry doesn't match } // ${match_name}_${task}_${interval_in_ms}@${node}=${rc} rc = sscanf(spec, "%*[^=]=%d", (int *) &op->rc); if (rc != 1) { out->err(out, "Invalid failed operation '%s' " "(result code must be integer)", spec); continue; // Keep checking other list entries } out->info(out, "Pretending action %d failed with rc=%d", action->id, op->rc); pcmk__set_graph_action_flags(action, pcmk__graph_action_failed); graph->abort_priority = PCMK_SCORE_INFINITY; pcmk__inject_failcount(out, fake_cib, cib_node, match_name, op->op_type, op->interval_ms, op->rc); break; } pcmk__inject_action_result(cib_resource, op, target_outcome); lrmd_free_event(op); rc = fake_cib->cmds->modify(fake_cib, PCMK_XE_STATUS, cib_node, cib_sync_call|cib_scope_local); CRM_ASSERT(rc == pcmk_ok); done: free(node); free_xml(cib_node); pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); pcmk__update_graph(graph, action); return pcmk_rc_ok; } /*! * \internal * \brief Simulate successfully executing a cluster action * * \param[in,out] graph Graph to update with action result * \param[in,out] action Cluster action to simulate * * \return Standard Pacemaker return code */ static int simulate_cluster_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { const char *node = crm_element_value(action->xml, PCMK__META_ON_NODE); const char *task = crm_element_value(action->xml, PCMK_XA_OPERATION); xmlNode *rsc = pcmk__xe_first_child(action->xml, PCMK_XE_PRIMITIVE, NULL, NULL); pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); out->message(out, "inject-cluster-action", node, task, rsc); pcmk__update_graph(graph, action); return pcmk_rc_ok; } /*! * \internal * \brief Simulate successfully executing a fencing action * * \param[in,out] graph Graph to update with action result * \param[in,out] action Fencing action to simulate * * \return Standard Pacemaker return code */ static int simulate_fencing_action(pcmk__graph_t *graph, pcmk__graph_action_t *action) { const char *op = crm_meta_value(action->params, PCMK__META_STONITH_ACTION); char *target = crm_element_value_copy(action->xml, PCMK__META_ON_NODE); out->message(out, "inject-fencing-action", target, op); if (!pcmk__str_eq(op, PCMK_ACTION_ON, pcmk__str_casei)) { int rc = pcmk_ok; GString *xpath = g_string_sized_new(512); // Set node state to offline xmlNode *cib_node = pcmk__inject_node_state_change(fake_cib, target, false); CRM_ASSERT(cib_node != NULL); crm_xml_add(cib_node, PCMK_XA_CRM_DEBUG_ORIGIN, __func__); rc = fake_cib->cmds->replace(fake_cib, PCMK_XE_STATUS, cib_node, cib_sync_call|cib_scope_local); CRM_ASSERT(rc == pcmk_ok); // Simulate controller clearing node's resource history and attributes pcmk__g_strcat(xpath, "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='", target, "']/" PCMK__XE_LRM, NULL); fake_cib->cmds->remove(fake_cib, (const char *) xpath->str, NULL, cib_xpath|cib_sync_call|cib_scope_local); g_string_truncate(xpath, 0); pcmk__g_strcat(xpath, "//" PCMK__XE_NODE_STATE "[@" PCMK_XA_UNAME "='", target, "']" "/" PCMK__XE_TRANSIENT_ATTRIBUTES, NULL); fake_cib->cmds->remove(fake_cib, (const char *) xpath->str, NULL, cib_xpath|cib_sync_call|cib_scope_local); free_xml(cib_node); g_string_free(xpath, TRUE); } pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed); pcmk__update_graph(graph, action); free(target); return pcmk_rc_ok; } enum pcmk__graph_status pcmk__simulate_transition(pcmk_scheduler_t *scheduler, cib_t *cib, const GList *op_fail_list) { pcmk__graph_t *transition = NULL; enum pcmk__graph_status graph_rc; pcmk__graph_functions_t simulation_fns = { simulate_pseudo_action, simulate_resource_action, simulate_cluster_action, simulate_fencing_action, }; out = scheduler->priv; fake_cib = cib; fake_op_fail_list = op_fail_list; if (!out->is_quiet(out)) { out->begin_list(out, NULL, NULL, "Executing Cluster Transition"); } pcmk__set_graph_functions(&simulation_fns); transition = pcmk__unpack_graph(scheduler->graph, crm_system_name); pcmk__log_graph(LOG_DEBUG, transition); fake_resource_list = scheduler->resources; do { graph_rc = pcmk__execute_graph(transition); } while (graph_rc == pcmk__graph_active); fake_resource_list = NULL; if (graph_rc != pcmk__graph_complete) { out->err(out, "Transition failed: %s", pcmk__graph_status2text(graph_rc)); pcmk__log_graph(LOG_ERR, transition); out->err(out, "An invalid transition was produced"); } pcmk__free_graph(transition); if (!out->is_quiet(out)) { // If not quiet, we'll need the resulting CIB for later display xmlNode *cib_object = NULL; int rc = fake_cib->cmds->query(fake_cib, NULL, &cib_object, cib_sync_call|cib_scope_local); CRM_ASSERT(rc == pcmk_ok); pe_reset_working_set(scheduler); scheduler->input = cib_object; out->end_list(out); } return graph_rc; } int pcmk__simulate(pcmk_scheduler_t *scheduler, pcmk__output_t *out, const pcmk_injections_t *injections, unsigned int flags, uint32_t section_opts, const char *use_date, const char *input_file, const char *graph_file, const char *dot_file) { int printed = pcmk_rc_no_output; int rc = pcmk_rc_ok; xmlNodePtr input = NULL; cib_t *cib = NULL; rc = cib__signon_query(out, &cib, &input); if (rc != pcmk_rc_ok) { goto simulate_done; } reset(scheduler, input, out, use_date, flags); cluster_status(scheduler); if ((cib->variant == cib_native) && pcmk_is_set(section_opts, pcmk_section_times)) { if (pcmk__our_nodename == NULL) { // Currently used only in the times section pcmk__query_node_name(out, 0, &pcmk__our_nodename, 0); } scheduler->localhost = pcmk__our_nodename; } if (!out->is_quiet(out)) { const bool show_pending = pcmk_is_set(flags, pcmk_sim_show_pending); if (pcmk_is_set(scheduler->flags, pcmk_sched_in_maintenance)) { printed = out->message(out, "maint-mode", scheduler->flags); } if (scheduler->disabled_resources || scheduler->blocked_resources) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); printed = out->info(out, "%d of %d resource instances DISABLED and " "%d BLOCKED from further action due to failure", scheduler->disabled_resources, scheduler->ninstances, scheduler->blocked_resources); } /* Most formatted output headers use caps for each word, but this one * only has the first word capitalized for compatibility with pcs. */ print_cluster_status(scheduler, (show_pending? pcmk_show_pending : 0), section_opts, "Current cluster status", (printed == pcmk_rc_ok)); printed = pcmk_rc_ok; } // If the user requested any injections, handle them if ((injections->node_down != NULL) || (injections->node_fail != NULL) || (injections->node_up != NULL) || (injections->op_inject != NULL) || (injections->ticket_activate != NULL) || (injections->ticket_grant != NULL) || (injections->ticket_revoke != NULL) || (injections->ticket_standby != NULL) || (injections->watchdog != NULL)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); pcmk__inject_scheduler_input(scheduler, cib, injections); printed = pcmk_rc_ok; rc = cib->cmds->query(cib, NULL, &input, cib_sync_call); if (rc != pcmk_rc_ok) { rc = pcmk_legacy2rc(rc); goto simulate_done; } cleanup_calculations(scheduler); reset(scheduler, input, out, use_date, flags); cluster_status(scheduler); } if (input_file != NULL) { rc = pcmk__xml_write_file(input, input_file, false, NULL); if (rc != pcmk_rc_ok) { goto simulate_done; } } if (pcmk_any_flags_set(flags, pcmk_sim_process | pcmk_sim_simulate)) { pcmk__output_t *logger_out = NULL; unsigned long long scheduler_flags = pcmk_sched_no_compat; if (pcmk_is_set(scheduler->flags, pcmk_sched_output_scores)) { scheduler_flags |= pcmk_sched_output_scores; } if (pcmk_is_set(scheduler->flags, pcmk_sched_show_utilization)) { scheduler_flags |= pcmk_sched_show_utilization; } if (pcmk_all_flags_set(scheduler->flags, pcmk_sched_output_scores |pcmk_sched_show_utilization)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Assignment Scores and Utilization Information"); printed = pcmk_rc_ok; } else if (pcmk_is_set(scheduler->flags, pcmk_sched_output_scores)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Assignment Scores"); printed = pcmk_rc_ok; } else if (pcmk_is_set(scheduler->flags, pcmk_sched_show_utilization)) { PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); out->begin_list(out, NULL, NULL, "Utilization Information"); printed = pcmk_rc_ok; } else { rc = pcmk__log_output_new(&logger_out); if (rc != pcmk_rc_ok) { goto simulate_done; } pe__register_messages(logger_out); pcmk__register_lib_messages(logger_out); scheduler->priv = logger_out; } pcmk__schedule_actions(input, scheduler_flags, scheduler); if (logger_out == NULL) { out->end_list(out); } else { logger_out->finish(logger_out, CRM_EX_OK, true, NULL); pcmk__output_free(logger_out); scheduler->priv = out; } input = NULL; /* Don't try and free it twice */ if (graph_file != NULL) { rc = pcmk__xml_write_file(scheduler->graph, graph_file, false, NULL); if (rc != pcmk_rc_ok) { rc = pcmk_rc_graph_error; goto simulate_done; } } if (dot_file != NULL) { rc = write_sim_dotfile(scheduler, dot_file, pcmk_is_set(flags, pcmk_sim_all_actions), pcmk_is_set(flags, pcmk_sim_verbose)); if (rc != pcmk_rc_ok) { rc = pcmk_rc_dot_error; goto simulate_done; } } if (!out->is_quiet(out)) { print_transition_summary(scheduler, printed == pcmk_rc_ok); } } rc = pcmk_rc_ok; if (!pcmk_is_set(flags, pcmk_sim_simulate)) { goto simulate_done; } PCMK__OUTPUT_SPACER_IF(out, printed == pcmk_rc_ok); if (pcmk__simulate_transition(scheduler, cib, injections->op_fail) != pcmk__graph_complete) { rc = pcmk_rc_invalid_transition; } if (out->is_quiet(out)) { goto simulate_done; } set_effective_date(scheduler, true, use_date); if (pcmk_is_set(flags, pcmk_sim_show_scores)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_output_scores); } if (pcmk_is_set(flags, pcmk_sim_show_utilization)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_show_utilization); } cluster_status(scheduler); print_cluster_status(scheduler, 0, section_opts, "Revised Cluster Status", true); simulate_done: cib__clean_up_connection(&cib); return rc; } int pcmk_simulate(xmlNodePtr *xml, pcmk_scheduler_t *scheduler, const pcmk_injections_t *injections, unsigned int flags, unsigned int section_opts, const char *use_date, const char *input_file, const char *graph_file, const char *dot_file) { pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; rc = pcmk__xml_output_new(&out, xml); if (rc != pcmk_rc_ok) { return rc; } pe__register_messages(out); pcmk__register_lib_messages(out); rc = pcmk__simulate(scheduler, out, injections, flags, section_opts, use_date, input_file, graph_file, dot_file); pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); return rc; } diff --git a/lib/pacemaker/pcmk_status.c b/lib/pacemaker/pcmk_status.c index 875b34523e..0be054d6c9 100644 --- a/lib/pacemaker/pcmk_status.c +++ b/lib/pacemaker/pcmk_status.c @@ -1,381 +1,381 @@ /* * 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 // stonith__register_messages() #include #include static stonith_t * fencing_connect(void) { stonith_t *st = stonith_api_new(); int rc = pcmk_rc_ok; if (st == NULL) { return NULL; } rc = st->cmds->connect(st, crm_system_name, NULL); if (rc == pcmk_rc_ok) { return st; } else { stonith_api_delete(st); return NULL; } } /*! * \internal * \brief Output the cluster status given a fencer and CIB connection * * \param[in,out] out Output object * \param[in,out] stonith Fencer connection * \param[in,out] cib CIB connection * \param[in] current_cib Current CIB XML * \param[in] pcmkd_state \p pacemakerd state * \param[in] fence_history How much of the fencing history to output * \param[in] show Group of \p pcmk_section_e flags * \param[in] show_opts Group of \p pcmk_show_opt_e flags * \param[in] only_node If a node name or tag, include only the * matching node(s) (if any) in the output. * If \p "*" or \p NULL, include all nodes * in the output. * \param[in] only_rsc If a resource ID or tag, include only the * matching resource(s) (if any) in the * output. If \p "*" or \p NULL, include all * resources in the output. * \param[in] neg_location_prefix Prefix denoting a ban in a constraint ID * \param[in] simple_output Whether to use a simple output format. * Note: This is for use by \p crm_mon only * and is planned to be deprecated. * * \return Standard Pacemaker return code */ int pcmk__output_cluster_status(pcmk__output_t *out, stonith_t *stonith, cib_t *cib, xmlNode *current_cib, enum pcmk_pacemakerd_state pcmkd_state, enum pcmk__fence_history fence_history, uint32_t show, uint32_t show_opts, const char *only_node, const char *only_rsc, const char *neg_location_prefix, bool simple_output) { xmlNode *cib_copy = pcmk__xml_copy(NULL, current_cib); stonith_history_t *stonith_history = NULL; int history_rc = 0; pcmk_scheduler_t *scheduler = NULL; GList *unames = NULL; GList *resources = NULL; int rc = pcmk_rc_ok; - if (cli_config_update(&cib_copy, NULL, FALSE) == FALSE) { + if (!pcmk__update_configured_schema(&cib_copy, false)) { cib__clean_up_connection(&cib); free_xml(cib_copy); rc = pcmk_rc_schema_validation; out->err(out, "Upgrade failed: %s", pcmk_rc_str(rc)); return rc; } /* get the stonith-history if there is evidence we need it */ if (fence_history != pcmk__fence_history_none) { history_rc = pcmk__get_fencing_history(stonith, &stonith_history, fence_history); } scheduler = pe_new_working_set(); pcmk__mem_assert(scheduler); pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_compat); scheduler->input = cib_copy; scheduler->priv = out; cluster_status(scheduler); if ((cib->variant == cib_native) && pcmk_is_set(show, pcmk_section_times)) { if (pcmk__our_nodename == NULL) { // Currently used only in the times section pcmk__query_node_name(out, 0, &pcmk__our_nodename, 0); } scheduler->localhost = pcmk__our_nodename; } /* Unpack constraints if any section will need them * (tickets may be referenced in constraints but not granted yet, * and bans need negative location constraints) */ if (pcmk_is_set(show, pcmk_section_bans) || pcmk_is_set(show, pcmk_section_tickets)) { pcmk__unpack_constraints(scheduler); } unames = pe__build_node_name_list(scheduler, only_node); resources = pe__build_rsc_list(scheduler, only_rsc); /* Always print DC if NULL. */ if (scheduler->dc_node == NULL) { show |= pcmk_section_dc; } if (simple_output) { rc = pcmk__output_simple_status(out, scheduler); } else { out->message(out, "cluster-status", scheduler, pcmkd_state, pcmk_rc2exitc(history_rc), stonith_history, fence_history, show, show_opts, neg_location_prefix, unames, resources); } g_list_free_full(unames, free); g_list_free_full(resources, free); stonith_history_free(stonith_history); stonith_history = NULL; pe_free_working_set(scheduler); return rc; } int pcmk_status(xmlNodePtr *xml) { cib_t *cib = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; uint32_t show_opts = pcmk_show_pending |pcmk_show_inactive_rscs |pcmk_show_timing; cib = cib_new(); if (cib == NULL) { return pcmk_rc_cib_corrupt; } rc = pcmk__xml_output_new(&out, xml); if (rc != pcmk_rc_ok) { cib_delete(cib); return rc; } pcmk__register_lib_messages(out); pe__register_messages(out); stonith__register_messages(out); rc = pcmk__status(out, cib, pcmk__fence_history_full, pcmk_section_all, show_opts, NULL, NULL, NULL, false, 0); pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); cib_delete(cib); return rc; } /*! * \internal * \brief Query and output the cluster status * * The operation is considered a success if we're able to get the \p pacemakerd * state. If possible, we'll also try to connect to the fencer and CIB and * output their respective status information. * * \param[in,out] out Output object * \param[in,out] cib CIB connection * \param[in] fence_history How much of the fencing history to output * \param[in] show Group of \p pcmk_section_e flags * \param[in] show_opts Group of \p pcmk_show_opt_e flags * \param[in] only_node If a node name or tag, include only the * matching node(s) (if any) in the output. * If \p "*" or \p NULL, include all nodes * in the output. * \param[in] only_rsc If a resource ID or tag, include only the * matching resource(s) (if any) in the * output. If \p "*" or \p NULL, include all * resources in the output. * \param[in] neg_location_prefix Prefix denoting a ban in a constraint ID * \param[in] simple_output Whether to use a simple output format. * Note: This is for use by \p crm_mon only * and is planned to be deprecated. * \param[in] timeout_ms How long to wait for a reply from the * \p pacemakerd API. If 0, * \p pcmk_ipc_dispatch_sync will be used. * If positive, \p pcmk_ipc_dispatch_main * will be used, and a new mainloop will be * created for this purpose (freed before * return). * * \return Standard Pacemaker return code */ int pcmk__status(pcmk__output_t *out, cib_t *cib, enum pcmk__fence_history fence_history, uint32_t show, uint32_t show_opts, const char *only_node, const char *only_rsc, const char *neg_location_prefix, bool simple_output, unsigned int timeout_ms) { xmlNode *current_cib = NULL; int rc = pcmk_rc_ok; stonith_t *stonith = NULL; enum pcmk_pacemakerd_state pcmkd_state = pcmk_pacemakerd_state_invalid; time_t last_updated = 0; if (cib == NULL) { return ENOTCONN; } if (cib->variant == cib_native) { rc = pcmk__pacemakerd_status(out, crm_system_name, timeout_ms, false, &pcmkd_state); if (rc != pcmk_rc_ok) { return rc; } last_updated = time(NULL); switch (pcmkd_state) { case pcmk_pacemakerd_state_running: case pcmk_pacemakerd_state_shutting_down: case pcmk_pacemakerd_state_remote: /* Fencer and CIB may still be available while shutting down or * running on a Pacemaker Remote node */ break; default: // Fencer and CIB are definitely unavailable out->message(out, "pacemakerd-health", NULL, pcmkd_state, NULL, last_updated); return rc; } if (fence_history != pcmk__fence_history_none) { stonith = fencing_connect(); } } rc = cib__signon_query(out, &cib, ¤t_cib); if (rc != pcmk_rc_ok) { if (pcmkd_state != pcmk_pacemakerd_state_invalid) { // Invalid at this point means we didn't query the pcmkd state out->message(out, "pacemakerd-health", NULL, pcmkd_state, NULL, last_updated); } goto done; } rc = pcmk__output_cluster_status(out, stonith, cib, current_cib, pcmkd_state, fence_history, show, show_opts, only_node, only_rsc, neg_location_prefix, simple_output); if (rc != pcmk_rc_ok) { out->err(out, "Error outputting status info from the fencer or CIB"); } done: stonith_api_delete(stonith); free_xml(current_cib); return pcmk_rc_ok; } /*! * \internal * \brief Output cluster status in Nagios Plugin format * * \param[in,out] out Output object * \param[in] scheduler Scheduler data * * \return Standard Pacemaker return code * \note This is for a deprecated crm_mon option and should be called only for * that. */ int pcmk__output_simple_status(pcmk__output_t *out, const pcmk_scheduler_t *scheduler) { int nodes_online = 0; int nodes_standby = 0; int nodes_maint = 0; GString *offline_nodes = NULL; bool no_dc = false; bool offline = false; bool has_warnings = false; if (scheduler->dc_node == NULL) { has_warnings = true; no_dc = true; } for (GList *iter = scheduler->nodes; iter != NULL; iter = iter->next) { pcmk_node_t *node = (pcmk_node_t *) iter->data; if (node->details->standby && node->details->online) { nodes_standby++; } else if (node->details->maintenance && node->details->online) { nodes_maint++; } else if (node->details->online) { nodes_online++; } else { pcmk__add_word(&offline_nodes, 1024, "offline node:"); pcmk__add_word(&offline_nodes, 0, pcmk__node_name(node)); has_warnings = true; offline = true; } } if (has_warnings) { out->info(out, "CLUSTER WARN: %s%s%s", no_dc ? "No DC" : "", no_dc && offline ? ", " : "", (offline? (const char *) offline_nodes->str : "")); if (offline_nodes != NULL) { g_string_free(offline_nodes, TRUE); } } else { char *nodes_standby_s = NULL; char *nodes_maint_s = NULL; if (nodes_standby > 0) { nodes_standby_s = crm_strdup_printf(", %d standby node%s", nodes_standby, pcmk__plural_s(nodes_standby)); } if (nodes_maint > 0) { nodes_maint_s = crm_strdup_printf(", %d maintenance node%s", nodes_maint, pcmk__plural_s(nodes_maint)); } out->info(out, "CLUSTER OK: %d node%s online%s%s, " "%d resource instance%s configured", nodes_online, pcmk__plural_s(nodes_online), nodes_standby_s != NULL ? nodes_standby_s : "", nodes_maint_s != NULL ? nodes_maint_s : "", scheduler->ninstances, pcmk__plural_s(scheduler->ninstances)); free(nodes_standby_s); free(nodes_maint_s); } if (has_warnings) { return pcmk_rc_error; } else { return pcmk_rc_ok; } /* coverity[leaked_storage] False positive */ } diff --git a/lib/pacemaker/pcmk_verify.c b/lib/pacemaker/pcmk_verify.c index f124e2da9d..e51f844e3f 100644 --- a/lib/pacemaker/pcmk_verify.c +++ b/lib/pacemaker/pcmk_verify.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 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 "libpacemaker_private.h" int pcmk__parse_cib(pcmk__output_t *out, const char *cib_source, xmlNodePtr *cib_object) { // @COMPAT Take an enum for cib_source instead of trying to figure it out? const char *first = cib_source; if (cib_source == NULL) { crm_info("Reading XML from: live cluster"); return cib__signon_query(out, NULL, cib_object); } while (isspace(*first)) { first++; } if (*first == '<') { *cib_object = pcmk__xml_parse(cib_source); } else { *cib_object = pcmk__xml_read(cib_source); } return (*cib_object == NULL)? ENODATA : pcmk_rc_ok; } int pcmk__verify(pcmk_scheduler_t *scheduler, pcmk__output_t *out, xmlNode *cib_object) { int rc = pcmk_rc_ok; xmlNode *status = NULL; xmlNode *cib_object_copy = NULL; if (!pcmk__xe_is(cib_object, PCMK_XE_CIB)) { rc = EBADMSG; out->err(out, "This tool can only check complete configurations (i.e. those starting with )."); goto verify_done; } status = pcmk_find_cib_element(cib_object, PCMK_XE_STATUS); if (status == NULL) { pcmk__xe_create(cib_object, PCMK_XE_STATUS); } - if (pcmk__validate_xml(cib_object, NULL, (xmlRelaxNGValidityErrorFunc) out->err, out) == FALSE) { + if (!pcmk__validate_xml(cib_object, NULL, + (xmlRelaxNGValidityErrorFunc) out->err, out)) { crm_config_error = TRUE; rc = pcmk_rc_schema_validation; goto verify_done; - } else if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) { + } else if (!pcmk__update_configured_schema(&cib_object, false)) { crm_config_error = TRUE; out->err(out, "The cluster will NOT be able to use this configuration.\n" "Please manually update the configuration to conform to the %s syntax.", - xml_latest_schema()); + pcmk__highest_schema_name()); rc = pcmk_rc_schema_validation; goto verify_done; } /* Process the configuration to set crm_config_error/crm_config_warning. * * @TODO Some parts of the configuration are unpacked only when needed (for * example, action configuration), so we aren't necessarily checking those. */ if (cib_object != NULL) { unsigned long long flags = pcmk_sched_no_counts|pcmk_sched_no_compat; if (status == NULL) { // No status available, so do minimal checks flags |= pcmk_sched_validate_only; } cib_object_copy = pcmk__xml_copy(NULL, cib_object); /* The scheduler takes ownership of the XML object and potentially * frees it later. We want the caller of pcmk__verify to retain * ownership of the passed-in XML object, hence we pass in a copy * to the scheduler. */ pcmk__schedule_actions(cib_object_copy, flags, scheduler); } verify_done: if (crm_config_error) { rc = pcmk_rc_schema_validation; pcmk__config_err("CIB did not pass schema validation"); } else if (crm_config_warning) { rc = pcmk_rc_schema_validation; } return rc; } int pcmk_verify(xmlNodePtr *xml, const char *cib_source) { pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; int rc = pcmk_rc_ok; xmlNode *cib_object = NULL; rc = pcmk__xml_output_new(&out, xml); if (rc != pcmk_rc_ok) { return rc; } pe__register_messages(out); pcmk__register_lib_messages(out); rc = pcmk__parse_cib(out, cib_source, &cib_object); if (rc != pcmk_rc_ok) { out->err(out, "Couldn't parse input"); goto done; } scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = errno; out->err(out, "Couldn't allocate scheduler data: %s", pcmk_rc_str(rc)); goto done; } scheduler->priv = out; rc = pcmk__verify(scheduler, out, cib_object); done: pe_free_working_set(scheduler); pcmk__xml_output_finish(out, pcmk_rc2exitc(rc), xml); free_xml(cib_object); return rc; } diff --git a/tools/cibadmin.c b/tools/cibadmin.c index 1e7dcbfdf7..b6767da6b1 100644 --- a/tools/cibadmin.c +++ b/tools/cibadmin.c @@ -1,953 +1,954 @@ /* * 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 #define SUMMARY "query and edit the Pacemaker configuration" #define INDENT " " enum cibadmin_section_type { cibadmin_section_all = 0, cibadmin_section_scope, cibadmin_section_xpath, }; static int request_id = 0; static cib_t *the_cib = NULL; static GMainLoop *mainloop = NULL; static crm_exit_t exit_code = CRM_EX_OK; static struct { const char *cib_action; int cmd_options; enum cibadmin_section_type section_type; char *cib_section; char *validate_with; gint message_timeout_sec; enum pcmk__acl_render_how acl_render_mode; gchar *cib_user; gchar *dest_node; gchar *input_file; gchar *input_xml; gboolean input_stdin; bool delete_all; gboolean allow_create; gboolean force; gboolean get_node_path; gboolean local; gboolean no_children; gboolean sync_call; /* @COMPAT: For "-!" version option. Not advertised nor marked as * deprecated, but accepted. */ gboolean extended_version; //! \deprecated gboolean no_bcast; } options; int do_init(void); static int do_work(xmlNode *input, xmlNode **output); void cibadmin_op_callback(xmlNode *msg, int call_id, int rc, xmlNode *output, void *user_data); static void print_xml_output(xmlNode * xml) { if (!xml) { return; } else if (xml->type != XML_ELEMENT_NODE) { return; } if (pcmk_is_set(options.cmd_options, cib_xpath_address)) { const char *id = crm_element_value(xml, PCMK_XA_ID); if (pcmk__xe_is(xml, PCMK__XE_XPATH_QUERY)) { xmlNode *child = NULL; for (child = xml->children; child; child = child->next) { print_xml_output(child); } } else if (id) { printf("%s\n", id); } } else { GString *buf = g_string_sized_new(1024); pcmk__xml_string(xml, pcmk__xml_fmt_pretty, buf, 0); fprintf(stdout, "%s", buf->str); g_string_free(buf, TRUE); } } // Upgrade requested but already at latest schema static void report_schema_unchanged(void) { const char *err = pcmk_rc_str(pcmk_rc_schema_unchanged); crm_info("Upgrade unnecessary: %s\n", err); printf("Upgrade unnecessary: %s\n", err); exit_code = CRM_EX_OK; } /*! * \internal * \brief Check whether the current CIB action is dangerous * \return true if \p options.cib_action is dangerous, or false otherwise */ static inline bool cib_action_is_dangerous(void) { return options.no_bcast || options.delete_all || pcmk__str_any_of(options.cib_action, PCMK__CIB_REQUEST_UPGRADE, PCMK__CIB_REQUEST_ERASE, NULL); } /*! * \internal * \brief Determine whether the given CIB scope is valid for \p cibadmin * * \param[in] scope Scope to validate * * \return true if \p scope is valid, or false otherwise * \note An invalid scope applies the operation to the entire CIB. */ static inline bool scope_is_valid(const char *scope) { return pcmk__str_any_of(scope, PCMK_XE_CONFIGURATION, PCMK_XE_NODES, PCMK_XE_RESOURCES, PCMK_XE_CONSTRAINTS, PCMK_XE_CRM_CONFIG, PCMK_XE_RSC_DEFAULTS, PCMK_XE_OP_DEFAULTS, PCMK_XE_ACLS, PCMK_XE_FENCING_TOPOLOGY, PCMK_XE_TAGS, PCMK_XE_ALERTS, PCMK_XE_STATUS, NULL); } static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.delete_all = false; if (pcmk__str_any_of(option_name, "-u", "--upgrade", NULL)) { options.cib_action = PCMK__CIB_REQUEST_UPGRADE; } else if (pcmk__str_any_of(option_name, "-Q", "--query", NULL)) { options.cib_action = PCMK__CIB_REQUEST_QUERY; } else if (pcmk__str_any_of(option_name, "-E", "--erase", NULL)) { options.cib_action = PCMK__CIB_REQUEST_ERASE; } else if (pcmk__str_any_of(option_name, "-B", "--bump", NULL)) { options.cib_action = PCMK__CIB_REQUEST_BUMP; } else if (pcmk__str_any_of(option_name, "-C", "--create", NULL)) { options.cib_action = PCMK__CIB_REQUEST_CREATE; } else if (pcmk__str_any_of(option_name, "-M", "--modify", NULL)) { options.cib_action = PCMK__CIB_REQUEST_MODIFY; } else if (pcmk__str_any_of(option_name, "-P", "--patch", NULL)) { options.cib_action = PCMK__CIB_REQUEST_APPLY_PATCH; } else if (pcmk__str_any_of(option_name, "-R", "--replace", NULL)) { options.cib_action = PCMK__CIB_REQUEST_REPLACE; } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) { options.cib_action = PCMK__CIB_REQUEST_DELETE; } else if (pcmk__str_any_of(option_name, "-d", "--delete-all", NULL)) { options.cib_action = PCMK__CIB_REQUEST_DELETE; options.delete_all = true; } else if (pcmk__str_any_of(option_name, "-a", "--empty", NULL)) { options.cib_action = "empty"; pcmk__str_update(&options.validate_with, optarg); } else if (pcmk__str_any_of(option_name, "-5", "--md5-sum", NULL)) { options.cib_action = "md5-sum"; } else if (pcmk__str_any_of(option_name, "-6", "--md5-sum-versioned", NULL)) { options.cib_action = "md5-sum-versioned"; } else { // Should be impossible return FALSE; } return TRUE; } static gboolean show_access_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_eq(optarg, "auto", pcmk__str_null_matches)) { options.acl_render_mode = pcmk__acl_render_default; } else if (g_strcmp0(optarg, "namespace") == 0) { options.acl_render_mode = pcmk__acl_render_namespace; } else if (g_strcmp0(optarg, "text") == 0) { options.acl_render_mode = pcmk__acl_render_text; } else if (g_strcmp0(optarg, "color") == 0) { options.acl_render_mode = pcmk__acl_render_color; } else { g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_USAGE, "Invalid value '%s' for option '%s'", optarg, option_name); return FALSE; } return TRUE; } static gboolean section_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (pcmk__str_any_of(option_name, "-o", "--scope", NULL)) { options.section_type = cibadmin_section_scope; } else if (pcmk__str_any_of(option_name, "-A", "--xpath", NULL)) { options.section_type = cibadmin_section_xpath; } else { // Should be impossible return FALSE; } pcmk__str_update(&options.cib_section, optarg); return TRUE; } static GOptionEntry command_entries[] = { { "upgrade", 'u', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Upgrade the configuration to the latest syntax", NULL }, { "query", 'Q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Query the contents of the CIB", NULL }, { "erase", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Erase the contents of the whole CIB", NULL }, { "bump", 'B', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Increase the CIB's epoch value by 1", NULL }, { "create", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Create an object in the CIB (will fail if object already exists)", NULL }, { "modify", 'M', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Find object somewhere in CIB's XML tree and update it (fails if object " "does not exist unless -c is also specified)", NULL }, { "patch", 'P', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Supply an update in the form of an XML diff (see crm_diff(8))", NULL }, { "replace", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Recursively replace an object in the CIB", NULL }, { "delete", 'D', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Delete first object matching supplied criteria (for example, " "<" PCMK_XE_OP " " PCMK_XA_ID "=\"rsc1_op1\" " PCMK_XA_NAME "=\"monitor\"/>).\n" INDENT "The XML element name and all attributes must match in order for " "the element to be deleted.", NULL }, { "delete-all", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "When used with --xpath, remove all matching objects in the " "configuration instead of just the first one", NULL }, { "empty", 'a', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Output an empty CIB. Accepts an optional schema name argument to use as " "the " PCMK_XA_VALIDATE_WITH " value.\n" INDENT "If no schema is given, the latest will be used.", "[schema]" }, { "md5-sum", '5', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate the on-disk CIB digest", NULL }, { "md5-sum-versioned", '6', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Calculate an on-the-wire versioned CIB digest", NULL }, { NULL } }; static GOptionEntry data_entries[] = { /* @COMPAT: These arguments should be last-wins. We can have an enum option * that stores the input type, along with a single string option that stores * the XML string for --xml-text, filename for --xml-file, or NULL for * --xml-pipe. */ { "xml-text", 'X', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.input_xml, "Retrieve XML from the supplied string", "value" }, { "xml-file", 'x', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &options.input_file, "Retrieve XML from the named file", "value" }, { "xml-pipe", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.input_stdin, "Retrieve XML from stdin", NULL }, { NULL } }; static GOptionEntry addl_entries[] = { { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force, "Force the action to be performed", NULL }, { "timeout", 't', G_OPTION_FLAG_NONE, G_OPTION_ARG_INT, &options.message_timeout_sec, "Time (in seconds) to wait before declaring the operation failed", "value" }, { "user", 'U', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.cib_user, "Run the command with permissions of the named user (valid only for the " "root and " CRM_DAEMON_USER " accounts)", "value" }, { "sync-call", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.sync_call, "Wait for call to complete before returning", NULL }, { "local", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.local, "Command takes effect locally (should be used only for queries)", NULL }, { "scope", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "Limit scope of operation to specific section of CIB\n" INDENT "Valid values: " PCMK_XE_CONFIGURATION ", " PCMK_XE_NODES ", " PCMK_XE_RESOURCES ", " PCMK_XE_CONSTRAINTS ", " PCMK_XE_CRM_CONFIG ", " PCMK_XE_RSC_DEFAULTS ",\n" INDENT " " PCMK_XE_OP_DEFAULTS ", " PCMK_XE_ACLS ", " PCMK_XE_FENCING_TOPOLOGY ", " PCMK_XE_TAGS ", " PCMK_XE_ALERTS ", " PCMK_XE_STATUS "\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "xpath", 'A', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, section_cb, "A valid XPath to use instead of --scope/-o\n" INDENT "If both --scope/-o and --xpath/-a are specified, the last one to " "appear takes effect", "value" }, { "node-path", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.get_node_path, "When performing XPath queries, return paths of any matches found\n" INDENT "(for example, " "\"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION "/" PCMK_XE_RESOURCES "/" PCMK_XE_CLONE "[@" PCMK_XA_ID "='dummy-clone']" "/" PCMK_XE_PRIMITIVE "[@" PCMK_XA_ID "='dummy']\")", NULL }, { "show-access", 'S', G_OPTION_FLAG_OPTIONAL_ARG, G_OPTION_ARG_CALLBACK, show_access_cb, "Whether to use syntax highlighting for ACLs (with -Q/--query and " "-U/--user)\n" INDENT "Allowed values: 'color' (default for terminal), 'text' (plain text, " "default for non-terminal),\n" INDENT " 'namespace', or 'auto' (use default value)\n" INDENT "Default value: 'auto'", "[value]" }, { "allow-create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.allow_create, "(Advanced) Allow target of --modify/-M to be created if it does not " "exist", NULL }, { "no-children", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.no_children, "(Advanced) When querying an object, do not include its children in the " "result", NULL }, { "node", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.dest_node, "(Advanced) Send command to the specified host", "value" }, // @COMPAT: Deprecated { "no-bcast", 'b', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.no_bcast, "deprecated", NULL }, // @COMPAT: Deprecated { "host", 'h', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.dest_node, "deprecated", NULL }, { NULL } }; static GOptionContext * build_arg_context(pcmk__common_args_t *args) { const char *desc = NULL; GOptionContext *context = NULL; GOptionEntry extra_prog_entries[] = { // @COMPAT: Deprecated { "extended-version", '!', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.extended_version, "deprecated", NULL }, { NULL } }; desc = "Examples:\n\n" "Query the configuration from the local node:\n\n" "\t# cibadmin --query --local\n\n" "Query just the cluster options configuration:\n\n" "\t# cibadmin --query --scope " PCMK_XE_CRM_CONFIG "\n\n" "Query all '" PCMK_META_TARGET_ROLE "' settings:\n\n" "\t# cibadmin --query --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_TARGET_ROLE"']\"\n\n" "Remove all '" PCMK_META_IS_MANAGED "' settings:\n\n" "\t# cibadmin --delete-all --xpath " "\"//" PCMK_XE_NVPAIR "[@" PCMK_XA_NAME "='" PCMK_META_IS_MANAGED "']\"\n\n" "Remove the resource named 'old':\n\n" "\t# cibadmin --delete --xml-text " "'<" PCMK_XE_PRIMITIVE " " PCMK_XA_ID "=\"old\"/>'\n\n" "Remove all resources from the configuration:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_RESOURCES " --xml-text '<" PCMK_XE_RESOURCES "/>'\n\n" "Replace complete configuration with contents of " "$HOME/pacemaker.xml:\n\n" "\t# cibadmin --replace --xml-file $HOME/pacemaker.xml\n\n" "Replace " PCMK_XE_CONSTRAINTS " section of configuration with " "contents of $HOME/constraints.xml:\n\n" "\t# cibadmin --replace --scope " PCMK_XE_CONSTRAINTS " --xml-file $HOME/constraints.xml\n\n" "Increase configuration version to prevent old configurations from " "being loaded accidentally:\n\n" "\t# cibadmin --modify --xml-text " "'<" PCMK_XE_CIB " " PCMK_XA_ADMIN_EPOCH "=\"" PCMK_XA_ADMIN_EPOCH "++\"/>'\n\n" "Edit the configuration with your favorite $EDITOR:\n\n" "\t# cibadmin --query > $HOME/local.xml\n\n" "\t# $EDITOR $HOME/local.xml\n\n" "\t# cibadmin --replace --xml-file $HOME/local.xml\n\n" "Assuming terminal, render configuration in color (green for " "writable, blue for readable, red for\n" "denied) to visualize permissions for user tony:\n\n" "\t# cibadmin --show-access=color --query --user tony | less -r\n\n" "SEE ALSO:\n" " crm(8), pcs(8), crm_shadow(8), crm_diff(8)\n"; context = pcmk__build_arg_context(args, NULL, NULL, ""); g_option_context_set_description(context, desc); pcmk__add_main_args(context, extra_prog_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command help", command_entries); pcmk__add_arg_group(context, "data", "Data:", "Show data help", data_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); return context; } int main(int argc, char **argv) { int rc = pcmk_rc_ok; const char *source = NULL; xmlNode *output = NULL; xmlNode *input = NULL; gchar *acl_cred = NULL; GError *error = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "ANSUXhotx"); GOptionContext *context = build_arg_context(args); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } if (g_strv_length(processed_args) > 1) { gchar *help = g_option_context_get_help(context, TRUE, NULL); GString *extra = g_string_sized_new(128); for (int lpc = 1; processed_args[lpc] != NULL; lpc++) { if (extra->len > 0) { g_string_append_c(extra, ' '); } g_string_append(extra, processed_args[lpc]); } exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "non-option ARGV-elements: %s\n\n%s", extra->str, help); g_free(help); g_string_free(extra, TRUE); goto done; } if (args->version || options.extended_version) { g_strfreev(processed_args); pcmk__free_arg_context(context); /* FIXME: When cibadmin is converted to use formatted output, this can * be replaced by out->version with the appropriate boolean flag. * * options.extended_version is deprecated and will be removed in a * future release. */ pcmk__cli_help(options.extended_version? '!' : 'v'); } /* At LOG_ERR, stderr for CIB calls is rather verbose. Several lines like * * (func@file:line) error: CIB failures * * In cibadmin we explicitly output the XML portion without the prefixes. So * we default to LOG_CRIT. */ pcmk__cli_init_logging("cibadmin", 0); set_crm_log_level(LOG_CRIT); if (args->verbosity > 0) { cib__set_call_options(options.cmd_options, crm_system_name, cib_verbose); for (int i = 0; i < args->verbosity; i++) { crm_bump_log_level(argc, argv); } } if (options.cib_action == NULL) { // @COMPAT: Create a default command if other tools have one gchar *help = g_option_context_get_help(context, TRUE, NULL); exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must specify a command option\n\n%s", help); g_free(help); goto done; } if (strcmp(options.cib_action, "empty") == 0) { // Output an empty CIB GString *buf = g_string_sized_new(1024); output = createEmptyCib(1); crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with); pcmk__xml_string(output, pcmk__xml_fmt_pretty, buf, 0); fprintf(stdout, "%s", buf->str); g_string_free(buf, TRUE); goto done; } if (cib_action_is_dangerous() && !options.force) { exit_code = CRM_EX_UNSAFE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command is considered dangerous. To prevent " "accidental destruction of the cluster, the --force flag " "is required in order to proceed."); goto done; } if (options.message_timeout_sec < 1) { // Set default timeout options.message_timeout_sec = 30; } if (options.section_type == cibadmin_section_xpath) { // Enable getting section by XPath cib__set_call_options(options.cmd_options, crm_system_name, cib_xpath); } else if (options.section_type == cibadmin_section_scope) { if (!scope_is_valid(options.cib_section)) { // @COMPAT: Consider requiring --force to proceed fprintf(stderr, "Invalid value '%s' for '--scope'. Operation will apply " "to the entire CIB.\n", options.cib_section); } } if (options.allow_create) { // Allow target of --modify/-M to be created if it does not exist cib__set_call_options(options.cmd_options, crm_system_name, cib_can_create); } if (options.delete_all) { // With cibadmin_section_xpath, remove all matching objects cib__set_call_options(options.cmd_options, crm_system_name, cib_multiple); } if (options.get_node_path) { /* Enable getting node path of XPath query matches. * Meaningful only if options.section_type == cibadmin_section_xpath. */ cib__set_call_options(options.cmd_options, crm_system_name, cib_xpath_address); } if (options.local) { // Configure command to take effect only locally cib__set_call_options(options.cmd_options, crm_system_name, cib_scope_local); } // @COMPAT: Deprecated option if (options.no_bcast) { // Configure command to take effect only locally and not to broadcast cib__set_call_options(options.cmd_options, crm_system_name, cib_inhibit_bcast|cib_scope_local); } if (options.no_children) { // When querying an object, don't include its children in the result cib__set_call_options(options.cmd_options, crm_system_name, cib_no_children); } if (options.sync_call || (options.acl_render_mode != pcmk__acl_render_none)) { /* Wait for call to complete before returning. * * The ACL render modes work only with sync calls due to differences in * output handling between sync/async. It shouldn't matter to the user * whether the call is synchronous; for a CIB query, we have to wait for * the result in order to display it in any case. */ cib__set_call_options(options.cmd_options, crm_system_name, cib_sync_call); } if (options.input_file != NULL) { input = pcmk__xml_read(options.input_file); source = options.input_file; } else if (options.input_xml != NULL) { input = pcmk__xml_parse(options.input_xml); source = "input string"; } else if (options.input_stdin) { input = pcmk__xml_read(NULL); source = "STDIN"; } else if (options.acl_render_mode != pcmk__acl_render_none) { char *username = pcmk__uid2username(geteuid()); bool required = pcmk_acl_required(username); free(username); if (required) { if (options.force) { fprintf(stderr, "The supplied command can provide skewed" " result since it is run under user that also" " gets guarded per ACLs on their own right." " Continuing since --force flag was" " provided.\n"); } else { exit_code = CRM_EX_UNSAFE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command can provide skewed result " "since it is run under user that also gets guarded " "per ACLs in their own right. To accept the risk " "of such a possible distortion (without even " "knowing it at this time), use the --force flag."); goto done; } } if (options.cib_user == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "The supplied command requires -U user specified."); goto done; } /* We already stopped/warned ACL-controlled users about consequences. * * Note: acl_cred takes ownership of options.cib_user here. * options.cib_user is set to NULL so that the CIB is obtained as the * user running the cibadmin command. The CIB must be obtained as a user * with full permissions in order to show the CIB correctly annotated * for the options.cib_user's permissions. */ acl_cred = options.cib_user; options.cib_user = NULL; } if (input != NULL) { crm_log_xml_debug(input, "[admin input]"); } else if (source != NULL) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Couldn't parse input from %s.", source); goto done; } if (pcmk__str_eq(options.cib_action, "md5-sum", pcmk__str_casei)) { char *digest = NULL; if (input == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Please supply XML to process with -X, -x, or -p"); goto done; } digest = calculate_on_disk_digest(input); fprintf(stderr, "Digest: "); fprintf(stdout, "%s\n", pcmk__s(digest, "")); free(digest); goto done; } else if (strcmp(options.cib_action, "md5-sum-versioned") == 0) { char *digest = NULL; const char *version = NULL; if (input == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Please supply XML to process with -X, -x, or -p"); goto done; } version = crm_element_value(input, PCMK_XA_CRM_FEATURE_SET); digest = calculate_xml_versioned_digest(input, FALSE, TRUE, version); fprintf(stderr, "Versioned (%s) digest: ", version); fprintf(stdout, "%s\n", pcmk__s(digest, "")); free(digest); goto done; } rc = do_init(); if (rc != pcmk_ok) { rc = pcmk_legacy2rc(rc); exit_code = pcmk_rc2exitc(rc); crm_err("Init failed, could not perform requested operations: %s", pcmk_rc_str(rc)); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Init failed, could not perform requested operations: %s", pcmk_rc_str(rc)); goto done; } rc = do_work(input, &output); if (rc > 0) { /* wait for the reply by creating a mainloop and running it until * the callbacks are invoked... */ request_id = rc; the_cib->cmds->register_callback(the_cib, request_id, options.message_timeout_sec, FALSE, NULL, "cibadmin_op_callback", cibadmin_op_callback); mainloop = g_main_loop_new(NULL, FALSE); crm_trace("%s waiting for reply from the local CIB", crm_system_name); crm_info("Starting mainloop"); g_main_loop_run(mainloop); } else if ((rc == -pcmk_err_schema_unchanged) && (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0)) { report_schema_unchanged(); } else if (rc < 0) { rc = pcmk_legacy2rc(rc); crm_err("Call failed: %s", pcmk_rc_str(rc)); fprintf(stderr, "Call failed: %s\n", pcmk_rc_str(rc)); if (rc == pcmk_rc_schema_validation) { if (strcmp(options.cib_action, PCMK__CIB_REQUEST_UPGRADE) == 0) { xmlNode *obj = NULL; if (the_cib->cmds->query(the_cib, NULL, &obj, options.cmd_options) == pcmk_ok) { pcmk__update_schema(&obj, NULL, true, false); } free_xml(obj); } else if (output) { - validate_xml_verbose(output); + // Show validation errors to stderr + pcmk__validate_xml(output, NULL, NULL, NULL); } } exit_code = pcmk_rc2exitc(rc); } if ((output != NULL) && (options.acl_render_mode != pcmk__acl_render_none)) { xmlDoc *acl_evaled_doc; rc = pcmk__acl_annotate_permissions(acl_cred, output->doc, &acl_evaled_doc); if (rc == pcmk_rc_ok) { xmlChar *rendered = NULL; rc = pcmk__acl_evaled_render(acl_evaled_doc, options.acl_render_mode, &rendered); if (rc != pcmk_rc_ok) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not render evaluated access: %s", pcmk_rc_str(rc)); goto done; } printf("%s\n", (char *) rendered); free(rendered); } else { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not evaluate access per request (%s, error: %s)", acl_cred, pcmk_rc_str(rc)); goto done; } } else if (output != NULL) { print_xml_output(output); } crm_trace("%s exiting normally", crm_system_name); done: g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.cib_user); g_free(options.dest_node); g_free(options.input_file); g_free(options.input_xml); free(options.cib_section); free(options.validate_with); g_free(acl_cred); free_xml(input); free_xml(output); rc = cib__clean_up_connection(&the_cib); if (exit_code == CRM_EX_OK) { exit_code = pcmk_rc2exitc(rc); } pcmk__output_and_clear_error(&error, NULL); crm_exit(exit_code); } static int do_work(xmlNode *input, xmlNode **output) { /* construct the request */ the_cib->call_timeout = options.message_timeout_sec; if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_REPLACE) == 0) && pcmk__xe_is(input, PCMK_XE_CIB)) { xmlNode *status = pcmk_find_cib_element(input, PCMK_XE_STATUS); if (status == NULL) { pcmk__xe_create(input, PCMK_XE_STATUS); } } crm_trace("Passing \"%s\" to variant_op...", options.cib_action); return cib_internal_op(the_cib, options.cib_action, options.dest_node, options.cib_section, input, output, options.cmd_options, options.cib_user); } int do_init(void) { int rc = pcmk_ok; the_cib = cib_new(); rc = the_cib->cmds->signon(the_cib, crm_system_name, cib_command); if (rc != pcmk_ok) { crm_err("Could not connect to the CIB: %s", pcmk_strerror(rc)); fprintf(stderr, "Could not connect to the CIB: %s\n", pcmk_strerror(rc)); } return rc; } void cibadmin_op_callback(xmlNode * msg, int call_id, int rc, xmlNode * output, void *user_data) { rc = pcmk_legacy2rc(rc); exit_code = pcmk_rc2exitc(rc); if (rc == pcmk_rc_schema_unchanged) { report_schema_unchanged(); } else if (rc != pcmk_rc_ok) { crm_warn("Call %s failed: %s " CRM_XS " rc=%d", options.cib_action, pcmk_rc_str(rc), rc); fprintf(stderr, "Call %s failed: %s\n", options.cib_action, pcmk_rc_str(rc)); print_xml_output(output); } else if ((strcmp(options.cib_action, PCMK__CIB_REQUEST_QUERY) == 0) && (output == NULL)) { crm_err("Query returned no output"); crm_log_xml_err(msg, "no output"); } else if (output == NULL) { crm_info("Call passed"); } else { crm_info("Call passed"); print_xml_output(output); } if (call_id == request_id) { g_main_loop_quit(mainloop); } else { crm_info("Message was not the response we were looking for (%d vs. %d)", call_id, request_id); } } diff --git a/tools/crm_resource_runtime.c b/tools/crm_resource_runtime.c index e92eb617db..eed2c601e4 100644 --- a/tools/crm_resource_runtime.c +++ b/tools/crm_resource_runtime.c @@ -1,2282 +1,2283 @@ /* * 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 static GList * build_node_info_list(const pcmk_resource_t *rsc) { GList *retval = NULL; for (const GList *iter = rsc->children; iter != NULL; iter = iter->next) { const pcmk_resource_t *child = (const pcmk_resource_t *) iter->data; for (const GList *iter2 = child->running_on; iter2 != NULL; iter2 = iter2->next) { const pcmk_node_t *node = (const pcmk_node_t *) iter2->data; node_info_t *ni = pcmk__assert_alloc(1, sizeof(node_info_t)); ni->node_name = node->details->uname; ni->promoted = pcmk_is_set(rsc->flags, pcmk_rsc_promotable) && child->fns->state(child, TRUE) == pcmk_role_promoted; retval = g_list_prepend(retval, ni); } } return retval; } GList * cli_resource_search(pcmk_resource_t *rsc, const char *requested_name, pcmk_scheduler_t *scheduler) { GList *retval = NULL; const pcmk_resource_t *parent = pe__const_top_resource(rsc, false); if (pcmk__is_clone(rsc)) { retval = build_node_info_list(rsc); /* The anonymous clone children's common ID is supplied */ } else if (pcmk__is_clone(parent) && !pcmk_is_set(rsc->flags, pcmk_rsc_unique) && rsc->clone_name && pcmk__str_eq(requested_name, rsc->clone_name, pcmk__str_casei) && !pcmk__str_eq(requested_name, rsc->id, pcmk__str_casei)) { retval = build_node_info_list(parent); } else if (rsc->running_on != NULL) { for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) { pcmk_node_t *node = (pcmk_node_t *) iter->data; node_info_t *ni = pcmk__assert_alloc(1, sizeof(node_info_t)); ni->node_name = node->details->uname; ni->promoted = (rsc->fns->state(rsc, TRUE) == pcmk_role_promoted); retval = g_list_prepend(retval, ni); } } return retval; } // \return Standard Pacemaker return code static int find_resource_attr(pcmk__output_t *out, cib_t * the_cib, const char *attr, const char *rsc, const char *attr_set_type, const char *set_name, const char *attr_id, const char *attr_name, char **value) { int rc = pcmk_rc_ok; xmlNode *xml_search = NULL; GString *xpath = NULL; const char *xpath_base = NULL; if(value) { *value = NULL; } if(the_cib == NULL) { return ENOTCONN; } xpath_base = pcmk_cib_xpath_for(PCMK_XE_RESOURCES); if (xpath_base == NULL) { crm_err(PCMK_XE_RESOURCES " CIB element not known (bug?)"); return ENOMSG; } xpath = g_string_sized_new(1024); pcmk__g_strcat(xpath, xpath_base, "//*[@" PCMK_XA_ID "=\"", rsc, "\"]", NULL); if (attr_set_type != NULL) { pcmk__g_strcat(xpath, "/", attr_set_type, NULL); if (set_name != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "=\"", set_name, "\"]", NULL); } } g_string_append(xpath, "//" PCMK_XE_NVPAIR); if (attr_id != NULL && attr_name!= NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", attr_id, "' " "and @" PCMK_XA_NAME "='", attr_name, "']", NULL); } else if (attr_id != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_ID "='", attr_id, "']", NULL); } else if (attr_name != NULL) { pcmk__g_strcat(xpath, "[@" PCMK_XA_NAME "='", attr_name, "']", NULL); } rc = the_cib->cmds->query(the_cib, (const char *) xpath->str, &xml_search, cib_sync_call | cib_scope_local | cib_xpath); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { goto done; } crm_log_xml_debug(xml_search, "Match"); if (xml_search->children != NULL) { rc = ENOTUNIQ; pcmk__warn_multiple_name_matches(out, xml_search, attr_name); out->spacer(out); } else if(value) { pcmk__str_update(value, crm_element_value(xml_search, attr)); } done: g_string_free(xpath, TRUE); free_xml(xml_search); return rc; } /* PRIVATE. Use the find_matching_attr_resources instead. */ static void find_matching_attr_resources_recursive(pcmk__output_t *out, GList /* */ **result, pcmk_resource_t *rsc, const char * attr_set, const char * attr_set_type, const char * attr_id, const char * attr_name, cib_t * cib, int depth) { int rc = pcmk_rc_ok; char *lookup_id = clone_strip(rsc->id); /* visit the children */ for(GList *gIter = rsc->children; gIter; gIter = gIter->next) { find_matching_attr_resources_recursive(out, result, (pcmk_resource_t *) gIter->data, attr_set, attr_set_type, attr_id, attr_name, cib, depth+1); /* do it only once for clones */ if (rsc->variant == pcmk_rsc_variant_clone) { break; } } rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, NULL); /* Post-order traversal. * The root is always on the list and it is the last item. */ if((0 == depth) || (pcmk_rc_ok == rc)) { /* push the head */ *result = g_list_append(*result, rsc); } free(lookup_id); } /* The result is a linearized pre-ordered tree of resources. */ static GList/**/ * find_matching_attr_resources(pcmk__output_t *out, pcmk_resource_t *rsc, const char * rsc_id, const char * attr_set, const char * attr_set_type, const char * attr_id, const char * attr_name, cib_t * cib, const char * cmd, gboolean force) { int rc = pcmk_rc_ok; char *lookup_id = NULL; GList * result = NULL; /* If --force is used, update only the requested resource (clone or primitive). * Otherwise, if the primitive has the attribute, use that. * Otherwise use the clone. */ if(force == TRUE) { return g_list_append(result, rsc); } if ((rsc->parent != NULL) && (rsc->parent->variant == pcmk_rsc_variant_clone)) { int rc = find_resource_attr(out, cib, PCMK_XA_ID, rsc_id, attr_set_type, attr_set, attr_id, attr_name, NULL); if(rc != pcmk_rc_ok) { rsc = rsc->parent; out->info(out, "Performing %s of '%s' on '%s', the parent of '%s'", cmd, attr_name, rsc->id, rsc_id); } return g_list_append(result, rsc); } else if ((rsc->parent == NULL) && (rsc->children != NULL) && (rsc->variant == pcmk_rsc_variant_clone)) { pcmk_resource_t *child = rsc->children->data; if (child->variant == pcmk_rsc_variant_primitive) { lookup_id = clone_strip(child->id); /* Could be a cloned group! */ rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, NULL); if(rc == pcmk_rc_ok) { rsc = child; out->info(out, "A value for '%s' already exists in child '%s', performing %s on that instead of '%s'", attr_name, lookup_id, cmd, rsc_id); } free(lookup_id); } return g_list_append(result, rsc); } /* If the resource is a group ==> children inherit the attribute if defined. */ find_matching_attr_resources_recursive(out, &result, rsc, attr_set, attr_set_type, attr_id, attr_name, cib, 0); return result; } // \return Standard Pacemaker return code int cli_resource_update_attribute(pcmk_resource_t *rsc, const char *requested_name, const char *attr_set, const char *attr_set_type, const char *attr_id, const char *attr_name, const char *attr_value, gboolean recursive, cib_t *cib, int cib_options, gboolean force) { pcmk__output_t *out = rsc->cluster->priv; int rc = pcmk_rc_ok; char *found_attr_id = NULL; GList/**/ *resources = NULL; const char *top_id = pe__const_top_resource(rsc, false)->id; if ((attr_id == NULL) && !force) { find_resource_attr(out, cib, PCMK_XA_ID, top_id, NULL, NULL, NULL, attr_name, NULL); } if (pcmk__str_eq(attr_set_type, PCMK_XE_INSTANCE_ATTRIBUTES, pcmk__str_casei)) { if (!force) { rc = find_resource_attr(out, cib, PCMK_XA_ID, top_id, PCMK_XE_META_ATTRIBUTES, attr_set, attr_id, attr_name, &found_attr_id); if ((rc == pcmk_rc_ok) && !out->is_quiet(out)) { out->err(out, "WARNING: There is already a meta attribute " "for '%s' called '%s' (id=%s)", top_id, attr_name, found_attr_id); out->err(out, " Delete '%s' first or use the force option " "to override", found_attr_id); } free(found_attr_id); if (rc == pcmk_rc_ok) { return ENOTUNIQ; } } resources = g_list_append(resources, rsc); } else if (pcmk__str_eq(attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) { crm_xml_add(rsc->xml, attr_name, attr_value); CRM_ASSERT(cib != NULL); rc = cib->cmds->replace(cib, PCMK_XE_RESOURCES, rsc->xml, cib_options); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { out->info(out, "Set attribute: " PCMK_XA_NAME "=%s value=%s", attr_name, attr_value); } return rc; } else { resources = find_matching_attr_resources(out, rsc, requested_name, attr_set, attr_set_type, attr_id, attr_name, cib, "update", force); } /* If the user specified attr_set or attr_id, the intent is to modify a * single resource, which will be the last item in the list. */ if ((attr_set != NULL) || (attr_id != NULL)) { GList *last = g_list_last(resources); resources = g_list_remove_link(resources, last); g_list_free(resources); resources = last; } for (GList *iter = resources; iter != NULL; iter = iter->next) { char *lookup_id = NULL; char *local_attr_set = NULL; const char *rsc_attr_id = attr_id; const char *rsc_attr_set = attr_set; xmlNode *xml_top = NULL; xmlNode *xml_obj = NULL; found_attr_id = NULL; rsc = (pcmk_resource_t *) iter->data; lookup_id = clone_strip(rsc->id); /* Could be a cloned group! */ rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, &found_attr_id); switch (rc) { case pcmk_rc_ok: crm_debug("Found a match for " PCMK_XA_NAME "='%s': " PCMK_XA_ID "='%s'", attr_name, found_attr_id); rsc_attr_id = found_attr_id; break; case ENXIO: if (rsc_attr_set == NULL) { local_attr_set = crm_strdup_printf("%s-%s", lookup_id, attr_set_type); rsc_attr_set = local_attr_set; } if (rsc_attr_id == NULL) { found_attr_id = crm_strdup_printf("%s-%s", rsc_attr_set, attr_name); rsc_attr_id = found_attr_id; } xml_top = pcmk__xe_create(NULL, (const char *) rsc->xml->name); crm_xml_add(xml_top, PCMK_XA_ID, lookup_id); xml_obj = pcmk__xe_create(xml_top, attr_set_type); crm_xml_add(xml_obj, PCMK_XA_ID, rsc_attr_set); break; default: free(lookup_id); free(found_attr_id); g_list_free(resources); return rc; } xml_obj = crm_create_nvpair_xml(xml_obj, rsc_attr_id, attr_name, attr_value); if (xml_top == NULL) { xml_top = xml_obj; } crm_log_xml_debug(xml_top, "Update"); rc = cib->cmds->modify(cib, PCMK_XE_RESOURCES, xml_top, cib_options); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { out->info(out, "Set '%s' option: " PCMK_XA_ID "=%s%s%s%s%s value=%s", lookup_id, found_attr_id, ((rsc_attr_set == NULL)? "" : " set="), pcmk__s(rsc_attr_set, ""), ((attr_name == NULL)? "" : " " PCMK_XA_NAME "="), pcmk__s(attr_name, ""), attr_value); } free_xml(xml_top); free(lookup_id); free(found_attr_id); free(local_attr_set); if (recursive && pcmk__str_eq(attr_set_type, PCMK_XE_META_ATTRIBUTES, pcmk__str_casei)) { GList *lpc = NULL; static bool need_init = true; if (need_init) { need_init = false; pcmk__unpack_constraints(rsc->cluster); pe__clear_resource_flags_on_all(rsc->cluster, pcmk_rsc_detect_loop); } /* We want to set the attribute only on resources explicitly * colocated with this one, so we use rsc->rsc_cons_lhs directly * rather than the with_this_colocations() method. */ pcmk__set_rsc_flags(rsc, pcmk_rsc_detect_loop); for (lpc = rsc->rsc_cons_lhs; lpc != NULL; lpc = lpc->next) { pcmk__colocation_t *cons = (pcmk__colocation_t *) lpc->data; crm_debug("Checking %s %d", cons->id, cons->score); if (!pcmk_is_set(cons->dependent->flags, pcmk_rsc_detect_loop) && (cons->score > 0)) { crm_debug("Setting %s=%s for dependent resource %s", attr_name, attr_value, cons->dependent->id); cli_resource_update_attribute(cons->dependent, cons->dependent->id, NULL, attr_set_type, NULL, attr_name, attr_value, recursive, cib, cib_options, force); } } } } g_list_free(resources); return rc; } // \return Standard Pacemaker return code int cli_resource_delete_attribute(pcmk_resource_t *rsc, const char *requested_name, const char *attr_set, const char *attr_set_type, const char *attr_id, const char *attr_name, cib_t *cib, int cib_options, gboolean force) { pcmk__output_t *out = rsc->cluster->priv; int rc = pcmk_rc_ok; GList/**/ *resources = NULL; if ((attr_id == NULL) && !force) { find_resource_attr(out, cib, PCMK_XA_ID, pe__const_top_resource(rsc, false)->id, NULL, NULL, NULL, attr_name, NULL); } if (pcmk__str_eq(attr_set_type, PCMK_XE_META_ATTRIBUTES, pcmk__str_casei)) { resources = find_matching_attr_resources(out, rsc, requested_name, attr_set, attr_set_type, attr_id, attr_name, cib, "delete", force); } else if (pcmk__str_eq(attr_set_type, ATTR_SET_ELEMENT, pcmk__str_none)) { pcmk__xe_remove_attr(rsc->xml, attr_name); CRM_ASSERT(cib != NULL); rc = cib->cmds->replace(cib, PCMK_XE_RESOURCES, rsc->xml, cib_options); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { out->info(out, "Deleted attribute: %s", attr_name); } return rc; } else { resources = g_list_append(resources, rsc); } for (GList *iter = resources; iter != NULL; iter = iter->next) { char *lookup_id = NULL; xmlNode *xml_obj = NULL; char *found_attr_id = NULL; const char *rsc_attr_id = attr_id; rsc = (pcmk_resource_t *) iter->data; lookup_id = clone_strip(rsc->id); rc = find_resource_attr(out, cib, PCMK_XA_ID, lookup_id, attr_set_type, attr_set, attr_id, attr_name, &found_attr_id); switch (rc) { case pcmk_rc_ok: break; case ENXIO: free(lookup_id); rc = pcmk_rc_ok; continue; default: free(lookup_id); g_list_free(resources); return rc; } if (rsc_attr_id == NULL) { rsc_attr_id = found_attr_id; } xml_obj = crm_create_nvpair_xml(NULL, rsc_attr_id, attr_name, NULL); crm_log_xml_debug(xml_obj, "Delete"); CRM_ASSERT(cib); rc = cib->cmds->remove(cib, PCMK_XE_RESOURCES, xml_obj, cib_options); rc = pcmk_legacy2rc(rc); if (rc == pcmk_rc_ok) { out->info(out, "Deleted '%s' option: " PCMK_XA_ID "=%s%s%s%s%s", lookup_id, found_attr_id, ((attr_set == NULL)? "" : " set="), pcmk__s(attr_set, ""), ((attr_name == NULL)? "" : " " PCMK_XA_NAME "="), pcmk__s(attr_name, "")); } free(lookup_id); free_xml(xml_obj); free(found_attr_id); } g_list_free(resources); return rc; } // \return Standard Pacemaker return code static int send_lrm_rsc_op(pcmk_ipc_api_t *controld_api, bool do_fail_resource, const char *host_uname, const char *rsc_id, pcmk_scheduler_t *scheduler) { pcmk__output_t *out = scheduler->priv; const char *router_node = host_uname; const char *rsc_api_id = NULL; const char *rsc_long_id = NULL; const char *rsc_class = NULL; const char *rsc_provider = NULL; const char *rsc_type = NULL; bool cib_only = false; pcmk_resource_t *rsc = pe_find_resource(scheduler->resources, rsc_id); if (rsc == NULL) { out->err(out, "Resource %s not found", rsc_id); return ENXIO; } else if (rsc->variant != pcmk_rsc_variant_primitive) { out->err(out, "We can only process primitive resources, not %s", rsc_id); return EINVAL; } rsc_class = crm_element_value(rsc->xml, PCMK_XA_CLASS); rsc_provider = crm_element_value(rsc->xml, PCMK_XA_PROVIDER), rsc_type = crm_element_value(rsc->xml, PCMK_XA_TYPE); if ((rsc_class == NULL) || (rsc_type == NULL)) { out->err(out, "Resource %s does not have a class and type", rsc_id); return EINVAL; } { pcmk_node_t *node = pe_find_node(scheduler->nodes, host_uname); if (node == NULL) { out->err(out, "Node %s not found", host_uname); return pcmk_rc_node_unknown; } if (!(node->details->online)) { if (do_fail_resource) { out->err(out, "Node %s is not online", host_uname); return ENOTCONN; } else { cib_only = true; } } if (!cib_only && pcmk__is_pacemaker_remote_node(node)) { node = pcmk__current_node(node->details->remote_rsc); if (node == NULL) { out->err(out, "No cluster connection to Pacemaker Remote node %s detected", host_uname); return ENOTCONN; } router_node = node->details->uname; } } if (rsc->clone_name) { rsc_api_id = rsc->clone_name; rsc_long_id = rsc->id; } else { rsc_api_id = rsc->id; } if (do_fail_resource) { return pcmk_controld_api_fail(controld_api, host_uname, router_node, rsc_api_id, rsc_long_id, rsc_class, rsc_provider, rsc_type); } else { return pcmk_controld_api_refresh(controld_api, host_uname, router_node, rsc_api_id, rsc_long_id, rsc_class, rsc_provider, rsc_type, cib_only); } } /*! * \internal * \brief Get resource name as used in failure-related node attributes * * \param[in] rsc Resource to check * * \return Newly allocated string containing resource's fail name * \note The caller is responsible for freeing the result. */ static inline char * rsc_fail_name(const pcmk_resource_t *rsc) { const char *name = (rsc->clone_name? rsc->clone_name : rsc->id); if (pcmk_is_set(rsc->flags, pcmk_rsc_unique)) { return strdup(name); } return clone_strip(name); } // \return Standard Pacemaker return code static int clear_rsc_history(pcmk_ipc_api_t *controld_api, const char *host_uname, const char *rsc_id, pcmk_scheduler_t *scheduler) { int rc = pcmk_rc_ok; /* Erase the resource's entire LRM history in the CIB, even if we're only * clearing a single operation's fail count. If we erased only entries for a * single operation, we might wind up with a wrong idea of the current * resource state, and we might not re-probe the resource. */ rc = send_lrm_rsc_op(controld_api, false, host_uname, rsc_id, scheduler); if (rc != pcmk_rc_ok) { return rc; } crm_trace("Processing %d mainloop inputs", pcmk_controld_api_replies_expected(controld_api)); while (g_main_context_iteration(NULL, FALSE)) { crm_trace("Processed mainloop input, %d still remaining", pcmk_controld_api_replies_expected(controld_api)); } return rc; } // \return Standard Pacemaker return code static int clear_rsc_failures(pcmk__output_t *out, pcmk_ipc_api_t *controld_api, const char *node_name, const char *rsc_id, const char *operation, const char *interval_spec, pcmk_scheduler_t *scheduler) { int rc = pcmk_rc_ok; const char *failed_value = NULL; const char *failed_id = NULL; char *interval_ms_s = NULL; GHashTable *rscs = NULL; GHashTableIter iter; /* Create a hash table to use as a set of resources to clean. This lets us * clean each resource only once (per node) regardless of how many failed * operations it has. */ rscs = pcmk__strkey_table(NULL, NULL); // Normalize interval to milliseconds for comparison to history entry if (operation) { guint interval_ms = 0U; pcmk_parse_interval_spec(interval_spec, &interval_ms); interval_ms_s = crm_strdup_printf("%u", interval_ms); } for (xmlNode *xml_op = pcmk__xe_first_child(scheduler->failed, NULL, NULL, NULL); xml_op != NULL; xml_op = pcmk__xe_next(xml_op)) { failed_id = crm_element_value(xml_op, PCMK__XA_RSC_ID); if (failed_id == NULL) { // Malformed history entry, should never happen continue; } // No resource specified means all resources match if (rsc_id) { pcmk_resource_t *fail_rsc = NULL; fail_rsc = pe_find_resource_with_flags(scheduler->resources, failed_id, pcmk_rsc_match_history |pcmk_rsc_match_anon_basename); if (!fail_rsc || !pcmk__str_eq(rsc_id, fail_rsc->id, pcmk__str_casei)) { continue; } } // Host name should always have been provided by this point failed_value = crm_element_value(xml_op, PCMK_XA_UNAME); if (!pcmk__str_eq(node_name, failed_value, pcmk__str_casei)) { continue; } // No operation specified means all operations match if (operation) { failed_value = crm_element_value(xml_op, PCMK_XA_OPERATION); if (!pcmk__str_eq(operation, failed_value, pcmk__str_casei)) { continue; } // Interval (if operation was specified) defaults to 0 (not all) failed_value = crm_element_value(xml_op, PCMK_META_INTERVAL); if (!pcmk__str_eq(interval_ms_s, failed_value, pcmk__str_casei)) { continue; } } g_hash_table_add(rscs, (gpointer) failed_id); } free(interval_ms_s); g_hash_table_iter_init(&iter, rscs); while (g_hash_table_iter_next(&iter, (gpointer *) &failed_id, NULL)) { crm_debug("Erasing failures of %s on %s", failed_id, node_name); rc = clear_rsc_history(controld_api, node_name, failed_id, scheduler); if (rc != pcmk_rc_ok) { return rc; } } g_hash_table_destroy(rscs); return rc; } // \return Standard Pacemaker return code static int clear_rsc_fail_attrs(const pcmk_resource_t *rsc, const char *operation, const char *interval_spec, const pcmk_node_t *node) { int rc = pcmk_rc_ok; int attr_options = pcmk__node_attr_none; char *rsc_name = rsc_fail_name(rsc); if (pcmk__is_pacemaker_remote_node(node)) { attr_options |= pcmk__node_attr_remote; } rc = pcmk__attrd_api_clear_failures(NULL, node->details->uname, rsc_name, operation, interval_spec, NULL, attr_options); free(rsc_name); return rc; } // \return Standard Pacemaker return code int cli_resource_delete(pcmk_ipc_api_t *controld_api, const char *host_uname, const pcmk_resource_t *rsc, const char *operation, const char *interval_spec, bool just_failures, pcmk_scheduler_t *scheduler, gboolean force) { pcmk__output_t *out = scheduler->priv; int rc = pcmk_rc_ok; pcmk_node_t *node = NULL; if (rsc == NULL) { return ENXIO; } else if (rsc->children) { for (const GList *lpc = rsc->children; lpc != NULL; lpc = lpc->next) { const pcmk_resource_t *child = (const pcmk_resource_t *) lpc->data; rc = cli_resource_delete(controld_api, host_uname, child, operation, interval_spec, just_failures, scheduler, force); if (rc != pcmk_rc_ok) { return rc; } } return pcmk_rc_ok; } else if (host_uname == NULL) { GList *lpc = NULL; GList *nodes = g_hash_table_get_values(rsc->known_on); if(nodes == NULL && force) { nodes = pcmk__copy_node_list(scheduler->nodes, false); } else if(nodes == NULL && rsc->exclusive_discover) { GHashTableIter iter; pcmk_node_t *node = NULL; g_hash_table_iter_init(&iter, rsc->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void**)&node)) { if(node->weight >= 0) { nodes = g_list_prepend(nodes, node); } } } else if(nodes == NULL) { nodes = g_hash_table_get_values(rsc->allowed_nodes); } for (lpc = nodes; lpc != NULL; lpc = lpc->next) { node = (pcmk_node_t *) lpc->data; if (node->details->online) { rc = cli_resource_delete(controld_api, node->details->uname, rsc, operation, interval_spec, just_failures, scheduler, force); } if (rc != pcmk_rc_ok) { g_list_free(nodes); return rc; } } g_list_free(nodes); return pcmk_rc_ok; } node = pe_find_node(scheduler->nodes, host_uname); if (node == NULL) { out->err(out, "Unable to clean up %s because node %s not found", rsc->id, host_uname); return ENODEV; } if (!node->details->rsc_discovery_enabled) { out->err(out, "Unable to clean up %s because resource discovery disabled on %s", rsc->id, host_uname); return EOPNOTSUPP; } if (controld_api == NULL) { out->err(out, "Dry run: skipping clean-up of %s on %s due to CIB_file", rsc->id, host_uname); return pcmk_rc_ok; } rc = clear_rsc_fail_attrs(rsc, operation, interval_spec, node); if (rc != pcmk_rc_ok) { out->err(out, "Unable to clean up %s failures on %s: %s", rsc->id, host_uname, pcmk_rc_str(rc)); return rc; } if (just_failures) { rc = clear_rsc_failures(out, controld_api, host_uname, rsc->id, operation, interval_spec, scheduler); } else { rc = clear_rsc_history(controld_api, host_uname, rsc->id, scheduler); } if (rc != pcmk_rc_ok) { out->err(out, "Cleaned %s failures on %s, but unable to clean history: %s", rsc->id, host_uname, pcmk_rc_str(rc)); } else { out->info(out, "Cleaned up %s on %s", rsc->id, host_uname); } return rc; } // \return Standard Pacemaker return code int cli_cleanup_all(pcmk_ipc_api_t *controld_api, const char *node_name, const char *operation, const char *interval_spec, pcmk_scheduler_t *scheduler) { pcmk__output_t *out = scheduler->priv; int rc = pcmk_rc_ok; int attr_options = pcmk__node_attr_none; const char *display_name = node_name? node_name : "all nodes"; if (controld_api == NULL) { out->info(out, "Dry run: skipping clean-up of %s due to CIB_file", display_name); return rc; } if (node_name) { pcmk_node_t *node = pe_find_node(scheduler->nodes, node_name); if (node == NULL) { out->err(out, "Unknown node: %s", node_name); return ENXIO; } if (pcmk__is_pacemaker_remote_node(node)) { attr_options |= pcmk__node_attr_remote; } } rc = pcmk__attrd_api_clear_failures(NULL, node_name, NULL, operation, interval_spec, NULL, attr_options); if (rc != pcmk_rc_ok) { out->err(out, "Unable to clean up all failures on %s: %s", display_name, pcmk_rc_str(rc)); return rc; } if (node_name) { rc = clear_rsc_failures(out, controld_api, node_name, NULL, operation, interval_spec, scheduler); if (rc != pcmk_rc_ok) { out->err(out, "Cleaned all resource failures on %s, but unable to clean history: %s", node_name, pcmk_rc_str(rc)); return rc; } } else { for (GList *iter = scheduler->nodes; iter; iter = iter->next) { pcmk_node_t *node = (pcmk_node_t *) iter->data; rc = clear_rsc_failures(out, controld_api, node->details->uname, NULL, operation, interval_spec, scheduler); if (rc != pcmk_rc_ok) { out->err(out, "Cleaned all resource failures on all nodes, but unable to clean history: %s", pcmk_rc_str(rc)); return rc; } } } out->info(out, "Cleaned up all resources on %s", display_name); return rc; } static void check_role(resource_checks_t *checks) { const char *role_s = g_hash_table_lookup(checks->rsc->meta, PCMK_META_TARGET_ROLE); if (role_s == NULL) { return; } switch (pcmk_parse_role(role_s)) { case pcmk_role_stopped: checks->flags |= rsc_remain_stopped; break; case pcmk_role_unpromoted: if (pcmk_is_set(pe__const_top_resource(checks->rsc, false)->flags, pcmk_rsc_promotable)) { checks->flags |= rsc_unpromotable; } break; default: break; } } static void check_managed(resource_checks_t *checks) { const char *managed_s = g_hash_table_lookup(checks->rsc->meta, PCMK_META_IS_MANAGED); if ((managed_s != NULL) && !crm_is_true(managed_s)) { checks->flags |= rsc_unmanaged; } } static void check_locked(resource_checks_t *checks) { if (checks->rsc->lock_node != NULL) { checks->flags |= rsc_locked; checks->lock_node = checks->rsc->lock_node->details->uname; } } static bool node_is_unhealthy(pcmk_node_t *node) { switch (pe__health_strategy(node->details->data_set)) { case pcmk__health_strategy_none: break; case pcmk__health_strategy_no_red: if (pe__node_health(node) < 0) { return true; } break; case pcmk__health_strategy_only_green: if (pe__node_health(node) <= 0) { return true; } break; case pcmk__health_strategy_progressive: case pcmk__health_strategy_custom: /* @TODO These are finite scores, possibly with rules, and possibly * combining with other scores, so attributing these as a cause is * nontrivial. */ break; } return false; } static void check_node_health(resource_checks_t *checks, pcmk_node_t *node) { if (node == NULL) { GHashTableIter iter; bool allowed = false; bool all_nodes_unhealthy = true; g_hash_table_iter_init(&iter, checks->rsc->allowed_nodes); while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { allowed = true; if (!node_is_unhealthy(node)) { all_nodes_unhealthy = false; break; } } if (allowed && all_nodes_unhealthy) { checks->flags |= rsc_node_health; } } else if (node_is_unhealthy(node)) { checks->flags |= rsc_node_health; } } int cli_resource_check(pcmk__output_t *out, pcmk_resource_t *rsc, pcmk_node_t *node) { resource_checks_t checks = { .rsc = rsc }; check_role(&checks); check_managed(&checks); check_locked(&checks); check_node_health(&checks, node); return out->message(out, "resource-check-list", &checks); } // \return Standard Pacemaker return code int cli_resource_fail(pcmk_ipc_api_t *controld_api, const char *host_uname, const char *rsc_id, pcmk_scheduler_t *scheduler) { crm_notice("Failing %s on %s", rsc_id, host_uname); return send_lrm_rsc_op(controld_api, true, host_uname, rsc_id, scheduler); } static GHashTable * generate_resource_params(pcmk_resource_t *rsc, pcmk_node_t *node, pcmk_scheduler_t *scheduler) { GHashTable *params = NULL; GHashTable *meta = NULL; GHashTable *combined = NULL; GHashTableIter iter; char *key = NULL; char *value = NULL; combined = pcmk__strkey_table(free, free); params = pe_rsc_params(rsc, node, scheduler); if (params != NULL) { g_hash_table_iter_init(&iter, params); while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) { pcmk__insert_dup(combined, key, value); } } meta = pcmk__strkey_table(free, free); get_meta_attributes(meta, rsc, NULL, scheduler); if (meta != NULL) { g_hash_table_iter_init(&iter, meta); while (g_hash_table_iter_next(&iter, (gpointer *) & key, (gpointer *) & value)) { char *crm_name = crm_meta_name(key); g_hash_table_insert(combined, crm_name, strdup(value)); } g_hash_table_destroy(meta); } return combined; } bool resource_is_running_on(pcmk_resource_t *rsc, const char *host) { bool found = true; GList *hIter = NULL; GList *hosts = NULL; if (rsc == NULL) { return false; } rsc->fns->location(rsc, &hosts, TRUE); for (hIter = hosts; host != NULL && hIter != NULL; hIter = hIter->next) { pcmk_node_t *node = (pcmk_node_t *) hIter->data; if (pcmk__strcase_any_of(host, node->details->uname, node->details->id, NULL)) { crm_trace("Resource %s is running on %s\n", rsc->id, host); goto done; } } if (host != NULL) { crm_trace("Resource %s is not running on: %s\n", rsc->id, host); found = false; } else if(host == NULL && hosts == NULL) { crm_trace("Resource %s is not running\n", rsc->id); found = false; } done: g_list_free(hosts); return found; } /*! * \internal * \brief Create a list of all resources active on host from a given list * * \param[in] host Name of host to check whether resources are active * \param[in] rsc_list List of resources to check * * \return New list of resources from list that are active on host */ static GList * get_active_resources(const char *host, GList *rsc_list) { GList *rIter = NULL; GList *active = NULL; for (rIter = rsc_list; rIter != NULL; rIter = rIter->next) { pcmk_resource_t *rsc = (pcmk_resource_t *) rIter->data; /* Expand groups to their members, because if we're restarting a member * other than the first, we can't otherwise tell which resources are * stopping and starting. */ if (rsc->variant == pcmk_rsc_variant_group) { active = g_list_concat(active, get_active_resources(host, rsc->children)); } else if (resource_is_running_on(rsc, host)) { active = g_list_append(active, strdup(rsc->id)); } } return active; } static void dump_list(GList *items, const char *tag) { int lpc = 0; GList *item = NULL; for (item = items; item != NULL; item = item->next) { crm_trace("%s[%d]: %s", tag, lpc, (char*)item->data); lpc++; } } static void display_list(pcmk__output_t *out, GList *items, const char *tag) { GList *item = NULL; for (item = items; item != NULL; item = item->next) { out->info(out, "%s%s", tag, (const char *)item->data); } } /*! * \internal * \brief Upgrade XML to latest schema version and use it as scheduler input * * This also updates the scheduler timestamp to the current time. * * \param[in,out] scheduler Scheduler data to update * \param[in,out] xml XML to use as input * * \return Standard Pacemaker return code * \note On success, caller is responsible for freeing memory allocated for * scheduler->now. - * \todo This follows the example of other callers of cli_config_update() - * and returns ENOKEY ("Required key not available") if that fails, - * but perhaps pcmk_rc_schema_validation would be better in that case. + * \todo This follows the example of other callers of + * pcmk__update_configured_schema() and returns ENOKEY ("Required key not + * available") if that fails, but perhaps pcmk_rc_schema_validation would + * be better in that case. */ int update_scheduler_input(pcmk_scheduler_t *scheduler, xmlNode **xml) { - if (cli_config_update(xml, NULL, FALSE) == FALSE) { + if (!pcmk__update_configured_schema(xml, false)) { return ENOKEY; } scheduler->input = *xml; scheduler->now = crm_time_new(NULL); return pcmk_rc_ok; } /*! * \internal * \brief Update scheduler XML input based on a CIB query * * \param[in] scheduler Scheduler data to initialize * \param[in] cib Connection to the CIB manager * * \return Standard Pacemaker return code * \note On success, caller is responsible for freeing memory allocated for * scheduler->input and scheduler->now. */ static int update_scheduler_input_to_cib(pcmk__output_t *out, pcmk_scheduler_t *scheduler, cib_t *cib) { xmlNode *cib_xml_copy = NULL; int rc = pcmk_rc_ok; rc = cib->cmds->query(cib, NULL, &cib_xml_copy, cib_scope_local | cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { out->err(out, "Could not obtain the current CIB: %s (%d)", pcmk_rc_str(rc), rc); return rc; } rc = update_scheduler_input(scheduler, &cib_xml_copy); if (rc != pcmk_rc_ok) { out->err(out, "Could not upgrade the current CIB XML"); free_xml(cib_xml_copy); return rc; } return rc; } // \return Standard Pacemaker return code static int update_dataset(cib_t *cib, pcmk_scheduler_t *scheduler, bool simulate) { char *pid = NULL; char *shadow_file = NULL; cib_t *shadow_cib = NULL; int rc = pcmk_rc_ok; pcmk__output_t *out = scheduler->priv; pe_reset_working_set(scheduler); pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts|pcmk_sched_no_compat); rc = update_scheduler_input_to_cib(out, scheduler, cib); if (rc != pcmk_rc_ok) { return rc; } if(simulate) { bool prev_quiet = false; pid = pcmk__getpid_s(); shadow_cib = cib_shadow_new(pid); shadow_file = get_shadow_file(pid); if (shadow_cib == NULL) { out->err(out, "Could not create shadow cib: '%s'", pid); rc = ENXIO; goto done; } rc = pcmk__xml_write_file(scheduler->input, shadow_file, false, NULL); if (rc != pcmk_rc_ok) { out->err(out, "Could not populate shadow cib: %s", pcmk_rc_str(rc)); goto done; } rc = shadow_cib->cmds->signon(shadow_cib, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { out->err(out, "Could not connect to shadow cib: %s", pcmk_rc_str(rc)); goto done; } pcmk__schedule_actions(scheduler->input, pcmk_sched_no_counts|pcmk_sched_no_compat, scheduler); prev_quiet = out->is_quiet(out); out->quiet = true; pcmk__simulate_transition(scheduler, shadow_cib, NULL); out->quiet = prev_quiet; rc = update_dataset(shadow_cib, scheduler, false); } else { cluster_status(scheduler); } done: // Do not free scheduler->input here, we need rsc->xml to be valid later on cib_delete(shadow_cib); free(pid); if(shadow_file) { unlink(shadow_file); free(shadow_file); } return rc; } /*! * \internal * \brief Find the maximum stop timeout of a resource and its children (if any) * * \param[in,out] rsc Resource to get timeout for * * \return Maximum stop timeout for \p rsc (in milliseconds) */ static guint max_rsc_stop_timeout(pcmk_resource_t *rsc) { long long result_ll; guint max_delay = 0; xmlNode *config = NULL; GHashTable *meta = NULL; if (rsc == NULL) { return 0; } // If resource is collective, use maximum of its children's stop timeouts if (rsc->children != NULL) { for (GList *iter = rsc->children; iter; iter = iter->next) { pcmk_resource_t *child = iter->data; guint delay = max_rsc_stop_timeout(child); if (delay > max_delay) { pcmk__rsc_trace(rsc, "Maximum stop timeout for %s is now %s " "due to %s", rsc->id, pcmk__readable_interval(delay), child->id); max_delay = delay; } } return max_delay; } // Get resource's stop action configuration from CIB config = pcmk__find_action_config(rsc, PCMK_ACTION_STOP, 0, true); /* Get configured timeout for stop action (fully evaluated for rules, * defaults, etc.). * * @TODO This currently ignores node (which might matter for rules) */ meta = pcmk__unpack_action_meta(rsc, NULL, PCMK_ACTION_STOP, 0, config); if ((pcmk__scan_ll(g_hash_table_lookup(meta, PCMK_META_TIMEOUT), &result_ll, -1LL) == pcmk_rc_ok) && (result_ll >= 0)) { max_delay = (guint) QB_MIN(result_ll, UINT_MAX); } g_hash_table_destroy(meta); return max_delay; } /*! * \internal * \brief Find a reasonable waiting time for stopping any one resource in a list * * \param[in,out] scheduler Scheduler data * \param[in] resources List of names of resources that will be stopped * * \return Rough estimate of a reasonable time to wait (in seconds) to stop any * one resource in \p resources * \note This estimate is very rough, simply the maximum stop timeout of all * given resources and their children, plus a small fudge factor. It does * not account for children that must be stopped in sequence, action * throttling, or any demotions needed. It checks the stop timeout, even * if the resources in question are actually being started. */ static guint wait_time_estimate(pcmk_scheduler_t *scheduler, const GList *resources) { guint max_delay = 0U; // Find maximum stop timeout in milliseconds for (const GList *item = resources; item != NULL; item = item->next) { pcmk_resource_t *rsc = pe_find_resource(scheduler->resources, (const char *) item->data); guint delay = max_rsc_stop_timeout(rsc); if (delay > max_delay) { pcmk__rsc_trace(rsc, "Wait time is now %s due to %s", pcmk__readable_interval(delay), rsc->id); max_delay = delay; } } return (max_delay / 1000U) + 5U; } #define waiting_for_starts(d, r, h) ((d != NULL) || \ (!resource_is_running_on((r), (h)))) /*! * \internal * \brief Restart a resource (on a particular host if requested). * * \param[in,out] out Output object * \param[in,out] rsc The resource to restart * \param[in] node Node to restart resource on (NULL for all) * \param[in] move_lifetime If not NULL, how long constraint should * remain in effect (as ISO 8601 string) * \param[in] timeout_ms Consider failed if actions do not complete * in this time (specified in milliseconds, * but a two-second granularity is actually * used; if 0, it will be calculated based on * the resource timeout) * \param[in,out] cib Connection to the CIB manager * \param[in] cib_options Group of enum cib_call_options flags to * use with CIB calls * \param[in] promoted_role_only If true, limit to promoted instances * \param[in] force If true, apply only to requested instance * if part of a collective resource * * \return Standard Pacemaker return code (exits on certain failures) */ int cli_resource_restart(pcmk__output_t *out, pcmk_resource_t *rsc, const pcmk_node_t *node, const char *move_lifetime, guint timeout_ms, cib_t *cib, int cib_options, gboolean promoted_role_only, gboolean force) { int rc = pcmk_rc_ok; int lpc = 0; int before = 0; guint step_timeout_s = 0; guint sleep_interval = 2U; guint timeout = timeout_ms / 1000U; bool stop_via_ban = false; char *rsc_id = NULL; char *lookup_id = NULL; char *orig_target_role = NULL; GList *list_delta = NULL; GList *target_active = NULL; GList *current_active = NULL; GList *restart_target_active = NULL; pcmk_scheduler_t *scheduler = NULL; pcmk_resource_t *parent = uber_parent(rsc); bool running = false; const char *id = rsc->clone_name ? rsc->clone_name : rsc->id; const char *host = node ? node->details->uname : NULL; /* If the implicit resource or primitive resource of a bundle is given, operate on the * bundle itself instead. */ if (pcmk__is_bundled(rsc)) { rsc = parent->parent; } running = resource_is_running_on(rsc, host); if (pcmk__is_clone(parent) && !running) { if (pcmk__is_unique_clone(parent)) { lookup_id = strdup(rsc->id); } else { lookup_id = clone_strip(rsc->id); } rsc = parent->fns->find_rsc(parent, lookup_id, node, pcmk_rsc_match_basename |pcmk_rsc_match_current_node); free(lookup_id); running = resource_is_running_on(rsc, host); } if (!running) { if (host) { out->err(out, "%s is not running on %s and so cannot be restarted", id, host); } else { out->err(out, "%s is not running anywhere and so cannot be restarted", id); } return ENXIO; } if (!pcmk_is_set(rsc->flags, pcmk_rsc_managed)) { out->err(out, "Unmanaged resources cannot be restarted."); return EAGAIN; } rsc_id = strdup(rsc->id); if (pcmk__is_unique_clone(parent)) { lookup_id = strdup(rsc->id); } else { lookup_id = clone_strip(rsc->id); } if (host) { if (pcmk__is_clone(rsc) || pe_bundle_replicas(rsc)) { stop_via_ban = true; } else if (pcmk__is_clone(parent)) { stop_via_ban = true; free(lookup_id); lookup_id = strdup(parent->id); } } /* grab full cib determine originally active resources disable or ban poll cib and watch for affected resources to get stopped without --timeout, calculate the stop timeout for each step and wait for that if we hit --timeout or the service timeout, re-enable or un-ban, report failure and indicate which resources we couldn't take down if everything stopped, re-enable or un-ban poll cib and watch for affected resources to get started without --timeout, calculate the start timeout for each step and wait for that if we hit --timeout or the service timeout, report (different) failure and indicate which resources we couldn't bring back up report success Optimizations: - use constraints to determine ordered list of affected resources - Allow a --no-deps option (aka. --force-restart) */ scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = errno; out->err(out, "Could not allocate scheduler data: %s", pcmk_rc_str(rc)); goto done; } scheduler->priv = out; rc = update_dataset(cib, scheduler, false); if(rc != pcmk_rc_ok) { out->err(out, "Could not get new resource list: %s (%d)", pcmk_rc_str(rc), rc); goto done; } restart_target_active = get_active_resources(host, scheduler->resources); current_active = get_active_resources(host, scheduler->resources); dump_list(current_active, "Origin"); if (stop_via_ban) { /* Stop the clone or bundle instance by banning it from the host */ out->quiet = true; rc = cli_resource_ban(out, lookup_id, host, move_lifetime, cib, cib_options, promoted_role_only, PCMK_ROLE_PROMOTED); } else { /* Stop the resource by setting PCMK_META_TARGET_ROLE to Stopped. * Remember any existing PCMK_META_TARGET_ROLE so we can restore it * later (though it only makes any difference if it's Unpromoted). */ find_resource_attr(out, cib, PCMK_XA_VALUE, lookup_id, NULL, NULL, NULL, PCMK_META_TARGET_ROLE, &orig_target_role); rc = cli_resource_update_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, PCMK_ACTION_STOPPED, FALSE, cib, cib_options, force); } if(rc != pcmk_rc_ok) { out->err(out, "Could not set " PCMK_META_TARGET_ROLE " for %s: %s (%d)", rsc_id, pcmk_rc_str(rc), rc); if (current_active != NULL) { g_list_free_full(current_active, free); current_active = NULL; } if (restart_target_active != NULL) { g_list_free_full(restart_target_active, free); restart_target_active = NULL; } goto done; } rc = update_dataset(cib, scheduler, true); if(rc != pcmk_rc_ok) { out->err(out, "Could not determine which resources would be stopped"); goto failure; } target_active = get_active_resources(host, scheduler->resources); dump_list(target_active, "Target"); list_delta = pcmk__subtract_lists(current_active, target_active, (GCompareFunc) strcmp); out->info(out, "Waiting for %d resources to stop:", g_list_length(list_delta)); display_list(out, list_delta, " * "); step_timeout_s = timeout / sleep_interval; while (list_delta != NULL) { before = g_list_length(list_delta); if(timeout_ms == 0) { step_timeout_s = wait_time_estimate(scheduler, list_delta) / sleep_interval; } /* We probably don't need the entire step timeout */ for(lpc = 0; (lpc < step_timeout_s) && (list_delta != NULL); lpc++) { sleep(sleep_interval); if(timeout) { timeout -= sleep_interval; crm_trace("%us remaining", timeout); } rc = update_dataset(cib, scheduler, FALSE); if(rc != pcmk_rc_ok) { out->err(out, "Could not determine which resources were stopped"); goto failure; } if (current_active != NULL) { g_list_free_full(current_active, free); } current_active = get_active_resources(host, scheduler->resources); g_list_free(list_delta); list_delta = pcmk__subtract_lists(current_active, target_active, (GCompareFunc) strcmp); dump_list(current_active, "Current"); dump_list(list_delta, "Delta"); } crm_trace("%d (was %d) resources remaining", g_list_length(list_delta), before); if(before == g_list_length(list_delta)) { /* aborted during stop phase, print the contents of list_delta */ out->err(out, "Could not complete shutdown of %s, %d resources remaining", rsc_id, g_list_length(list_delta)); display_list(out, list_delta, " * "); rc = ETIME; goto failure; } } if (stop_via_ban) { rc = cli_resource_clear(lookup_id, host, NULL, cib, cib_options, true, force); } else if (orig_target_role) { rc = cli_resource_update_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, orig_target_role, FALSE, cib, cib_options, force); free(orig_target_role); orig_target_role = NULL; } else { rc = cli_resource_delete_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, cib, cib_options, force); } if(rc != pcmk_rc_ok) { out->err(out, "Could not unset " PCMK_META_TARGET_ROLE " for %s: %s (%d)", rsc_id, pcmk_rc_str(rc), rc); goto done; } if (target_active != NULL) { g_list_free_full(target_active, free); } target_active = restart_target_active; list_delta = pcmk__subtract_lists(target_active, current_active, (GCompareFunc) strcmp); out->info(out, "Waiting for %d resources to start again:", g_list_length(list_delta)); display_list(out, list_delta, " * "); step_timeout_s = timeout / sleep_interval; while (waiting_for_starts(list_delta, rsc, host)) { before = g_list_length(list_delta); if(timeout_ms == 0) { step_timeout_s = wait_time_estimate(scheduler, list_delta) / sleep_interval; } /* We probably don't need the entire step timeout */ for (lpc = 0; (lpc < step_timeout_s) && waiting_for_starts(list_delta, rsc, host); lpc++) { sleep(sleep_interval); if(timeout) { timeout -= sleep_interval; crm_trace("%ds remaining", timeout); } rc = update_dataset(cib, scheduler, false); if(rc != pcmk_rc_ok) { out->err(out, "Could not determine which resources were started"); goto failure; } /* It's OK if dependent resources moved to a different node, * so we check active resources on all nodes. */ if (current_active != NULL) { g_list_free_full(current_active, free); } current_active = get_active_resources(NULL, scheduler->resources); g_list_free(list_delta); list_delta = pcmk__subtract_lists(target_active, current_active, (GCompareFunc) strcmp); dump_list(current_active, "Current"); dump_list(list_delta, "Delta"); } if(before == g_list_length(list_delta)) { /* aborted during start phase, print the contents of list_delta */ out->err(out, "Could not complete restart of %s, %d resources remaining", rsc_id, g_list_length(list_delta)); display_list(out, list_delta, " * "); rc = ETIME; goto failure; } } rc = pcmk_rc_ok; goto done; failure: if (stop_via_ban) { cli_resource_clear(lookup_id, host, NULL, cib, cib_options, true, force); } else if (orig_target_role) { cli_resource_update_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, orig_target_role, FALSE, cib, cib_options, force); free(orig_target_role); } else { cli_resource_delete_attribute(rsc, rsc_id, NULL, PCMK_XE_META_ATTRIBUTES, NULL, PCMK_META_TARGET_ROLE, cib, cib_options, force); } done: if (list_delta != NULL) { g_list_free(list_delta); } if (current_active != NULL) { g_list_free_full(current_active, free); } if (target_active != NULL && (target_active != restart_target_active)) { g_list_free_full(target_active, free); } if (restart_target_active != NULL) { g_list_free_full(restart_target_active, free); } free(rsc_id); free(lookup_id); pe_free_working_set(scheduler); return rc; } static inline bool action_is_pending(const pcmk_action_t *action) { if (pcmk_any_flags_set(action->flags, pcmk_action_optional|pcmk_action_pseudo) || !pcmk_is_set(action->flags, pcmk_action_runnable) || pcmk__str_eq(PCMK_ACTION_NOTIFY, action->task, pcmk__str_casei)) { return false; } return true; } /*! * \internal * \brief Check whether any actions in a list are pending * * \param[in] actions List of actions to check * * \return true if any actions in the list are pending, otherwise false */ static bool actions_are_pending(const GList *actions) { for (const GList *action = actions; action != NULL; action = action->next) { const pcmk_action_t *a = (const pcmk_action_t *) action->data; if (action_is_pending(a)) { crm_notice("Waiting for %s (flags=%#.8x)", a->uuid, a->flags); return true; } } return false; } static void print_pending_actions(pcmk__output_t *out, GList *actions) { GList *action; out->info(out, "Pending actions:"); for (action = actions; action != NULL; action = action->next) { pcmk_action_t *a = (pcmk_action_t *) action->data; if (!action_is_pending(a)) { continue; } if (a->node) { out->info(out, "\tAction %d: %s\ton %s", a->id, a->uuid, pcmk__node_name(a->node)); } else { out->info(out, "\tAction %d: %s", a->id, a->uuid); } } } /* For --wait, timeout (in seconds) to use if caller doesn't specify one */ #define WAIT_DEFAULT_TIMEOUT_S (60 * 60) /* For --wait, how long to sleep between cluster state checks */ #define WAIT_SLEEP_S (2) /*! * \internal * \brief Wait until all pending cluster actions are complete * * This waits until either the CIB's transition graph is idle or a timeout is * reached. * * \param[in,out] out Output object * \param[in] timeout_ms Consider failed if actions do not complete in * this time (specified in milliseconds, but * one-second granularity is actually used; if 0, a * default will be used) * \param[in,out] cib Connection to the CIB manager * * \return Standard Pacemaker return code */ int wait_till_stable(pcmk__output_t *out, guint timeout_ms, cib_t * cib) { pcmk_scheduler_t *scheduler = NULL; xmlXPathObjectPtr search; int rc = pcmk_rc_ok; bool pending_unknown_state_resources; time_t expire_time = time(NULL); time_t time_diff; bool printed_version_warning = out->is_quiet(out); // i.e. don't print if quiet char *xpath = NULL; if (timeout_ms == 0) { expire_time += WAIT_DEFAULT_TIMEOUT_S; } else { expire_time += (timeout_ms + 999) / 1000; } scheduler = pe_new_working_set(); if (scheduler == NULL) { return ENOMEM; } xpath = crm_strdup_printf("/" PCMK_XE_CIB "/" PCMK_XE_STATUS "/" PCMK__XE_NODE_STATE "/" PCMK__XE_LRM "/" PCMK__XE_LRM_RESOURCES "/" PCMK__XE_LRM_RESOURCE "/" PCMK__XE_LRM_RSC_OP "[@" PCMK__XA_RC_CODE "='%d']", PCMK_OCF_UNKNOWN); do { /* Abort if timeout is reached */ time_diff = expire_time - time(NULL); if (time_diff <= 0) { print_pending_actions(out, scheduler->actions); rc = ETIME; break; } crm_info("Waiting up to %lld seconds for cluster actions to complete", (long long) time_diff); if (rc == pcmk_rc_ok) { /* this avoids sleep on first loop iteration */ sleep(WAIT_SLEEP_S); } /* Get latest transition graph */ pe_reset_working_set(scheduler); rc = update_scheduler_input_to_cib(out, scheduler, cib); if (rc != pcmk_rc_ok) { break; } pcmk__schedule_actions(scheduler->input, pcmk_sched_no_counts|pcmk_sched_no_compat, scheduler); if (!printed_version_warning) { /* If the DC has a different version than the local node, the two * could come to different conclusions about what actions need to be * done. Warn the user in this case. * * @TODO A possible long-term solution would be to reimplement the * wait as a new controller operation that would be forwarded to the * DC. However, that would have potential problems of its own. */ const char *dc_version = g_hash_table_lookup(scheduler->config_hash, PCMK_OPT_DC_VERSION); if (!pcmk__str_eq(dc_version, PACEMAKER_VERSION "-" BUILD_VERSION, pcmk__str_casei)) { out->info(out, "warning: wait option may not work properly in " "mixed-version cluster"); printed_version_warning = true; } } search = xpath_search(scheduler->input, xpath); pending_unknown_state_resources = (numXpathResults(search) > 0); freeXpathObject(search); } while (actions_are_pending(scheduler->actions) || pending_unknown_state_resources); pe_free_working_set(scheduler); free(xpath); return rc; } static const char * get_action(const char *rsc_action) { const char *action = NULL; if (pcmk__str_eq(rsc_action, "validate", pcmk__str_casei)) { action = PCMK_ACTION_VALIDATE_ALL; } else if (pcmk__str_eq(rsc_action, "force-check", pcmk__str_casei)) { action = PCMK_ACTION_MONITOR; } else if (pcmk__strcase_any_of(rsc_action, "force-start", "force-stop", "force-demote", "force-promote", NULL)) { action = rsc_action+6; } else { action = rsc_action; } return action; } /*! * \brief Set up environment variables as expected by resource agents * * When the cluster executes resource agents, it adds certain environment * variables (directly or via resource meta-attributes) expected by some * resource agents. Add the essential ones that many resource agents expect, so * the behavior is the same for command-line execution. * * \param[in,out] params Resource parameters that will be passed to agent * \param[in] timeout_ms Action timeout (in milliseconds) * \param[in] check_level OCF check level * \param[in] verbosity Verbosity level */ static void set_agent_environment(GHashTable *params, guint timeout_ms, int check_level, int verbosity) { g_hash_table_insert(params, crm_meta_name(PCMK_META_TIMEOUT), crm_strdup_printf("%u", timeout_ms)); pcmk__insert_dup(params, PCMK_XA_CRM_FEATURE_SET, CRM_FEATURE_SET); if (check_level >= 0) { char *level = crm_strdup_printf("%d", check_level); setenv("OCF_CHECK_LEVEL", level, 1); free(level); } pcmk__set_env_option(PCMK__ENV_DEBUG, ((verbosity > 0)? "1" : "0"), true); if (verbosity > 1) { setenv("OCF_TRACE_RA", "1", 1); } /* A resource agent using the standard ocf-shellfuncs library will not print * messages to stderr if it doesn't have a controlling terminal (e.g. if * crm_resource is called via script or ssh). This forces it to do so. */ setenv("OCF_TRACE_FILE", "/dev/stderr", 0); } /*! * \internal * \brief Apply command-line overrides to resource parameters * * \param[in,out] params Parameters to be passed to agent * \param[in] overrides Parameters to override (or NULL if none) */ static void apply_overrides(GHashTable *params, GHashTable *overrides) { if (overrides != NULL) { GHashTableIter iter; char *name = NULL; char *value = NULL; g_hash_table_iter_init(&iter, overrides); while (g_hash_table_iter_next(&iter, (gpointer *) &name, (gpointer *) &value)) { pcmk__insert_dup(params, name, value); } } } crm_exit_t cli_resource_execute_from_params(pcmk__output_t *out, const char *rsc_name, const char *rsc_class, const char *rsc_prov, const char *rsc_type, const char *rsc_action, GHashTable *params, GHashTable *override_hash, guint timeout_ms, int resource_verbose, gboolean force, int check_level) { const char *class = rsc_class; const char *action = get_action(rsc_action); crm_exit_t exit_code = CRM_EX_OK; svc_action_t *op = NULL; // If no timeout was provided, use the same default as the cluster if (timeout_ms == 0U) { timeout_ms = PCMK_DEFAULT_ACTION_TIMEOUT_MS; } set_agent_environment(params, timeout_ms, check_level, resource_verbose); apply_overrides(params, override_hash); op = services__create_resource_action(rsc_name? rsc_name : "test", rsc_class, rsc_prov, rsc_type, action, 0, QB_MIN(timeout_ms, INT_MAX), params, 0); if (op == NULL) { out->err(out, "Could not execute %s using %s%s%s:%s: %s", action, rsc_class, (rsc_prov? ":" : ""), (rsc_prov? rsc_prov : ""), rsc_type, strerror(ENOMEM)); g_hash_table_destroy(params); return CRM_EX_OSERR; } if (pcmk__str_eq(rsc_class, PCMK_RESOURCE_CLASS_SERVICE, pcmk__str_casei)) { class = resources_find_service_class(rsc_type); } if (!pcmk__strcase_any_of(class, PCMK_RESOURCE_CLASS_OCF, PCMK_RESOURCE_CLASS_LSB, NULL)) { services__format_result(op, CRM_EX_UNIMPLEMENT_FEATURE, PCMK_EXEC_ERROR, "Manual execution of the %s standard is " "unsupported", pcmk__s(class, "unspecified")); } if (op->rc != PCMK_OCF_UNKNOWN) { exit_code = op->rc; goto done; } services_action_sync(op); // Map results to OCF codes for consistent reporting to user { enum ocf_exitcode ocf_code = services_result2ocf(class, action, op->rc); // Cast variable instead of function return to keep compilers happy exit_code = (crm_exit_t) ocf_code; } done: out->message(out, "resource-agent-action", resource_verbose, rsc_class, rsc_prov, rsc_type, rsc_name, rsc_action, override_hash, exit_code, op->status, services__exit_reason(op), op->stdout_data, op->stderr_data); services_action_free(op); return exit_code; } /*! * \internal * \brief Get the timeout the cluster would use for an action * * \param[in] rsc Resource that action is for * \param[in] action Name of action */ static guint get_action_timeout(pcmk_resource_t *rsc, const char *action) { long long timeout_ms = -1LL; xmlNode *op = pcmk__find_action_config(rsc, action, 0, true); GHashTable *meta = pcmk__unpack_action_meta(rsc, NULL, action, 0, op); if ((pcmk__scan_ll(g_hash_table_lookup(meta, PCMK_META_TIMEOUT), &timeout_ms, -1LL) != pcmk_rc_ok) || (timeout_ms <= 0LL)) { timeout_ms = PCMK_DEFAULT_ACTION_TIMEOUT_MS; } g_hash_table_destroy(meta); return (guint) QB_MIN(timeout_ms, UINT_MAX); } crm_exit_t cli_resource_execute(pcmk_resource_t *rsc, const char *requested_name, const char *rsc_action, GHashTable *override_hash, guint timeout_ms, cib_t *cib, pcmk_scheduler_t *scheduler, int resource_verbose, gboolean force, int check_level) { pcmk__output_t *out = scheduler->priv; crm_exit_t exit_code = CRM_EX_OK; const char *rid = NULL; const char *rtype = NULL; const char *rprov = NULL; const char *rclass = NULL; GHashTable *params = NULL; if (pcmk__strcase_any_of(rsc_action, "force-start", "force-demote", "force-promote", NULL)) { if (pcmk__is_clone(rsc)) { GList *nodes = cli_resource_search(rsc, requested_name, scheduler); if(nodes != NULL && force == FALSE) { out->err(out, "It is not safe to %s %s here: the cluster claims it is already active", rsc_action, rsc->id); out->err(out, "Try setting " PCMK_META_TARGET_ROLE "=" PCMK_ROLE_STOPPED " first or specifying the force option"); return CRM_EX_UNSAFE; } g_list_free_full(nodes, free); } } if (pcmk__is_clone(rsc)) { /* Grab the first child resource in the hope it's not a group */ rsc = rsc->children->data; } if (rsc->variant == pcmk_rsc_variant_group) { out->err(out, "Sorry, the %s option doesn't support group resources", rsc_action); return CRM_EX_UNIMPLEMENT_FEATURE; } else if (pcmk__is_bundled(rsc)) { out->err(out, "Sorry, the %s option doesn't support bundled resources", rsc_action); return CRM_EX_UNIMPLEMENT_FEATURE; } rclass = crm_element_value(rsc->xml, PCMK_XA_CLASS); rprov = crm_element_value(rsc->xml, PCMK_XA_PROVIDER); rtype = crm_element_value(rsc->xml, PCMK_XA_TYPE); params = generate_resource_params(rsc, NULL /* @TODO use local node */, scheduler); if (timeout_ms == 0U) { timeout_ms = get_action_timeout(rsc, get_action(rsc_action)); } rid = pcmk__is_anonymous_clone(rsc->parent)? requested_name : rsc->id; exit_code = cli_resource_execute_from_params(out, rid, rclass, rprov, rtype, rsc_action, params, override_hash, timeout_ms, resource_verbose, force, check_level); return exit_code; } // \return Standard Pacemaker return code int cli_resource_move(const pcmk_resource_t *rsc, const char *rsc_id, const char *host_name, const char *move_lifetime, cib_t *cib, int cib_options, pcmk_scheduler_t *scheduler, gboolean promoted_role_only, gboolean force) { pcmk__output_t *out = scheduler->priv; int rc = pcmk_rc_ok; unsigned int count = 0; pcmk_node_t *current = NULL; pcmk_node_t *dest = pe_find_node(scheduler->nodes, host_name); bool cur_is_dest = false; if (dest == NULL) { return pcmk_rc_node_unknown; } if (promoted_role_only && !pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) { const pcmk_resource_t *p = pe__const_top_resource(rsc, false); if (pcmk_is_set(p->flags, pcmk_rsc_promotable)) { out->info(out, "Using parent '%s' for move instead of '%s'.", rsc->id, rsc_id); rsc_id = p->id; rsc = p; } else { out->info(out, "Ignoring --promoted option: %s is not promotable", rsc_id); promoted_role_only = FALSE; } } current = pe__find_active_requires(rsc, &count); if (pcmk_is_set(rsc->flags, pcmk_rsc_promotable)) { unsigned int promoted_count = 0; pcmk_node_t *promoted_node = NULL; for (const GList *iter = rsc->children; iter; iter = iter->next) { const pcmk_resource_t *child = (const pcmk_resource_t *) iter->data; enum rsc_role_e child_role = child->fns->state(child, TRUE); if (child_role == pcmk_role_promoted) { rsc = child; promoted_node = pcmk__current_node(child); promoted_count++; } } if (promoted_role_only || (promoted_count != 0)) { count = promoted_count; current = promoted_node; } } if (count > 1) { if (pcmk__is_clone(rsc)) { current = NULL; } else { return pcmk_rc_multiple; } } if (pcmk__same_node(current, dest)) { cur_is_dest = true; if (force) { crm_info("%s is already %s on %s, reinforcing placement with location constraint.", rsc_id, promoted_role_only?"promoted":"active", pcmk__node_name(dest)); } else { return pcmk_rc_already; } } /* Clear any previous prefer constraints across all nodes. */ cli_resource_clear(rsc_id, NULL, scheduler->nodes, cib, cib_options, false, force); /* Clear any previous ban constraints on 'dest'. */ cli_resource_clear(rsc_id, dest->details->uname, scheduler->nodes, cib, cib_options, TRUE, force); /* Record an explicit preference for 'dest' */ rc = cli_resource_prefer(out, rsc_id, dest->details->uname, move_lifetime, cib, cib_options, promoted_role_only, PCMK_ROLE_PROMOTED); crm_trace("%s%s now prefers %s%s", rsc->id, (promoted_role_only? " (promoted)" : ""), pcmk__node_name(dest), force?"(forced)":""); /* only ban the previous location if current location != destination location. * it is possible to use -M to enforce a location without regard of where the * resource is currently located */ if (force && !cur_is_dest) { /* Ban the original location if possible */ if(current) { (void)cli_resource_ban(out, rsc_id, current->details->uname, move_lifetime, cib, cib_options, promoted_role_only, PCMK_ROLE_PROMOTED); } else if(count > 1) { out->info(out, "Resource '%s' is currently %s in %d locations. " "One may now move to %s", rsc_id, (promoted_role_only? "promoted" : "active"), count, pcmk__node_name(dest)); out->info(out, "To prevent '%s' from being %s at a specific location, " "specify a node.", rsc_id, (promoted_role_only? "promoted" : "active")); } else { crm_trace("Not banning %s from its current location: not active", rsc_id); } } return rc; } diff --git a/tools/crm_simulate.c b/tools/crm_simulate.c index d503b4cc84..55490c3fac 100644 --- a/tools/crm_simulate.c +++ b/tools/crm_simulate.c @@ -1,586 +1,586 @@ /* * Copyright 2009-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 #include #define SUMMARY "crm_simulate - simulate a Pacemaker cluster's response to events" struct { char *dot_file; char *graph_file; gchar *input_file; pcmk_injections_t *injections; unsigned int flags; gchar *output_file; long long repeat; gboolean store; gchar *test_dir; char *use_date; char *xml_file; } options = { .flags = pcmk_sim_show_pending | pcmk_sim_sanitized, .repeat = 1 }; uint32_t section_opts = 0; char *temp_shadow = NULL; crm_exit_t exit_code = CRM_EX_OK; #define INDENT " " static pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; static gboolean all_actions_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_all_actions; return TRUE; } static gboolean attrs_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { section_opts |= pcmk_section_attributes; return TRUE; } static gboolean failcounts_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { section_opts |= pcmk_section_failcounts | pcmk_section_failures; return TRUE; } static gboolean in_place_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.store = TRUE; options.flags |= pcmk_sim_process | pcmk_sim_simulate; return TRUE; } static gboolean live_check_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { if (options.xml_file) { free(options.xml_file); } options.xml_file = NULL; options.flags &= ~pcmk_sim_sanitized; return TRUE; } static gboolean node_down_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->node_down = g_list_append(options.injections->node_down, g_strdup(optarg)); return TRUE; } static gboolean node_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->node_fail = g_list_append(options.injections->node_fail, g_strdup(optarg)); return TRUE; } static gboolean node_up_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__simulate_node_config = true; options.injections->node_up = g_list_append(options.injections->node_up, g_strdup(optarg)); return TRUE; } static gboolean op_fail_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_simulate; options.injections->op_fail = g_list_append(options.injections->op_fail, g_strdup(optarg)); return TRUE; } static gboolean op_inject_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->op_inject = g_list_append(options.injections->op_inject, g_strdup(optarg)); return TRUE; } static gboolean pending_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_show_pending; return TRUE; } static gboolean process_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process; return TRUE; } static gboolean quorum_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.injections->quorum, optarg); return TRUE; } static gboolean save_dotfile_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process; pcmk__str_update(&options.dot_file, optarg); return TRUE; } static gboolean save_graph_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process; pcmk__str_update(&options.graph_file, optarg); return TRUE; } static gboolean show_scores_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_show_scores; return TRUE; } static gboolean simulate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_simulate; return TRUE; } static gboolean ticket_activate_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_activate = g_list_append(options.injections->ticket_activate, g_strdup(optarg)); return TRUE; } static gboolean ticket_grant_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_grant = g_list_append(options.injections->ticket_grant, g_strdup(optarg)); return TRUE; } static gboolean ticket_revoke_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_revoke = g_list_append(options.injections->ticket_revoke, g_strdup(optarg)); return TRUE; } static gboolean ticket_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.injections->ticket_standby = g_list_append(options.injections->ticket_standby, g_strdup(optarg)); return TRUE; } static gboolean utilization_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { options.flags |= pcmk_sim_process | pcmk_sim_show_utilization; return TRUE; } static gboolean watchdog_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.injections->watchdog, optarg); return TRUE; } static gboolean xml_file_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.xml_file, optarg); options.flags |= pcmk_sim_sanitized; return TRUE; } static gboolean xml_pipe_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { pcmk__str_update(&options.xml_file, "-"); options.flags |= pcmk_sim_sanitized; return TRUE; } static GOptionEntry operation_entries[] = { { "run", 'R', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, process_cb, "Process the supplied input and show what actions the cluster will take in response", NULL }, { "simulate", 'S', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, simulate_cb, "Like --run, but also simulate taking those actions and show the resulting new status", NULL }, { "in-place", 'X', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, in_place_cb, "Like --simulate, but also store the results back to the input file", NULL }, { "show-attrs", 'A', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, attrs_cb, "Show node attributes", NULL }, { "show-failcounts", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, failcounts_cb, "Show resource fail counts", NULL }, { "show-scores", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, show_scores_cb, "Show allocation scores", NULL }, { "show-utilization", 'U', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, utilization_cb, "Show utilization information", NULL }, { "profile", 'P', 0, G_OPTION_ARG_FILENAME, &options.test_dir, "Process all the XML files in the named directory to create profiling data", "DIR" }, { "repeat", 'N', 0, G_OPTION_ARG_INT, &options.repeat, "With --profile, repeat each test N times and print timings", "N" }, /* Deprecated */ { "pending", 'j', G_OPTION_FLAG_NO_ARG|G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, pending_cb, "Display pending state if '" PCMK_META_RECORD_PENDING "' is enabled", NULL }, { NULL } }; static GOptionEntry synthetic_entries[] = { { "node-up", 'u', 0, G_OPTION_ARG_CALLBACK, node_up_cb, "Simulate bringing a node online", "NODE" }, { "node-down", 'd', 0, G_OPTION_ARG_CALLBACK, node_down_cb, "Simulate taking a node offline", "NODE" }, { "node-fail", 'f', 0, G_OPTION_ARG_CALLBACK, node_fail_cb, "Simulate a node failing", "NODE" }, { "op-inject", 'i', 0, G_OPTION_ARG_CALLBACK, op_inject_cb, "Generate a failure for the cluster to react to in the simulation.\n" INDENT "See `Operation Specification` help for more information.", "OPSPEC" }, { "op-fail", 'F', 0, G_OPTION_ARG_CALLBACK, op_fail_cb, "If the specified task occurs during the simulation, have it fail with return code ${rc}.\n" INDENT "The transition will normally stop at the failed action.\n" INDENT "Save the result with --save-output and re-run with --xml-file.\n" INDENT "See `Operation Specification` help for more information.", "OPSPEC" }, { "set-datetime", 't', 0, G_OPTION_ARG_STRING, &options.use_date, "Set date/time (ISO 8601 format, see https://en.wikipedia.org/wiki/ISO_8601)", "DATETIME" }, { "quorum", 'q', 0, G_OPTION_ARG_CALLBACK, quorum_cb, "Set to '1' (or 'true') to indicate cluster has quorum", "QUORUM" }, { "watchdog", 'w', 0, G_OPTION_ARG_CALLBACK, watchdog_cb, "Set to '1' (or 'true') to indicate cluster has an active watchdog device", "DEVICE" }, { "ticket-grant", 'g', 0, G_OPTION_ARG_CALLBACK, ticket_grant_cb, "Simulate granting a ticket", "TICKET" }, { "ticket-revoke", 'r', 0, G_OPTION_ARG_CALLBACK, ticket_revoke_cb, "Simulate revoking a ticket", "TICKET" }, { "ticket-standby", 'b', 0, G_OPTION_ARG_CALLBACK, ticket_standby_cb, "Simulate making a ticket standby", "TICKET" }, { "ticket-activate", 'e', 0, G_OPTION_ARG_CALLBACK, ticket_activate_cb, "Simulate activating a ticket", "TICKET" }, { NULL } }; static GOptionEntry artifact_entries[] = { { "save-input", 'I', 0, G_OPTION_ARG_FILENAME, &options.input_file, "Save the input configuration to the named file", "FILE" }, { "save-output", 'O', 0, G_OPTION_ARG_FILENAME, &options.output_file, "Save the output configuration to the named file", "FILE" }, { "save-graph", 'G', 0, G_OPTION_ARG_CALLBACK, save_graph_cb, "Save the transition graph (XML format) to the named file", "FILE" }, { "save-dotfile", 'D', 0, G_OPTION_ARG_CALLBACK, save_dotfile_cb, "Save the transition graph (DOT format) to the named file", "FILE" }, { "all-actions", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, all_actions_cb, "Display all possible actions in DOT graph (even if not part of transition)", NULL }, { NULL } }; static GOptionEntry source_entries[] = { { "live-check", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, live_check_cb, "Connect to CIB manager and use the current CIB contents as input", NULL }, { "xml-file", 'x', 0, G_OPTION_ARG_CALLBACK, xml_file_cb, "Retrieve XML from the named file", "FILE" }, { "xml-pipe", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, xml_pipe_cb, "Retrieve XML from stdin", NULL }, { NULL } }; static int setup_input(pcmk__output_t *out, const char *input, const char *output, GError **error) { int rc = pcmk_rc_ok; xmlNode *cib_object = NULL; char *local_output = NULL; if (input == NULL) { /* Use live CIB */ rc = cib__signon_query(out, NULL, &cib_object); if (rc != pcmk_rc_ok) { // cib__signon_query() outputs any relevant error return rc; } } else if (pcmk__str_eq(input, "-", pcmk__str_casei)) { cib_object = pcmk__xml_read(NULL); } else { cib_object = pcmk__xml_read(input); } if (pcmk_find_cib_element(cib_object, PCMK_XE_STATUS) == NULL) { pcmk__xe_create(cib_object, PCMK_XE_STATUS); } - if (cli_config_update(&cib_object, NULL, FALSE) == FALSE) { + if (!pcmk__update_configured_schema(&cib_object, false)) { free_xml(cib_object); return pcmk_rc_transform_failed; } - if (validate_xml(cib_object, NULL, FALSE) != TRUE) { + if (!pcmk__validate_xml(cib_object, NULL, NULL, NULL)) { free_xml(cib_object); return pcmk_rc_schema_validation; } if (output == NULL) { char *pid = pcmk__getpid_s(); local_output = get_shadow_file(pid); temp_shadow = strdup(local_output); output = local_output; free(pid); } rc = pcmk__xml_write_file(cib_object, output, false, NULL); if (rc != pcmk_rc_ok) { g_set_error(error, PCMK__EXITC_ERROR, CRM_EX_CANTCREAT, "Could not create '%s': %s", output, pcmk_rc_str(rc)); } else { setenv("CIB_file", output, 1); } free_xml(cib_object); free(local_output); return rc; } static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; GOptionEntry extra_prog_entries[] = { { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &(args->quiet), "Display only essential output", NULL }, { NULL } }; const char *description = "Operation Specification:\n\n" "The OPSPEC in any command line option is of the form\n" "${resource}_${task}_${interval_in_ms}@${node}=${rc}\n" "(memcached_monitor_20000@bart.example.com=7, for example).\n" "${rc} is an OCF return code. For more information on these\n" "return codes, refer to https://clusterlabs.org/pacemaker/doc/2.1/Pacemaker_Administration/html/agents.html#ocf-return-codes\n\n" "Examples:\n\n" "Pretend a recurring monitor action found memcached stopped on node\n" "fred.example.com and, during recovery, that the memcached stop\n" "action failed:\n\n" "\tcrm_simulate -LS --op-inject memcached:0_monitor_20000@bart.example.com=7 " "--op-fail memcached:0_stop_0@fred.example.com=1 --save-output /tmp/memcached-test.xml\n\n" "Now see what the reaction to the stop failed would be:\n\n" "\tcrm_simulate -S --xml-file /tmp/memcached-test.xml\n\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, NULL); pcmk__add_main_args(context, extra_prog_entries); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "operations", "Operations:", "Show operations options", operation_entries); pcmk__add_arg_group(context, "synthetic", "Synthetic Cluster Events:", "Show synthetic cluster event options", synthetic_entries); pcmk__add_arg_group(context, "artifact", "Artifact Options:", "Show artifact options", artifact_entries); pcmk__add_arg_group(context, "source", "Data Source:", "Show data source options", source_entries); return context; } int main(int argc, char **argv) { int rc = pcmk_rc_ok; pcmk_scheduler_t *scheduler = NULL; pcmk__output_t *out = NULL; GError *error = NULL; GOptionGroup *output_group = NULL; pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY); gchar **processed_args = pcmk__cmdline_preproc(argv, "bdefgiqrtuwxDFGINOP"); GOptionContext *context = build_arg_context(args, &output_group); options.injections = calloc(1, sizeof(pcmk_injections_t)); if (options.injections == NULL) { rc = ENOMEM; goto done; } /* This must come before g_option_context_parse_strv. */ options.xml_file = strdup("-"); pcmk__register_formats(output_group, formats); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } pcmk__cli_init_logging("crm_simulate", args->verbosity); rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); if (rc != pcmk_rc_ok) { fprintf(stderr, "Error creating output format %s: %s\n", args->output_ty, pcmk_rc_str(rc)); exit_code = CRM_EX_ERROR; goto done; } if (pcmk__str_eq(args->output_ty, "text", pcmk__str_null_matches) && !pcmk_is_set(options.flags, pcmk_sim_show_scores) && !pcmk_is_set(options.flags, pcmk_sim_show_utilization)) { pcmk__output_text_set_fancy(out, true); } pe__register_messages(out); pcmk__register_lib_messages(out); out->quiet = args->quiet; if (args->version) { out->version(out, false); goto done; } if (args->verbosity > 0) { options.flags |= pcmk_sim_verbose; #ifdef PCMK__COMPAT_2_0 /* Redirect stderr to stdout so we can grep the output */ close(STDERR_FILENO); dup2(STDOUT_FILENO, STDERR_FILENO); #endif } scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = ENOMEM; g_set_error(&error, PCMK__RC_ERROR, rc, "Could not allocate scheduler data"); goto done; } if (pcmk_is_set(options.flags, pcmk_sim_show_scores)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_output_scores); } if (pcmk_is_set(options.flags, pcmk_sim_show_utilization)) { pcmk__set_scheduler_flags(scheduler, pcmk_sched_show_utilization); } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_compat); if (options.test_dir != NULL) { scheduler->priv = out; pcmk__profile_dir(options.test_dir, options.repeat, scheduler, options.use_date); rc = pcmk_rc_ok; goto done; } rc = setup_input(out, options.xml_file, options.store? options.xml_file : options.output_file, &error); if (rc != pcmk_rc_ok) { goto done; } rc = pcmk__simulate(scheduler, out, options.injections, options.flags, section_opts, options.use_date, options.input_file, options.graph_file, options.dot_file); done: pcmk__output_and_clear_error(&error, NULL); /* There sure is a lot to free in options. */ free(options.dot_file); free(options.graph_file); g_free(options.input_file); g_free(options.output_file); g_free(options.test_dir); free(options.use_date); free(options.xml_file); pcmk_free_injections(options.injections); pcmk__free_arg_context(context); g_strfreev(processed_args); if (scheduler != NULL) { pe_free_working_set(scheduler); } fflush(stderr); if (temp_shadow) { unlink(temp_shadow); free(temp_shadow); } if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); } if (out != NULL) { out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } pcmk__unregister_formats(); crm_exit(exit_code); } diff --git a/tools/crm_ticket.c b/tools/crm_ticket.c index 4ed0c75ef8..8fb4a95690 100644 --- a/tools/crm_ticket.c +++ b/tools/crm_ticket.c @@ -1,664 +1,664 @@ /* * Copyright 2012-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 GError *error = NULL; #define SUMMARY "Perform tasks related to cluster tickets\n\n" \ "Allows ticket attributes to be queried, modified and deleted." struct { gchar *attr_default; gchar *attr_id; char *attr_name; char *attr_value; gboolean force; char *get_attr_name; gboolean quiet; gchar *set_name; char ticket_cmd; gchar *ticket_id; gchar *xml_file; } options = { .ticket_cmd = 'S' }; GList *attr_delete; GHashTable *attr_set; bool modified = false; int cib_options = cib_sync_call; static pcmk__output_t *out = NULL; #define INDENT " " static pcmk__supported_format_t formats[] = { PCMK__SUPPORTED_FORMAT_NONE, PCMK__SUPPORTED_FORMAT_TEXT, PCMK__SUPPORTED_FORMAT_XML, { NULL, NULL, NULL } }; static gboolean attr_value_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.attr_value, optarg); if (!options.attr_name || !options.attr_value) { return TRUE; } pcmk__insert_dup(attr_set, options.attr_name, options.attr_value); pcmk__str_update(&options.attr_name, NULL); pcmk__str_update(&options.attr_value, NULL); modified = true; return TRUE; } static gboolean command_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (pcmk__str_any_of(option_name, "--info", "-l", NULL)) { options.ticket_cmd = 'l'; } else if (pcmk__str_any_of(option_name, "--details", "-L", NULL)) { options.ticket_cmd = 'L'; } else if (pcmk__str_any_of(option_name, "--raw", "-w", NULL)) { options.ticket_cmd = 'w'; } else if (pcmk__str_any_of(option_name, "--query-xml", "-q", NULL)) { options.ticket_cmd = 'q'; } else if (pcmk__str_any_of(option_name, "--constraints", "-c", NULL)) { options.ticket_cmd = 'c'; } else if (pcmk__str_any_of(option_name, "--cleanup", "-C", NULL)) { options.ticket_cmd = 'C'; } return TRUE; } static gboolean delete_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { attr_delete = g_list_append(attr_delete, strdup(optarg)); modified = true; return TRUE; } static gboolean get_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.get_attr_name, optarg); options.ticket_cmd = 'G'; return TRUE; } static gboolean grant_standby_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { if (pcmk__str_any_of(option_name, "--grant", "-g", NULL)) { pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_TRUE); modified = true; } else if (pcmk__str_any_of(option_name, "--revoke", "-r", NULL)) { pcmk__insert_dup(attr_set, PCMK__XA_GRANTED, PCMK_VALUE_FALSE); modified = true; } else if (pcmk__str_any_of(option_name, "--standby", "-s", NULL)) { pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_TRUE); modified = true; } else if (pcmk__str_any_of(option_name, "--activate", "-a", NULL)) { pcmk__insert_dup(attr_set, PCMK_XA_STANDBY, PCMK_VALUE_FALSE); modified = true; } return TRUE; } static gboolean set_attr_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **err) { pcmk__str_update(&options.attr_name, optarg); if (!options.attr_name || !options.attr_value) { return TRUE; } pcmk__insert_dup(attr_set, options.attr_name, options.attr_value); pcmk__str_update(&options.attr_name, NULL); pcmk__str_update(&options.attr_value, NULL); modified = true; return TRUE; } static GOptionEntry query_entries[] = { { "info", 'l', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the information of ticket(s)", NULL }, { "details", 'L', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the details of ticket(s)", NULL }, { "raw", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the IDs of ticket(s)", NULL }, { "query-xml", 'q', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Query the XML of ticket(s)", NULL }, { "constraints", 'c', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Display the " PCMK_XE_RSC_TICKET " constraints that apply to ticket(s)", NULL }, { NULL } }; static GOptionEntry command_entries[] = { { "grant", 'g', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Grant a ticket to this cluster site", NULL }, { "revoke", 'r', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Revoke a ticket from this cluster site", NULL }, { "standby", 's', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Tell this cluster site this ticket is standby", NULL }, { "activate", 'a', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, grant_standby_cb, "Tell this cluster site this ticket is active", NULL }, { NULL } }; static GOptionEntry advanced_entries[] = { { "get-attr", 'G', 0, G_OPTION_ARG_CALLBACK, get_attr_cb, "Display the named attribute for a ticket", "ATTRIBUTE" }, { "set-attr", 'S', 0, G_OPTION_ARG_CALLBACK, set_attr_cb, "Set the named attribute for a ticket", "ATTRIBUTE" }, { "delete-attr", 'D', 0, G_OPTION_ARG_CALLBACK, delete_attr_cb, "Delete the named attribute for a ticket", "ATTRIBUTE" }, { "cleanup", 'C', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb, "Delete all state of a ticket at this cluster site", NULL }, { NULL} }; static GOptionEntry addl_entries[] = { { "attr-value", 'v', 0, G_OPTION_ARG_CALLBACK, attr_value_cb, "Attribute value to use with -S", "VALUE" }, { "default", 'd', 0, G_OPTION_ARG_STRING, &options.attr_default, "(Advanced) Default attribute value to display if none is found\n" INDENT "(for use with -G)", "VALUE" }, { "force", 'f', 0, G_OPTION_ARG_NONE, &options.force, "(Advanced) Force the action to be performed", NULL }, { "ticket", 't', 0, G_OPTION_ARG_STRING, &options.ticket_id, "Ticket ID", "ID" }, { "xml-file", 'x', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, &options.xml_file, NULL, NULL }, { NULL } }; static GOptionEntry deprecated_entries[] = { { "set-name", 'n', 0, G_OPTION_ARG_STRING, &options.set_name, "(Advanced) ID of the " PCMK_XE_INSTANCE_ATTRIBUTES " object to change", "ID" }, { "nvpair", 'i', 0, G_OPTION_ARG_STRING, &options.attr_id, "(Advanced) ID of the nvpair object to change/delete", "ID" }, { "quiet", 'Q', 0, G_OPTION_ARG_NONE, &options.quiet, "Print only the value on stdout", NULL }, { NULL } }; static void ticket_grant_warning(gchar *ticket_id) { out->err(out, "This command cannot help you verify whether '%s' has " "been already granted elsewhere.\n" "If you really want to grant '%s' to this site now, and " "you know what you are doing,\n" "please specify --force.", ticket_id, ticket_id); } static void ticket_revoke_warning(gchar *ticket_id) { out->err(out, "Revoking '%s' can trigger the specified '" PCMK_XA_LOSS_POLICY "'(s) relating to '%s'.\n\n" "You can check that with:\n" "crm_ticket --ticket %s --constraints\n\n" "Otherwise before revoking '%s', you may want to make '%s'" "standby with:\n" "crm_ticket --ticket %s --standby\n\n" "If you really want to revoke '%s' from this site now, and " "you know what you are doing,\n" "please specify --force.", ticket_id, ticket_id, ticket_id, ticket_id, ticket_id, ticket_id, ticket_id); } static GOptionContext * build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) { GOptionContext *context = NULL; const char *description = "Examples:\n\n" "Display the info of tickets:\n\n" "\tcrm_ticket --info\n\n" "Display the detailed info of tickets:\n\n" "\tcrm_ticket --details\n\n" "Display the XML of 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --query-xml\n\n" "Display the " PCMK_XE_RSC_TICKET " constraints that apply to 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --constraints\n\n" "Grant 'ticketA' to this cluster site:\n\n" "\tcrm_ticket --ticket ticketA --grant\n\n" "Revoke 'ticketA' from this cluster site:\n\n" "\tcrm_ticket --ticket ticketA --revoke\n\n" "Make 'ticketA' standby (the cluster site will treat a granted\n" "'ticketA' as 'standby', and the dependent resources will be\n" "stopped or demoted gracefully without triggering loss-policies):\n\n" "\tcrm_ticket --ticket ticketA --standby\n\n" "Activate 'ticketA' from being standby:\n\n" "\tcrm_ticket --ticket ticketA --activate\n\n" "Get the value of the 'granted' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --get-attr granted\n\n" "Set the value of the 'standby' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --set-attr standby --attr-value true\n\n" "Delete the 'granted' attribute for 'ticketA':\n\n" "\tcrm_ticket --ticket ticketA --delete-attr granted\n\n" "Erase the operation history of 'ticketA' at this cluster site,\n" "causing the cluster site to 'forget' the existing ticket state:\n\n" "\tcrm_ticket --ticket ticketA --cleanup\n\n"; context = pcmk__build_arg_context(args, "text (default), xml", group, NULL); g_option_context_set_description(context, description); pcmk__add_arg_group(context, "queries", "Queries:", "Show queries", query_entries); pcmk__add_arg_group(context, "commands", "Commands:", "Show command options", command_entries); pcmk__add_arg_group(context, "advanced", "Advanced Options:", "Show advanced options", advanced_entries); pcmk__add_arg_group(context, "additional", "Additional Options:", "Show additional options", addl_entries); pcmk__add_arg_group(context, "deprecated", "Deprecated Options:", "Show deprecated options", deprecated_entries); return context; } int main(int argc, char **argv) { pcmk_scheduler_t *scheduler = NULL; xmlNode *cib_xml_copy = NULL; cib_t *cib_conn = NULL; crm_exit_t exit_code = CRM_EX_OK; int rc = pcmk_rc_ok; GOptionGroup *output_group = NULL; pcmk__common_args_t *args = NULL; GOptionContext *context = NULL; gchar **processed_args = NULL; attr_set = pcmk__strkey_table(free, free); attr_delete = NULL; args = pcmk__new_common_args(SUMMARY); context = build_arg_context(args, &output_group); processed_args = pcmk__cmdline_preproc(argv, "dintvxCDGS"); pcmk__register_formats(output_group, formats); if (!g_option_context_parse_strv(context, &processed_args, &error)) { exit_code = CRM_EX_USAGE; goto done; } pcmk__cli_init_logging("crm_ticket", args->verbosity); rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Error creating output format %s: %s", args->output_ty, pcmk_rc_str(rc)); goto done; } pe__register_messages(out); pcmk__register_lib_messages(out); if (args->version) { out->version(out, false); goto done; } scheduler = pe_new_working_set(); if (scheduler == NULL) { rc = errno; exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not allocate scheduler data: %s", pcmk_rc_str(rc)); goto done; } pcmk__set_scheduler_flags(scheduler, pcmk_sched_no_counts|pcmk_sched_no_compat); cib_conn = cib_new(); if (cib_conn == NULL) { exit_code = CRM_EX_DISCONNECT; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB manager"); goto done; } rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not connect to the CIB: %s", pcmk_rc_str(rc)); goto done; } if (options.xml_file != NULL) { cib_xml_copy = pcmk__xml_read(options.xml_file); } else { rc = cib_conn->cmds->query(cib_conn, NULL, &cib_xml_copy, cib_scope_local | cib_sync_call); rc = pcmk_legacy2rc(rc); if (rc != pcmk_rc_ok) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get local CIB: %s", pcmk_rc_str(rc)); goto done; } } - if (!cli_config_update(&cib_xml_copy, NULL, FALSE)) { + if (!pcmk__update_configured_schema(&cib_xml_copy, false)) { exit_code = CRM_EX_CONFIG; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not update local CIB to latest schema version"); goto done; } scheduler->input = cib_xml_copy; scheduler->now = crm_time_new(NULL); cluster_status(scheduler); /* For recording the tickets that are referenced in PCMK_XE_RSC_TICKET * constraints but have never been granted yet. */ pcmk__unpack_constraints(scheduler); if (options.ticket_cmd == 'l' || options.ticket_cmd == 'L' || options.ticket_cmd == 'w') { bool raw = false; bool details = false; if (options.ticket_cmd == 'L') { details = true; } else if (options.ticket_cmd == 'w') { raw = true; } rc = pcmk__ticket_info(out, scheduler, options.ticket_id, details, raw); exit_code = pcmk_rc2exitc(rc); if (rc == ENXIO) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "No such ticket '%s'", options.ticket_id); } else if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not get ticket info: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'q') { rc = pcmk__ticket_state(out, cib_conn, options.ticket_id); if (rc != pcmk_rc_ok && rc != pcmk_rc_duplicate_id) { exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not query ticket XML: %s", pcmk_rc_str(rc)); } else { exit_code = CRM_EX_OK; } } else if (options.ticket_cmd == 'c') { rc = pcmk__ticket_constraints(out, cib_conn, options.ticket_id); exit_code = pcmk_rc2exitc(rc); if (rc != pcmk_rc_ok) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not show ticket constraints: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'G') { if (options.ticket_id == NULL) { exit_code = CRM_EX_NOSUCH; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } rc = pcmk__ticket_get_attr(out, scheduler, options.ticket_id, options.get_attr_name, options.attr_default); exit_code = pcmk_rc2exitc(rc); } else if (options.ticket_cmd == 'C') { if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } rc = pcmk__ticket_delete(out, cib_conn, scheduler, options.ticket_id, options.force); exit_code = pcmk_rc2exitc(rc); switch (rc) { case ENXIO: g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "No such ticket '%s'", options.ticket_id); break; case EACCES: ticket_revoke_warning(options.ticket_id); break; case pcmk_rc_ok: case pcmk_rc_duplicate_id: break; default: g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not clean up ticket: %s", pcmk_rc_str(rc)); break; } } else if (modified) { if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } if (options.attr_value && (pcmk__str_empty(options.attr_name))) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply attribute name with -S for -v %s", options.attr_value); goto done; } if (options.attr_name && (pcmk__str_empty(options.attr_value))) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply attribute value with -v for -S %s", options.attr_value); goto done; } if (attr_delete != NULL) { rc = pcmk__ticket_remove_attr(out, cib_conn, scheduler, options.ticket_id, attr_delete, options.force); if (rc == EACCES) { ticket_revoke_warning(options.ticket_id); exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Ticket modification not allowed without --force"); } } else { rc = pcmk__ticket_set_attr(out, cib_conn, scheduler, options.ticket_id, attr_set, options.force); if (rc == EACCES) { const char *value = NULL; value = g_hash_table_lookup(attr_set, PCMK__XA_GRANTED); if (crm_is_true(value)) { ticket_grant_warning(options.ticket_id); } else { ticket_revoke_warning(options.ticket_id); } exit_code = pcmk_rc2exitc(rc); g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Ticket modification not allowed without --force"); } } exit_code = pcmk_rc2exitc(rc); if (rc != pcmk_rc_ok && error == NULL) { g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Could not modify ticket: %s", pcmk_rc_str(rc)); } } else if (options.ticket_cmd == 'S') { /* Correct usage was handled in the "if (modified)" block above, so * this is just for reporting usage errors */ if (pcmk__str_empty(options.attr_name)) { // We only get here if ticket_cmd was left as default exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply a command"); goto done; } if (options.ticket_id == NULL) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply ticket ID with -t"); goto done; } if (pcmk__str_empty(options.attr_value)) { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Must supply value with -v for -S %s", options.attr_name); goto done; } } else { exit_code = CRM_EX_USAGE; g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Unknown command: %c", options.ticket_cmd); } done: if (attr_set) { g_hash_table_destroy(attr_set); } attr_set = NULL; if (attr_delete) { g_list_free_full(attr_delete, free); } attr_delete = NULL; pe_free_working_set(scheduler); scheduler = NULL; cib__clean_up_connection(&cib_conn); g_strfreev(processed_args); pcmk__free_arg_context(context); g_free(options.attr_default); g_free(options.attr_id); free(options.attr_name); free(options.attr_value); free(options.get_attr_name); g_free(options.set_name); g_free(options.ticket_id); g_free(options.xml_file); pcmk__output_and_clear_error(&error, out); if (out != NULL) { out->finish(out, exit_code, true, NULL); pcmk__output_free(out); } pcmk__unregister_formats(); crm_exit(exit_code); }