diff --git a/cts/cli/crm_diff_new.xml b/cts/cli/crm_diff_new.xml
index 7fa0cbbd1b..121c929f3c 100644
--- a/cts/cli/crm_diff_new.xml
+++ b/cts/cli/crm_diff_new.xml
@@ -1,54 +1,54 @@
-
+
diff --git a/cts/cli/regression.crm_diff.exp b/cts/cli/crm_diff_patchset.xml
similarity index 93%
copy from cts/cli/regression.crm_diff.exp
copy to cts/cli/crm_diff_patchset.xml
index 6b1a7109dd..e492702e99 100644
--- a/cts/cli/regression.crm_diff.exp
+++ b/cts/cli/crm_diff_patchset.xml
@@ -1,66 +1,63 @@
-=#=#=#= Begin test: Create an XML patchset =#=#=#=
-
+
-=#=#=#= End test: Create an XML patchset - Error occurred (1) =#=#=#=
-* Passed: crm_diff - Create an XML patchset
diff --git a/cts/cli/regression.crm_diff.exp b/cts/cli/crm_diff_patchset_cib.xml
similarity index 78%
copy from cts/cli/regression.crm_diff.exp
copy to cts/cli/crm_diff_patchset_cib.xml
index 6b1a7109dd..de918955a8 100644
--- a/cts/cli/regression.crm_diff.exp
+++ b/cts/cli/crm_diff_patchset_cib.xml
@@ -1,66 +1,60 @@
-=#=#=#= Begin test: Create an XML patchset =#=#=#=
-
+
-
+
-
+
-
-
+
-
+
-
-
-=#=#=#= End test: Create an XML patchset - Error occurred (1) =#=#=#=
-* Passed: crm_diff - Create an XML patchset
diff --git a/cts/cli/regression.crm_diff.exp b/cts/cli/regression.crm_diff.exp
index 6b1a7109dd..dfde097564 100644
--- a/cts/cli/regression.crm_diff.exp
+++ b/cts/cli/regression.crm_diff.exp
@@ -1,66 +1,1415 @@
-=#=#=#= Begin test: Create an XML patchset =#=#=#=
+=#=#=#= Begin test: Create an XML patchset from files =#=#=#=
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Create an XML patchset from files - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from files
+=#=#=#= Begin test: Create an XML patchset from files (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Create an XML patchset from files (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from files (XML)
+=#=#=#= Begin test: Create an XML patchset from strings =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Create an XML patchset from strings - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from strings
+=#=#=#= Begin test: Create an XML patchset from strings (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Create an XML patchset from strings (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from strings (XML)
+=#=#=#= Begin test: Create an XML patchset from old file, new string =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Create an XML patchset from old file, new string - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from old file, new string
+=#=#=#= Begin test: Create an XML patchset from old file, new string (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Create an XML patchset from old file, new string (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from old file, new string (XML)
+=#=#=#= Begin test: Create an XML patchset from old string, new file =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Create an XML patchset from old string, new file - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from old string, new file
+=#=#=#= Begin test: Create an XML patchset from old string, new file (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Create an XML patchset from old string, new file (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset from old string, new file (XML)
+=#=#=#= Begin test: Create an XML patchset as CIB =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Create an XML patchset as CIB - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset as CIB
+=#=#=#= Begin test: Create an XML patchset as CIB (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Create an XML patchset as CIB (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset as CIB (XML)
+=#=#=#= Begin test: Create an XML patchset with no versions =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Create an XML patchset with no versions - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset with no versions
+=#=#=#= Begin test: Create an XML patchset with no versions (XML) =#=#=#=
+
+
+
+
+
+
+
-=#=#=#= End test: Create an XML patchset - Error occurred (1) =#=#=#=
-* Passed: crm_diff - Create an XML patchset
+]]>
+
+
+=#=#=#= End test: Create an XML patchset with no versions (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Create an XML patchset with no versions (XML)
+=#=#=#= Begin test: Create an XML patchset as CIB, with no versions =#=#=#=
+crm_diff: -u/--no-version incompatible with -c/--cib
+=#=#=#= End test: Create an XML patchset as CIB, with no versions - Incorrect usage (64) =#=#=#=
+* Passed: crm_diff - Create an XML patchset as CIB, with no versions
+=#=#=#= Begin test: Create an XML patchset as CIB, with no versions (XML) =#=#=#=
+
+
+
+ crm_diff: -u/--no-version incompatible with -c/--cib
+
+
+
+=#=#=#= End test: Create an XML patchset as CIB, with no versions (XML) - Incorrect usage (64) =#=#=#=
+* Passed: crm_diff - Create an XML patchset as CIB, with no versions (XML)
+=#=#=#= Begin test: Apply an XML patchset to a file =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Apply an XML patchset to a file - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset to a file
+=#=#=#= Begin test: Apply an XML patchset to a file (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Apply an XML patchset to a file (XML) - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset to a file (XML)
+=#=#=#= Begin test: Apply an XML patchset to a string =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Apply an XML patchset to a string - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset to a string
+=#=#=#= Begin test: Apply an XML patchset to a string (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Apply an XML patchset to a string (XML) - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset to a string (XML)
+=#=#=#= Begin test: Apply an XML patchset as CIB =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Apply an XML patchset as CIB - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset as CIB
+=#=#=#= Begin test: Apply an XML patchset as CIB (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+=#=#=#= End test: Apply an XML patchset as CIB (XML) - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset as CIB (XML)
+=#=#=#= Begin test: Apply an XML patchset with no versions =#=#=#=
+Warning: -u/--no-version ignored with -p/--patch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=#=#=#= End test: Apply an XML patchset with no versions - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset with no versions
+=#=#=#= Begin test: Apply an XML patchset with no versions (XML) =#=#=#=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+]]>
+
+
+ Warning: -u/--no-version ignored with -p/--patch
+
+
+
+=#=#=#= End test: Apply an XML patchset with no versions (XML) - OK (0) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset with no versions (XML)
+=#=#=#= Begin test: Apply an XML patchset as CIB, with no versions =#=#=#=
+crm_diff: -u/--no-version incompatible with -c/--cib
+=#=#=#= End test: Apply an XML patchset as CIB, with no versions - Incorrect usage (64) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset as CIB, with no versions
+=#=#=#= Begin test: Apply an XML patchset as CIB, with no versions (XML) =#=#=#=
+
+
+
+ crm_diff: -u/--no-version incompatible with -c/--cib
+
+
+
+=#=#=#= End test: Apply an XML patchset as CIB, with no versions (XML) - Incorrect usage (64) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset as CIB, with no versions (XML)
+=#=#=#= Begin test: Apply an XML patchset generated as CIB =#=#=#=
+crm_diff: Could not apply patch: Application of update diff failed
+=#=#=#= End test: Apply an XML patchset generated as CIB - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset generated as CIB
+=#=#=#= Begin test: Apply an XML patchset generated as CIB (XML) =#=#=#=
+
+
+
+ crm_diff: Could not apply patch: Application of update diff failed
+
+
+
+=#=#=#= End test: Apply an XML patchset generated as CIB (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset generated as CIB (XML)
+=#=#=#= Begin test: Apply an XML patchset generated as CIB, as CIB =#=#=#=
+crm_diff: Could not apply patch: Application of update diff failed
+=#=#=#= End test: Apply an XML patchset generated as CIB, as CIB - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset generated as CIB, as CIB
+=#=#=#= Begin test: Apply an XML patchset generated as CIB, as CIB (XML) =#=#=#=
+
+
+
+ crm_diff: Could not apply patch: Application of update diff failed
+
+
+
+=#=#=#= End test: Apply an XML patchset generated as CIB, as CIB (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset generated as CIB, as CIB (XML)
+=#=#=#= Begin test: Apply an XML patchset generated as CIB, with no versions =#=#=#=
+Warning: -u/--no-version ignored with -p/--patch
+crm_diff: Could not apply patch: Application of update diff failed
+=#=#=#= End test: Apply an XML patchset generated as CIB, with no versions - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset generated as CIB, with no versions
+=#=#=#= Begin test: Apply an XML patchset generated as CIB, with no versions (XML) =#=#=#=
+
+
+
+ Warning: -u/--no-version ignored with -p/--patch
+ crm_diff: Could not apply patch: Application of update diff failed
+
+
+
+=#=#=#= End test: Apply an XML patchset generated as CIB, with no versions (XML) - Error occurred (1) =#=#=#=
+* Passed: crm_diff - Apply an XML patchset generated as CIB, with no versions (XML)
diff --git a/cts/cli/regression.crm_simulate.exp b/cts/cli/regression.crm_simulate.exp
index cc442403a1..4a30dfecbd 100644
--- a/cts/cli/regression.crm_simulate.exp
+++ b/cts/cli/regression.crm_simulate.exp
@@ -1,806 +1,806 @@
=#=#=#= Begin test: Show allocation scores with crm_simulate =#=#=#=
-
+
=#=#=#= End test: Show allocation scores with crm_simulate - OK (0) =#=#=#=
* Passed: crm_simulate - Show allocation scores with crm_simulate
=#=#=#= Begin test: Show utilization with crm_simulate =#=#=#=
4 of 32 resource instances DISABLED and 0 BLOCKED from further action due to failure
Current cluster status:
* Node List:
* Online: [ cluster01 cluster02 ]
* GuestOnline: [ httpd-bundle-0 httpd-bundle-1 ]
* Full List of Resources:
* Clone Set: ping-clone [ping]:
* Started: [ cluster01 cluster02 ]
* Fencing (stonith:fence_xvm): Started cluster01
* dummy (ocf:pacemaker:Dummy): Started cluster02
* Clone Set: inactive-clone [inactive-dhcpd] (disabled):
* Stopped (disabled): [ cluster01 cluster02 ]
* Resource Group: inactive-group (disabled):
* inactive-dummy-1 (ocf:pacemaker:Dummy): Stopped (disabled)
* inactive-dummy-2 (ocf:pacemaker:Dummy): Stopped (disabled)
* Container bundle set: httpd-bundle [pcmk:http]:
* httpd-bundle-0 (192.168.122.131) (ocf:heartbeat:apache): Started cluster01
* httpd-bundle-1 (192.168.122.132) (ocf:heartbeat:apache): Started cluster02
* httpd-bundle-2 (192.168.122.133) (ocf:heartbeat:apache): Stopped
* Resource Group: exim-group:
* Public-IP (ocf:heartbeat:IPaddr): Started cluster02
* Email (lsb:exim): Started cluster02
* Clone Set: mysql-clone-group [mysql-group]:
* Started: [ cluster01 cluster02 ]
* Clone Set: promotable-clone [promotable-rsc] (promotable):
* Promoted: [ cluster02 ]
* Unpromoted: [ cluster01 ]
Utilization Information:
Only 'private' parameters to 1m-interval monitor for dummy on cluster02 changed: 0:0;16:2:0:4a9e64d6-e1dd-4395-917c-1596312eafe4
* Original: cluster01 capacity:
* Original: cluster02 capacity:
* pcmk__assign_resource: ping:0 utilization on cluster02:
* pcmk__assign_resource: ping:1 utilization on cluster01:
* pcmk__assign_resource: Fencing utilization on cluster01:
* pcmk__assign_resource: dummy utilization on cluster02:
* pcmk__assign_resource: httpd-bundle-docker-0 utilization on cluster01:
* pcmk__assign_resource: httpd-bundle-docker-1 utilization on cluster02:
* pcmk__assign_resource: httpd-bundle-ip-192.168.122.131 utilization on cluster01:
* pcmk__assign_resource: httpd-bundle-0 utilization on cluster01:
* pcmk__assign_resource: httpd:0 utilization on httpd-bundle-0:
* pcmk__assign_resource: httpd-bundle-ip-192.168.122.132 utilization on cluster02:
* pcmk__assign_resource: httpd-bundle-1 utilization on cluster02:
* pcmk__assign_resource: httpd:1 utilization on httpd-bundle-1:
* pcmk__assign_resource: httpd-bundle-2 utilization on cluster01:
* pcmk__assign_resource: httpd:2 utilization on httpd-bundle-2:
* pcmk__assign_resource: Public-IP utilization on cluster02:
* pcmk__assign_resource: Email utilization on cluster02:
* pcmk__assign_resource: mysql-proxy:0 utilization on cluster02:
* pcmk__assign_resource: mysql-proxy:1 utilization on cluster01:
* pcmk__assign_resource: promotable-rsc:0 utilization on cluster02:
* pcmk__assign_resource: promotable-rsc:1 utilization on cluster01:
* Remaining: cluster01 capacity:
* Remaining: cluster02 capacity:
Transition Summary:
* Start httpd-bundle-2 ( cluster01 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
* Start httpd:2 ( httpd-bundle-2 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
=#=#=#= End test: Show utilization with crm_simulate - OK (0) =#=#=#=
* Passed: crm_simulate - Show utilization with crm_simulate
=#=#=#= Begin test: Simulate injecting a failure =#=#=#=
4 of 32 resource instances DISABLED and 0 BLOCKED from further action due to failure
Current cluster status:
* Node List:
* Online: [ cluster01 cluster02 ]
* GuestOnline: [ httpd-bundle-0 httpd-bundle-1 ]
* Full List of Resources:
* Clone Set: ping-clone [ping]:
* Started: [ cluster01 cluster02 ]
* Fencing (stonith:fence_xvm): Started cluster01
* dummy (ocf:pacemaker:Dummy): Started cluster02
* Clone Set: inactive-clone [inactive-dhcpd] (disabled):
* Stopped (disabled): [ cluster01 cluster02 ]
* Resource Group: inactive-group (disabled):
* inactive-dummy-1 (ocf:pacemaker:Dummy): Stopped (disabled)
* inactive-dummy-2 (ocf:pacemaker:Dummy): Stopped (disabled)
* Container bundle set: httpd-bundle [pcmk:http]:
* httpd-bundle-0 (192.168.122.131) (ocf:heartbeat:apache): Started cluster01
* httpd-bundle-1 (192.168.122.132) (ocf:heartbeat:apache): Started cluster02
* httpd-bundle-2 (192.168.122.133) (ocf:heartbeat:apache): Stopped
* Resource Group: exim-group:
* Public-IP (ocf:heartbeat:IPaddr): Started cluster02
* Email (lsb:exim): Started cluster02
* Clone Set: mysql-clone-group [mysql-group]:
* Started: [ cluster01 cluster02 ]
* Clone Set: promotable-clone [promotable-rsc] (promotable):
* Promoted: [ cluster02 ]
* Unpromoted: [ cluster01 ]
Performing Requested Modifications:
* Injecting ping_monitor_10000@cluster02=1 into the configuration
* Injecting attribute fail-count-ping#monitor_10000=1 into /node_state '2'
* Injecting attribute last-failure-ping#monitor_10000= into /node_state '2'
Transition Summary:
* Recover ping:0 ( cluster02 )
* Start httpd-bundle-2 ( cluster01 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
* Start httpd:2 ( httpd-bundle-2 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
Executing Cluster Transition:
* Cluster action: clear_failcount for ping on cluster02
* Pseudo action: ping-clone_stop_0
* Pseudo action: httpd-bundle_start_0
* Resource action: ping stop on cluster02
* Pseudo action: ping-clone_stopped_0
* Pseudo action: ping-clone_start_0
* Pseudo action: httpd-bundle-clone_start_0
* Resource action: ping start on cluster02
* Resource action: ping monitor=10000 on cluster02
* Pseudo action: ping-clone_running_0
* Pseudo action: httpd-bundle-clone_running_0
* Pseudo action: httpd-bundle_running_0
Revised Cluster Status:
* Node List:
* Online: [ cluster01 cluster02 ]
* GuestOnline: [ httpd-bundle-0 httpd-bundle-1 ]
* Full List of Resources:
* Clone Set: ping-clone [ping]:
* Started: [ cluster01 cluster02 ]
* Fencing (stonith:fence_xvm): Started cluster01
* dummy (ocf:pacemaker:Dummy): Started cluster02
* Clone Set: inactive-clone [inactive-dhcpd] (disabled):
* Stopped (disabled): [ cluster01 cluster02 ]
* Resource Group: inactive-group (disabled):
* inactive-dummy-1 (ocf:pacemaker:Dummy): Stopped (disabled)
* inactive-dummy-2 (ocf:pacemaker:Dummy): Stopped (disabled)
* Container bundle set: httpd-bundle [pcmk:http]:
* httpd-bundle-0 (192.168.122.131) (ocf:heartbeat:apache): Started cluster01
* httpd-bundle-1 (192.168.122.132) (ocf:heartbeat:apache): Started cluster02
* httpd-bundle-2 (192.168.122.133) (ocf:heartbeat:apache): Stopped
* Resource Group: exim-group:
* Public-IP (ocf:heartbeat:IPaddr): Started cluster02
* Email (lsb:exim): Started cluster02
* Clone Set: mysql-clone-group [mysql-group]:
* Started: [ cluster01 cluster02 ]
* Clone Set: promotable-clone [promotable-rsc] (promotable):
* Promoted: [ cluster02 ]
* Unpromoted: [ cluster01 ]
=#=#=#= End test: Simulate injecting a failure - OK (0) =#=#=#=
* Passed: crm_simulate - Simulate injecting a failure
=#=#=#= Begin test: Simulate bringing a node down =#=#=#=
4 of 32 resource instances DISABLED and 0 BLOCKED from further action due to failure
Current cluster status:
* Node List:
* Online: [ cluster01 cluster02 ]
* GuestOnline: [ httpd-bundle-0 httpd-bundle-1 ]
* Full List of Resources:
* Clone Set: ping-clone [ping]:
* Started: [ cluster01 cluster02 ]
* Fencing (stonith:fence_xvm): Started cluster01
* dummy (ocf:pacemaker:Dummy): Started cluster02
* Clone Set: inactive-clone [inactive-dhcpd] (disabled):
* Stopped (disabled): [ cluster01 cluster02 ]
* Resource Group: inactive-group (disabled):
* inactive-dummy-1 (ocf:pacemaker:Dummy): Stopped (disabled)
* inactive-dummy-2 (ocf:pacemaker:Dummy): Stopped (disabled)
* Container bundle set: httpd-bundle [pcmk:http]:
* httpd-bundle-0 (192.168.122.131) (ocf:heartbeat:apache): Started cluster01
* httpd-bundle-1 (192.168.122.132) (ocf:heartbeat:apache): Started cluster02
* httpd-bundle-2 (192.168.122.133) (ocf:heartbeat:apache): Stopped
* Resource Group: exim-group:
* Public-IP (ocf:heartbeat:IPaddr): Started cluster02
* Email (lsb:exim): Started cluster02
* Clone Set: mysql-clone-group [mysql-group]:
* Started: [ cluster01 cluster02 ]
* Clone Set: promotable-clone [promotable-rsc] (promotable):
* Promoted: [ cluster02 ]
* Unpromoted: [ cluster01 ]
Performing Requested Modifications:
* Taking node cluster01 offline
Transition Summary:
* Fence (off) httpd-bundle-0 (resource: httpd-bundle-docker-0) 'guest is unclean'
* Start Fencing ( cluster02 )
* Start httpd-bundle-0 ( cluster02 ) due to unrunnable httpd-bundle-docker-0 start (blocked)
* Stop httpd:0 ( httpd-bundle-0 ) due to unrunnable httpd-bundle-docker-0 start
* Start httpd-bundle-2 ( cluster02 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
* Start httpd:2 ( httpd-bundle-2 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
Executing Cluster Transition:
* Resource action: Fencing start on cluster02
* Pseudo action: stonith-httpd-bundle-0-off on httpd-bundle-0
* Pseudo action: httpd-bundle_stop_0
* Pseudo action: httpd-bundle_start_0
* Resource action: Fencing monitor=60000 on cluster02
* Pseudo action: httpd-bundle-clone_stop_0
* Pseudo action: httpd_stop_0
* Pseudo action: httpd-bundle-clone_stopped_0
* Pseudo action: httpd-bundle-clone_start_0
* Pseudo action: httpd-bundle_stopped_0
* Pseudo action: httpd-bundle-clone_running_0
* Pseudo action: httpd-bundle_running_0
Revised Cluster Status:
* Node List:
* Online: [ cluster02 ]
* OFFLINE: [ cluster01 ]
* GuestOnline: [ httpd-bundle-1 ]
* Full List of Resources:
* Clone Set: ping-clone [ping]:
* Started: [ cluster02 ]
* Stopped: [ cluster01 ]
* Fencing (stonith:fence_xvm): Started cluster02
* dummy (ocf:pacemaker:Dummy): Started cluster02
* Clone Set: inactive-clone [inactive-dhcpd] (disabled):
* Stopped (disabled): [ cluster01 cluster02 ]
* Resource Group: inactive-group (disabled):
* inactive-dummy-1 (ocf:pacemaker:Dummy): Stopped (disabled)
* inactive-dummy-2 (ocf:pacemaker:Dummy): Stopped (disabled)
* Container bundle set: httpd-bundle [pcmk:http]:
* httpd-bundle-0 (192.168.122.131) (ocf:heartbeat:apache): FAILED
* httpd-bundle-1 (192.168.122.132) (ocf:heartbeat:apache): Started cluster02
* httpd-bundle-2 (192.168.122.133) (ocf:heartbeat:apache): Stopped
* Resource Group: exim-group:
* Public-IP (ocf:heartbeat:IPaddr): Started cluster02
* Email (lsb:exim): Started cluster02
* Clone Set: mysql-clone-group [mysql-group]:
* Started: [ cluster02 ]
* Stopped: [ cluster01 ]
* Clone Set: promotable-clone [promotable-rsc] (promotable):
* Promoted: [ cluster02 ]
* Stopped: [ cluster01 ]
=#=#=#= End test: Simulate bringing a node down - OK (0) =#=#=#=
* Passed: crm_simulate - Simulate bringing a node down
=#=#=#= Begin test: Simulate a node failing =#=#=#=
4 of 32 resource instances DISABLED and 0 BLOCKED from further action due to failure
Current cluster status:
* Node List:
* Online: [ cluster01 cluster02 ]
* GuestOnline: [ httpd-bundle-0 httpd-bundle-1 ]
* Full List of Resources:
* Clone Set: ping-clone [ping]:
* Started: [ cluster01 cluster02 ]
* Fencing (stonith:fence_xvm): Started cluster01
* dummy (ocf:pacemaker:Dummy): Started cluster02
* Clone Set: inactive-clone [inactive-dhcpd] (disabled):
* Stopped (disabled): [ cluster01 cluster02 ]
* Resource Group: inactive-group (disabled):
* inactive-dummy-1 (ocf:pacemaker:Dummy): Stopped (disabled)
* inactive-dummy-2 (ocf:pacemaker:Dummy): Stopped (disabled)
* Container bundle set: httpd-bundle [pcmk:http]:
* httpd-bundle-0 (192.168.122.131) (ocf:heartbeat:apache): Started cluster01
* httpd-bundle-1 (192.168.122.132) (ocf:heartbeat:apache): Started cluster02
* httpd-bundle-2 (192.168.122.133) (ocf:heartbeat:apache): Stopped
* Resource Group: exim-group:
* Public-IP (ocf:heartbeat:IPaddr): Started cluster02
* Email (lsb:exim): Started cluster02
* Clone Set: mysql-clone-group [mysql-group]:
* Started: [ cluster01 cluster02 ]
* Clone Set: promotable-clone [promotable-rsc] (promotable):
* Promoted: [ cluster02 ]
* Unpromoted: [ cluster01 ]
Performing Requested Modifications:
* Failing node cluster02
Transition Summary:
* Fence (off) httpd-bundle-1 (resource: httpd-bundle-docker-1) 'guest is unclean'
* Fence (reboot) cluster02 'peer is no longer part of the cluster'
* Stop ping:0 ( cluster02 ) due to node availability
* Stop dummy ( cluster02 ) due to node availability
* Stop httpd-bundle-ip-192.168.122.132 ( cluster02 ) due to node availability
* Stop httpd-bundle-docker-1 ( cluster02 ) due to node availability
* Stop httpd-bundle-1 ( cluster02 ) due to unrunnable httpd-bundle-docker-1 start
* Stop httpd:1 ( httpd-bundle-1 ) due to unrunnable httpd-bundle-docker-1 start
* Start httpd-bundle-2 ( cluster01 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
* Start httpd:2 ( httpd-bundle-2 ) due to unrunnable httpd-bundle-docker-2 start (blocked)
* Move Public-IP ( cluster02 -> cluster01 )
* Move Email ( cluster02 -> cluster01 )
* Stop mysql-proxy:0 ( cluster02 ) due to node availability
* Stop promotable-rsc:0 ( Promoted cluster02 ) due to node availability
Executing Cluster Transition:
* Pseudo action: httpd-bundle-1_stop_0
* Pseudo action: promotable-clone_demote_0
* Pseudo action: httpd-bundle_stop_0
* Pseudo action: httpd-bundle_start_0
* Fencing cluster02 (reboot)
* Pseudo action: ping-clone_stop_0
* Pseudo action: dummy_stop_0
* Pseudo action: httpd-bundle-docker-1_stop_0
* Pseudo action: exim-group_stop_0
* Pseudo action: Email_stop_0
* Pseudo action: mysql-clone-group_stop_0
* Pseudo action: promotable-rsc_demote_0
* Pseudo action: promotable-clone_demoted_0
* Pseudo action: promotable-clone_stop_0
* Pseudo action: stonith-httpd-bundle-1-off on httpd-bundle-1
* Pseudo action: ping_stop_0
* Pseudo action: ping-clone_stopped_0
* Pseudo action: httpd-bundle-clone_stop_0
* Pseudo action: httpd-bundle-ip-192.168.122.132_stop_0
* Pseudo action: Public-IP_stop_0
* Pseudo action: mysql-group:0_stop_0
* Pseudo action: mysql-proxy_stop_0
* Pseudo action: promotable-rsc_stop_0
* Pseudo action: promotable-clone_stopped_0
* Pseudo action: httpd_stop_0
* Pseudo action: httpd-bundle-clone_stopped_0
* Pseudo action: httpd-bundle-clone_start_0
* Pseudo action: exim-group_stopped_0
* Pseudo action: exim-group_start_0
* Resource action: Public-IP start on cluster01
* Resource action: Email start on cluster01
* Pseudo action: mysql-group:0_stopped_0
* Pseudo action: mysql-clone-group_stopped_0
* Pseudo action: httpd-bundle_stopped_0
* Pseudo action: httpd-bundle-clone_running_0
* Pseudo action: exim-group_running_0
* Pseudo action: httpd-bundle_running_0
Revised Cluster Status:
* Node List:
* Online: [ cluster01 ]
* OFFLINE: [ cluster02 ]
* GuestOnline: [ httpd-bundle-0 ]
* Full List of Resources:
* Clone Set: ping-clone [ping]:
* Started: [ cluster01 ]
* Stopped: [ cluster02 ]
* Fencing (stonith:fence_xvm): Started cluster01
* dummy (ocf:pacemaker:Dummy): Stopped
* Clone Set: inactive-clone [inactive-dhcpd] (disabled):
* Stopped (disabled): [ cluster01 cluster02 ]
* Resource Group: inactive-group (disabled):
* inactive-dummy-1 (ocf:pacemaker:Dummy): Stopped (disabled)
* inactive-dummy-2 (ocf:pacemaker:Dummy): Stopped (disabled)
* Container bundle set: httpd-bundle [pcmk:http]:
* httpd-bundle-0 (192.168.122.131) (ocf:heartbeat:apache): Started cluster01
* httpd-bundle-1 (192.168.122.132) (ocf:heartbeat:apache): FAILED
* httpd-bundle-2 (192.168.122.133) (ocf:heartbeat:apache): Stopped
* Resource Group: exim-group:
* Public-IP (ocf:heartbeat:IPaddr): Started cluster01
* Email (lsb:exim): Started cluster01
* Clone Set: mysql-clone-group [mysql-group]:
* Started: [ cluster01 ]
* Stopped: [ cluster02 ]
* Clone Set: promotable-clone [promotable-rsc] (promotable):
* Unpromoted: [ cluster01 ]
* Stopped: [ cluster02 ]
=#=#=#= End test: Simulate a node failing - OK (0) =#=#=#=
* Passed: crm_simulate - Simulate a node failing
=#=#=#= Begin test: Run crm_simulate with invalid CIB (enum violation) =#=#=#=
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-1.2 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-1.3 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.0 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.1 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.2 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.3 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.4 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.5 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.6 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.7 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.8 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.9 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-2.10 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.0 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.1 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.2 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.3 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.4 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.5 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.6 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.7 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.8 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.9 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-3.10 does not validate
Invalid attribute first-action for element rsc_order
Element constraints has extra content: rsc_order
pcmk__update_schema debug: Schema pacemaker-4.0 does not validate
Cannot upgrade configuration (claiming pacemaker-1.2 schema) to at least pacemaker-4.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: Run crm_simulate with invalid CIB (unrecognized validate-with) =#=#=#=
=#=#=#= 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: Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1) =#=#=#=
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 debug: Schema pacemaker-4.0 validates
pcmk__update_schema info: Transformed the configuration schema to pacemaker-4.0
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: Run crm_simulate with valid CIB, but without validate-with attribute =#=#=#=
=#=#=#= End test: Run crm_simulate with valid CIB, but without validate-with attribute - Invalid configuration (78) =#=#=#=
* Passed: crm_simulate - Run crm_simulate with valid CIB, but without validate-with attribute
=#=#=#= Begin test: Run crm_simulate with invalid CIB, also without validate-with attribute =#=#=#=
=#=#=#= End test: Run crm_simulate with invalid CIB, also without validate-with attribute - Invalid configuration (78) =#=#=#=
* Passed: crm_simulate - Run crm_simulate with invalid CIB, also without validate-with attribute
diff --git a/cts/cli/regression.crm_verify.exp b/cts/cli/regression.crm_verify.exp
index 765527560f..999e26baa2 100644
--- a/cts/cli/regression.crm_verify.exp
+++ b/cts/cli/regression.crm_verify.exp
@@ -1,106 +1,106 @@
=#=#=#= Begin test: Verify a file-specified invalid configuration =#=#=#=
Configuration invalid (with errors) (-V may provide more detail)
=#=#=#= End test: Verify a file-specified invalid configuration - Invalid configuration (78) =#=#=#=
* Passed: crm_verify - Verify a file-specified invalid configuration
=#=#=#= Begin test: Verify a file-specified invalid configuration (XML) =#=#=#=
-
+error: Resource test2:0 is of type systemd and therefore cannot be used as a promotable clone resourceerror: Ignoring <clone> resource 'test2-clone' because configuration is invaliderror: CIB did not pass schema validationConfiguration invalid (with errors)
=#=#=#= End test: Verify a file-specified invalid configuration (XML) - Invalid configuration (78) =#=#=#=
* Passed: crm_verify - Verify a file-specified invalid configuration (XML)
=#=#=#= Begin test: Verify a file-specified invalid configuration (verbose) =#=#=#=
unpack_config warning: Blind faith: not fencing unseen nodes
error: Resource test2:0 is of type systemd and therefore cannot be used as a promotable clone resource
error: Ignoring resource 'test2-clone' because configuration is invalid
error: CIB did not pass schema validation
Configuration invalid (with errors)
=#=#=#= End test: Verify a file-specified invalid configuration (verbose) - Invalid configuration (78) =#=#=#=
* Passed: crm_verify - Verify a file-specified invalid configuration (verbose)
=#=#=#= Begin test: Verify a file-specified invalid configuration (verbose) (XML) =#=#=#=
unpack_config warning: Blind faith: not fencing unseen nodes
-
+error: Resource test2:0 is of type systemd and therefore cannot be used as a promotable clone resourceerror: Ignoring <clone> resource 'test2-clone' because configuration is invaliderror: CIB did not pass schema validationConfiguration invalid (with errors)
=#=#=#= End test: Verify a file-specified invalid configuration (verbose) (XML) - Invalid configuration (78) =#=#=#=
* Passed: crm_verify - Verify a file-specified invalid configuration (verbose) (XML)
=#=#=#= Begin test: Verify a file-specified invalid configuration (quiet) =#=#=#=
=#=#=#= End test: Verify a file-specified invalid configuration (quiet) - Invalid configuration (78) =#=#=#=
* Passed: crm_verify - Verify a file-specified invalid configuration (quiet)
=#=#=#= Begin test: Verify a file-specified invalid configuration (quiet) (XML) =#=#=#=
-
+error: Resource test2:0 is of type systemd and therefore cannot be used as a promotable clone resourceerror: Ignoring <clone> resource 'test2-clone' because configuration is invaliderror: CIB did not pass schema validation
=#=#=#= End test: Verify a file-specified invalid configuration (quiet) (XML) - Invalid configuration (78) =#=#=#=
* Passed: crm_verify - Verify a file-specified invalid configuration (quiet) (XML)
=#=#=#= Begin test: Verify another file-specified invalid configuration (XML) =#=#=#=
-
+error: Resource start-up disabled since no STONITH resources have been definederror: Either configure some or disable STONITH with the stonith-enabled optionerror: NOTE: Clusters with shared data need STONITH to ensure data integritywarning: Node pcmk-1 is unclean but cannot be fencedwarning: Node pcmk-2 is unclean but cannot be fencederror: CIB did not pass schema validationConfiguration invalid (with errors)
=#=#=#= End test: Verify another file-specified invalid configuration (XML) - Invalid configuration (78) =#=#=#=
* Passed: crm_verify - Verify another file-specified invalid configuration (XML)
=#=#=#= Begin test: Verify a file-specified valid configuration (XML) =#=#=#=
-
+
=#=#=#= End test: Verify a file-specified valid configuration (XML) - OK (0) =#=#=#=
* Passed: crm_verify - Verify a file-specified valid configuration (XML)
=#=#=#= Begin test: Verify a piped-in valid configuration (XML) =#=#=#=
=#=#=#= End test: Verify a piped-in valid configuration (XML) - OK (0) =#=#=#=
* Passed: crm_verify - Verify a piped-in valid configuration (XML)
=#=#=#= Begin test: Verbosely verify a file-specified valid configuration (XML) =#=#=#=
-
+
=#=#=#= End test: Verbosely verify a file-specified valid configuration (XML) - OK (0) =#=#=#=
* Passed: crm_verify - Verbosely verify a file-specified valid configuration (XML)
=#=#=#= Begin test: Verbosely verify a piped-in valid configuration (XML) =#=#=#=
=#=#=#= End test: Verbosely verify a piped-in valid configuration (XML) - OK (0) =#=#=#=
* Passed: crm_verify - Verbosely verify a piped-in valid configuration (XML)
=#=#=#= Begin test: Verify a string-supplied valid configuration (XML) =#=#=#=
=#=#=#= End test: Verify a string-supplied valid configuration (XML) - OK (0) =#=#=#=
* Passed: crm_verify - Verify a string-supplied valid configuration (XML)
=#=#=#= Begin test: Verbosely verify a string-supplied valid configuration (XML) =#=#=#=
=#=#=#= End test: Verbosely verify a string-supplied valid configuration (XML) - OK (0) =#=#=#=
* Passed: crm_verify - Verbosely verify a string-supplied valid configuration (XML)
diff --git a/cts/cts-cli.in b/cts/cts-cli.in
index b2d2218f77..d5338c3fe2 100644
--- a/cts/cts-cli.in
+++ b/cts/cts-cli.in
@@ -1,3374 +1,3451 @@
#!@PYTHON@
"""Regression tests for Pacemaker's command line tools."""
# pylint doesn't like the module name "cts-cli" which is an invalid complaint for this file
# but probably something we want to continue warning about elsewhere
# pylint: disable=invalid-name
# pacemaker imports need to come after we modify sys.path, which pylint will complain about.
# pylint: disable=wrong-import-position
# We know this is a very long file.
# pylint: disable=too-many-lines
__copyright__ = "Copyright 2024-2025 the Pacemaker project contributors"
__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"
import argparse
from contextlib import contextmanager
from datetime import datetime, timedelta
import fileinput
from functools import partial
from gettext import ngettext
from multiprocessing import Pool, cpu_count
import os
import pathlib
import re
from shutil import copyfile
import signal
from string import Formatter
import subprocess
import sys
from tempfile import NamedTemporaryFile, TemporaryDirectory, mkstemp
import types
# These imports allow running from a source checkout after running `make`.
if os.path.exists("@abs_top_srcdir@/python"):
sys.path.insert(0, "@abs_top_srcdir@/python")
# pylint: disable=comparison-of-constants,comparison-with-itself,condition-evals-to-constant
if os.path.exists("@abs_top_builddir@/python") and "@abs_top_builddir@" != "@abs_top_srcdir@":
sys.path.insert(0, "@abs_top_builddir@/python")
from pacemaker._cts.errors import XmlValidationError
from pacemaker._cts.validate import validate
from pacemaker.buildoptions import BuildOptions
from pacemaker.exitstatus import ExitStatus
# Individual tool tests are split out, but can also be accessed as a group with "tools"
tools_tests = ["cibadmin", "crm_attribute", "crm_standby", "crm_resource",
"crm_ticket", "crmadmin", "crm_shadow", "crm_verify", "crm_simulate",
"crm_diff"]
# The default list of tests to run, in the order they should be run
default_tests = ["access_render", "daemons", "dates", "error_codes"] + tools_tests + \
["crm_mon", "acls", "validity", "upgrade", "rules", "feature_set"]
other_tests = ["agents"]
# The directory containing this program
test_home = os.path.dirname(os.path.realpath(__file__))
# Where test data is stored
cts_cli_data = f"{test_home}/cli"
# The name of the shadow CIB
SHADOW_NAME = "cts-cli"
# Arguments to pass to valgrind
VALGRIND_ARGS = ["-q", "--gen-suppressions=all", "--show-reachable=no", "--leak-check=full",
"--trace-children=no", "--time-stamp=yes", "--num-callers=20",
f"--suppressions={test_home}/valgrind-pcmk.suppressions"]
class PluralFormatter(Formatter):
"""
Special string formatting class for selecting singular vs. plurals.
Use like so:
fmt = PluralFormatter()
print(fmt.format("{0} {0}:plural,test,tests} succeeded", n_tests))
"""
def format_field(self, value, format_spec):
"""Convert a value to a formatted representation."""
if format_spec.startswith("plural,"):
eles = format_spec.split(',')
if len(eles) == 2:
singular = eles[1]
plural = singular + "s"
else:
singular = eles[1]
plural = eles[2]
return ngettext(singular, plural, value)
return super().format_field(value, format_spec)
def cleanup_shadow_dir():
"""Remove any previously created shadow CIB directory."""
subprocess.run(["crm_shadow", "--force", "--delete", SHADOW_NAME],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
def copy_existing_cib(existing):
"""
Generate a CIB by copying an existing one to a temporary location.
This is suitable for use with the cib_gen= parameter to the TestGroup class.
"""
(fp, new) = mkstemp(prefix="cts-cli.cib.xml.")
os.close(fp)
copyfile(existing, new)
return new
def current_cib():
"""Return the complete current CIB."""
with environ({"CIB_user": "root"}):
return subprocess.check_output(["cibadmin", "-Q"], encoding="utf-8")
def make_test_group(desc, cmd, **kwargs):
"""
Create a TestGroup that replicates the same test for multiple classes.
The given description, cmd, and kwargs will be passed as arguments to each
Test subclass. The resulting objects will then be added to a TestGroup
and returned.
The main purpose of this function is to be able to run the same test for
both text and XML formats without having to duplicate everything.
"""
tests = []
for c in [Test, ValidatingTest]:
# Insert "--output-as=" after the command name.
splitup = cmd.split()
splitup.insert(1, c.format_args)
obj = c(desc, " ".join(splitup), **kwargs)
tests.append(obj)
return TestGroup(tests)
def create_shadow_cib(shadow_dir, create_empty=True, validate_with=None,
valgrind=False):
"""
Create a shadow CIB file.
Keyword arguments:
create_empty -- If True, the shadow CIB will be empty. Otherwise, the
shadow CIB will be a copy of the currently active
cluster configuration.
validate_with -- If not None, the schema version to validate the CIB
against
valgrind -- If True, run the create operation under valgrind
"""
args = ["crm_shadow", "--batch", "--force"]
if create_empty:
args += ["--create-empty", SHADOW_NAME]
else:
args += ["--create", SHADOW_NAME]
if validate_with is not None:
args += ["--validate-with", validate_with]
if valgrind:
args = ["valgrind"] + VALGRIND_ARGS + args
os.environ["CIB_shadow_dir"] = shadow_dir
os.environ["CIB_shadow"] = SHADOW_NAME
subprocess.run(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
delete_shadow_resource_defaults()
def delete_shadow_resource_defaults():
"""Clear out the rsc_defaults section from a shadow CIB file."""
# A newly created empty CIB might or might not have a rsc_defaults section
# depending on whether the --with-resource-stickiness-default configure
# option was used. To ensure regression tests behave the same either way,
# delete any rsc_defaults after creating or erasing a CIB.
subprocess.run(["cibadmin", "--delete", "--xml-text", ""],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True)
# The above command might or might not bump the CIB version, so reset it
# to ensure future changes result in the same version for comparison.
reset_shadow_cib_version()
def reset_shadow_cib_version():
"""Set various version numbers in a shadow CIB file back to 0."""
with fileinput.input(files=[shadow_path()], inplace=True) as f:
for line in f:
line = re.sub('epoch="[0-9]*"', 'epoch="1"', line)
line = re.sub('num_updates="[0-9]*"', 'num_updates="0"', line)
line = re.sub('admin_epoch="[0-9]*"', 'admin_epoch="0"', line)
print(line, end='')
def run_cmd_list(cmds):
"""
Run one or more shell commands.
cmds can be:
* A string
* A Python function
* A list of the above
Raises subprocess.CalledProcessError on error.
"""
if cmds is None:
return
if isinstance(cmds, (str, types.FunctionType)):
cmds = [cmds]
for c in cmds:
if isinstance(c, types.FunctionType):
c()
else:
subprocess.run(c, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, universal_newlines=True, check=True)
def sanitize_output(s):
"""
Replace content in the output expected to change between test runs.
This is stuff like version numbers, timestamps, source line numbers,
build options, system names and messages, etc.
"""
# A list of tuples of regular expressions and their replacements.
replacements = [
(r'Created new pacemaker-.* configuration', r'Created new pacemaker configuration'),
(r'Device not configured', r'No such device or address'),
(r'^Entity: line [0-9]+: ', r''),
(r'(Injecting attribute last-failure-ping#monitor_10000=)[0-9]*', r'\1'),
(r'Last change: .*', r'Last change:'),
(r'Last updated: .*', r'Last updated:'),
(r'^Migration will take effect until: .*', r'Migration will take effect until:'),
(r'(\* Possible values.*: .*)\(default: [^)]*\)', r'\1(default: )'),
(r"""-X '.*'""", r"""-X '...'"""),
(r' api-version="[^"]*"', r' api-version="X"'),
(r'\(apply_upgrade@.*\.c:[0-9]+\)', r'apply_upgrade'),
(r'\(invert_action@.*\.c:[0-9]+\)', r'invert_action'),
(r'\(pcmk__update_schema@.*\.c:[0-9]+\)', r'pcmk__update_schema'),
(r'(
"""
# Create a test CIB that has ACL roles
basic_tests = [
Test("Configure some ACLs", "cibadmin -M -o acls -p", update_cib=True,
stdin=acl_cib),
Test("Enable ACLs", "crm_attribute -n enable-acl -v true",
update_cib=True),
# Run cibadmin --show-access on the test CIB as an ACL-restricted user
Test("An instance of ACLs render (into color)",
"cibadmin --force --show-access=color -Q --user tony"),
Test("An instance of ACLs render (into namespacing)",
"cibadmin --force --show-access=namespace -Q --user tony"),
Test("An instance of ACLs render (into text)",
"cibadmin --force --show-access=text -Q --user tony"),
]
return [
ShadowTestGroup(basic_tests),
]
class DaemonsRegressionTest(RegressionTest):
"""A class for testing command line options of pacemaker daemons."""
@property
def name(self):
"""Return the name of this regression test."""
return "daemons"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
return [
Test("Get CIB manager metadata", "pacemaker-based metadata"),
Test("Get controller metadata", "pacemaker-controld metadata"),
Test("Get fencer metadata", "pacemaker-fenced metadata"),
Test("Get scheduler metadata", "pacemaker-schedulerd metadata"),
]
class DatesRegressionTest(RegressionTest):
"""A class for testing handling of ISO8601 dates."""
@property
def name(self):
"""Return the name of this regression test."""
return "dates"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
invalid_periods = [
"",
"2019-01-01 00:00:00Z", # Start with no end
"2019-01-01 00:00:00Z/", # Start with only a trailing slash
"PT2S/P1M", # Two durations
"2019-13-01 00:00:00Z/P1M", # Out-of-range month
"20191077T15/P1M", # Out-of-range day
"2019-10-01T25:00:00Z/P1M", # Out-of-range hour
"2019-10-01T24:00:01Z/P1M", # Hour 24 with anything but :00:00
"PT5H/20191001T007000Z", # Out-of-range minute
"2019-10-01 00:00:80Z/P1M", # Out-of-range second
"2019-10-01 00:00:10 +25:00/P1M", # Out-of-range offset hour
"20191001T000010 -00:61/P1M", # Out-of-range offset minute
"P1Y/2019-02-29 00:00:00Z", # Feb. 29 in non-leap-year
"2019-01-01 00:00:00Z/P", # Duration with no values
"P1Z/2019-02-20 00:00:00Z", # Invalid duration unit
"P1YM/2019-02-20 00:00:00Z", # No number for duration unit
]
# Ensure invalid period specifications are rejected
invalid_period_tests = []
for p in invalid_periods:
invalid_period_tests.append(Test(f"Invalid period - [{p}]",
f"iso8601 -p '{p}'",
expected_rc=ExitStatus.INVALID_PARAM))
year_tests = []
for y in ["06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "40"]:
year_tests.extend([
Test(f"20{y}-W01-7",
f"iso8601 -d '20{y}-W01-7 00Z'"),
Test(f"20{y}-W01-7 - round-trip",
f"iso8601 -d '20{y}-W01-7 00Z' -W -E '20{y}-W01-7 00:00:00Z'"),
Test(f"20{y}-W01-1",
f"iso8601 -d '20{y}-W01-1 00Z'"),
Test(f"20{y}-W01-1 - round-trip",
f"iso8601 -d '20{y}-W01-1 00Z' -W -E '20{y}-W01-1 00:00:00Z'")
])
return invalid_period_tests + [
make_test_group("'2005-040/2005-043' period", "iso8601 -p '2005-040/2005-043'"),
Test("2014-01-01 00:30:00 - 1 Hour",
"iso8601 -d '2014-01-01 00:30:00Z' -D P-1H -E '2013-12-31 23:30:00Z'"),
Test("Valid date - Feb 29 in leap year",
"iso8601 -d '2020-02-29 00:00:00Z' -E '2020-02-29 00:00:00Z'"),
Test("Valid date - using 'T' and offset",
"iso8601 -d '20191201T131211 -05:00' -E '2019-12-01 18:12:11Z'"),
Test("24:00:00 equivalent to 00:00:00 of next day",
"iso8601 -d '2019-12-31 24:00:00Z' -E '2020-01-01 00:00:00Z'"),
] + year_tests + [
make_test_group("2009-W53-07",
"iso8601 -d '2009-W53-7 00:00:00Z' -W -E '2009-W53-7 00:00:00Z'"),
Test("epoch + 2 Years 5 Months 6 Minutes",
"iso8601 -d 'epoch' -D P2Y5MT6M -E '1972-06-01 00:06:00Z'"),
Test("2009-01-31 + 1 Month",
"iso8601 -d '20090131T000000Z' -D P1M -E '2009-02-28 00:00:00Z'"),
Test("2009-01-31 + 2 Months",
"iso8601 -d '2009-01-31 00:00:00Z' -D P2M -E '2009-03-31 00:00:00Z'"),
Test("2009-01-31 + 3 Months",
"iso8601 -d '2009-01-31 00:00:00Z' -D P3M -E '2009-04-30 00:00:00Z'"),
make_test_group("2009-03-31 - 1 Month",
"iso8601 -d '2009-03-31 01:00:00 +01:00' -D P-1M -E '2009-02-28 00:00:00Z'"),
make_test_group("2038-01-01 + 3 Months",
"iso8601 -d '2038-01-01 00:00:00Z' -D P3M -E '2038-04-01 00:00:00Z'"),
]
class ErrorCodeRegressionTest(RegressionTest):
"""A class for testing error code reporting."""
@property
def name(self):
"""Return the name of this regression test."""
return "error_codes"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
# Legacy return codes
#
# Don't test unknown legacy code. FreeBSD includes a colon in strerror(),
# while other distros do not.
legacy_tests = [
make_test_group("Get legacy return code", "crm_error 201"),
make_test_group("Get legacy return code (with name)", "crm_error -n 201"),
make_test_group("Get multiple legacy return codes", "crm_error 201 202"),
make_test_group("Get multiple legacy return codes (with names)",
"crm_error -n 201 202"),
# We can only rely on our custom codes, so we'll spot-check codes 201-209
Test("List legacy return codes (spot check)",
"crm_error -l | grep 20[1-9]"),
ValidatingTest("List legacy return codes (spot check)",
"crm_error -l --output-as=xml | grep -Ev '&1 | sed -e 's/Digest:.*/Digest:/'"),
Test("Require --force for CIB erasure", "cibadmin -E",
expected_rc=ExitStatus.UNSAFE, update_cib=True),
Test("Allow CIB erasure with --force", "cibadmin -E --force"),
# Verify the output after erasure
Test("Query CIB", "cibadmin -Q",
setup=delete_shadow_resource_defaults,
update_cib=True),
]
# Add some stuff to the empty CIB so we know that erasing it did something.
basic_tests_setup = [
"""cibadmin -C -o nodes --xml-text ''""",
"""cibadmin -C -o crm_config --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''"""
]
return [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
]
class CrmAttributeRegressionTest(RegressionTest):
"""A class for testing crm_attribute."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_attribute"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
options_tests = [
make_test_group("List all available options (invalid type)",
"crm_attribute --list-options=asdf",
expected_rc=ExitStatus.USAGE),
make_test_group("List non-advanced cluster options",
"crm_attribute --list-options=cluster"),
make_test_group("List all available cluster options",
"crm_attribute --list-options=cluster --all"),
Test("Return usage error if both -p and OCF_RESOURCE_INSTANCE are empty strings",
"crm_attribute -N cluster01 -p '' -G",
expected_rc=ExitStatus.USAGE),
]
value_update_tests = [
Test("Query the value of an attribute that does not exist",
"crm_attribute -n ABCD --query --quiet",
expected_rc=ExitStatus.NOSUCH),
Test("Configure something before erasing",
"crm_attribute -n test_attr -v 5", update_cib=True),
Test("Test '++' XML attribute update syntax",
"""cibadmin -M --score --xml-text=''""",
update_cib=True),
Test("Test '+=' XML attribute update syntax",
"""cibadmin -M --score --xml-text=''""",
update_cib=True),
make_test_group("Test '++' nvpair value update syntax",
"crm_attribute -n test_attr -v 'value++' --score",
update_cib=True),
make_test_group("Test '+=' nvpair value update syntax",
"crm_attribute -n test_attr -v 'value+=2' --score",
update_cib=True),
Test("Test '++' XML attribute update syntax (--score not set)",
"""cibadmin -M --xml-text=''""",
update_cib=True),
Test("Test '+=' XML attribute update syntax (--score not set)",
"""cibadmin -M --xml-text=''""",
update_cib=True),
make_test_group("Test '++' nvpair value update syntax (--score not set)",
"crm_attribute -n test_attr -v 'value++'",
update_cib=True),
make_test_group("Test '+=' nvpair value update syntax (--score not set)",
"crm_attribute -n test_attr -v 'value+=2'",
update_cib=True),
]
query_set_tests = [
Test("Set cluster option", "crm_attribute -n cluster-delay -v 60s",
update_cib=True),
Test("Query new cluster option",
"cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay"),
Test("Set no-quorum policy",
"crm_attribute -n no-quorum-policy -v ignore", update_cib=True),
Test("Delete nvpair",
"""cibadmin -D -o crm_config --xml-text ''""",
update_cib=True),
Test("Create operation should fail",
"""cibadmin -C -o crm_config --xml-text ''""",
expected_rc=ExitStatus.EXISTS, update_cib=True),
Test("Modify cluster options section",
"""cibadmin -M -o crm_config --xml-text ''""",
update_cib=True),
Test("Query updated cluster option",
"cibadmin -Q -o crm_config | grep cib-bootstrap-options-cluster-delay",
update_cib=True),
Test("Set duplicate cluster option",
"crm_attribute -n cluster-delay -v 40s -s duplicate",
update_cib=True),
Test("Setting multiply defined cluster option should fail",
"crm_attribute -n cluster-delay -v 30s",
expected_rc=ExitStatus.MULTIPLE, update_cib=True),
Test("Set cluster option with -s",
"crm_attribute -n cluster-delay -v 30s -s duplicate",
update_cib=True),
Test("Delete cluster option with -i",
"crm_attribute -n cluster-delay -D -i cib-bootstrap-options-cluster-delay",
update_cib=True),
Test("Create node1 and bring it online",
"crm_simulate --live-check --in-place --node-up=node1",
update_cib=True),
Test("Create node attribute",
"crm_attribute -n ram -v 1024M -N node1 -t nodes",
update_cib=True),
Test("Query new node attribute",
"cibadmin -Q -o nodes | grep node1-ram",
update_cib=True),
Test("Create second node attribute",
"crm_attribute -n rattr -v XYZ -N node1 -t nodes",
update_cib=True),
Test("Query node attributes by pattern",
"crm_attribute -t nodes -P 'ra.*' -N node1 --query"),
Test("Update node attributes by pattern",
"crm_attribute -t nodes -P 'rat.*' -N node1 -v 10",
update_cib=True),
Test("Delete node attributes by pattern",
"crm_attribute -t nodes -P 'rat.*' -N node1 -D",
update_cib=True),
Test("Set a transient (fail-count) node attribute",
"crm_attribute -n fail-count-foo -v 3 -N node1 -t status",
update_cib=True),
Test("Query a fail count", "crm_failcount --query -r foo -N node1",
update_cib=True),
Test("Show node attributes with crm_simulate",
"crm_simulate --live-check --show-attrs"),
Test("Set a second transient node attribute",
"crm_attribute -n fail-count-bar -v 5 -N node1 -t status",
update_cib=True),
Test("Query transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 --query"),
Test("Update transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 -v 10",
update_cib=True),
Test("Delete transient node attributes by pattern",
"crm_attribute -t status -P fail-count -N node1 -D",
update_cib=True),
Test("crm_attribute given invalid delete usage",
"crm_attribute -t nodes -N node1 -D",
expected_rc=ExitStatus.USAGE),
Test("Set a utilization node attribute",
"crm_attribute -n cpu -v 1 -N node1 -z",
update_cib=True),
Test("Query utilization node attribute",
"crm_attribute --query -n cpu -N node1 -z"),
# This update will fail because it has version numbers
Test("Replace operation should fail",
"""cibadmin -Q | sed -e 's/epoch="[^"]*"/epoch="1"/' | cibadmin -R -p""",
expected_rc=ExitStatus.OLD),
]
promotable_tests = [
make_test_group("Query a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Delete a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -D"),
make_test_group("Query after deleting a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Update a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -v 1"),
make_test_group("Query after updating a nonexistent promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G"),
make_test_group("Update an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -v 5"),
make_test_group("Query after updating an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G"),
make_test_group("Delete an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -D"),
make_test_group("Query after deleting an existing promotable score attribute",
"crm_attribute -N cluster01 -p promotable-rsc -G",
expected_rc=ExitStatus.NOSUCH),
]
# Test for an issue with legacy command line parsing when the resource is
# specified in the environment (CLBZ#5509)
ocf_rsc_instance_tests = [
make_test_group("Update a promotable score attribute to -INFINITY",
"crm_attribute -N cluster01 -p -v -INFINITY",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
make_test_group("Query after updating a promotable score attribute to -INFINITY",
"crm_attribute -N cluster01 -p -G",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
Test("Try OCF_RESOURCE_INSTANCE if -p is specified with an empty string",
"crm_attribute -N cluster01 -p '' -G",
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"}),
]
return options_tests + [
ShadowTestGroup(value_update_tests),
ShadowTestGroup(query_set_tests),
TestGroup(promotable_tests + ocf_rsc_instance_tests,
env={"OCF_RESOURCE_INSTANCE": "promotable-rsc"},
cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
]
class CrmStandbyRegressionTest(RegressionTest):
"""A class for testing crm_standby."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_standby"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
Test("Default standby value", "crm_standby -N node1 -G"),
Test("Set standby status", "crm_standby -N node1 -v true",
update_cib=True),
Test("Query standby value", "crm_standby -N node1 -G"),
Test("Delete standby value", "crm_standby -N node1 -D",
update_cib=True),
]
return [
ShadowTestGroup(basic_tests,
setup="""cibadmin -C -o nodes --xml-text ''"""),
]
class CrmResourceRegressionTest(RegressionTest):
"""A class for testing crm_resource."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_resource"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
options_tests = [
Test("crm_resource run with extra arguments", "crm_resource foo bar",
expected_rc=ExitStatus.USAGE),
Test("List all available resource options (invalid type)",
"crm_resource --list-options=asdf",
expected_rc=ExitStatus.USAGE),
Test("List all available resource options (invalid type)",
"crm_resource --list-options=asdf --output-as=xml",
expected_rc=ExitStatus.USAGE),
make_test_group("List non-advanced primitive meta-attributes",
"crm_resource --list-options=primitive"),
make_test_group("List all available primitive meta-attributes",
"crm_resource --list-options=primitive --all"),
make_test_group("List non-advanced fencing parameters",
"crm_resource --list-options=fencing"),
make_test_group("List all available fencing parameters",
"crm_resource --list-options=fencing --all"),
]
basic_tests = [
Test("Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
Test("crm_resource given both -r and resource config",
"crm_resource -r xyz --class ocf --provider pacemaker --agent Dummy",
expected_rc=ExitStatus.USAGE),
Test("crm_resource given resource config with invalid action",
"crm_resource --class ocf --provider pacemaker --agent Dummy -D",
expected_rc=ExitStatus.USAGE),
Test("Create a resource meta attribute",
"crm_resource -r dummy --meta -p is-managed -v false",
update_cib=True),
Test("Query a resource meta attribute",
"crm_resource -r dummy --meta -g is-managed",
update_cib=True),
Test("Remove a resource meta attribute",
"crm_resource -r dummy --meta -d is-managed",
update_cib=True),
ValidatingTest("Create another resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped --output-as=xml"),
ValidatingTest("Show why a resource is not running",
"crm_resource -Y -r dummy --output-as=xml"),
ValidatingTest("Remove another resource meta attribute",
"crm_resource -r dummy --meta -d target-role --output-as=xml"),
ValidatingTest("Get a non-existent attribute from a resource element",
"crm_resource -r dummy --get-parameter nonexistent --element --output-as=xml"),
make_test_group("Get a non-existent attribute from a resource element",
"crm_resource -r dummy --get-parameter nonexistent --element",
update_cib=True),
Test("Get an existent attribute from a resource element",
"crm_resource -r dummy --get-parameter class --element",
update_cib=True),
ValidatingTest("Set a non-existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml",
update_cib=True),
ValidatingTest("Set an existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element --output-as=xml",
update_cib=True),
ValidatingTest("Delete an existent attribute for a resource element",
"crm_resource -r dummy -d description --element --output-as=xml",
update_cib=True),
ValidatingTest("Delete a non-existent attribute for a resource element",
"crm_resource -r dummy -d description --element --output-as=xml",
update_cib=True),
Test("Set a non-existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element",
update_cib=True),
Test("Set an existent attribute for a resource element",
"crm_resource -r dummy --set-parameter=description -v test_description --element",
update_cib=True),
Test("Delete an existent attribute for a resource element",
"crm_resource -r dummy -d description --element",
update_cib=True),
Test("Delete a non-existent attribute for a resource element",
"crm_resource -r dummy -d description --element",
update_cib=True),
Test("Create a resource attribute", "crm_resource -r dummy -p delay -v 10s",
update_cib=True),
make_test_group("List the configured resources", "crm_resource -L",
update_cib=True),
Test("Implicitly list the configured resources", "crm_resource"),
Test("List IDs of instantiated resources", "crm_resource -l"),
make_test_group("Show XML configuration of resource", "crm_resource -q -r dummy"),
Test("Require a destination when migrating a resource that is stopped",
"crm_resource -r dummy -M",
update_cib=True, expected_rc=ExitStatus.USAGE),
Test("Don't support migration to non-existent locations",
"crm_resource -r dummy -M -N i.do.not.exist",
update_cib=True, expected_rc=ExitStatus.NOSUCH),
Test("Create a fencing resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
Test("Bring resources online", "crm_simulate --live-check --in-place",
update_cib=True),
Test("Try to move a resource to its existing location",
"crm_resource -r dummy --move --node node1",
update_cib=True, expected_rc=ExitStatus.EXISTS),
Test("Try to move a resource that doesn't exist",
"crm_resource -r xyz --move --node node1",
expected_rc=ExitStatus.NOSUCH),
Test("Move a resource from its existing location",
"crm_resource -r dummy --move",
update_cib=True),
Test("Clear out constraints generated by --move",
"crm_resource -r dummy --clear",
update_cib=True),
Test("Ban a resource on unknown node",
"crm_resource -r dummy -B -N host1",
expected_rc=ExitStatus.NOSUCH),
Test("Create two more nodes and bring them online",
"crm_simulate --live-check --in-place --node-up=node2 --node-up=node3",
update_cib=True),
Test("Ban dummy from node1", "crm_resource -r dummy -B -N node1",
update_cib=True),
Test("Show where a resource is running", "crm_resource -r dummy -W"),
Test("Show constraints on a resource", "crm_resource -a -r dummy"),
ValidatingTest("Ban dummy from node2",
"crm_resource -r dummy -B -N node2 --output-as=xml",
update_cib=True),
Test("Relocate resources due to ban",
"crm_simulate --live-check --in-place -S",
update_cib=True),
ValidatingTest("Move dummy to node1",
"crm_resource -r dummy -M -N node1 --output-as=xml",
update_cib=True),
Test("Clear implicit constraints for dummy on node2",
"crm_resource -r dummy -U -N node2",
update_cib=True),
Test("Drop the status section",
"cibadmin -R -o status --xml-text ''"),
Test("Create a clone",
"""cibadmin -C -o resources --xml-text ''"""),
Test("Create a resource meta attribute",
"crm_resource -r test-primitive --meta -p is-managed -v false",
update_cib=True),
Test("Create a resource meta attribute in the primitive",
"crm_resource -r test-primitive --meta -p is-managed -v false --force",
update_cib=True),
Test("Update resource meta attribute with duplicates",
"crm_resource -r test-clone --meta -p is-managed -v true",
update_cib=True),
Test("Update resource meta attribute with duplicates (force clone)",
"crm_resource -r test-clone --meta -p is-managed -v true --force",
update_cib=True),
Test("Update child resource meta attribute with duplicates",
"crm_resource -r test-primitive --meta -p is-managed -v false",
update_cib=True),
Test("Delete resource meta attribute with duplicates",
"crm_resource -r test-clone --meta -d is-managed",
update_cib=True),
Test("Delete resource meta attribute in parent",
"crm_resource -r test-primitive --meta -d is-managed",
update_cib=True),
Test("Create a resource meta attribute in the primitive",
"crm_resource -r test-primitive --meta -p is-managed -v false --force",
update_cib=True),
Test("Update existing resource meta attribute",
"crm_resource -r test-clone --meta -p is-managed -v true",
update_cib=True),
Test("Create a resource meta attribute in the parent",
"crm_resource -r test-clone --meta -p is-managed -v true --force",
update_cib=True),
Test("Delete resource parent meta attribute (force)",
"crm_resource -r test-clone --meta -d is-managed --force",
update_cib=True),
# Restore meta-attributes before running this test
Test("Delete resource child meta attribute",
"crm_resource -r test-primitive --meta -d is-managed",
setup=["crm_resource -r test-primitive --meta -p is-managed -v true --force",
"crm_resource -r test-clone --meta -p is-managed -v true --force"],
update_cib=True),
Test("Create the dummy-group resource group",
"""cibadmin -C -o resources --xml-text '"""
""""""
""""""
"""'""",
update_cib=True),
Test("Create a resource meta attribute in dummy1",
"crm_resource -r dummy1 --meta -p is-managed -v true",
update_cib=True),
Test("Create a resource meta attribute in dummy-group",
"crm_resource -r dummy-group --meta -p is-managed -v false",
update_cib=True),
Test("Delete the dummy-group resource group",
"cibadmin -D -o resources --xml-text ''",
update_cib=True),
Test("Specify a lifetime when moving a resource",
"crm_resource -r dummy --move --node node2 --lifetime=PT1H",
update_cib=True),
Test("Try to move a resource previously moved with a lifetime",
"crm_resource -r dummy --move --node node1",
update_cib=True),
Test("Ban dummy from node1 for a short time",
"crm_resource -r dummy -B -N node1 --lifetime=PT1S",
update_cib=True),
Test("Remove expired constraints",
"sleep 2 && crm_resource --clear --expired",
update_cib=True),
# Clear has already been tested elsewhere, but we need to get rid of the
# constraints so testing delete works. It won't delete if there's still
# a reference to the resource somewhere.
Test("Clear all implicit constraints for dummy",
"crm_resource -r dummy -U",
update_cib=True),
Test("Set a node health strategy",
"crm_attribute -n node-health-strategy -v migrate-on-red",
update_cib=True),
Test("Set a node health attribute",
"crm_attribute -N node3 -n '#health-cts-cli' -v red",
update_cib=True),
ValidatingTest("Show why a resource is not running on an unhealthy node",
"crm_resource -N node3 -Y -r dummy --output-as=xml"),
Test("Delete a resource",
"crm_resource -D -r dummy -t primitive",
update_cib=True),
]
constraint_tests = []
for rsc in ["prim1", "prim2", "prim3", "prim4", "prim5", "prim6", "prim7",
"prim8", "prim9", "prim10", "prim11", "prim12", "prim13",
"group", "clone"]:
constraint_tests.extend([
make_test_group(f"Check locations and constraints for {rsc}",
f"crm_resource -a -r {rsc}"),
make_test_group(f"Recursively check locations and constraints for {rsc}",
f"crm_resource -A -r {rsc}"),
])
constraint_tests.extend([
Test("Check locations and constraints for group member (referring to group)",
"crm_resource -a -r gr2"),
Test("Check locations and constraints for group member (without referring to group)",
"crm_resource -a -r gr2 --force"),
])
colocation_tests = [
ValidatingTest("Set a meta-attribute for primitive and resources colocated with it",
"crm_resource -r prim5 --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"),
Test("Set a meta-attribute for group and resource colocated with it",
"crm_resource -r group --meta --set-parameter=target-role -v Stopped --recursive"),
ValidatingTest("Set a meta-attribute for clone and resource colocated with it",
"crm_resource -r clone --meta --set-parameter=target-role -v Stopped --recursive --output-as=xml"),
]
digest_tests = [
ValidatingTest("Show resource digests",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml"),
Test("Show resource digests with overrides",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml CRM_meta_interval=10000 CRM_meta_timeout=20000"),
make_test_group("Show resource operations", "crm_resource --list-operations"),
]
basic2_tests = [
make_test_group("List a promotable clone resource",
"crm_resource --locate -r promotable-clone"),
make_test_group("List the primitive of a promotable clone resource",
"crm_resource --locate -r promotable-rsc"),
make_test_group("List a single instance of a promotable clone resource",
"crm_resource --locate -r promotable-rsc:0"),
make_test_group("List another instance of a promotable clone resource",
"crm_resource --locate -r promotable-rsc:1"),
Test("Try to move an instance of a cloned resource",
"crm_resource -r promotable-rsc:0 --move --node cluster01",
expected_rc=ExitStatus.INVALID_PARAM),
]
basic_tests_setup = [
"crm_attribute -n no-quorum-policy -v ignore",
"crm_simulate --live-check --in-place --node-up=node1"
]
return options_tests + [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
TestGroup(constraint_tests, env={"CIB_file": f"{cts_cli_data}/constraints.xml"}),
TestGroup(colocation_tests, cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/constraints.xml")),
TestGroup(digest_tests, env={"CIB_file": f"{cts_cli_data}/crm_resource_digests.xml"}),
TestGroup(basic2_tests, env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ValidatingTest("Check that CIB_file=\"-\" works - crm_resource",
"crm_resource --digests -r rsc1 -N node1 --output-as=xml",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crm_resource_digests.xml")),
]
class CrmTicketRegressionTest(RegressionTest):
"""A class for testing crm_ticket."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_ticket"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
Test("Default ticket granted state",
"crm_ticket -t ticketA -G granted -d false"),
Test("Set ticket granted state", "crm_ticket -t ticketA -r --force",
update_cib=True),
make_test_group("List ticket IDs", "crm_ticket -w"),
make_test_group("Query ticket state", "crm_ticket -t ticketA -q"),
make_test_group("Query ticket granted state",
"crm_ticket -t ticketA -G granted"),
Test("Delete ticket granted state",
"crm_ticket -t ticketA -D granted --force",
update_cib=True),
Test("Make a ticket standby", "crm_ticket -t ticketA -s",
update_cib=True),
Test("Query ticket standby state", "crm_ticket -t ticketA -G standby"),
Test("Activate a ticket", "crm_ticket -t ticketA -a",
update_cib=True),
make_test_group("List ticket details", "crm_ticket -L -t ticketA"),
Test("Add a second ticket", "crm_ticket -t ticketB -G granted -d false",
update_cib=True),
Test("Set second ticket granted state",
"crm_ticket -t ticketB -r --force",
update_cib=True),
make_test_group("List tickets", "crm_ticket -l"),
Test("Delete second ticket",
"""cibadmin --delete --xml-text ''""",
update_cib=True),
Test("Delete ticket standby state", "crm_ticket -t ticketA -D standby",
update_cib=True),
Test("Add a constraint to a ticket",
"""cibadmin -C -o constraints --xml-text ''""",
update_cib=True),
make_test_group("Query ticket constraints", "crm_ticket -t ticketA -c"),
Test("Delete ticket constraint",
"""cibadmin --delete --xml-text ''""",
update_cib=True),
]
basic_tests_setup = [
"""cibadmin -C -o crm_config --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''"""
]
return [
ShadowTestGroup(basic_tests, setup=basic_tests_setup),
]
class CrmadminRegressionTest(RegressionTest):
"""A class for testing crmadmin."""
@property
def name(self):
"""Return the name of this regression test."""
return "crmadmin"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
make_test_group("List all nodes", "crmadmin -N"),
make_test_group("Minimally list all nodes", "crmadmin -N -q"),
Test("List all nodes as bash exports", "crmadmin -N -B"),
make_test_group("List cluster nodes",
"crmadmin -N cluster"),
make_test_group("List guest nodes",
"crmadmin -N guest"),
make_test_group("List remote nodes",
"crmadmin -N remote"),
make_test_group("List cluster,remote nodes",
"crmadmin -N cluster,remote"),
make_test_group("List guest,remote nodes",
"crmadmin -N guest,remote"),
]
return [
TestGroup(basic_tests,
env={"CIB_file": f"{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml"}),
Test("Check that CIB_file=\"-\" works", "crmadmin -N",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crmadmin-cluster-remote-guest-nodes.xml")),
]
class CrmShadowRegressionTest(RegressionTest):
"""A class for testing crm_shadow."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_shadow"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
no_instance_tests = [
make_test_group("Get active shadow instance (no active instance)",
"crm_shadow --which",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's file name (no active instance)",
"crm_shadow --file",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's contents (no active instance)",
"crm_shadow --display",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (no active instance)",
"crm_shadow --diff",
expected_rc=ExitStatus.NOSUCH),
]
# Create new shadow instance based on active CIB
# Don't use create_shadow_cib() here; test explicitly
new_instance_tests = [
make_test_group("Create copied shadow instance",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
# Query shadow instance based on active CIB
make_test_group("Get active shadow instance (copied)",
"crm_shadow --which"),
make_test_group("Get active shadow instance's file name (copied)",
"crm_shadow --file"),
make_test_group("Get active shadow instance's contents (copied)",
"crm_shadow --display"),
make_test_group("Get active shadow instance's diff (copied)",
"crm_shadow --diff"),
]
# Make some changes to the shadow file
modify_cib = """export CIB_file=$(crm_shadow --file) && """ \
"""cibadmin --modify --xml-text '' && """ \
"""cibadmin --delete --xml-text '' && """ \
"""cibadmin --create -o resources --xml-text '' && """ \
"""cibadmin --create -o status --xml-text ''"""
more_tests = [
# We can't use make_test_group() here because we only want to run
# the modify_cib setup code once, and make_test_group will pass all
# kwargs to every instance it creates.
Test("Get active shadow instance's diff (after changes)",
"crm_shadow --diff",
setup=modify_cib, expected_rc=ExitStatus.ERROR),
ValidatingTest("Get active shadow instance's diff (after changes)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
TestGroup([
# Commit the modified shadow CIB to a temp active CIB file
Test("Commit shadow instance",
f"crm_shadow --commit {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Commit shadow instance (force)",
f"crm_shadow --commit {SHADOW_NAME} --force"),
Test("Get active shadow instance's diff (after commit)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
Test("Commit shadow instance (force) (all)",
f"crm_shadow --commit {SHADOW_NAME} --force --all"),
Test("Get active shadow instance's diff (after commit all)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
], cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
TestGroup([
# Repeat sequence with XML output
ValidatingTest("Commit shadow instance",
f"crm_shadow --commit {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Commit shadow instance (force)",
f"crm_shadow --commit {SHADOW_NAME} --force --output-as=xml"),
ValidatingTest("Get active shadow instance's diff (after commit)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
ValidatingTest("Commit shadow instance (force) (all)",
f"crm_shadow --commit {SHADOW_NAME} --force --all --output-as=xml"),
ValidatingTest("Get active shadow instance's diff (after commit all)",
"crm_shadow --diff --output-as=xml",
expected_rc=ExitStatus.ERROR),
# Commit an inactive shadow instance with no active instance
make_test_group("Commit shadow instance (no active instance)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_shadow": None},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (no active instance) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Commit an inactive shadow instance with an active instance
make_test_group("Commit shadow instance (mismatch)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (mismatch) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Commit an active shadow instance whose shadow file is missing
make_test_group("Commit shadow instance (nonexistent shadow file)",
"crm_shadow --commit nonexistent_shadow",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (nonexistent shadow file) (force)",
"crm_shadow --commit nonexistent_shadow --force",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (nonexistent shadow file)",
"crm_shadow --diff",
env={"CIB_shadow": "nonexistent_shadow"},
expected_rc=ExitStatus.NOSUCH),
# Commit an active shadow instance when the CIB file is missing
make_test_group("Commit shadow instance (nonexistent CIB file)",
f"crm_shadow --commit {SHADOW_NAME}",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.USAGE),
make_test_group("Commit shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --commit {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.NOSUCH),
make_test_group("Get active shadow instance's diff (nonexistent CIB file)",
"crm_shadow --diff",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"},
expected_rc=ExitStatus.NOSUCH),
], cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
]
delete_1_tests = [
# Delete an active shadow instance
Test("Delete shadow instance", f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (force)", f"crm_shadow --delete {SHADOW_NAME} --force"),
ShadowTestGroup([
ValidatingTest("Delete shadow instance",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
])
]
delete_2_tests = [
# Delete an inactive shadow instance with no active instance
Test("Delete shadow instance (no active instance)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (no active instance) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_3_tests = [
ValidatingTest("Delete shadow instance (no active instance)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (no active instance) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
]
delete_4_tests = [
# Delete an inactive shadow instance with an active instance
Test("Delete shadow instance (mismatch)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (mismatch) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_5_tests = [
ValidatingTest("Delete shadow instance (mismatch)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (mismatch) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
# Delete an active shadow instance whose shadow file is missing
Test("Delete shadow instance (nonexistent shadow file)",
"crm_shadow --delete nonexistent_shadow",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (nonexistent shadow file) (force)",
"crm_shadow --delete nonexistent_shadow --force"),
ValidatingTest("Delete shadow instance (nonexistent shadow file)",
"crm_shadow --delete nonexistent_shadow --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (nonexistent shadow file) (force)",
"crm_shadow --delete nonexistent_shadow --force --output-as=xml"),
]
delete_6_tests = [
# Delete an active shadow instance when the CIB file is missing
Test("Delete shadow instance (nonexistent CIB file)",
f"crm_shadow --delete {SHADOW_NAME}",
expected_rc=ExitStatus.USAGE),
Test("Delete shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force"),
]
delete_7_tests = [
ValidatingTest("Delete shadow instance (nonexistent CIB file)",
f"crm_shadow --delete {SHADOW_NAME} --output-as=xml",
expected_rc=ExitStatus.USAGE),
ValidatingTest("Delete shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --delete {SHADOW_NAME} --force --output-as=xml"),
]
create_1_tests = [
# Create new shadow instance based on active CIB with no instance active
make_test_group("Create copied shadow instance (no active instance)",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Create new shadow instance based on active CIB with other instance active
make_test_group("Create copied shadow instance (mismatch)",
f"crm_shadow --create {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Create new shadow instance based on CIB (shadow file already exists)
make_test_group("Create copied shadow instance (file already exists)",
f"crm_shadow --create {SHADOW_NAME} --batch",
expected_rc=ExitStatus.CANTCREAT),
make_test_group("Create copied shadow instance (file already exists) (force)",
f"crm_shadow --create {SHADOW_NAME} --batch --force"),
# Create new shadow instance based on active CIB when the CIB file is missing
make_test_group("Create copied shadow instance (nonexistent CIB file) (force)",
f"crm_shadow --create {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH,
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
]
create_2_tests = [
# Create new empty shadow instance
make_test_group("Create empty shadow instance",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
# Create empty shadow instance with no active instance
make_test_group("Create empty shadow instance (no active instance)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": None}),
# Create empty shadow instance with other instance active
make_test_group("Create empty shadow instance (mismatch)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_shadow": "nonexistent_shadow"}),
# Create empty shadow instance when the CIB file is missing
make_test_group("Create empty shadow instance (nonexistent CIB file)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
setup=f"crm_shadow --delete {SHADOW_NAME} --force",
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
# Create empty shadow instance (shadow file already exists)
make_test_group("Create empty shadow instance (file already exists)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch",
expected_rc=ExitStatus.CANTCREAT),
make_test_group("Create empty shadow instance (file already exists) (force)",
f"crm_shadow --create-empty {SHADOW_NAME} --batch --force"),
# Query shadow instance with an empty CIB.
# --which and --file queries were done earlier.
TestGroup([
make_test_group("Get active shadow instance's contents (empty CIB)",
"crm_shadow --display"),
make_test_group("Get active shadow instance's diff (empty CIB)",
"crm_shadow --diff",
expected_rc=ExitStatus.ERROR),
], setup=delete_shadow_resource_defaults),
]
reset_1_tests = [
Test("Resetting active shadow instance to active CIB requires force",
f"crm_shadow --reset {SHADOW_NAME} --batch",
expected_rc=ExitStatus.USAGE),
Test("Reset active shadow instance to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force"),
Test("Active shadow instance no different from active CIB after reset",
"crm_shadow --diff"),
Test("Active shadow instance differs from active CIB after change",
"crm_shadow --diff",
setup="crm_attribute -n admin_epoch -v 99",
expected_rc=ExitStatus.ERROR),
ValidatingTest("Reset active shadow instance to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force --output-as=xml"),
ValidatingTest("Active shadow instance no different from active CIB after reset",
"crm_shadow --diff --output-as=xml"),
ValidatingTest("Active shadow instance differs from active CIB after change",
"crm_shadow --diff --output-as=xml",
setup="crm_attribute -n admin_epoch -v 199",
expected_rc=ExitStatus.ERROR),
make_test_group("Reset shadow instance to active CIB with nonexistent shadow file",
f"crm_shadow --reset {SHADOW_NAME} --batch --force",
setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
Test("Active shadow instance no different from active CIB after force-reset",
"crm_shadow --diff"),
]
reset_2_tests = [
make_test_group("Reset inactive shadow instance (none active) to active CIB",
f"crm_shadow --reset {SHADOW_NAME} --force --batch"),
]
reset_3_tests = [
make_test_group("Reset inactive shadow instance while another instance active",
f"crm_shadow --reset {SHADOW_NAME} --batch --force"),
]
reset_4_tests = [
make_test_group("Reset shadow instance with nonexistent CIB",
f"crm_shadow --reset {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH),
]
# Switch shadow instances
switch_tests = [
make_test_group("Switch to new shadow instance",
f"crm_shadow --switch {SHADOW_NAME} --batch"),
TestGroup([
make_test_group("Switch to nonexistent shadow instance",
f"crm_shadow --switch {SHADOW_NAME} --batch",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Switch to nonexistent shadow instance (force)",
f"crm_shadow --switch {SHADOW_NAME} --batch --force",
expected_rc=ExitStatus.NOSUCH),
], setup=f"crm_shadow --delete {SHADOW_NAME} --force"),
]
return no_instance_tests + [
ShadowTestGroup(new_instance_tests + more_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(delete_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ShadowTestGroup(delete_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(delete_3_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(delete_4_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(delete_5_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(delete_6_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(delete_7_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(create_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(create_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"},
create=False),
ShadowTestGroup(reset_1_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
ShadowTestGroup(reset_2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": None}),
ShadowTestGroup(reset_3_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml",
"CIB_shadow": "nonexistent_shadow"}),
ShadowTestGroup(reset_4_tests,
env={"CIB_file": f"{cts_cli_data}/nonexistent_cib.xml"}),
ShadowTestGroup(switch_tests,
env={"CIB_shadow": "nonexistent_shadow"},
create_empty=True),
]
class CrmVerifyRegressionTest(RegressionTest):
"""A class for testing crm_verify."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_verify"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
invalid_tests = [
make_test_group("Verify a file-specified invalid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml",
expected_rc=ExitStatus.CONFIG),
make_test_group("Verify a file-specified invalid configuration (verbose)",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --verbose",
expected_rc=ExitStatus.CONFIG),
make_test_group("Verify a file-specified invalid configuration (quiet)",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_bz.xml --quiet",
expected_rc=ExitStatus.CONFIG),
ValidatingTest("Verify another file-specified invalid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_verify_invalid_no_stonith.xml --output-as=xml",
expected_rc=ExitStatus.CONFIG),
]
with open(f"{test_home}/cli/crm_mon.xml", encoding="utf-8") as f:
cib_contents = f.read()
valid_tests = [
ValidatingTest("Verify a file-specified valid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml"),
ValidatingTest("Verify a piped-in valid configuration",
"crm_verify -p --output-as=xml",
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
ValidatingTest("Verbosely verify a file-specified valid configuration",
f"crm_verify --xml-file {cts_cli_data}/crm_mon.xml --output-as=xml --verbose"),
ValidatingTest("Verbosely verify a piped-in valid configuration",
"crm_verify -p --output-as=xml --verbose",
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
ValidatingTest("Verify a string-supplied valid configuration",
f"crm_verify -X '{cib_contents}' --output-as=xml"),
ValidatingTest("Verbosely verify a string-supplied valid configuration",
f"crm_verify -X '{cib_contents}' --output-as=xml --verbose"),
]
return invalid_tests + valid_tests
class CrmSimulateRegressionTest(RegressionTest):
"""A class for testing crm_simulate."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_simulate"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
good_cib = """
"""
bad_cib = good_cib.replace("start", "break")
bad_version_cib = good_cib.replace("pacemaker-1.2", "pacemaker-9999.0")
recoverable_cib = good_cib.replace("", "")
no_version_cib = good_cib.replace('validate-with="pacemaker-1.2" ', "")
no_version_bad_cib = bad_version_cib.replace('epoch="3"', 'epoch="30"').replace("start", "break")
basic_tests = [
Test("Show allocation scores with crm_simulate",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml --show-scores --output-as=xml"),
Test("Show utilization with crm_simulate",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml --show-utilization"),
Test("Simulate injecting a failure",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S -i ping_monitor_10000@cluster02=1"),
Test("Simulate bringing a node down",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S --node-down=cluster01"),
Test("Simulate a node failing",
f"crm_simulate -x {cts_cli_data}/crm_mon.xml -S --node-fail=cluster02"),
Test("Run crm_simulate with invalid CIB (enum violation)",
"crm_simulate -p -S",
stdin=bad_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid CIB (unrecognized validate-with)",
"crm_simulate -p -S",
stdin=bad_version_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid, but possibly recoverable CIB (valid with X.Y+1)",
"crm_simulate -p -S",
stdin=recoverable_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"}),
Test("Run crm_simulate with valid CIB, but without validate-with attribute",
"crm_simulate -p -S",
stdin=no_version_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
Test("Run crm_simulate with invalid CIB, also without validate-with attribute",
"crm_simulate -p -S",
stdin=no_version_bad_cib,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"},
expected_rc=ExitStatus.CONFIG),
]
return [
ShadowTestGroup(basic_tests, create=False,
env={"CIB_shadow": None}),
]
class CrmDiffRegressionTest(RegressionTest):
"""A class for testing crm_diff."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_diff"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
+
+ old_file = f"{cts_cli_data}/crm_diff_old.xml"
+ new_file = f"{cts_cli_data}/crm_diff_new.xml"
+ patch_file = f"{cts_cli_data}/crm_diff_patchset.xml"
+ cib_patch_file = f"{cts_cli_data}/crm_diff_patchset_cib.xml"
+
+ # Enclose the strings in quotes now rather than in the command lines
+ with open(f"{cts_cli_data}/crm_diff_old.xml", "r") as file:
+ old_str = f"'{file.read()}'"
+ with open(f"{cts_cli_data}/crm_diff_new.xml", "r") as file:
+ new_str = f"'{file.read()}'"
+
return [
- Test("Create an XML patchset",
- f"crm_diff -o {cts_cli_data}/crm_diff_old.xml -n {cts_cli_data}/crm_diff_new.xml",
- expected_rc=ExitStatus.ERROR)
+ make_test_group("Create an XML patchset from files",
+ f"crm_diff -o {old_file} -n {new_file}",
+ expected_rc=ExitStatus.ERROR),
+ make_test_group("Create an XML patchset from strings",
+ f"crm_diff -O {old_str} -N {new_str}",
+ expected_rc=ExitStatus.ERROR),
+ make_test_group("Create an XML patchset from old file, new string",
+ f"crm_diff -o {old_file} -N {new_str}",
+ expected_rc=ExitStatus.ERROR),
+ make_test_group("Create an XML patchset from old string, new file",
+ f"crm_diff -O {old_str} -n {new_file}",
+ expected_rc=ExitStatus.ERROR),
+
+ make_test_group("Create an XML patchset as CIB",
+ f"crm_diff -o {old_file} -n {new_file} --cib",
+ expected_rc=ExitStatus.ERROR),
+ make_test_group("Create an XML patchset with no versions",
+ f"crm_diff -o {old_file} -n {new_file} --no-version",
+ expected_rc=ExitStatus.ERROR),
+ make_test_group("Create an XML patchset as CIB, with no versions",
+ f"crm_diff -o {old_file} -n {new_file} --cib --no-version",
+ expected_rc=ExitStatus.USAGE),
+
+ # Patch must be a file (cannot be a string).
+ #
+ # patch_file was generated using the following command:
+ #
+ # # crm_diff -o {old_file} -n {new_file}
+ #
+ make_test_group("Apply an XML patchset to a file",
+ f"crm_diff -o {old_file} -p {patch_file}"),
+ make_test_group("Apply an XML patchset to a string",
+ f"crm_diff -O {old_str} -p {patch_file}"),
+
+ make_test_group("Apply an XML patchset as CIB",
+ f"crm_diff -o {old_file} -p {patch_file} --cib"),
+ make_test_group("Apply an XML patchset with no versions",
+ f"crm_diff -o {old_file} -p {patch_file} --no-version"),
+ make_test_group("Apply an XML patchset as CIB, with no versions",
+ f"crm_diff -o {old_file} -p {patch_file} --cib --no-version",
+ expected_rc=ExitStatus.USAGE),
+
+ # cib_patch_file was generated using the following command:
+ #
+ # # crm_diff -o {old_file} -n {new_file} --cib
+ #
+ # Thus a digest was added to the patchset, and attribute position
+ # changes were ignored.
+ #
+ # @FIXME Currently these all fail due to digest mismatch. The issue
+ # goes back to at least Pacemaker 1.1.24. However, note that they
+ # fail with a generic error code, not a digest error code.
+ #
+ # It seems reasonable that a patchset generated by crm_diff should
+ # possible to apply to the old XML using crm_diff.
+ make_test_group("Apply an XML patchset generated as CIB",
+ f"crm_diff -o {old_file} -p {cib_patch_file}",
+ expected_rc=ExitStatus.ERROR),
+ make_test_group("Apply an XML patchset generated as CIB, as CIB",
+ f"crm_diff -o {old_file} -p {cib_patch_file} --cib",
+ expected_rc=ExitStatus.ERROR),
+ make_test_group("Apply an XML patchset generated as CIB, with no versions",
+ f"crm_diff -o {old_file} -p {cib_patch_file} --no-version",
+ expected_rc=ExitStatus.ERROR),
+
+ # @TODO We could add tests where the old and new CIBs have the same
+ # version info. In that case, at the time of writing, generating a
+ # patchset with --cib and then trying to apply it will result in
+ # ExitStatus.OLD.
]
class CrmMonRegressionTest(RegressionTest):
"""A class for testing crm_mon."""
@property
def name(self):
"""Return the name of this regression test."""
return "crm_mon"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
make_test_group("Basic output", "crm_mon -1"),
make_test_group("Output without node section",
"crm_mon -1 --exclude=nodes"),
# The next test doesn't need to be performed for other output formats. It's
# really just a test to make sure that blank lines are correct.
Test("Output with only the node section",
"crm_mon -1 --exclude=all --include=nodes"),
# XML includes everything already so there's no need for a complete test
Test("Complete text output", "crm_mon -1 --include=all"),
# XML includes detailed output already
Test("Complete text output with detail", "crm_mon -1R --include=all"),
Test("Complete brief text output", "crm_mon -1 --include=all --brief"),
Test("Complete text output grouped by node",
"crm_mon -1 --include=all --group-by-node"),
# XML does not have a brief output option
Test("Complete brief text output grouped by node",
"crm_mon -1 --include=all --group-by-node --brief"),
ValidatingTest("Output grouped by node",
"crm_mon --output-as=xml --group-by-node"),
make_test_group("Complete output filtered by node",
"crm_mon -1 --include=all --node=cluster01"),
make_test_group("Complete output filtered by tag",
"crm_mon -1 --include=all --node=even-nodes"),
make_test_group("Complete output filtered by resource tag",
"crm_mon -1 --include=all --resource=fencing-rscs"),
make_test_group("Output filtered by node that doesn't exist",
"crm_mon -1 --node=blah"),
Test("Basic text output with inactive resources", "crm_mon -1 -r"),
# XML already includes inactive resources
Test("Basic text output with inactive resources, filtered by node",
"crm_mon -1 -r --node=cluster02"),
make_test_group("Complete output filtered by primitive resource",
"crm_mon -1 --include=all --resource=Fencing"),
make_test_group("Complete output filtered by group resource",
"crm_mon -1 --include=all --resource=exim-group"),
Test("Complete text output filtered by group resource member",
"crm_mon -1 --include=all --resource=Public-IP"),
ValidatingTest("Output filtered by group resource member",
"crm_mon --output-as=xml --resource=Email"),
make_test_group("Complete output filtered by clone resource",
"crm_mon -1 --include=all --resource=ping-clone"),
make_test_group("Complete output filtered by clone resource instance",
"crm_mon -1 --include=all --resource=ping"),
Test("Complete text output filtered by exact clone resource instance",
"crm_mon -1 --include=all --show-detail --resource=ping:0"),
ValidatingTest("Output filtered by exact clone resource instance",
"crm_mon --output-as=xml --resource=ping:1"),
make_test_group("Output filtered by resource that doesn't exist",
"crm_mon -1 --resource=blah"),
Test("Basic text output with inactive resources, filtered by tag",
"crm_mon -1 -r --resource=inactive-rscs"),
Test("Basic text output with inactive resources, filtered by bundle resource",
"crm_mon -1 -r --resource=httpd-bundle"),
ValidatingTest("Output filtered by inactive bundle resource",
"crm_mon --output-as=xml --resource=httpd-bundle"),
Test("Basic text output with inactive resources, filtered by bundled IP address resource",
"crm_mon -1 -r --resource=httpd-bundle-ip-192.168.122.131"),
ValidatingTest("Output filtered by bundled IP address resource",
"crm_mon --output-as=xml --resource=httpd-bundle-ip-192.168.122.132"),
Test("Basic text output with inactive resources, filtered by bundled container",
"crm_mon -1 -r --resource=httpd-bundle-docker-1"),
ValidatingTest("Output filtered by bundled container",
"crm_mon --output-as=xml --resource=httpd-bundle-docker-2"),
Test("Basic text output with inactive resources, filtered by bundle connection",
"crm_mon -1 -r --resource=httpd-bundle-0"),
ValidatingTest("Output filtered by bundle connection",
"crm_mon --output-as=xml --resource=httpd-bundle-0"),
Test("Basic text output with inactive resources, filtered by bundled primitive resource",
"crm_mon -1 -r --resource=httpd"),
ValidatingTest("Output filtered by bundled primitive resource",
"crm_mon --output-as=xml --resource=httpd"),
Test("Complete text output, filtered by clone name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-clone-group"),
ValidatingTest("Output, filtered by clone name in cloned group",
"crm_mon --output-as=xml --resource=mysql-clone-group"),
Test("Complete text output, filtered by group name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-group"),
ValidatingTest("Output, filtered by group name in cloned group",
"crm_mon --output-as=xml --resource=mysql-group"),
Test("Complete text output, filtered by exact group instance name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-group:1"),
ValidatingTest("Output, filtered by exact group instance name in cloned group",
"crm_mon --output-as=xml --resource=mysql-group:1"),
Test("Complete text output, filtered by primitive name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-proxy"),
ValidatingTest("Output, filtered by primitive name in cloned group",
"crm_mon --output-as=xml --resource=mysql-proxy"),
Test("Complete text output, filtered by exact primitive instance name in cloned group",
"crm_mon -1 --include=all --show-detail --resource=mysql-proxy:1"),
ValidatingTest("Output, filtered by exact primitive instance name in cloned group",
"crm_mon --output-as=xml --resource=mysql-proxy:1"),
]
partial_tests = [
Test("Output of partially active resources", "crm_mon -1 --show-detail"),
ValidatingTest("Output of partially active resources", "crm_mon --output-as=xml"),
Test("Output of partially active resources, with inactive resources",
"crm_mon -1 -r --show-detail"),
# XML already includes inactive resources
Test("Complete brief text output, with inactive resources",
"crm_mon -1 -r --include=all --brief --show-detail"),
# XML does not have a brief output option
Test("Text output of partially active group", "crm_mon -1 --resource=partially-active-group"),
Test("Text output of partially active group, with inactive resources",
"crm_mon -1 --resource=partially-active-group -r"),
Test("Text output of active member of partially active group",
"crm_mon -1 --resource=dummy-1"),
Test("Text output of inactive member of partially active group",
"crm_mon -1 --resource=dummy-2 --show-detail"),
Test("Complete brief text output grouped by node, with inactive resources",
"crm_mon -1 -r --include=all --group-by-node --brief --show-detail"),
Test("Text output of partially active resources, with inactive resources, filtered by node",
"crm_mon -1 -r --node=cluster01"),
ValidatingTest("Output of partially active resources, filtered by node",
"crm_mon --output-as=xml --node=cluster01"),
]
unmanaged_tests = [
make_test_group("Output of active unmanaged resource on offline node",
"crm_mon -1"),
Test("Brief text output of active unmanaged resource on offline node",
"crm_mon -1 --brief"),
Test("Brief text output of active unmanaged resource on offline node, grouped by node",
"crm_mon -1 --brief --group-by-node"),
]
maint1_tests = [
make_test_group("Output of all resources with maintenance-mode enabled",
"crm_mon -1 -r",
setup="crm_attribute -n maintenance-mode -v true",
teardown="crm_attribute -n maintenance-mode -v false"),
make_test_group("Output of all resources with maintenance enabled for a node",
"crm_mon -1 -r",
setup="crm_attribute -n maintenance -N cluster02 -v true",
teardown="crm_attribute -n maintenance -N cluster02 -v false"),
]
maint2_tests = [
# The fence resource is excluded, for comparison
make_test_group("Output of all resources with maintenance meta attribute true",
"crm_mon -1 -r"),
]
t180_tests = [
Test("Text output of guest node's container on different node from its remote resource",
"crm_mon -1"),
Test("Complete text output of guest node's container on different node from its remote resource",
"crm_mon -1 --show-detail"),
]
return [
TestGroup(basic_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon.xml"}),
Test("Check that CIB_file=\"-\" works", "crm_mon -1",
env={"CIB_file": "-"},
stdin=pathlib.Path(f"{cts_cli_data}/crm_mon.xml")),
TestGroup(partial_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-partial.xml"}),
TestGroup(unmanaged_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-unmanaged.xml"}),
TestGroup(maint1_tests,
cib_gen=partial(copy_existing_cib, f"{cts_cli_data}/crm_mon.xml")),
TestGroup(maint2_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-rsc-maint.xml"}),
TestGroup(t180_tests,
env={"CIB_file": f"{cts_cli_data}/crm_mon-T180.xml"}),
]
class AclsRegressionTest(RegressionTest):
"""A class for testing access control lists."""
@property
def name(self):
"""Return the name of this regression test."""
return "acls"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
acl_cib = """
"""
basic_tests = [
Test("Configure some ACLs", "cibadmin -M -o acls -p",
update_cib=True, stdin=acl_cib),
Test("Enable ACLs", "crm_attribute -n enable-acl -v true",
update_cib=True),
Test("Set cluster option", "crm_attribute -n no-quorum-policy -v ignore",
update_cib=True),
Test("New ACL role",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("New ACL target",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Another ACL role",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Another ACL target",
"""cibadmin --create -o acls --xml-text ''""",
update_cib=True),
Test("Updated ACL",
"""cibadmin --replace -o acls --xml-text ''""",
update_cib=True),
]
no_acl_tests = [
Test("unknownguy: Query configuration", "cibadmin -Q",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("unknownguy: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
deny_cib_tests = [
Test("l33t-haxor: Query configuration",
"cibadmin -Q",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
observer_tests = [
Test("niceguy: Query configuration", "cibadmin -Q"),
Test("niceguy: Set enable-acl",
"crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("niceguy: Set stonith-enabled",
"crm_attribute -n stonith-enabled -v false",
update_cib=True),
Test("niceguy: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("root: Query configuration", "cibadmin -Q",
env={"CIB_user": "root"}),
Test("root: Set stonith-enabled", "crm_attribute -n stonith-enabled -v true",
update_cib=True, env={"CIB_user": "root"}),
Test("root: Create a resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True, env={"CIB_user": "root"}),
# For use with later tests
Test("root: Create another resource (with description)",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True, env={"CIB_user": "root"}),
]
deny_cib_2_tests = [
Test("l33t-haxor: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Query a resource meta attribute",
"crm_resource -r dummy --meta -g target-role",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
Test("l33t-haxor: Remove a resource meta attribute",
"crm_resource -r dummy --meta -d target-role",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
]
observer_2_tests = [
Test("niceguy: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Stopped",
update_cib=True),
Test("niceguy: Query a resource meta attribute",
"crm_resource -r dummy --meta -g target-role",
update_cib=True),
Test("niceguy: Remove a resource meta attribute",
"crm_resource -r dummy --meta -d target-role",
update_cib=True),
Test("niceguy: Create a resource meta attribute",
"crm_resource -r dummy --meta -p target-role -v Started",
update_cib=True),
]
read_meta_tests = [
Test("badidea: Query configuration - implied deny", "cibadmin -Q"),
]
deny_cib_3_tests = [
Test("betteridea: Query configuration - explicit deny", "cibadmin -Q"),
]
replace_tests = [
TestGroup([
AclTest("niceguy: Replace - remove acls",
"cibadmin --replace -p",
setup="cibadmin --delete --xml-text ''",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - create resource",
"cibadmin --replace -p",
setup="""cibadmin -C -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - modify attribute (deny)",
"cibadmin --replace -p",
setup="crm_attribute -n enable-acl -v false",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - delete attribute (deny)",
"cibadmin --replace -p",
setup="""cibadmin --replace --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("niceguy: Replace - create attribute (deny)",
"cibadmin --replace -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
], env={"CIB_user": "niceguy"}),
# admin role
TestGroup([
AclTest("bob: Replace - create attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("bob: Replace - modify attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("bob: Replace - delete attribute (direct allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
], env={"CIB_user": "bob"}),
# super_user role
TestGroup([
AclTest("joe: Replace - create attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("joe: Replace - modify attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("joe: Replace - delete attribute (inherited allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
], env={"CIB_user": "joe"}),
# rsc_writer role
TestGroup([
AclTest("mike: Replace - create attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("mike: Replace - modify attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''"""),
AclTest("mike: Replace - delete attribute (allow overrides deny)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''"""),
# Create an additional resource for deny-overrides-allow testing
AclTest("mike: Create another resource",
"""cibadmin -C -o resources --xml-text ''""",
update_cib=True),
], env={"CIB_user": "mike"}),
# rsc_denied role
TestGroup([
AclTest("chris: Replace - create attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("chris: Replace - modify attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --modify --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
AclTest("chris: Replace - delete attribute (deny overrides allow)",
"cibadmin --replace -o resources -p",
setup="""cibadmin --replace -o resources --xml-text ''""",
expected_rc=ExitStatus.INSUFFICIENT_PRIV),
], env={"CIB_user": "chris"}),
]
loop_tests = [
# no ACL
TestGroup(no_acl_tests, env={"CIB_user": "unknownguy"}),
# deny /cib permission
TestGroup(deny_cib_tests, env={"CIB_user": "l33t-haxor"}),
# observer role
TestGroup(observer_tests, env={"CIB_user": "niceguy"}),
# deny /cib permission
TestGroup(deny_cib_2_tests, env={"CIB_user": "l33t-haxor"}),
# observer role
TestGroup(observer_2_tests, env={"CIB_user": "niceguy"}),
# read //meta_attributes
TestGroup(read_meta_tests, env={"CIB_user": "badidea"}),
# deny /cib, read //meta_attributes
TestGroup(deny_cib_3_tests, env={"CIB_user": "betteridea"}),
] + replace_tests
return [
ShadowTestGroup(basic_tests + [
TestGroup(loop_tests,
env={"PCMK_trace_functions": "pcmk__check_acl,pcmk__apply_creation_acl"})]),
]
class ValidityRegressionTest(RegressionTest):
"""A class for testing CIB validity."""
@property
def name(self):
"""Return the name of this regression test."""
return "validity"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
# sanitize_output() strips out validate-with, so there's no point in
# outputting the CIB after tests that modify it
Test("Try to set unrecognized validate-with",
"cibadmin -M --xml-text ''",
expected_rc=ExitStatus.CONFIG),
Test("Try to remove validate-with attribute",
"cibadmin -R -p",
stdin=StdinCmd("""cibadmin -Q | sed 's#validate-with="[^"]*"##'"""),
expected_rc=ExitStatus.CONFIG),
Test("Try to use rsc_order first-action value disallowed by schema",
"cibadmin -M -o constraints --xml-text ''",
expected_rc=ExitStatus.CONFIG, update_cib=True),
Test("Try to use configuration legal only with schema after configured one",
"cibadmin -C -o configuration --xml-text ''",
expected_rc=ExitStatus.CONFIG, update_cib=True),
Test("Disable schema validation",
"cibadmin -M --xml-text ''",
expected_rc=ExitStatus.OK),
Test("Set invalid rsc_order first-action value (schema validation disabled)",
"cibadmin -M -o constraints --xml-text ''",
expected_rc=ExitStatus.OK, update_cib=True),
Test("Run crm_simulate with invalid rsc_order first-action "
"(schema validation disabled)",
"crm_simulate -SL",
expected_rc=ExitStatus.OK),
]
basic_tests_setup = [
"""cibadmin -C -o resources --xml-text ''""",
"""cibadmin -C -o resources --xml-text ''""",
"""cibadmin -C -o constraints --xml-text ''""",
]
return [
ShadowTestGroup(basic_tests, validate_with="pacemaker-1.2",
setup=basic_tests_setup,
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema,invert_action"}),
]
class UpgradeRegressionTest(RegressionTest):
"""A class for testing upgrading the CIB."""
@property
def name(self):
"""Return the name of this regression test."""
return "upgrade"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
resource_cib = """
"""
basic_tests = [
Test("Set stonith-enabled=false", "crm_attribute -n stonith-enabled -v false",
update_cib=True),
Test("Configure the initial resource", "cibadmin -M -o resources -p",
update_cib=True, stdin=resource_cib),
Test("Upgrade to latest CIB schema (trigger 2.10.xsl + the wrapping)",
"cibadmin --upgrade --force -V -V",
update_cib=True),
Test("Query a resource instance attribute (shall survive)",
"crm_resource -r mySmartFuse -g requires",
update_cib=True),
]
return [
ShadowTestGroup(basic_tests, validate_with="pacemaker-2.10",
env={"PCMK_trace_functions": "apply_upgrade,pcmk__update_schema"})
]
class RulesRegressionTest(RegressionTest):
"""A class for testing support for CIB rules."""
@property
def name(self):
"""Return the name of this regression test."""
return "rules"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
tomorrow = datetime.now() + timedelta(days=1)
rule_cib = f""""""
usage_tests = [
make_test_group("crm_rule given no arguments", "crm_rule",
expected_rc=ExitStatus.USAGE),
make_test_group("crm_rule given no rule to check", "crm_rule -c",
expected_rc=ExitStatus.USAGE),
make_test_group("crm_rule given invalid input XML",
"crm_rule -c -r blahblah -X invalidxml",
expected_rc=ExitStatus.DATAERR),
make_test_group("crm_rule given invalid input XML on stdin",
"crm_rule -c -r blahblah -X -",
stdin=StdinCmd("echo invalidxml"),
expected_rc=ExitStatus.DATAERR),
]
basic_tests = [
make_test_group("Try to check a rule that doesn't exist",
"crm_rule -c -r blahblah",
expected_rc=ExitStatus.NOSUCH),
make_test_group("Try to check a rule that has too many date_expressions",
"crm_rule -c -r cli-rule-too-many-date-expressions",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
make_test_group("Verify basic rule is expired",
"crm_rule -c -r cli-prefer-rule-dummy-expired",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify basic rule worked in the past",
"crm_rule -c -r cli-prefer-rule-dummy-expired -d 20180101"),
make_test_group("Verify basic rule is not yet in effect",
"crm_rule -c -r cli-prefer-rule-dummy-not-yet",
expected_rc=ExitStatus.NOT_YET_IN_EFFECT),
make_test_group("Verify date_spec rule with years has expired",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify multiple rules at once",
"crm_rule -c -r cli-prefer-rule-dummy-not-yet -r cli-prefer-rule-dummy-date_spec-only-years",
expected_rc=ExitStatus.EXPIRED),
make_test_group("Verify date_spec rule with years is in effect",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-only-years -d 20190201"),
make_test_group("Try to check a rule whose date_spec does not contain years=",
"crm_rule -c -r cli-prefer-rule-dummy-date_spec-without-years",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
make_test_group("Try to check a rule with no date_expression",
"crm_rule -c -r cli-no-date_expression-rule",
expected_rc=ExitStatus.UNIMPLEMENT_FEATURE),
]
return usage_tests + [
TestGroup(basic_tests, cib_gen=partial(write_cib, rule_cib))
]
class FeatureSetRegressionTest(RegressionTest):
"""A class for testing support for version-specific features."""
@property
def name(self):
"""Return the name of this regression test."""
return "feature_set"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
basic_tests = [
# Import the test CIB
Test("Import the test CIB",
f"cibadmin --replace --xml-file {cts_cli_data}/crm_mon-feature_set.xml",
update_cib=True),
Test("Complete text output, no mixed status",
"crm_mon -1 --show-detail"),
ValidatingTest("Output, no mixed status", "crm_mon --output-as=xml"),
# Modify the CIB to fake that the cluster has mixed versions
Test("Fake inconsistent feature set",
"crm_attribute --node=cluster02 --name=#feature-set --update=3.15.0 --lifetime=reboot",
update_cib=True),
Test("Complete text output, mixed status",
"crm_mon -1 --show-detail"),
ValidatingTest("Output, mixed status", "crm_mon --output-as=xml"),
]
return [
ShadowTestGroup(basic_tests),
]
# Tests that depend on resource agents and must be run in an installed
# environment
class AgentRegressionTest(RegressionTest):
"""A class for testing resource agents."""
@property
def name(self):
"""Return the name of this regression test."""
return "agents"
@property
def tests(self):
"""A list of Test instances to be run as part of this regression test."""
return [
make_test_group("Validate a valid resource configuration",
"crm_resource --validate --class ocf --provider pacemaker --agent Dummy"),
# Make the Dummy configuration invalid (op_sleep can't be a generic string)
make_test_group("Validate an invalid resource configuration",
"crm_resource --validate --class ocf --provider pacemaker --agent Dummy",
expected_rc=ExitStatus.NOT_CONFIGURED,
env={"OCF_RESKEY_op_sleep": "asdf"}),
]
def build_options():
"""Handle command line arguments."""
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter,
description="Command line tool regression tests",
epilog=f"Default tests: {' '.join(default_tests)}\n"
"Other tests: agents (must be run in an installed environment)")
parser.add_argument("-j", "--jobs", metavar="JOBS", default=cpu_count() - 1, type=int,
help="The number of tests to run simultaneously")
parser.add_argument("-p", "--path", metavar="DIR", action="append",
help="Look for executables in DIR (may be specified multiple times)")
parser.add_argument("-r", "--run-only", metavar="TEST", choices=default_tests + ["tools"] + other_tests,
action="append",
help="Run only specified tests (may be specified multiple times)")
parser.add_argument("-s", "--save", action="store_true",
help="Save actual output as expected output")
parser.add_argument("-v", "--valgrind", action="store_true",
help="Run all commands under valgrind")
parser.add_argument("-V", "--verbose", action="store_true",
help="Display any differences from expected output")
args = parser.parse_args()
if args.path is None:
args.path = []
return args
def setup_environment(valgrind):
"""Set various environment variables needed for operation."""
if valgrind:
os.environ["G_SLICE"] = "always-malloc"
# Ensure all command output is in portable locale for comparison
os.environ["LC_ALL"] = "C"
# Log test errors to stderr
os.environ["PCMK_stderr"] = "1"
# Because we will change the value of PCMK_trace_functions and then reset it
# back to some initial value at various points, it's easiest to assume it is
# defined but empty by default
if "PCMK_trace_functions" not in os.environ:
os.environ["PCMK_trace_functions"] = ""
def path_prepend(p):
"""Add another directory to the front of $PATH."""
old = os.environ["PATH"]
os.environ["PATH"] = f"{p}:{old}"
def setup_path(opts_path):
"""Set the PATH environment variable appropriately for the tests."""
srcdir = os.path.dirname(test_home)
# Add any search paths given on the command line
for p in opts_path:
path_prepend(p)
if os.path.exists(f"{srcdir}/tools/crm_simulate"):
print(f"Using local binaries from: {srcdir}")
path_prepend(f"{srcdir}/tools")
for daemon in ["based", "controld", "fenced", "schedulerd"]:
path_prepend(f"{srcdir}/daemons/{daemon}")
print(f"Using local schemas from: {srcdir}/xml")
os.environ["PCMK_schema_directory"] = f"{srcdir}/xml"
else:
path_prepend(BuildOptions.DAEMON_DIR)
os.environ["PCMK_schema_directory"] = BuildOptions.SCHEMA_DIR
def _run_one(valgrind, r):
"""Run and return a TestGroup object."""
# See comments in run_regression_tests.
r.run(valgrind=valgrind)
return r
def run_regression_tests(regs, jobs, valgrind=False):
"""Run the given tests and return the modified objects."""
executed = []
with Pool(processes=jobs) as pool:
# What we really want to do here is:
# pool.map(lambda r: r.run(),regs)
#
# However, multiprocessing uses pickle somehow in its operation, and python
# doesn't want to pickle a lambda (nor a nested function within this one).
# Thus, we need to use the _run_one wrapper at the file level just to call
# run(). Further, if we don't return the modified object from that and then
# return the list of modified objects here, it looks like the rest of the
# program will use the originals, before this was ever run.
executed = pool.map(partial(_run_one, valgrind), regs)
return executed
def results(regs, save, verbose):
"""Print the output from each regression test, returning the number whose output differs."""
output_differs = 0
if verbose:
print("\n\nResults")
sys.stdout.flush()
for r in regs:
r.write()
if save:
dest = f"{test_home}/cli/regression.{r.name}.exp"
copyfile(r.results_file, dest)
r.diff(verbose)
if not r.identical:
output_differs += 1
return output_differs
def summary(regs, output_differs, verbose):
"""Print the summary output for the entire test run."""
test_failures = 0
test_successes = 0
for r in regs:
test_failures += r.failures
test_successes += r.successes
print("\n\nSummary")
sys.stdout.flush()
# First, print all the Passed/Failed lines from each Test run.
for r in regs:
print("\n".join(r.summary))
fmt = PluralFormatter()
# Then, print information specific to each result possibility. Basically,
# if there were failures then we print the output differences, leave the
# failed output files in place, and exit with an error. Otherwise, clean up
# anything that passed.
if test_failures > 0 and output_differs > 0:
print(fmt.format("{0} {0:plural,test} failed; see output in:",
test_failures))
for r in regs:
r.process_results(verbose)
return ExitStatus.ERROR
if test_failures > 0:
print(fmt.format("{0} {0:plural,test} failed", test_failures))
for r in regs:
r.process_results(verbose)
return ExitStatus.ERROR
if output_differs:
print(fmt.format("{0} {0:plural,test} passed but output was "
"unexpected; see output in:", test_successes))
for r in regs:
r.process_results(verbose)
return ExitStatus.DIGEST
print(fmt.format("{0} {0:plural,test} passed", test_successes))
for r in regs:
r.cleanup()
return ExitStatus.OK
regression_classes = [
AccessRenderRegressionTest,
DaemonsRegressionTest,
DatesRegressionTest,
ErrorCodeRegressionTest,
CibadminRegressionTest,
CrmAttributeRegressionTest,
CrmStandbyRegressionTest,
CrmResourceRegressionTest,
CrmTicketRegressionTest,
CrmadminRegressionTest,
CrmShadowRegressionTest,
CrmVerifyRegressionTest,
CrmSimulateRegressionTest,
CrmDiffRegressionTest,
CrmMonRegressionTest,
AclsRegressionTest,
ValidityRegressionTest,
UpgradeRegressionTest,
RulesRegressionTest,
FeatureSetRegressionTest,
AgentRegressionTest,
]
def main():
"""Run command line regression tests as specified by arguments."""
opts = build_options()
setup_environment(opts.valgrind)
setup_path(opts.path)
# Filter the list of all regression test classes to include only those that
# were requested on the command line. If empty, this defaults to default_tests.
if not opts.run_only:
opts.run_only = default_tests
if opts.run_only == ["tools"]:
opts.run_only = tools_tests
regs = []
for cls in regression_classes:
obj = cls()
if obj.name in opts.run_only:
regs.append(obj)
regs = run_regression_tests(regs, max(1, opts.jobs), valgrind=opts.valgrind)
output_differs = results(regs, opts.save, opts.verbose)
rc = summary(regs, output_differs, opts.verbose)
sys.exit(rc)
if __name__ == "__main__":
main()
# vim: set filetype=python:
diff --git a/daemons/controld/controld_te_actions.c b/daemons/controld/controld_te_actions.c
index 1df83e82b0..fba2463d1e 100644
--- a/daemons/controld/controld_te_actions.c
+++ b/daemons/controld/controld_te_actions.c
@@ -1,760 +1,761 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 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 // lrmd_event_data_t, lrmd_free_event()
#include
#include
#include
#include
static GHashTable *te_targets = NULL;
void send_rsc_command(pcmk__graph_action_t *action);
static void te_update_job_count(pcmk__graph_action_t *action, int offset);
static void
te_start_action_timer(const pcmk__graph_t *graph, pcmk__graph_action_t *action)
{
action->timer = pcmk__create_timer(action->timeout + graph->network_delay,
action_timer_callback, action);
pcmk__assert(action->timer != 0);
}
/*!
* \internal
* \brief Execute a graph pseudo-action
*
* \param[in,out] graph Transition graph being executed
* \param[in,out] pseudo Pseudo-action to execute
*
* \return Standard Pacemaker return code
*/
static int
execute_pseudo_action(pcmk__graph_t *graph, pcmk__graph_action_t *pseudo)
{
const char *task = crm_element_value(pseudo->xml, PCMK_XA_OPERATION);
/* send to peers as well? */
if (pcmk__str_eq(task, PCMK_ACTION_MAINTENANCE_NODES, pcmk__str_casei)) {
GHashTableIter iter;
pcmk__node_status_t *node = NULL;
g_hash_table_iter_init(&iter, pcmk__peer_cache);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) {
xmlNode *cmd = NULL;
if (controld_is_local_node(node->name)) {
continue;
}
cmd = pcmk__new_request(pcmk_ipc_controld, CRM_SYSTEM_TENGINE,
node->name, CRM_SYSTEM_CRMD, task,
pseudo->xml);
pcmk__cluster_send_message(node, pcmk_ipc_controld, cmd);
pcmk__xml_free(cmd);
}
remote_ra_process_maintenance_nodes(pseudo->xml);
} else {
/* Check action for Pacemaker Remote node side effects */
remote_ra_process_pseudo(pseudo->xml);
}
crm_debug("Pseudo-action %d (%s) fired and confirmed", pseudo->id,
crm_element_value(pseudo->xml, PCMK__XA_OPERATION_KEY));
te_action_confirmed(pseudo, graph);
return pcmk_rc_ok;
}
static int
get_target_rc(pcmk__graph_action_t *action)
{
int exit_status;
pcmk__scan_min_int(crm_meta_value(action->params, PCMK__META_OP_TARGET_RC),
&exit_status, 0);
return exit_status;
}
/*!
* \internal
* \brief Execute a cluster action from a transition graph
*
* \param[in,out] graph Transition graph being executed
* \param[in,out] action Cluster action to execute
*
* \return Standard Pacemaker return code
*/
static int
execute_cluster_action(pcmk__graph_t *graph, pcmk__graph_action_t *action)
{
char *counter = NULL;
xmlNode *cmd = NULL;
gboolean is_local = FALSE;
const char *id = NULL;
const char *task = NULL;
const char *value = NULL;
const char *on_node = NULL;
const char *router_node = NULL;
gboolean rc = TRUE;
gboolean no_wait = FALSE;
const pcmk__node_status_t *node = NULL;
id = pcmk__xe_id(action->xml);
CRM_CHECK(!pcmk__str_empty(id), return EPROTO);
task = crm_element_value(action->xml, PCMK_XA_OPERATION);
CRM_CHECK(!pcmk__str_empty(task), return EPROTO);
on_node = crm_element_value(action->xml, PCMK__META_ON_NODE);
CRM_CHECK(!pcmk__str_empty(on_node), return pcmk_rc_node_unknown);
router_node = crm_element_value(action->xml, PCMK__XA_ROUTER_NODE);
if (router_node == NULL) {
router_node = on_node;
if (pcmk__str_eq(task, PCMK_ACTION_LRM_DELETE, pcmk__str_none)) {
const char *mode = crm_element_value(action->xml, PCMK__XA_MODE);
if (pcmk__str_eq(mode, PCMK__VALUE_CIB, pcmk__str_none)) {
router_node = controld_globals.cluster->priv->node_name;
}
}
}
if (controld_is_local_node(router_node)) {
is_local = TRUE;
}
value = crm_meta_value(action->params, PCMK__META_OP_NO_WAIT);
if (crm_is_true(value)) {
no_wait = TRUE;
}
crm_info("Handling controller request '%s' (%s on %s)%s%s",
id, task, on_node, (is_local? " locally" : ""),
(no_wait? " without waiting" : ""));
if (is_local
&& pcmk__str_eq(task, PCMK_ACTION_DO_SHUTDOWN, pcmk__str_none)) {
/* defer until everything else completes */
crm_info("Controller request '%s' is a local shutdown", id);
graph->completion_action = pcmk__graph_shutdown;
graph->abort_reason = "local shutdown";
te_action_confirmed(action, graph);
return pcmk_rc_ok;
} else if (pcmk__str_eq(task, PCMK_ACTION_DO_SHUTDOWN, pcmk__str_none)) {
pcmk__node_status_t *peer =
pcmk__get_node(0, router_node, NULL,
pcmk__node_search_cluster_member);
pcmk__update_peer_expected(__func__, peer, CRMD_JOINSTATE_DOWN);
}
cmd = pcmk__new_request(pcmk_ipc_controld, CRM_SYSTEM_TENGINE, router_node,
CRM_SYSTEM_CRMD, task, action->xml);
counter = pcmk__transition_key(controld_globals.transition_graph->id,
action->id, get_target_rc(action),
controld_globals.te_uuid);
crm_xml_add(cmd, PCMK__XA_TRANSITION_KEY, counter);
node = pcmk__get_node(0, router_node, NULL,
pcmk__node_search_cluster_member);
rc = pcmk__cluster_send_message(node, pcmk_ipc_controld, cmd);
free(counter);
pcmk__xml_free(cmd);
if (rc == FALSE) {
crm_err("Action %d failed: send", action->id);
return ECOMM;
} else if (no_wait) {
te_action_confirmed(action, graph);
} else {
if (action->timeout <= 0) {
crm_err("Action %d: %s on %s had an invalid timeout (%dms). Using %ums instead",
action->id, task, on_node, action->timeout, graph->network_delay);
action->timeout = (int) graph->network_delay;
}
te_start_action_timer(graph, action);
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Synthesize an executor event for a resource action timeout
*
* \param[in] action Resource action that timed out
* \param[in] target_rc Expected result of action that timed out
*
* Synthesize an executor event for a resource action timeout. (If the executor
* gets a timeout while waiting for a resource action to complete, that will be
* reported via the usual callback. This timeout means we didn't hear from the
* executor itself or the controller that relayed the action to the executor.)
*
* \return Newly created executor event for result of \p action
* \note The caller is responsible for freeing the return value using
* lrmd_free_event().
*/
static lrmd_event_data_t *
synthesize_timeout_event(const pcmk__graph_action_t *action, int target_rc)
{
lrmd_event_data_t *op = NULL;
const char *target = crm_element_value(action->xml, PCMK__META_ON_NODE);
const char *reason = NULL;
char *dynamic_reason = NULL;
if (pcmk__str_eq(target, pcmk__cluster_local_node_name(),
pcmk__str_casei)) {
reason = "Local executor did not return result in time";
} else {
const char *router_node = NULL;
router_node = crm_element_value(action->xml, PCMK__XA_ROUTER_NODE);
if (router_node == NULL) {
router_node = target;
}
dynamic_reason = crm_strdup_printf("Controller on %s did not return "
"result in time", router_node);
reason = dynamic_reason;
}
op = pcmk__event_from_graph_action(NULL, action, PCMK_EXEC_TIMEOUT,
PCMK_OCF_UNKNOWN_ERROR, reason);
op->call_id = -1;
op->user_data = pcmk__transition_key(controld_globals.transition_graph->id,
action->id, target_rc,
controld_globals.te_uuid);
free(dynamic_reason);
return op;
}
static void
controld_record_action_event(pcmk__graph_action_t *action,
lrmd_event_data_t *op)
{
cib_t *cib_conn = controld_globals.cib_conn;
xmlNode *state = NULL;
xmlNode *rsc = NULL;
xmlNode *action_rsc = NULL;
int rc = pcmk_ok;
const char *rsc_id = NULL;
const char *target = crm_element_value(action->xml, PCMK__META_ON_NODE);
const char *task_uuid = crm_element_value(action->xml,
PCMK__XA_OPERATION_KEY);
const char *target_uuid = crm_element_value(action->xml,
PCMK__META_ON_NODE_UUID);
int target_rc = get_target_rc(action);
action_rsc = pcmk__xe_first_child(action->xml, PCMK_XE_PRIMITIVE, NULL,
NULL);
if (action_rsc == NULL) {
return;
}
rsc_id = pcmk__xe_id(action_rsc);
CRM_CHECK(rsc_id != NULL,
crm_log_xml_err(action->xml, "Bad:action"); return);
/*
update the CIB
*/
state = pcmk__xe_create(NULL, PCMK__XE_NODE_STATE);
crm_xml_add(state, PCMK_XA_ID, target_uuid);
crm_xml_add(state, PCMK_XA_UNAME, target);
rsc = pcmk__xe_create(state, PCMK__XE_LRM);
crm_xml_add(rsc, PCMK_XA_ID, target_uuid);
rsc = pcmk__xe_create(rsc, PCMK__XE_LRM_RESOURCES);
rsc = pcmk__xe_create(rsc, PCMK__XE_LRM_RESOURCE);
crm_xml_add(rsc, PCMK_XA_ID, rsc_id);
-
- crm_copy_xml_element(action_rsc, rsc, PCMK_XA_TYPE);
- crm_copy_xml_element(action_rsc, rsc, PCMK_XA_CLASS);
- crm_copy_xml_element(action_rsc, rsc, PCMK_XA_PROVIDER);
+ crm_xml_add(rsc, PCMK_XA_TYPE, crm_element_value(action_rsc, PCMK_XA_TYPE));
+ crm_xml_add(rsc, PCMK_XA_CLASS,
+ crm_element_value(action_rsc, PCMK_XA_CLASS));
+ crm_xml_add(rsc, PCMK_XA_PROVIDER,
+ crm_element_value(action_rsc, PCMK_XA_PROVIDER));
pcmk__create_history_xml(rsc, op, CRM_FEATURE_SET, target_rc, target,
__func__);
rc = cib_conn->cmds->modify(cib_conn, PCMK_XE_STATUS, state, cib_none);
fsa_register_cib_callback(rc, NULL, cib_action_updated);
pcmk__xml_free(state);
crm_trace("Sent CIB update (call ID %d) for synthesized event of action %d (%s on %s)",
rc, action->id, task_uuid, target);
pcmk__set_graph_action_flags(action, pcmk__graph_action_sent_update);
}
void
controld_record_action_timeout(pcmk__graph_action_t *action)
{
lrmd_event_data_t *op = NULL;
const char *target = crm_element_value(action->xml, PCMK__META_ON_NODE);
const char *task_uuid = crm_element_value(action->xml,
PCMK__XA_OPERATION_KEY);
int target_rc = get_target_rc(action);
crm_warn("%s %d: %s on %s timed out",
action->xml->name, action->id, task_uuid, target);
op = synthesize_timeout_event(action, target_rc);
controld_record_action_event(action, op);
lrmd_free_event(op);
}
/*!
* \internal
* \brief Execute a resource action from a transition graph
*
* \param[in,out] graph Transition graph being executed
* \param[in,out] action Resource action to execute
*
* \return Standard Pacemaker return code
*/
static int
execute_rsc_action(pcmk__graph_t *graph, pcmk__graph_action_t *action)
{
/* never overwrite stop actions in the CIB with
* anything other than completed results
*
* Writing pending stops makes it look like the
* resource is running again
*/
xmlNode *cmd = NULL;
xmlNode *rsc_op = NULL;
gboolean rc = TRUE;
gboolean no_wait = FALSE;
gboolean is_local = FALSE;
char *counter = NULL;
const char *task = NULL;
const char *value = NULL;
const char *on_node = NULL;
const char *router_node = NULL;
const char *task_uuid = NULL;
pcmk__assert((action != NULL) && (action->xml != NULL));
pcmk__clear_graph_action_flags(action, pcmk__graph_action_executed);
on_node = crm_element_value(action->xml, PCMK__META_ON_NODE);
CRM_CHECK(!pcmk__str_empty(on_node), return pcmk_rc_node_unknown);
rsc_op = action->xml;
task = crm_element_value(rsc_op, PCMK_XA_OPERATION);
task_uuid = crm_element_value(action->xml, PCMK__XA_OPERATION_KEY);
router_node = crm_element_value(rsc_op, PCMK__XA_ROUTER_NODE);
if (!router_node) {
router_node = on_node;
}
counter = pcmk__transition_key(controld_globals.transition_graph->id,
action->id, get_target_rc(action),
controld_globals.te_uuid);
crm_xml_add(rsc_op, PCMK__XA_TRANSITION_KEY, counter);
if (controld_is_local_node(router_node)) {
is_local = TRUE;
}
value = crm_meta_value(action->params, PCMK__META_OP_NO_WAIT);
if (crm_is_true(value)) {
no_wait = TRUE;
}
cmd = pcmk__new_request(pcmk_ipc_controld, CRM_SYSTEM_TENGINE, router_node,
CRM_SYSTEM_LRMD, CRM_OP_INVOKE_LRM, rsc_op);
if (is_local) {
/* shortcut local resource commands */
ha_msg_input_t data = {
.msg = cmd,
.xml = rsc_op,
};
fsa_data_t msg = {
.id = 0,
.data = &data,
.data_type = fsa_dt_ha_msg,
.fsa_input = I_NULL,
.fsa_cause = C_FSA_INTERNAL,
.actions = A_LRM_INVOKE,
.origin = __func__,
};
do_lrm_invoke(A_LRM_INVOKE, C_FSA_INTERNAL, controld_globals.fsa_state,
I_NULL, &msg);
} else {
const pcmk__node_status_t *node =
pcmk__get_node(0, router_node, NULL,
pcmk__node_search_cluster_member);
crm_notice("Asking %s to execute %s on %s%s "
QB_XS " transition %s action %d",
router_node, task_uuid, on_node,
(no_wait? " without waiting" : ""), counter, action->id);
rc = pcmk__cluster_send_message(node, pcmk_ipc_execd, cmd);
}
free(counter);
pcmk__xml_free(cmd);
pcmk__set_graph_action_flags(action, pcmk__graph_action_executed);
if (rc == FALSE) {
crm_err("Action %d failed: send", action->id);
return ECOMM;
} else if (no_wait) {
/* Just mark confirmed. Don't bump the job count only to immediately
* decrement it.
*/
crm_info("Action %d confirmed - no wait", action->id);
pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed);
pcmk__update_graph(controld_globals.transition_graph, action);
trigger_graph();
} else if (pcmk_is_set(action->flags, pcmk__graph_action_confirmed)) {
crm_debug("Action %d: %s %s on %s(timeout %dms) was already confirmed.",
action->id, task, task_uuid, on_node, action->timeout);
} else {
if (action->timeout <= 0) {
crm_err("Action %d: %s %s on %s had an invalid timeout (%dms). Using %ums instead",
action->id, task, task_uuid, on_node, action->timeout, graph->network_delay);
action->timeout = (int) graph->network_delay;
}
te_update_job_count(action, 1);
te_start_action_timer(graph, action);
}
return pcmk_rc_ok;
}
struct te_peer_s
{
char *name;
int jobs;
int migrate_jobs;
};
static void te_peer_free(gpointer p)
{
struct te_peer_s *peer = p;
free(peer->name);
free(peer);
}
void te_reset_job_counts(void)
{
GHashTableIter iter;
struct te_peer_s *peer = NULL;
if(te_targets == NULL) {
te_targets = pcmk__strkey_table(NULL, te_peer_free);
}
g_hash_table_iter_init(&iter, te_targets);
while (g_hash_table_iter_next(&iter, NULL, (gpointer *) & peer)) {
peer->jobs = 0;
peer->migrate_jobs = 0;
}
}
static void
te_update_job_count_on(const char *target, int offset, bool migrate)
{
struct te_peer_s *r = NULL;
if(target == NULL || te_targets == NULL) {
return;
}
r = g_hash_table_lookup(te_targets, target);
if(r == NULL) {
r = pcmk__assert_alloc(1, sizeof(struct te_peer_s));
r->name = pcmk__str_copy(target);
g_hash_table_insert(te_targets, r->name, r);
}
r->jobs += offset;
if(migrate) {
r->migrate_jobs += offset;
}
crm_trace("jobs[%s] = %d", target, r->jobs);
}
static void
te_update_job_count(pcmk__graph_action_t *action, int offset)
{
const char *task = crm_element_value(action->xml, PCMK_XA_OPERATION);
const char *target = crm_element_value(action->xml, PCMK__META_ON_NODE);
if ((action->type != pcmk__rsc_graph_action) || (target == NULL)) {
/* No limit on these */
return;
}
/* if we have a router node, this means the action is performing
* on a remote node. For now, we count all actions occurring on a
* remote node against the job list on the cluster node hosting
* the connection resources */
target = crm_element_value(action->xml, PCMK__XA_ROUTER_NODE);
if ((target == NULL)
&& pcmk__strcase_any_of(task, PCMK_ACTION_MIGRATE_TO,
PCMK_ACTION_MIGRATE_FROM, NULL)) {
const char *t1 = crm_meta_value(action->params,
PCMK__META_MIGRATE_SOURCE);
const char *t2 = crm_meta_value(action->params,
PCMK__META_MIGRATE_TARGET);
te_update_job_count_on(t1, offset, TRUE);
te_update_job_count_on(t2, offset, TRUE);
return;
} else if (target == NULL) {
target = crm_element_value(action->xml, PCMK__META_ON_NODE);
}
te_update_job_count_on(target, offset, FALSE);
}
/*!
* \internal
* \brief Check whether a graph action is allowed to be executed on a node
*
* \param[in] graph Transition graph being executed
* \param[in] action Graph action being executed
* \param[in] target Name of node where action should be executed
*
* \return true if action is allowed, otherwise false
*/
static bool
allowed_on_node(const pcmk__graph_t *graph, const pcmk__graph_action_t *action,
const char *target)
{
int limit = 0;
struct te_peer_s *r = NULL;
const char *task = crm_element_value(action->xml, PCMK_XA_OPERATION);
const char *id = crm_element_value(action->xml, PCMK__XA_OPERATION_KEY);
if(target == NULL) {
/* No limit on these */
return true;
} else if(te_targets == NULL) {
return false;
}
r = g_hash_table_lookup(te_targets, target);
limit = throttle_get_job_limit(target);
if(r == NULL) {
r = pcmk__assert_alloc(1, sizeof(struct te_peer_s));
r->name = pcmk__str_copy(target);
g_hash_table_insert(te_targets, r->name, r);
}
if(limit <= r->jobs) {
crm_trace("Peer %s is over their job limit of %d (%d): deferring %s",
target, limit, r->jobs, id);
return false;
} else if(graph->migration_limit > 0 && r->migrate_jobs >= graph->migration_limit) {
if (pcmk__strcase_any_of(task, PCMK_ACTION_MIGRATE_TO,
PCMK_ACTION_MIGRATE_FROM, NULL)) {
crm_trace("Peer %s is over their migration job limit of %d (%d): deferring %s",
target, graph->migration_limit, r->migrate_jobs, id);
return false;
}
}
crm_trace("Peer %s has not hit their limit yet. current jobs = %d limit= %d limit", target, r->jobs, limit);
return true;
}
/*!
* \internal
* \brief Check whether a graph action is allowed to be executed
*
* \param[in] graph Transition graph being executed
* \param[in] action Graph action being executed
*
* \return true if action is allowed, otherwise false
*/
static bool
graph_action_allowed(pcmk__graph_t *graph, pcmk__graph_action_t *action)
{
const char *target = NULL;
const char *task = crm_element_value(action->xml, PCMK_XA_OPERATION);
if (action->type != pcmk__rsc_graph_action) {
/* No limit on these */
return true;
}
/* if we have a router node, this means the action is performing
* on a remote node. For now, we count all actions occurring on a
* remote node against the job list on the cluster node hosting
* the connection resources */
target = crm_element_value(action->xml, PCMK__XA_ROUTER_NODE);
if ((target == NULL)
&& pcmk__strcase_any_of(task, PCMK_ACTION_MIGRATE_TO,
PCMK_ACTION_MIGRATE_FROM, NULL)) {
target = crm_meta_value(action->params, PCMK__META_MIGRATE_SOURCE);
if (!allowed_on_node(graph, action, target)) {
return false;
}
target = crm_meta_value(action->params, PCMK__META_MIGRATE_TARGET);
} else if (target == NULL) {
target = crm_element_value(action->xml, PCMK__META_ON_NODE);
}
return allowed_on_node(graph, action, target);
}
/*!
* \brief Confirm a graph action (and optionally update graph)
*
* \param[in,out] action Action to confirm
* \param[in,out] graph Update and trigger this graph (if non-NULL)
*/
void
te_action_confirmed(pcmk__graph_action_t *action, pcmk__graph_t *graph)
{
if (!pcmk_is_set(action->flags, pcmk__graph_action_confirmed)) {
if ((action->type == pcmk__rsc_graph_action)
&& (crm_element_value(action->xml, PCMK__META_ON_NODE) != NULL)) {
te_update_job_count(action, -1);
}
pcmk__set_graph_action_flags(action, pcmk__graph_action_confirmed);
}
if (graph) {
pcmk__update_graph(graph, action);
trigger_graph();
}
}
static pcmk__graph_functions_t te_graph_fns = {
execute_pseudo_action,
execute_rsc_action,
execute_cluster_action,
controld_execute_fence_action,
graph_action_allowed,
};
/*
* \internal
* \brief Register the transitioner's graph functions with \p libpacemaker
*/
void
controld_register_graph_functions(void)
{
pcmk__set_graph_functions(&te_graph_fns);
}
void
notify_crmd(pcmk__graph_t *graph)
{
const char *type = "unknown";
enum crmd_fsa_input event = I_NULL;
crm_debug("Processing transition completion in state %s",
fsa_state2string(controld_globals.fsa_state));
CRM_CHECK(graph->complete, graph->complete = true);
switch (graph->completion_action) {
case pcmk__graph_wait:
type = "stop";
if (controld_globals.fsa_state == S_TRANSITION_ENGINE) {
event = I_TE_SUCCESS;
}
break;
case pcmk__graph_done:
type = "done";
if (controld_globals.fsa_state == S_TRANSITION_ENGINE) {
event = I_TE_SUCCESS;
}
break;
case pcmk__graph_restart:
type = "restart";
if (controld_globals.fsa_state == S_TRANSITION_ENGINE) {
if (controld_get_period_transition_timer() > 0) {
controld_stop_transition_timer();
controld_start_transition_timer();
} else {
event = I_PE_CALC;
}
} else if (controld_globals.fsa_state == S_POLICY_ENGINE) {
controld_set_fsa_action_flags(A_PE_INVOKE);
controld_trigger_fsa();
}
break;
case pcmk__graph_shutdown:
type = "shutdown";
if (pcmk_is_set(controld_globals.fsa_input_register, R_SHUTDOWN)) {
event = I_STOP;
} else {
crm_err("We didn't ask to be shut down, yet the scheduler is telling us to");
event = I_TERMINATE;
}
}
crm_debug("Transition %d status: %s - %s", graph->id, type,
pcmk__s(graph->abort_reason, "unspecified reason"));
graph->abort_reason = NULL;
graph->completion_action = pcmk__graph_done;
if (event != I_NULL) {
register_fsa_input(C_FSA_INTERNAL, event, NULL);
} else {
controld_trigger_fsa();
}
}
diff --git a/include/crm/common/xml.h b/include/crm/common/xml.h
index a4cdcde69d..b5a5b08f0e 100644
--- a/include/crm/common/xml.h
+++ b/include/crm/common/xml.h
@@ -1,48 +1,49 @@
/*
* Copyright 2004-2025 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 // bool
#include // xmlNode
// xml.h is a wrapper for the following headers
#include
#include
#include
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Wrappers for and extensions to libxml2
* \ingroup core
*/
/* @COMPAT Create and apply patchset functions must remain public and
* undeprecated until we create replacements
*/
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);
+int xml_apply_patchset(xmlNode *xml, const xmlNode *patchset,
+ bool check_version);
#ifdef __cplusplus
}
#endif
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
#include
#endif
#endif
diff --git a/include/crm/common/xml_element.h b/include/crm/common/xml_element.h
index c49603b545..7487cce1bf 100644
--- a/include/crm/common/xml_element.h
+++ b/include/crm/common/xml_element.h
@@ -1,71 +1,53 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 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_ELEMENT__H
#define PCMK__CRM_COMMON_XML_ELEMENT__H
#include // struct timeval
#include // guint
#include // xmlNode
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Wrappers for and extensions to libxml2 for XML elements
* \ingroup core
*/
const char *crm_xml_add(xmlNode *node, const char *name, const char *value);
const char *crm_xml_add_int(xmlNode *node, const char *name, int value);
const char *crm_xml_add_ll(xmlNode *node, const char *name, long long value);
const char *crm_xml_add_ms(xmlNode *node, const char *name, guint ms);
const char *crm_xml_add_timeval(xmlNode *xml, const char *name_sec,
const char *name_usec,
const struct timeval *value);
const char *crm_element_value(const xmlNode *data, const char *name);
int crm_element_value_int(const xmlNode *data, const char *name, int *dest);
int crm_element_value_ll(const xmlNode *data, const char *name, long long *dest);
int crm_element_value_ms(const xmlNode *data, const char *name, guint *dest);
int crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest);
int crm_element_value_timeval(const xmlNode *data, const char *name_sec,
const char *name_usec, struct timeval *dest);
char *crm_element_value_copy(const xmlNode *data, const char *name);
-/*!
- * \brief Copy an element from one XML object to another
- *
- * \param[in] obj1 Source XML
- * \param[in,out] obj2 Destination XML
- * \param[in] element Name of element to copy
- *
- * \return Pointer to copied value (from source)
- */
-static inline const char *
-crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2, const char *element)
-{
- const char *value = crm_element_value(obj1, element);
-
- crm_xml_add(obj2, element, value);
- return value;
-}
-
#ifdef __cplusplus
}
#endif
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
#include
#endif
#endif // PCMK__CRM_COMMON_XML_ELEMENT__H
diff --git a/include/crm/common/xml_element_compat.h b/include/crm/common/xml_element_compat.h
index 7c6e1f1c84..50e677afbc 100644
--- a/include/crm/common/xml_element_compat.h
+++ b/include/crm/common/xml_element_compat.h
@@ -1,42 +1,46 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 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_ELEMENT_COMPAT__H
#define PCMK__CRM_COMMON_XML_ELEMENT_COMPAT__H
#include // gboolean
#include // xmlNode
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief Deprecated Pacemaker XML element API
* \ingroup core
* \deprecated Do not include this header directly. The nvpair APIs in this
* header, and the header itself, will be removed in a future
* release.
*/
//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
xmlNode *expand_idref(xmlNode *input, xmlNode *top);
//! \deprecated Do not use Pacemaker for general-purpose XML manipulation
void crm_xml_set_id(xmlNode *xml, const char *format, ...) G_GNUC_PRINTF(2, 3);
//! \deprecated Do not use
xmlNode *sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive);
+//! \deprecated Do not use
+const char *crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2,
+ const char *element);
+
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_XML_ELEMENT_COMPAT__H
diff --git a/include/crm/common/xml_names.h b/include/crm/common/xml_names.h
index 6dfd23af8b..2e937660da 100644
--- a/include/crm/common/xml_names.h
+++ b/include/crm/common/xml_names.h
@@ -1,463 +1,465 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 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_NAMES__H
#define PCMK__CRM_COMMON_XML_NAMES__H
#ifdef __cplusplus
extern "C" {
#endif
/*!
* \file
* \brief Defined string constants for XML element and attribute names
* \ingroup core
*/
/* For consistency, new constants should start with "PCMK_", followed by:
* - "XE" for XML element names
* - "XA" for XML attribute names
* - "OPT" for cluster option (property) names
* - "META" for meta-attribute names
* - "VALUE" for enumerated values (such as for options or for XML attributes)
* - "NODE_ATTR" for node attribute names
*
* Old names that don't follow this policy should eventually be deprecated and
* replaced with names that do.
*
* Symbols should be public if the user may specify them somewhere (especially
* the CIB) or if they're part of a well-defined structure that a user may need
* to parse. They should be internal if they're used only internally to
* Pacemaker (such as daemon IPC/CPG message XML).
*
* Constants belong in the following locations:
* * "XE" and "XA": xml_names.h and xml_names_internal.h
* * "OPT", "META", and "VALUE": options.h and options_internal.h
* * "NODE_ATTR": nodes.h and nodes_internal.h
*
* For meta-attributes that can be specified as either XML attributes or nvpair
* names, use "META" unless using both "XA" and "META" constants adds clarity.
* An example is operation attributes, which can be specified either as
* attributes of the PCMK_XE_OP element or as nvpairs in a meta-attribute set
* beneath the PCMK_XE_OP element.
*/
/*
* XML elements
*/
#define PCMK_XE_ACL_GROUP "acl_group"
#define PCMK_XE_ACL_PERMISSION "acl_permission"
#define PCMK_XE_ACL_ROLE "acl_role"
#define PCMK_XE_ACL_TARGET "acl_target"
#define PCMK_XE_ACLS "acls"
#define PCMK_XE_ACTION "action"
#define PCMK_XE_ACTIONS "actions"
#define PCMK_XE_AGENT "agent"
#define PCMK_XE_AGENT_STATUS "agent-status"
#define PCMK_XE_AGENTS "agents"
#define PCMK_XE_ALERT "alert"
#define PCMK_XE_ALERTS "alerts"
#define PCMK_XE_ALLOCATIONS "allocations"
#define PCMK_XE_ALLOCATIONS_UTILIZATIONS "allocations_utilizations"
#define PCMK_XE_ATTRIBUTE "attribute"
#define PCMK_XE_BAN "ban"
#define PCMK_XE_BANS "bans"
#define PCMK_XE_BUNDLE "bundle"
#define PCMK_XE_CAPACITY "capacity"
#define PCMK_XE_CHANGE "change"
#define PCMK_XE_CHANGE_ATTR "change-attr"
#define PCMK_XE_CHANGE_LIST "change-list"
#define PCMK_XE_CHANGE_RESULT "change-result"
#define PCMK_XE_CHECK "check"
#define PCMK_XE_CIB "cib"
#define PCMK_XE_CLONE "clone"
#define PCMK_XE_CLUSTER_ACTION "cluster_action"
#define PCMK_XE_CLUSTER_INFO "cluster-info"
#define PCMK_XE_CLUSTER_OPTIONS "cluster_options"
#define PCMK_XE_CLUSTER_PROPERTY_SET "cluster_property_set"
#define PCMK_XE_CLUSTER_STATUS "cluster_status"
#define PCMK_XE_COMMAND "command"
#define PCMK_XE_CONFIGURATION "configuration"
#define PCMK_XE_CONSTRAINT "constraint"
#define PCMK_XE_CONSTRAINTS "constraints"
#define PCMK_XE_CONTENT "content"
#define PCMK_XE_CRM_CONFIG "crm_config"
#define PCMK_XE_CRM_MON "crm_mon"
#define PCMK_XE_CRM_MON_DISCONNECTED "crm-mon-disconnected"
#define PCMK_XE_CURRENT_DC "current_dc"
#define PCMK_XE_DATE "date"
#define PCMK_XE_DATE_EXPRESSION "date_expression"
#define PCMK_XE_DATE_SPEC "date_spec"
#define PCMK_XE_DC "dc"
#define PCMK_XE_DEPRECATED "deprecated"
#define PCMK_XE_DIFF "diff"
#define PCMK_XE_DIGEST "digest"
#define PCMK_XE_DIGESTS "digests"
#define PCMK_XE_DOCKER "docker"
#define PCMK_XE_DURATION "duration"
#define PCMK_XE_DURATION_ENDS "duration_ends"
#define PCMK_XE_END "end"
#define PCMK_XE_ERROR "error"
#define PCMK_XE_ERRORS "errors"
#define PCMK_XE_EXPRESSION "expression"
#define PCMK_XE_FAILURE "failure"
#define PCMK_XE_FAILURES "failures"
#define PCMK_XE_FEATURE "feature"
#define PCMK_XE_FEATURES "features"
#define PCMK_XE_FENCE_EVENT "fence_event"
#define PCMK_XE_FENCE_HISTORY "fence_history"
#define PCMK_XE_FENCING_ACTION "fencing_action"
#define PCMK_XE_FENCING_LEVEL "fencing-level"
#define PCMK_XE_FENCING_TOPOLOGY "fencing-topology"
#define PCMK_XE_GROUP "group"
#define PCMK_XE_INJECT_ATTR "inject_attr"
#define PCMK_XE_INJECT_SPEC "inject_spec"
#define PCMK_XE_INSTANCE_ATTRIBUTES "instance_attributes"
#define PCMK_XE_INSTRUCTION "instruction"
#define PCMK_XE_ITEM "item"
#define PCMK_XE_LAST_CHANGE "last_change"
#define PCMK_XE_LAST_FENCED "last-fenced"
#define PCMK_XE_LAST_UPDATE "last_update"
#define PCMK_XE_LIST "list"
#define PCMK_XE_LONGDESC "longdesc"
#define PCMK_XE_META_ATTRIBUTES "meta_attributes"
#define PCMK_XE_METADATA "metadata"
#define PCMK_XE_MODIFICATIONS "modifications"
#define PCMK_XE_MODIFY_NODE "modify_node"
#define PCMK_XE_MODIFY_TICKET "modify_ticket"
#define PCMK_XE_NETWORK "network"
#define PCMK_XE_NODE "node"
#define PCMK_XE_NODE_ACTION "node_action"
#define PCMK_XE_NODE_ATTRIBUTES "node_attributes"
#define PCMK_XE_NODE_HISTORY "node_history"
#define PCMK_XE_NODE_INFO "node-info"
#define PCMK_XE_NODE_WEIGHT "node_weight"
#define PCMK_XE_NODES "nodes"
#define PCMK_XE_NODES_CONFIGURED "nodes_configured"
#define PCMK_XE_NVPAIR "nvpair"
#define PCMK_XE_OBJ_REF "obj_ref"
#define PCMK_XE_OP "op"
#define PCMK_XE_OP_DEFAULTS "op_defaults"
#define PCMK_XE_OP_EXPRESSION "op_expression"
#define PCMK_XE_OPERATION "operation"
#define PCMK_XE_OPERATION_HISTORY "operation_history"
#define PCMK_XE_OPERATIONS "operations"
#define PCMK_XE_OPTION "option"
#define PCMK_XE_OUTPUT "output"
#define PCMK_XE_OVERRIDE "override"
#define PCMK_XE_OVERRIDES "overrides"
#define PCMK_XE_PACEMAKER_RESULT "pacemaker-result"
#define PCMK_XE_PACEMAKERD "pacemakerd"
#define PCMK_XE_PARAMETER "parameter"
#define PCMK_XE_PARAMETERS "parameters"
+#define PCMK_XE_PATCHSET "patchset"
#define PCMK_XE_PERIOD "period"
#define PCMK_XE_PODMAN "podman"
#define PCMK_XE_PORT_MAPPING "port-mapping"
#define PCMK_XE_POSITION "position"
#define PCMK_XE_PRIMITIVE "primitive"
#define PCMK_XE_PROMOTION_SCORE "promotion_score"
#define PCMK_XE_PROVIDER "provider"
#define PCMK_XE_PROVIDERS "providers"
#define PCMK_XE_PSEUDO_ACTION "pseudo_action"
#define PCMK_XE_REASON "reason"
#define PCMK_XE_RECIPIENT "recipient"
#define PCMK_XE_REPLICA "replica"
#define PCMK_XE_RESOURCE "resource"
#define PCMK_XE_RESOURCE_AGENT "resource-agent"
#define PCMK_XE_RESOURCE_AGENT_ACTION "resource-agent-action"
#define PCMK_XE_RESOURCE_CONFIG "resource_config"
#define PCMK_XE_RESOURCE_HISTORY "resource_history"
#define PCMK_XE_RESOURCE_REF "resource_ref"
#define PCMK_XE_RESOURCE_SET "resource_set"
#define PCMK_XE_RESOURCES "resources"
#define PCMK_XE_RESOURCES_CONFIGURED "resources_configured"
#define PCMK_XE_RESULT_CODE "result-code"
#define PCMK_XE_REVISED_CLUSTER_STATUS "revised_cluster_status"
#define PCMK_XE_ROLE "role"
#define PCMK_XE_RSC_ACTION "rsc_action"
#define PCMK_XE_RSC_COLOCATION "rsc_colocation"
#define PCMK_XE_RSC_DEFAULTS "rsc_defaults"
#define PCMK_XE_RSC_EXPRESSION "rsc_expression"
#define PCMK_XE_RSC_LOCATION "rsc_location"
#define PCMK_XE_RSC_ORDER "rsc_order"
#define PCMK_XE_RSC_TICKET "rsc_ticket"
#define PCMK_XE_RULE "rule"
#define PCMK_XE_RULE_CHECK "rule-check"
#define PCMK_XE_SELECT "select"
#define PCMK_XE_SELECT_ATTRIBUTES "select_attributes"
#define PCMK_XE_SELECT_FENCING "select_fencing"
#define PCMK_XE_SELECT_NODES "select_nodes"
#define PCMK_XE_SELECT_RESOURCES "select_resources"
#define PCMK_XE_SHADOW "shadow"
#define PCMK_XE_SHORTDESC "shortdesc"
#define PCMK_XE_SOURCE "source"
#define PCMK_XE_SPECIAL "special"
#define PCMK_XE_STACK "stack"
#define PCMK_XE_START "start"
#define PCMK_XE_STATUS "status"
#define PCMK_XE_STORAGE "storage"
#define PCMK_XE_STORAGE_MAPPING "storage-mapping"
#define PCMK_XE_SUMMARY "summary"
#define PCMK_XE_TAG "tag"
#define PCMK_XE_TAGS "tags"
#define PCMK_XE_TARGET "target"
#define PCMK_XE_TEMPLATE "template"
#define PCMK_XE_TICKET "ticket"
#define PCMK_XE_TICKETS "tickets"
#define PCMK_XE_TIMING "timing"
#define PCMK_XE_TIMINGS "timings"
#define PCMK_XE_TRANSITION "transition"
+#define PCMK_XE_UPDATED "updated"
#define PCMK_XE_UTILIZATION "utilization"
#define PCMK_XE_UTILIZATIONS "utilizations"
#define PCMK_XE_VALIDATE "validate"
#define PCMK_XE_VERSION "version"
#define PCMK_XE_XML "xml"
#define PCMK_XE_XML_PATCHSET "xml-patchset"
/*
* XML attributes
*/
#define PCMK_XA_ACTION "action"
#define PCMK_XA_ACTIVE "active"
#define PCMK_XA_ADD_HOST "add-host"
#define PCMK_XA_ADMIN_EPOCH "admin_epoch"
#define PCMK_XA_ADVANCED "advanced"
#define PCMK_XA_AGENT "agent"
#define PCMK_XA_API_VERSION "api-version"
#define PCMK_XA_ATTRIBUTE "attribute"
#define PCMK_XA_AUTHOR "author"
#define PCMK_XA_AUTOMATIC "automatic"
#define PCMK_XA_BLOCKED "blocked"
#define PCMK_XA_BOOLEAN_OP "boolean-op"
#define PCMK_XA_BUILD "build"
#define PCMK_XA_CACHED "cached"
#define PCMK_XA_CALL "call"
#define PCMK_XA_CIB_LAST_WRITTEN "cib-last-written"
#define PCMK_XA_CIB_NODE "cib_node"
#define PCMK_XA_CLASS "class"
#define PCMK_XA_CLIENT "client"
#define PCMK_XA_CODE "code"
#define PCMK_XA_COMMENT "comment"
#define PCMK_XA_COMPLETED "completed"
#define PCMK_XA_CONTROL_PORT "control-port"
#define PCMK_XA_COUNT "count"
#define PCMK_XA_CRM_DEBUG_ORIGIN "crm-debug-origin"
#define PCMK_XA_CRM_FEATURE_SET "crm_feature_set"
#define PCMK_XA_CRM_TIMESTAMP "crm-timestamp"
#define PCMK_XA_CRMD "crmd"
#define PCMK_XA_DAYS "days"
#define PCMK_XA_DC_UUID "dc-uuid"
#define PCMK_XA_DEFAULT "default"
#define PCMK_XA_DELEGATE "delegate"
#define PCMK_XA_DESCRIPTION "description"
#define PCMK_XA_DEST "dest"
#define PCMK_XA_DEVICE "device"
#define PCMK_XA_DEVICES "devices"
#define PCMK_XA_DISABLED "disabled"
#define PCMK_XA_DURATION "duration"
#define PCMK_XA_END "end"
#define PCMK_XA_EPOCH "epoch"
#define PCMK_XA_EXEC "exec"
#define PCMK_XA_EXEC_TIME "exec-time"
#define PCMK_XA_EXECUTION_CODE "execution_code"
#define PCMK_XA_EXECUTION_DATE "execution-date"
#define PCMK_XA_EXECUTION_MESSAGE "execution_message"
#define PCMK_XA_EXIT_REASON "exit-reason"
#define PCMK_XA_EXITCODE "exitcode"
#define PCMK_XA_EXITREASON "exitreason"
#define PCMK_XA_EXITSTATUS "exitstatus"
#define PCMK_XA_EXPECTED "expected"
#define PCMK_XA_EXPECTED_UP "expected_up"
#define PCMK_XA_EXPIRES "expires"
#define PCMK_XA_EXTENDED_STATUS "extended-status"
#define PCMK_XA_FAIL_COUNT "fail-count"
#define PCMK_XA_FAILED "failed"
#define PCMK_XA_FAILURE_IGNORED "failure_ignored"
#define PCMK_XA_FEATURE_SET "feature_set"
#define PCMK_XA_FEATURES "features"
#define PCMK_XA_FILE "file"
#define PCMK_XA_FIRST "first"
#define PCMK_XA_FIRST_ACTION "first-action"
#define PCMK_XA_FOR "for"
#define PCMK_XA_FORMAT "format"
#define PCMK_XA_FUNCTION "function"
#define PCMK_XA_GENERATED "generated"
#define PCMK_XA_HASH "hash"
#define PCMK_XA_HAVE_QUORUM "have-quorum"
#define PCMK_XA_HEALTH "health"
#define PCMK_XA_HOST "host"
#define PCMK_XA_HOST_INTERFACE "host-interface"
#define PCMK_XA_HOST_NETMASK "host-netmask"
#define PCMK_XA_HOURS "hours"
#define PCMK_XA_ID "id"
#define PCMK_XA_ID_AS_RESOURCE "id_as_resource"
#define PCMK_XA_ID_REF "id-ref"
#define PCMK_XA_IMAGE "image"
#define PCMK_XA_INDEX "index"
#define PCMK_XA_INFLUENCE "influence"
#define PCMK_XA_INSTANCE "instance"
#define PCMK_XA_INTERNAL_PORT "internal-port"
#define PCMK_XA_INTERVAL "interval"
#define PCMK_XA_IP_RANGE_START "ip-range-start"
#define PCMK_XA_IS_DC "is_dc"
#define PCMK_XA_KIND "kind"
#define PCMK_XA_LANG "lang"
#define PCMK_XA_LAST_FAILURE "last-failure"
#define PCMK_XA_LAST_GRANTED "last-granted"
#define PCMK_XA_LAST_RC_CHANGE "last-rc-change"
#define PCMK_XA_LAST_UPDATED "last_updated"
#define PCMK_XA_LOCKED_TO "locked_to"
#define PCMK_XA_LOCKED_TO_HYPHEN "locked-to"
#define PCMK_XA_LOSS_POLICY "loss-policy"
#define PCMK_XA_MAINTENANCE "maintenance"
#define PCMK_XA_MAINTENANCE_MODE "maintenance-mode"
#define PCMK_XA_MANAGED "managed"
#define PCMK_XA_MESSAGE "message"
#define PCMK_XA_MINUTES "minutes"
#define PCMK_XA_MIXED_VERSION "mixed_version"
#define PCMK_XA_MONTHDAYS "monthdays"
#define PCMK_XA_MONTHS "months"
#define PCMK_XA_MULTI_STATE "multi_state"
#define PCMK_XA_NAME "name"
#define PCMK_XA_NETWORK "network"
#define PCMK_XA_NEXT_ROLE "next-role"
#define PCMK_XA_NO_QUORUM_PANIC "no-quorum-panic"
#define PCMK_XA_NO_QUORUM_POLICY "no-quorum-policy"
#define PCMK_XA_NODE "node"
#define PCMK_XA_NODE_ATTRIBUTE "node-attribute"
#define PCMK_XA_NODE_NAME "node_name"
#define PCMK_XA_NODE_PATH "node_path"
#define PCMK_XA_NODEID "nodeid"
#define PCMK_XA_NODES_RUNNING_ON "nodes_running_on"
#define PCMK_XA_NUM_UPDATES "num_updates"
#define PCMK_XA_NUMBER "number"
#define PCMK_XA_NUMBER_RESOURCES "number_resources"
#define PCMK_XA_OBJECT_TYPE "object-type"
#define PCMK_XA_ON_TARGET "on_target"
#define PCMK_XA_ONLINE "online"
#define PCMK_XA_OP "op"
#define PCMK_XA_OP_KEY "op_key"
#define PCMK_XA_OPERATION "operation"
#define PCMK_XA_OPTIONS "options"
#define PCMK_XA_ORIGIN "origin"
#define PCMK_XA_ORPHAN "orphan"
#define PCMK_XA_ORPHANED "orphaned"
#define PCMK_XA_PACEMAKERD_STATE "pacemakerd-state"
#define PCMK_XA_PATH "path"
#define PCMK_XA_PENDING "pending"
#define PCMK_XA_PORT "port"
#define PCMK_XA_PRESENT "present"
#define PCMK_XA_PRIORITY_FENCING_DELAY_MS "priority-fencing-delay-ms"
#define PCMK_XA_PROGRAM "program"
#define PCMK_XA_PROMOTABLE "promotable"
#define PCMK_XA_PROMOTED_MAX "promoted-max"
#define PCMK_XA_PROMOTED_ONLY "promoted-only"
#define PCMK_XA_PROVIDER "provider"
#define PCMK_XA_QUEUE_TIME "queue-time"
#define PCMK_XA_QUEUED "queued"
#define PCMK_XA_QUORUM "quorum"
#define PCMK_XA_RANGE "range"
#define PCMK_XA_RC "rc"
#define PCMK_XA_RC_TEXT "rc_text"
#define PCMK_XA_REASON "reason"
#define PCMK_XA_REFERENCE "reference"
#define PCMK_XA_RELOADABLE "reloadable"
#define PCMK_XA_REMAIN_STOPPED "remain_stopped"
#define PCMK_XA_REMOTE_CLEAR_PORT "remote-clear-port"
#define PCMK_XA_REMOTE_NODE "remote_node"
#define PCMK_XA_REMOTE_TLS_PORT "remote-tls-port"
#define PCMK_XA_REPLICAS "replicas"
#define PCMK_XA_REPLICAS_PER_HOST "replicas-per-host"
#define PCMK_XA_REQUEST "request"
#define PCMK_XA_REQUIRE_ALL "require-all"
#define PCMK_XA_RESOURCE "resource"
#define PCMK_XA_RESOURCE_AGENT "resource_agent"
#define PCMK_XA_RESOURCE_DISCOVERY "resource-discovery"
#define PCMK_XA_RESOURCES_RUNNING "resources_running"
#define PCMK_XA_RESULT "result"
#define PCMK_XA_ROLE "role"
#define PCMK_XA_RSC "rsc"
#define PCMK_XA_RSC_PATTERN "rsc-pattern"
#define PCMK_XA_RSC_ROLE "rsc-role"
#define PCMK_XA_RULE_ID "rule-id"
#define PCMK_XA_RUN_COMMAND "run-command"
#define PCMK_XA_RUNNING "running"
#define PCMK_XA_RUNNING_ON "running_on"
#define PCMK_XA_SCOPE "scope"
#define PCMK_XA_SCORE "score"
#define PCMK_XA_SCORE_ATTRIBUTE "score-attribute"
#define PCMK_XA_SEQUENTIAL "sequential"
#define PCMK_XA_SECONDS "seconds"
#define PCMK_XA_SHUTDOWN "shutdown"
#define PCMK_XA_SOURCE "source"
#define PCMK_XA_SOURCE_DIR "source-dir"
#define PCMK_XA_SOURCE_DIR_ROOT "source-dir-root"
#define PCMK_XA_SPEC "spec"
#define PCMK_XA_STANDARD "standard"
#define PCMK_XA_STANDBY "standby"
#define PCMK_XA_STANDBY_ONFAIL "standby_onfail"
#define PCMK_XA_START "start"
#define PCMK_XA_STATE "state"
#define PCMK_XA_STATUS "status"
#define PCMK_XA_STONITH_ENABLED "stonith-enabled"
#define PCMK_XA_STONITH_TIMEOUT_MS "stonith-timeout-ms"
#define PCMK_XA_STOP_ALL_RESOURCES "stop-all-resources"
#define PCMK_XA_SYMMETRIC_CLUSTER "symmetric-cluster"
#define PCMK_XA_SYMMETRICAL "symmetrical"
#define PCMK_XA_SYS_FROM "sys_from"
#define PCMK_XA_TAG "tag"
#define PCMK_XA_TARGET "target"
#define PCMK_XA_TARGET_ATTRIBUTE "target-attribute"
#define PCMK_XA_TARGET_DIR "target-dir"
#define PCMK_XA_TARGET_PATTERN "target-pattern"
#define PCMK_XA_TARGET_ROLE "target_role"
#define PCMK_XA_TARGET_VALUE "target-value"
#define PCMK_XA_TASK "task"
#define PCMK_XA_TEMPLATE "template"
#define PCMK_XA_TICKET "ticket"
#define PCMK_XA_TIME "time"
#define PCMK_XA_THEN "then"
#define PCMK_XA_THEN_ACTION "then-action"
#define PCMK_XA_TYPE "type"
#define PCMK_XA_UNAME "uname"
#define PCMK_XA_UNCLEAN "unclean"
#define PCMK_XA_UNHEALTHY "unhealthy"
#define PCMK_XA_UNIQUE "unique"
#define PCMK_XA_UNMANAGED "unmanaged"
#define PCMK_XA_UPDATE_CLIENT "update-client"
#define PCMK_XA_UPDATE_ORIGIN "update-origin"
#define PCMK_XA_UPDATE_USER "update-user"
#define PCMK_XA_USER "user"
#define PCMK_XA_VALID "valid"
#define PCMK_XA_VALIDATE_WITH "validate-with"
#define PCMK_XA_VALUE "value"
#define PCMK_XA_VALUE_SOURCE "value-source"
#define PCMK_XA_VERSION "version"
#define PCMK_XA_WATCHDOG "watchdog"
#define PCMK_XA_WEEKDAYS "weekdays"
#define PCMK_XA_WEEKS "weeks"
#define PCMK_XA_WEEKYEARS "weekyears"
#define PCMK_XA_WEIGHT "weight"
#define PCMK_XA_WHEN "when"
#define PCMK_XA_WITH_QUORUM "with_quorum"
#define PCMK_XA_WITH_RSC "with-rsc"
#define PCMK_XA_WITH_RSC_ROLE "with-rsc-role"
#define PCMK_XA_XPATH "xpath"
#define PCMK_XA_YEARDAYS "yeardays"
#define PCMK_XA_YEARS "years"
#ifdef __cplusplus
}
#endif
#endif // PCMK__CRM_COMMON_XML_NAMES__H
diff --git a/include/crm/crm.h b/include/crm/crm.h
index b8a213cb4d..a819902cc9 100644
--- a/include/crm/crm.h
+++ b/include/crm/crm.h
@@ -1,153 +1,153 @@
/*
* Copyright 2004-2025 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_CRM__H
# define PCMK__CRM_CRM__H
# include
# include
# include
# include
# include
# include
#include
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file
* \brief A dumping ground
* \ingroup core
*/
#ifndef PCMK_ALLOW_DEPRECATED
/*!
* \brief Allow use of deprecated Pacemaker APIs
*
* By default, external code using Pacemaker headers is allowed to use
* deprecated Pacemaker APIs. If PCMK_ALLOW_DEPRECATED is defined to 0 before
* including any Pacemaker headers, deprecated APIs will be unusable. It is
* strongly recommended to leave this unchanged for production and release
* builds, to avoid breakage when users upgrade to new Pacemaker releases that
* deprecate more APIs. This should be defined to 0 only for development and
* testing builds when desiring to check for usage of currently deprecated APIs.
*/
#define PCMK_ALLOW_DEPRECATED 1
#endif
/*!
* The CRM feature set assists with compatibility in mixed-version clusters.
* The major version number increases when nodes with different versions
* would not work (rolling upgrades are not allowed). The minor version
* number increases when mixed-version clusters are allowed only during
* rolling upgrades (a node with the oldest feature set will be elected DC). The
* minor-minor version number is ignored, but allows resource agents to detect
* cluster support for various features.
*
* The feature set also affects the processing of old saved CIBs (such as for
* many scheduler regression tests).
*
* Particular feature points currently tested by Pacemaker code:
*
* >=3.2.0: DC supports PCMK_EXEC_INVALID and PCMK_EXEC_NOT_CONNECTED
* >=3.19.0: DC supports PCMK__CIB_REQUEST_COMMIT_TRANSACT
*/
-#define CRM_FEATURE_SET "3.20.1"
+#define CRM_FEATURE_SET "3.20.2"
/* Pacemaker's CPG protocols use fixed-width binary fields for the sender and
* recipient of a CPG message. This imposes an arbitrary limit on cluster node
* names.
*/
//! \brief Maximum length of a Corosync cluster node name (in bytes)
#define MAX_NAME 256
# define CRM_META "CRM_meta"
// NOTE: sbd (as of at least 1.5.2) uses this
extern char *crm_system_name;
/* Sub-systems */
# define CRM_SYSTEM_DC "dc"
#define CRM_SYSTEM_DCIB "dcib" // Primary instance of CIB manager
# define CRM_SYSTEM_CIB "cib"
# define CRM_SYSTEM_CRMD "crmd"
# define CRM_SYSTEM_LRMD "lrmd"
# define CRM_SYSTEM_PENGINE "pengine"
# define CRM_SYSTEM_TENGINE "tengine"
# define CRM_SYSTEM_MCP "pacemakerd"
// Names of internally generated node attributes
// @TODO Replace these with PCMK_NODE_ATTR_*
# define CRM_ATTR_UNAME "#uname"
# define CRM_ATTR_ID "#id"
# define CRM_ATTR_KIND "#kind"
# define CRM_ATTR_ROLE "#role"
# define CRM_ATTR_IS_DC "#is_dc"
# define CRM_ATTR_CLUSTER_NAME "#cluster-name"
# define CRM_ATTR_SITE_NAME "#site-name"
# define CRM_ATTR_UNFENCED "#node-unfenced"
# define CRM_ATTR_DIGESTS_ALL "#digests-all"
# define CRM_ATTR_DIGESTS_SECURE "#digests-secure"
# define CRM_ATTR_PROTOCOL "#attrd-protocol"
# define CRM_ATTR_FEATURE_SET "#feature-set"
/* Valid operations */
# define CRM_OP_NOOP "noop"
# define CRM_OP_JOIN_ANNOUNCE "join_announce"
# define CRM_OP_JOIN_OFFER "join_offer"
# define CRM_OP_JOIN_REQUEST "join_request"
# define CRM_OP_JOIN_ACKNAK "join_ack_nack"
# define CRM_OP_JOIN_CONFIRM "join_confirm"
# define CRM_OP_PING "ping"
# define CRM_OP_NODE_INFO "node-info"
# define CRM_OP_THROTTLE "throttle"
# define CRM_OP_VOTE "vote"
# define CRM_OP_NOVOTE "no-vote"
# define CRM_OP_HELLO "hello"
# define CRM_OP_PECALC "pe_calc"
# define CRM_OP_QUIT "quit"
# define CRM_OP_SHUTDOWN_REQ "req_shutdown"
# define CRM_OP_SHUTDOWN PCMK_ACTION_DO_SHUTDOWN
# define CRM_OP_REGISTER "register"
# define CRM_OP_IPC_FWD "ipc_fwd"
# define CRM_OP_INVOKE_LRM "lrm_invoke"
# define CRM_OP_LRM_DELETE PCMK_ACTION_LRM_DELETE
# define CRM_OP_LRM_FAIL "lrm_fail"
# define CRM_OP_PROBED "probe_complete"
# define CRM_OP_REPROBE "probe_again"
# define CRM_OP_CLEAR_FAILCOUNT PCMK_ACTION_CLEAR_FAILCOUNT
# define CRM_OP_REMOTE_STATE "remote_state"
# define CRM_OP_RM_NODE_CACHE "rm_node_cache"
# define CRM_OP_MAINTENANCE_NODES PCMK_ACTION_MAINTENANCE_NODES
/* Possible cluster membership states */
# define CRMD_JOINSTATE_DOWN "down"
# define CRMD_JOINSTATE_PENDING "pending"
# define CRMD_JOINSTATE_MEMBER "member"
# define CRMD_JOINSTATE_NACK "banned"
# include
# include
# include
# include
#ifdef __cplusplus
}
#endif
#if !defined(PCMK_ALLOW_DEPRECATED) || (PCMK_ALLOW_DEPRECATED == 1)
#include
#endif
#endif
diff --git a/include/crm_internal.h b/include/crm_internal.h
index 891f2e5b68..337d309b99 100644
--- a/include/crm_internal.h
+++ b/include/crm_internal.h
@@ -1,99 +1,100 @@
/*
- * Copyright 2006-2024 the Pacemaker project contributors
+ * Copyright 2006-2025 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_INTERNAL__H
#define PCMK__CRM_INTERNAL__H
#ifndef PCMK__CONFIG_H
#define PCMK__CONFIG_H
#include
#endif
#include
/* Our minimum glib dependency is 2.42. Define that as both the minimum and
* maximum glib APIs that are allowed (i.e. APIs that were already deprecated
* in 2.42, and APIs introduced after 2.42, cannot be used by Pacemaker code).
*/
#define GLIB_VERSION_MIN_REQUIRED GLIB_VERSION_2_42
#define GLIB_VERSION_MAX_ALLOWED GLIB_VERSION_2_42
#define G_LOG_DOMAIN "Pacemaker"
#include
#include
#include
/* Public API headers can guard including deprecated API headers with this
* symbol, thus preventing internal code (which includes this header) from using
* deprecated APIs, while still allowing external code to use them by default.
*/
#define PCMK_ALLOW_DEPRECATED 0
#include
#include
+#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifdef __cplusplus
extern "C" {
#endif
#define N_(String) (String)
#ifdef ENABLE_NLS
#define _(String) gettext(String)
#else
#define _(String) (String)
#endif
/*
* IPC service names that are only used internally
*/
#define PCMK__SERVER_BASED_RO "cib_ro"
#define PCMK__SERVER_BASED_RW "cib_rw"
#define PCMK__SERVER_BASED_SHM "cib_shm"
/*
* IPC commands that can be sent to Pacemaker daemons
*/
#define PCMK__ATTRD_CMD_PEER_REMOVE "peer-remove"
#define PCMK__ATTRD_CMD_UPDATE "update"
#define PCMK__ATTRD_CMD_UPDATE_BOTH "update-both"
#define PCMK__ATTRD_CMD_UPDATE_DELAY "update-delay"
#define PCMK__ATTRD_CMD_QUERY "query"
#define PCMK__ATTRD_CMD_REFRESH "refresh"
#define PCMK__ATTRD_CMD_SYNC_RESPONSE "sync-response"
#define PCMK__ATTRD_CMD_CLEAR_FAILURE "clear-failure"
#define PCMK__ATTRD_CMD_CONFIRM "confirm"
#define PCMK__CONTROLD_CMD_NODES "list-nodes"
#define ST__LEVEL_MIN 1
#define ST__LEVEL_MAX 9
#ifdef __cplusplus
}
#endif
#endif // CRM_INTERNAL__H
diff --git a/lib/common/digest.c b/lib/common/digest.c
index 3479f8a425..b9213bd560 100644
--- a/lib/common/digest.c
+++ b/lib/common/digest.c
@@ -1,393 +1,398 @@
/*
* Copyright 2015-2025 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 // GString, etc.
#include // gnutls_hash_fast(), gnutls_hash_get_len()
#include // gnutls_strerror()
#include
#include
#include "crmcommon_private.h"
#define BEST_EFFORT_STATUS 0
/*
* Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in
* the CIB as a whole, a particular resource's agent parameters, and the device
* parameters last used to unfence a particular node.
*
* "v2" digests hash pcmk__xml_string() directly, while less efficient "v1"
* digests do the same with a prefixed space, suffixed newline, and optional
* pre-sorting.
*
* On-disk CIB digests use v1 without sorting.
*
* Operation digests use v1 with sorting, and are stored in a resource's
* operation history in the CIB status section. They come in three flavors:
* - a digest of (nearly) all resource parameters and options, used to detect
* any resource configuration change;
* - a digest of resource parameters marked as nonreloadable, used to decide
* whether a reload or full restart is needed after a configuration change;
* - and a digest of resource parameters not marked as private, used in
* simulations where private parameters have been removed from the input.
*
* Unfencing digests are set as node attributes, and are used to require
* that nodes be unfenced again after a device's configuration changes.
*/
/*!
* \internal
* \brief Dump XML in a format used with v1 digests
*
* \param[in] xml Root of XML to dump
*
* \return Newly allocated buffer containing dumped XML
*/
static GString *
dump_xml_for_digest(const xmlNode *xml)
{
GString *buffer = g_string_sized_new(1024);
/* for compatibility with the old result which is used for v1 digests */
g_string_append_c(buffer, ' ');
pcmk__xml_string(xml, 0, buffer, 0);
g_string_append_c(buffer, '\n');
return buffer;
}
/*!
* \internal
* \brief Calculate and return v1 digest of XML tree
*
* \param[in] input Root of XML to digest
*
* \return Newly allocated string containing digest
*
* \note Example return value: "c048eae664dba840e1d2060f00299e9d"
*/
static char *
calculate_xml_digest_v1(const xmlNode *input)
{
GString *buffer = dump_xml_for_digest(input);
char *digest = NULL;
// buffer->len > 2 for initial space and trailing newline
CRM_CHECK(buffer->len > 2,
g_string_free(buffer, TRUE);
return NULL);
digest = crm_md5sum((const char *) buffer->str);
crm_log_xml_trace(input, "digest:source");
g_string_free(buffer, TRUE);
return digest;
}
/*!
* \internal
* \brief Calculate and return the digest of a CIB, suitable for storing on disk
*
* \param[in] input Root of XML to digest
*
* \return Newly allocated string containing digest
*/
char *
pcmk__digest_on_disk_cib(const xmlNode *input)
{
/* Always use the v1 format for on-disk digests.
* * Switching to v2 affects even full-restart upgrades, so it would be a
* compatibility nightmare.
* * We only use this once at startup. All other invocations are in a
* separate child process.
*/
return calculate_xml_digest_v1(input);
}
/*!
* \internal
* \brief Calculate and return digest of a \c PCMK_XE_PARAMETERS element
*
* This is intended for parameters of a resource operation (also known as
* resource action). A \c PCMK_XE_PARAMETERS element from a different source
* (for example, resource agent metadata) may have child elements, which are not
* allowed here.
*
* The digest is invariant to changes in the order of XML attributes.
*
* \param[in] input XML element to digest (must have no children)
*
* \return Newly allocated string containing digest
*/
char *
pcmk__digest_op_params(const xmlNode *input)
{
/* Switching to v2 digests would likely cause restarts during rolling
* upgrades.
*
* @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise.
*/
char *digest = NULL;
xmlNode *sorted = NULL;
pcmk__assert(input->children == NULL);
sorted = pcmk__xe_create(NULL, (const char *) input->name);
pcmk__xe_copy_attrs(sorted, input, pcmk__xaf_none);
pcmk__xe_sort_attrs(sorted);
digest = calculate_xml_digest_v1(sorted);
pcmk__xml_free(sorted);
return digest;
}
/*!
* \internal
* \brief Calculate and return the digest of an XML tree
*
* \param[in] xml XML tree to digest
* \param[in] filter Whether to filter certain XML attributes
*
* \return Newly allocated string containing digest
*/
char *
pcmk__digest_xml(const xmlNode *xml, bool filter)
{
/* @TODO Filtering accounts for significant CPU usage. Consider removing if
* possible.
*/
char *digest = NULL;
GString *buf = g_string_sized_new(1024);
pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0);
digest = crm_md5sum(buf->str);
+ if (digest == NULL) {
+ goto done;
+ }
pcmk__if_tracing(
{
char *trace_file = crm_strdup_printf("%s/digest-%s",
pcmk__get_tmpdir(), digest);
crm_trace("Saving %s.%s.%s to %s",
crm_element_value(xml, PCMK_XA_ADMIN_EPOCH),
crm_element_value(xml, PCMK_XA_EPOCH),
crm_element_value(xml, PCMK_XA_NUM_UPDATES),
trace_file);
save_xml_to_file(xml, "digest input", trace_file);
free(trace_file);
},
{}
);
+
+done:
g_string_free(buf, TRUE);
return digest;
}
/*!
* \internal
* \brief Check whether calculated digest of given XML matches expected digest
*
* \param[in] input Root of XML tree to digest
* \param[in] expected Expected digest in on-disk format
*
* \return true if digests match, false on mismatch or error
*/
bool
pcmk__verify_digest(const xmlNode *input, const char *expected)
{
char *calculated = NULL;
bool passed;
if (input != NULL) {
calculated = pcmk__digest_on_disk_cib(input);
if (calculated == NULL) {
crm_perror(LOG_ERR, "Could not calculate digest for comparison");
return false;
}
}
passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
if (passed) {
crm_trace("Digest comparison passed: %s", calculated);
} else {
crm_err("Digest comparison failed: expected %s, calculated %s",
expected, calculated);
}
free(calculated);
return passed;
}
/*!
* \internal
* \brief Check whether an XML attribute should be excluded from CIB digests
*
* \param[in] name XML attribute name
*
* \return true if XML attribute should be excluded from CIB digest calculation
*/
bool
pcmk__xa_filterable(const char *name)
{
static const char *filter[] = {
PCMK_XA_CRM_DEBUG_ORIGIN,
PCMK_XA_CIB_LAST_WRITTEN,
PCMK_XA_UPDATE_ORIGIN,
PCMK_XA_UPDATE_CLIENT,
PCMK_XA_UPDATE_USER,
};
for (int i = 0; i < PCMK__NELEM(filter); i++) {
if (strcmp(name, filter[i]) == 0) {
return true;
}
}
return false;
}
char *
crm_md5sum(const char *buffer)
{
char *digest = NULL;
gchar *raw_digest = NULL;
if (buffer == NULL) {
return NULL;
}
raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1);
if (raw_digest == NULL) {
crm_err("Failed to calculate hash");
return NULL;
}
digest = pcmk__str_copy(raw_digest);
g_free(raw_digest);
crm_trace("Digest %s.", digest);
return digest;
}
// Return true if a is an attribute that should be filtered
static bool
should_filter_for_digest(xmlAttrPtr a, void *user_data)
{
if (strncmp((const char *) a->name, CRM_META "_",
sizeof(CRM_META " ") - 1) == 0) {
return true;
}
return pcmk__str_any_of((const char *) a->name,
PCMK_XA_ID,
PCMK_XA_CRM_FEATURE_SET,
PCMK__XA_OP_DIGEST,
PCMK__META_ON_NODE,
PCMK__META_ON_NODE_UUID,
"pcmk_external_ip",
NULL);
}
/*!
* \internal
* \brief Remove XML attributes not needed for operation digest
*
* \param[in,out] param_set XML with operation parameters
*/
void
pcmk__filter_op_for_digest(xmlNode *param_set)
{
char *key = NULL;
char *timeout = NULL;
guint interval_ms = 0;
if (param_set == NULL) {
return;
}
/* Timeout is useful for recurring operation digests, so grab it before
* removing meta-attributes
*/
key = crm_meta_name(PCMK_META_INTERVAL);
if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
interval_ms = 0;
}
free(key);
key = NULL;
if (interval_ms != 0) {
key = crm_meta_name(PCMK_META_TIMEOUT);
timeout = crm_element_value_copy(param_set, key);
}
// Remove all CRM_meta_* attributes and certain other attributes
pcmk__xe_remove_matching_attrs(param_set, false, should_filter_for_digest,
NULL);
// Add timeout back for recurring operation digests
if (timeout != NULL) {
crm_xml_add(param_set, key, timeout);
}
free(timeout);
free(key);
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include
#include
char *
calculate_on_disk_digest(xmlNode *input)
{
return calculate_xml_digest_v1(input);
}
char *
calculate_operation_digest(xmlNode *input, const char *version)
{
xmlNode *sorted = sorted_xml(input, NULL, true);
char *digest = calculate_xml_digest_v1(sorted);
pcmk__xml_free(sorted);
return digest;
}
char *
calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
gboolean do_filter, const char *version)
{
if ((version == NULL) || (compare_version("3.0.5", version) > 0)) {
xmlNode *sorted = NULL;
char *digest = NULL;
if (sort) {
xmlNode *sorted = sorted_xml(input, NULL, true);
input = sorted;
}
crm_trace("Using v1 digest algorithm for %s",
pcmk__s(version, "unknown feature set"));
digest = calculate_xml_digest_v1(input);
pcmk__xml_free(sorted);
return digest;
}
crm_trace("Using v2 digest algorithm for %s", version);
return pcmk__digest_xml(input, do_filter);
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/patchset.c b/lib/common/patchset.c
index 3ed81f0e3a..81c6521bb5 100644
--- a/lib/common/patchset.c
+++ b/lib/common/patchset.c
@@ -1,1029 +1,1030 @@
/*
* Copyright 2004-2025 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 // xmlNode
#include
#include
#include
#include // CRM_XML_LOG_BASE, etc.
#include "crmcommon_private.h"
+static const char *const vfields[] = {
+ PCMK_XA_ADMIN_EPOCH,
+ PCMK_XA_EPOCH,
+ PCMK_XA_NUM_UPDATES,
+};
+
/* Add changes for specified XML to patchset.
* For patchset format, refer to diff schema.
*/
static void
add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset)
{
xmlNode *cIter = NULL;
xmlAttr *pIter = NULL;
xmlNode *change = NULL;
xml_node_private_t *nodepriv = xml->_private;
const char *value = NULL;
if (nodepriv == NULL) {
/* Elements that shouldn't occur in a CIB don't have _private set. They
* should be stripped out, ignored, or have an error thrown by any code
* that processes their parent, so we ignore any changes to them.
*/
return;
}
// If this XML node is new, just report that
if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
GString *xpath = pcmk__element_xpath(xml->parent);
if (xpath != NULL) {
int position = pcmk__xml_position(xml, pcmk__xf_deleted);
change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_CREATE);
crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
crm_xml_add_int(change, PCMK_XE_POSITION, position);
pcmk__xml_copy(change, xml);
g_string_free(xpath, TRUE);
}
return;
}
// Check each of the XML node's attributes for changes
for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
pIter = pIter->next) {
xmlNode *attr = NULL;
nodepriv = pIter->_private;
if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) {
continue;
}
if (change == NULL) {
GString *xpath = pcmk__element_xpath(xml);
if (xpath != NULL) {
change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MODIFY);
crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
change = pcmk__xe_create(change, PCMK_XE_CHANGE_LIST);
g_string_free(xpath, TRUE);
}
}
attr = pcmk__xe_create(change, PCMK_XE_CHANGE_ATTR);
crm_xml_add(attr, PCMK_XA_NAME, (const char *) pIter->name);
if (nodepriv->flags & pcmk__xf_deleted) {
crm_xml_add(attr, PCMK_XA_OPERATION, "unset");
} else {
crm_xml_add(attr, PCMK_XA_OPERATION, "set");
value = pcmk__xml_attr_value(pIter);
crm_xml_add(attr, PCMK_XA_VALUE, value);
}
}
if (change) {
xmlNode *result = NULL;
change = pcmk__xe_create(change->parent, PCMK_XE_CHANGE_RESULT);
result = pcmk__xe_create(change, (const char *)xml->name);
for (pIter = pcmk__xe_first_attr(xml); pIter != NULL;
pIter = pIter->next) {
nodepriv = pIter->_private;
if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
value = crm_element_value(xml, (const char *) pIter->name);
crm_xml_add(result, (const char *)pIter->name, value);
}
}
}
// Now recursively do the same for each child node of this node
for (cIter = pcmk__xml_first_child(xml); cIter != NULL;
cIter = pcmk__xml_next(cIter)) {
add_xml_changes_to_patchset(cIter, patchset);
}
nodepriv = xml->_private;
if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
GString *xpath = pcmk__element_xpath(xml);
crm_trace("%s.%s moved to position %d",
xml->name, pcmk__xe_id(xml),
pcmk__xml_position(xml, pcmk__xf_skip));
if (xpath != NULL) {
change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_MOVE);
crm_xml_add(change, PCMK_XA_PATH, (const char *) xpath->str);
crm_xml_add_int(change, PCMK_XE_POSITION,
pcmk__xml_position(xml, pcmk__xf_deleted));
g_string_free(xpath, TRUE);
}
}
}
static bool
is_config_change(xmlNode *xml)
{
GList *gIter = NULL;
xml_node_private_t *nodepriv = NULL;
xml_doc_private_t *docpriv;
xmlNode *config = pcmk__xe_first_child(xml, PCMK_XE_CONFIGURATION, NULL,
NULL);
if (config) {
nodepriv = config->_private;
}
if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
return TRUE;
}
if ((xml->doc != NULL) && (xml->doc->_private != NULL)) {
docpriv = xml->doc->_private;
for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
pcmk__deleted_xml_t *deleted_obj = gIter->data;
if (strstr(deleted_obj->path,
"/" PCMK_XE_CIB "/" PCMK_XE_CONFIGURATION) != NULL) {
return TRUE;
}
}
}
return FALSE;
}
static xmlNode *
xml_create_patchset_v2(xmlNode *source, xmlNode *target)
{
int lpc = 0;
GList *gIter = NULL;
xml_doc_private_t *docpriv;
xmlNode *v = NULL;
xmlNode *version = NULL;
xmlNode *patchset = NULL;
- const char *vfields[] = {
- PCMK_XA_ADMIN_EPOCH,
- PCMK_XA_EPOCH,
- PCMK_XA_NUM_UPDATES,
- };
pcmk__assert(target != NULL);
if (!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) {
return NULL;
}
pcmk__assert(target->doc != NULL);
docpriv = target->doc->_private;
patchset = pcmk__xe_create(NULL, PCMK_XE_DIFF);
crm_xml_add_int(patchset, PCMK_XA_FORMAT, 2);
version = pcmk__xe_create(patchset, PCMK_XE_VERSION);
v = pcmk__xe_create(version, PCMK_XE_SOURCE);
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
const char *value = crm_element_value(source, vfields[lpc]);
if (value == NULL) {
value = "1";
}
crm_xml_add(v, vfields[lpc], value);
}
v = pcmk__xe_create(version, PCMK_XE_TARGET);
for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
const char *value = crm_element_value(target, vfields[lpc]);
if (value == NULL) {
value = "1";
}
crm_xml_add(v, vfields[lpc], value);
}
for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) {
pcmk__deleted_xml_t *deleted_obj = gIter->data;
xmlNode *change = pcmk__xe_create(patchset, PCMK_XE_CHANGE);
crm_xml_add(change, PCMK_XA_OPERATION, PCMK_VALUE_DELETE);
crm_xml_add(change, PCMK_XA_PATH, deleted_obj->path);
if (deleted_obj->position >= 0) {
crm_xml_add_int(change, PCMK_XE_POSITION, deleted_obj->position);
}
}
add_xml_changes_to_patchset(target, patchset);
return patchset;
}
xmlNode *
xml_create_patchset(int format, xmlNode *source, xmlNode *target,
bool *config_changed, bool manage_version)
{
bool local_config_changed = false;
if (format == 0) {
format = 2;
}
if (format != 2) {
crm_err("Unknown patch format: %d", format);
return NULL;
}
xml_acl_disable(target);
if ((target == NULL)
|| !pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty)) {
crm_trace("No change %d", format);
return NULL;
}
if (config_changed == NULL) {
config_changed = &local_config_changed;
}
*config_changed = is_config_change(target);
if (manage_version) {
int counter = 0;
if (*config_changed) {
crm_xml_add(target, PCMK_XA_NUM_UPDATES, "0");
crm_element_value_int(target, PCMK_XA_EPOCH, &counter);
crm_xml_add_int(target, PCMK_XA_EPOCH, counter + 1);
} else {
crm_element_value_int(target, PCMK_XA_NUM_UPDATES, &counter);
crm_xml_add_int(target, PCMK_XA_NUM_UPDATES, counter + 1);
}
}
return xml_create_patchset_v2(source, target);
}
/*!
* \internal
* \brief Add a digest of a patchset's target XML to the patchset
*
* \param[in,out] patchset XML patchset
* \param[in] target Target XML
*/
void
pcmk__xml_patchset_add_digest(xmlNode *patchset, const xmlNode *target)
{
char *digest = NULL;
CRM_CHECK((patchset != NULL) && (target != NULL), return);
/* If tracking is enabled and the document is dirty, we could get an
* incorrect digest. Call pcmk__xml_commit_changes() before calling this.
*/
CRM_CHECK(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty),
return);
digest = pcmk__digest_xml(target, true);
crm_xml_add(patchset, PCMK__XA_DIGEST, digest);
free(digest);
}
/*!
* \internal
* \brief Get the source and target CIB versions from an XML patchset
*
* Each output object will contain, in order, the following version fields from
* the source and target, respectively:
* * \c PCMK_XA_ADMIN_EPOCH
* * \c PCMK_XA_EPOCH
* * \c PCMK_XA_NUM_UPDATES
*
* If source versions or target versions are absent from the patchset, then
* \p source and \p target (respectively) are left unmodified. This is not
* treated as an error. An unparsable version is an error, however.
*
* \param[in] patchset XML patchset
* \param[out] source Where to store versions from source CIB
* \param[out] target Where to store versions from target CIB
*
* \return Standard Pacemaker return code
*/
int
pcmk__xml_patchset_versions(const xmlNode *patchset, int source[3],
int target[3])
{
- static const char *const vfields[] = {
- PCMK_XA_ADMIN_EPOCH,
- PCMK_XA_EPOCH,
- PCMK_XA_NUM_UPDATES,
- };
-
int format = 0;
const xmlNode *version = NULL;
const xmlNode *source_xml = NULL;
const xmlNode *target_xml = NULL;
CRM_CHECK((patchset != NULL) && (source != NULL) && (target != NULL),
return EINVAL);
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_err("Unknown patch format: %d", format);
return EINVAL;
}
version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL, NULL);
source_xml = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL, NULL);
target_xml = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL, NULL);
/* @COMPAT Consider requiring source_xml and target_xml to be non-NULL. As
* of pcs version 0.10.8, pcs creates a patchset using crm_diff
* --no-version. The behavior and documentation of the crm_diff options
* --cib and --no-version are questionable and should be re-examined. Even
* without --no-version, crm_diff does not update the target version in the
* generated patchset. So a diff based on a manual CIB XML edit is likely to
* have unchanged version numbers. (Pacemaker tools bump the CIB versions
* automatically when editing the CIB.)
*
* Until then, we may be applying a patchset that has no version info. We
* will allow either source version or target version to be missing (even
* though both should be present or both should be missing). However, return
* an error if any of the three vfields is missing from a source or target
* version element that is present. That level of sanity check should be
* okay.
*
* We leave the destination arrays unmodified in case of absent versions,
* instead of setting them to some default value like { 0, 0, 0 }.
* xml_patch_version_check() sets its own defaults in case of absent
* versions.
*/
for (int i = 0; i < PCMK__NELEM(vfields); i++) {
if (source_xml != NULL) {
if (crm_element_value_int(source_xml, vfields[i],
&(source[i])) != 0) {
return EINVAL;
}
crm_trace("Got source[%s]=%d", vfields[i], source[i]);
} else {
crm_trace("No source versions found; keeping source[%s]=%d",
vfields[i], source[i]);
}
if (target_xml != NULL) {
if (crm_element_value_int(target_xml, vfields[i],
&(target[i])) != 0) {
return EINVAL;
}
crm_trace("Got target[%s]=%d", vfields[i], target[i]);
} else {
crm_trace("No target versions found; keeping target[%s]=%d",
vfields[i], target[i]);
}
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Check whether patchset can be applied to current CIB
*
- * \param[in] xml Root of current CIB
+ * \param[in] cib_root Root of current CIB
* \param[in] patchset Patchset to check
*
* \return Standard Pacemaker return code
*/
static int
-xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset)
+check_patchset_versions(const xmlNode *cib_root, const xmlNode *patchset)
{
- int lpc = 0;
- bool changed = FALSE;
-
- int this[] = { 0, 0, 0 };
- int add[] = { 0, 0, 0 };
- int del[] = { 0, 0, 0 };
+ int current[] = { 0, 0, 0 };
+ int source[] = { 0, 0, 0 };
+ int target[] = { 0, 0, 0 };
int rc = pcmk_rc_ok;
- const char *vfields[] = {
- PCMK_XA_ADMIN_EPOCH,
- PCMK_XA_EPOCH,
- PCMK_XA_NUM_UPDATES,
- };
-
- for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
- crm_element_value_int(xml, vfields[lpc], &(this[lpc]));
- crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]);
- if (this[lpc] < 0) {
- this[lpc] = 0;
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ /* @COMPAT We should probably fail with EINVAL for negative or invalid.
+ *
+ * Preserve behavior for xml_apply_patchset(). Use new behavior in a
+ * future replacement.
+ */
+ if (crm_element_value_int(cib_root, vfields[i], &(current[i])) == 0) {
+ crm_trace("Got %d for current[%s]%s",
+ current[i], vfields[i],
+ ((current[i] < 0)? ", using 0" : ""));
+ } else {
+ crm_debug("Failed to get value for current[%s], using 0",
+ vfields[i]);
+ }
+ if (current[i] < 0) {
+ current[i] = 0;
}
}
- /* Set some defaults in case nothing is present */
- add[0] = this[0];
- add[1] = this[1];
- add[2] = this[2] + 1;
- for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
- del[lpc] = this[lpc];
+ /* Set some defaults in case nothing is present.
+ *
+ * @COMPAT We should probably skip this step, and fail immediately below if
+ * target[i] < source[i].
+ *
+ * Preserve behavior for xml_apply_patchset(). Use new behavior in a future
+ * replacement.
+ */
+ target[0] = current[0];
+ target[1] = current[1];
+ target[2] = current[2] + 1;
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ source[i] = current[i];
}
- rc = pcmk__xml_patchset_versions(patchset, del, add);
+ rc = pcmk__xml_patchset_versions(patchset, source, target);
if (rc != pcmk_rc_ok) {
return rc;
}
- for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
- if (this[lpc] < del[lpc]) {
- crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)",
- vfields[lpc], this[0], this[1], this[2],
- del[0], del[1], del[2], add[0], add[1], add[2]);
+ // Ensure current version matches patchset source version
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ if (current[i] < source[i]) {
+ crm_debug("Current %s is too low "
+ "(%d.%d.%d < %d.%d.%d --> %d.%d.%d)",
+ vfields[i], current[0], current[1], current[2],
+ source[0], source[1], source[2],
+ target[0], target[1], target[2]);
return pcmk_rc_diff_resync;
-
- } else if (this[lpc] > del[lpc]) {
- crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p",
- vfields[lpc], this[0], this[1], this[2],
- del[0], del[1], del[2], add[0], add[1], add[2], patchset);
+ }
+ if (current[i] > source[i]) {
+ crm_info("Current %s is too high "
+ "(%d.%d.%d > %d.%d.%d --> %d.%d.%d)",
+ vfields[i], current[0], current[1], current[2],
+ source[0], source[1], source[2],
+ target[0], target[1], target[2]);
crm_log_xml_info(patchset, "OldPatch");
return pcmk_rc_old_data;
}
}
- for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
- if (add[lpc] > del[lpc]) {
- changed = TRUE;
+ // Ensure target version is newer than source version
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ if (target[i] > source[i]) {
+ crm_debug("Can apply patch %d.%d.%d to %d.%d.%d",
+ target[0], target[1], target[2],
+ current[0], current[1], current[2]);
+ return pcmk_rc_ok;
}
}
- if (!changed) {
- crm_notice("Versions did not change in patch %d.%d.%d",
- add[0], add[1], add[2]);
- return pcmk_rc_old_data;
- }
-
- crm_debug("Can apply patch %d.%d.%d to %d.%d.%d",
- add[0], add[1], add[2], this[0], this[1], this[2]);
- return pcmk_rc_ok;
+ crm_notice("Versions did not change in patch %d.%d.%d",
+ target[0], target[1], target[2]);
+ return pcmk_rc_old_data;
}
// Return first child matching element name and optionally id or position
static xmlNode *
first_matching_xml_child(const xmlNode *parent, const char *name,
const char *id, int position)
{
xmlNode *cIter = NULL;
for (cIter = pcmk__xml_first_child(parent); cIter != NULL;
cIter = pcmk__xml_next(cIter)) {
if (strcmp((const char *) cIter->name, name) != 0) {
continue;
} else if (id) {
const char *cid = pcmk__xe_id(cIter);
if ((cid == NULL) || (strcmp(cid, id) != 0)) {
continue;
}
}
// "position" makes sense only for XML comments for now
if ((cIter->type == XML_COMMENT_NODE)
&& (position >= 0)
&& (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) {
continue;
}
return cIter;
}
return NULL;
}
/*!
* \internal
* \brief Simplified, more efficient alternative to pcmk__xpath_find_one()
*
* \param[in] top Root of XML to search
* \param[in] key Search xpath
* \param[in] target_position If deleting, where to delete
*
* \return XML child matching xpath if found, NULL otherwise
*
* \note This only works on simplified xpaths found in v2 patchset diffs,
* i.e. the only allowed search predicate is [@id='XXX'].
*/
static xmlNode *
search_v2_xpath(const xmlNode *top, const char *key, int target_position)
{
xmlNode *target = (xmlNode *) top->doc;
const char *current = key;
char *section;
char *remainder;
char *id;
char *tag;
char *path = NULL;
int rc;
size_t key_len;
CRM_CHECK(key != NULL, return NULL);
key_len = strlen(key);
/* These are scanned from key after a slash, so they can't be bigger
* than key_len - 1 characters plus a null terminator.
*/
remainder = pcmk__assert_alloc(key_len, sizeof(char));
section = pcmk__assert_alloc(key_len, sizeof(char));
id = pcmk__assert_alloc(key_len, sizeof(char));
tag = pcmk__assert_alloc(key_len, sizeof(char));
do {
// Look for /NEXT_COMPONENT/REMAINING_COMPONENTS
rc = sscanf(current, "/%[^/]%s", section, remainder);
if (rc > 0) {
// Separate FIRST_COMPONENT into TAG[@id='ID']
int f = sscanf(section, "%[^[][@" PCMK_XA_ID "='%[^']", tag, id);
int current_position = -1;
/* The target position is for the final component tag, so only use
* it if there is nothing left to search after this component.
*/
if ((rc == 1) && (target_position >= 0)) {
current_position = target_position;
}
switch (f) {
case 1:
target = first_matching_xml_child(target, tag, NULL,
current_position);
break;
case 2:
target = first_matching_xml_child(target, tag, id,
current_position);
break;
default:
// This should not be possible
target = NULL;
break;
}
current = remainder;
}
// Continue if something remains to search, and we've matched so far
} while ((rc == 2) && target);
if (target) {
crm_trace("Found %s for %s",
(path = (char *) xmlGetNodePath(target)), key);
free(path);
} else {
crm_debug("No match for %s", key);
}
free(remainder);
free(section);
free(tag);
free(id);
return target;
}
typedef struct xml_change_obj_s {
const xmlNode *change;
xmlNode *match;
} xml_change_obj_t;
static gint
sort_change_obj_by_position(gconstpointer a, gconstpointer b)
{
const xml_change_obj_t *change_obj_a = a;
const xml_change_obj_t *change_obj_b = b;
int position_a = -1;
int position_b = -1;
crm_element_value_int(change_obj_a->change, PCMK_XE_POSITION, &position_a);
crm_element_value_int(change_obj_b->change, PCMK_XE_POSITION, &position_b);
if (position_a < position_b) {
return -1;
} else if (position_a > position_b) {
return 1;
}
return 0;
}
/*!
* \internal
* \brief Apply a version 2 patchset to an XML node
*
* \param[in,out] xml XML to apply patchset to
* \param[in] patchset Patchset to apply
*
* \return Standard Pacemaker return code
*/
static int
apply_v2_patchset(xmlNode *xml, const xmlNode *patchset)
{
int rc = pcmk_rc_ok;
const xmlNode *change = NULL;
GList *change_objs = NULL;
GList *gIter = NULL;
for (change = pcmk__xml_first_child(patchset); change != NULL;
change = pcmk__xml_next(change)) {
xmlNode *match = NULL;
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
const char *xpath = crm_element_value(change, PCMK_XA_PATH);
int position = -1;
if (op == NULL) {
continue;
}
crm_trace("Processing %s %s", change->name, op);
/* PCMK_VALUE_DELETE changes for XML comments are generated with
* PCMK_XE_POSITION
*/
if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
crm_element_value_int(change, PCMK_XE_POSITION, &position);
}
match = search_v2_xpath(xml, xpath, position);
crm_trace("Performing %s on %s with %p", op, xpath, match);
if ((match == NULL) && (strcmp(op, PCMK_VALUE_DELETE) == 0)) {
crm_debug("No %s match for %s in %p", op, xpath, xml->doc);
continue;
} else if (match == NULL) {
crm_err("No %s match for %s in %p", op, xpath, xml->doc);
rc = pcmk_rc_diff_failed;
continue;
} else if (pcmk__str_any_of(op,
PCMK_VALUE_CREATE, PCMK_VALUE_MOVE, NULL)) {
// Delay the adding of a PCMK_VALUE_CREATE object
xml_change_obj_t *change_obj =
pcmk__assert_alloc(1, sizeof(xml_change_obj_t));
change_obj->change = change;
change_obj->match = match;
change_objs = g_list_append(change_objs, change_obj);
if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
// Temporarily put the PCMK_VALUE_MOVE object after the last sibling
if ((match->parent != NULL) && (match->parent->last != NULL)) {
xmlAddNextSibling(match->parent->last, match);
}
}
} else if (strcmp(op, PCMK_VALUE_DELETE) == 0) {
pcmk__xml_free(match);
} else if (strcmp(op, PCMK_VALUE_MODIFY) == 0) {
const xmlNode *child = pcmk__xe_first_child(change,
PCMK_XE_CHANGE_RESULT,
NULL, NULL);
const xmlNode *attrs = pcmk__xml_first_child(child);
if (attrs == NULL) {
rc = ENOMSG;
continue;
}
// Remove all attributes
pcmk__xe_remove_matching_attrs(match, false, NULL, NULL);
for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL;
pIter = pIter->next) {
const char *name = (const char *) pIter->name;
const char *value = pcmk__xml_attr_value(pIter);
crm_xml_add(match, name, value);
}
} else {
crm_err("Unknown operation: %s", op);
rc = pcmk_rc_diff_failed;
}
}
// Changes should be generated in the right order. Double checking.
change_objs = g_list_sort(change_objs, sort_change_obj_by_position);
for (gIter = change_objs; gIter; gIter = gIter->next) {
xml_change_obj_t *change_obj = gIter->data;
xmlNode *match = change_obj->match;
const char *op = NULL;
const char *xpath = NULL;
change = change_obj->change;
op = crm_element_value(change, PCMK_XA_OPERATION);
xpath = crm_element_value(change, PCMK_XA_PATH);
crm_trace("Continue performing %s on %s with %p", op, xpath, match);
if (strcmp(op, PCMK_VALUE_CREATE) == 0) {
int position = 0;
xmlNode *child = NULL;
xmlNode *match_child = NULL;
match_child = match->children;
crm_element_value_int(change, PCMK_XE_POSITION, &position);
while ((match_child != NULL)
&& (position != pcmk__xml_position(match_child, pcmk__xf_skip))) {
match_child = match_child->next;
}
child = pcmk__xml_copy(match, change->children);
if (match_child != NULL) {
crm_trace("Adding %s at position %d", child->name, position);
xmlAddPrevSibling(match_child, child);
} else {
crm_trace("Adding %s at position %d (end)",
child->name, position);
}
} else if (strcmp(op, PCMK_VALUE_MOVE) == 0) {
int position = 0;
crm_element_value_int(change, PCMK_XE_POSITION, &position);
if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
xmlNode *match_child = NULL;
int p = position;
if (p > pcmk__xml_position(match, pcmk__xf_skip)) {
p++; // Skip ourselves
}
pcmk__assert(match->parent != NULL);
match_child = match->parent->children;
while ((match_child != NULL)
&& (p != pcmk__xml_position(match_child, pcmk__xf_skip))) {
match_child = match_child->next;
}
crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)",
match->name, position,
pcmk__xml_position(match, pcmk__xf_skip),
match->prev, (match_child? "next":"last"),
(match_child? match_child : match->parent->last));
if (match_child) {
xmlAddPrevSibling(match_child, match);
} else {
pcmk__assert(match->parent->last != NULL);
xmlAddNextSibling(match->parent->last, match);
}
} else {
crm_trace("%s is already in position %d",
match->name, position);
}
if (position != pcmk__xml_position(match, pcmk__xf_skip)) {
crm_err("Moved %s.%s to position %d instead of %d (%p)",
match->name, pcmk__xe_id(match),
pcmk__xml_position(match, pcmk__xf_skip),
position, match->prev);
rc = pcmk_rc_diff_failed;
}
}
}
g_list_free_full(change_objs, free);
return rc;
}
int
-xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version)
+xml_apply_patchset(xmlNode *xml, const xmlNode *patchset, bool check_version)
{
int format = 1;
int rc = pcmk_ok;
xmlNode *old = NULL;
const char *digest = NULL;
if (patchset == NULL) {
return rc;
}
pcmk__log_xml_patchset(LOG_TRACE, patchset);
if (check_version) {
- rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset));
+ rc = pcmk_rc2legacy(check_patchset_versions(xml, patchset));
if (rc != pcmk_ok) {
return rc;
}
}
digest = crm_element_value(patchset, PCMK__XA_DIGEST);
if (digest != NULL) {
/* Make original XML available for logging in case result doesn't have
* expected digest
*/
pcmk__if_tracing(old = pcmk__xml_copy(NULL, xml), {});
}
if (rc == pcmk_ok) {
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_err("Unknown patch format: %d", format);
rc = -EINVAL;
} else {
rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset));
}
}
if ((rc == pcmk_ok) && (digest != NULL)) {
char *new_digest = NULL;
new_digest = pcmk__digest_xml(xml, true);
if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) {
crm_info("v%d digest mis-match: expected %s, calculated %s",
format, digest, new_digest);
rc = -pcmk_err_diff_failed;
pcmk__if_tracing(
{
save_xml_to_file(old, "PatchDigest:input", NULL);
save_xml_to_file(xml, "PatchDigest:result", NULL);
save_xml_to_file(patchset, "PatchDigest:diff", NULL);
},
{}
);
} else {
crm_trace("v%d digest matched: expected %s, calculated %s",
format, digest, new_digest);
}
free(new_digest);
}
pcmk__xml_free(old);
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().
*
* \retval \c true if \p element was modified
* \retval \c false otherwise
*/
bool
pcmk__cib_element_in_patchset(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;
int format = 1;
pcmk__assert(patchset != NULL);
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_warn("Unknown patch format: %d", format);
return false;
}
CRM_CHECK(element_xpath != NULL, return false); // Unsupported element
/* Matches if and only if element_xpath is part of a changed path
* (supported values for element never contain XML IDs with schema
* validation enabled)
*
* @TODO Use POSIX word boundary instead of (/|$), if it works:
* https://www.regular-expressions.info/wordboundaries.html.
*/
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(change, PCMK_XE_CHANGE)) {
const char *op = crm_element_value(change, PCMK_XA_OPERATION);
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;
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include
// Return value of true means failure; false means success
bool
xml_patch_versions(const xmlNode *patchset, int add[3], int del[3])
{
- static const char *const vfields[] = {
- PCMK_XA_ADMIN_EPOCH,
- PCMK_XA_EPOCH,
- PCMK_XA_NUM_UPDATES,
- };
-
const xmlNode *version = pcmk__xe_first_child(patchset, PCMK_XE_VERSION,
NULL, NULL);
const xmlNode *source = pcmk__xe_first_child(version, PCMK_XE_SOURCE, NULL,
NULL);
const xmlNode *target = pcmk__xe_first_child(version, PCMK_XE_TARGET, NULL,
NULL);
int format = 1;
crm_element_value_int(patchset, PCMK_XA_FORMAT, &format);
if (format != 2) {
crm_err("Unknown patch format: %d", format);
return true;
}
if (source != NULL) {
for (int i = 0; i < PCMK__NELEM(vfields); i++) {
crm_element_value_int(source, vfields[i], &(del[i]));
crm_trace("Got %d for del[%s]", del[i], vfields[i]);
}
}
if (target != NULL) {
for (int i = 0; i < PCMK__NELEM(vfields); i++) {
crm_element_value_int(target, vfields[i], &(add[i]));
crm_trace("Got %d for add[%s]", add[i], vfields[i]);
}
}
return false;
}
void
patchset_process_digest(xmlNode *patch, const xmlNode *source,
const xmlNode *target, bool with_digest)
{
char *digest = NULL;
if ((patch == NULL) || (source == NULL) || (target == NULL)
|| !with_digest) {
return;
}
CRM_LOG_ASSERT(!pcmk__xml_doc_all_flags_set(target->doc, pcmk__xf_dirty));
digest = pcmk__digest_xml(target, true);
crm_xml_add(patch, PCMK__XA_DIGEST, digest);
free(digest);
return;
}
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/lib/common/xml_display.c b/lib/common/xml_display.c
index d886d34c4b..d6865f7474 100644
--- a/lib/common/xml_display.c
+++ b/lib/common/xml_display.c
@@ -1,391 +1,396 @@
/*
- * Copyright 2004-2024 the Pacemaker project contributors
+ * Copyright 2004-2025 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 // PCMK__XML_LOG_BASE, etc.
#include "crmcommon_private.h"
static int show_xml_node(pcmk__output_t *out, GString *buffer,
const char *prefix, const xmlNode *data, int depth,
uint32_t options);
// Log an XML library error
void
pcmk__log_xmllib_err(void *ctx, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
pcmk__if_tracing(
{
PCMK__XML_LOG_BASE(LOG_ERR, TRUE,
crm_abort(__FILE__, __PRETTY_FUNCTION__,
__LINE__, "xml library error", TRUE,
TRUE),
"XML Error: ", fmt, ap);
},
{
PCMK__XML_LOG_BASE(LOG_ERR, TRUE, 0, "XML Error: ", fmt, ap);
}
);
va_end(ap);
}
/*!
* \internal
* \brief Output an XML comment with depth-based indentation
*
* \param[in,out] out Output object
* \param[in] data XML node to output
* \param[in] depth Current indentation level
* \param[in] options Group of \p pcmk__xml_fmt_options flags
*
* \return Standard Pacemaker return code
*
* \note This currently produces output only for text-like output objects.
*/
static int
show_xml_comment(pcmk__output_t *out, const xmlNode *data, int depth,
uint32_t options)
{
if (pcmk_is_set(options, pcmk__xml_fmt_open)) {
int width = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0;
return out->info(out, "%*s",
width, "", (const char *) data->content);
}
return pcmk_rc_no_output;
}
/*!
* \internal
* \brief Output an XML element in a formatted way
*
* \param[in,out] out Output object
* \param[in,out] buffer Where to build output strings
* \param[in] prefix String to prepend to every line of output
* \param[in] data XML node to output
* \param[in] depth Current indentation level
* \param[in] options Group of \p pcmk__xml_fmt_options flags
*
* \return Standard Pacemaker return code
*
* \note This is a recursive helper function for \p show_xml_node().
* \note This currently produces output only for text-like output objects.
* \note \p buffer may be overwritten many times. The caller is responsible for
* freeing it using \p g_string_free() but should not rely on its
* contents.
*/
static int
show_xml_element(pcmk__output_t *out, GString *buffer, const char *prefix,
const xmlNode *data, int depth, uint32_t options)
{
int spaces = pcmk_is_set(options, pcmk__xml_fmt_pretty)? (2 * depth) : 0;
int rc = pcmk_rc_no_output;
if (pcmk_is_set(options, pcmk__xml_fmt_open)) {
const char *hidden = crm_element_value(data, PCMK__XA_HIDDEN);
g_string_truncate(buffer, 0);
for (int lpc = 0; lpc < spaces; lpc++) {
g_string_append_c(buffer, ' ');
}
pcmk__g_strcat(buffer, "<", data->name, NULL);
for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL;
attr = attr->next) {
xml_node_private_t *nodepriv = attr->_private;
const char *p_name = (const char *) attr->name;
const char *p_value = pcmk__xml_attr_value(attr);
gchar *p_copy = NULL;
- if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
+ if ((nodepriv == NULL)
+ || pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
continue;
}
if ((hidden != NULL) && (p_name[0] != '\0')
&& (strstr(hidden, p_name) != NULL)) {
p_value = "*****";
} else {
p_copy = pcmk__xml_escape(p_value, true);
p_value = p_copy;
}
pcmk__g_strcat(buffer, " ", p_name, "=\"",
pcmk__s(p_value, ""), "\"", NULL);
g_free(p_copy);
}
if ((data->children != NULL)
&& pcmk_is_set(options, pcmk__xml_fmt_children)) {
g_string_append_c(buffer, '>');
} else {
g_string_append(buffer, "/>");
}
rc = out->info(out, "%s%s%s",
pcmk__s(prefix, ""), pcmk__str_empty(prefix)? "" : " ",
buffer->str);
}
if (data->children == NULL) {
return rc;
}
if (pcmk_is_set(options, pcmk__xml_fmt_children)) {
for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
child = pcmk__xml_next(child)) {
int temp_rc = show_xml_node(out, buffer, prefix, child, depth + 1,
options
|pcmk__xml_fmt_open
|pcmk__xml_fmt_close);
rc = pcmk__output_select_rc(rc, temp_rc);
}
}
if (pcmk_is_set(options, pcmk__xml_fmt_close)) {
int temp_rc = out->info(out, "%s%s%*s%s>",
pcmk__s(prefix, ""),
pcmk__str_empty(prefix)? "" : " ",
spaces, "", data->name);
rc = pcmk__output_select_rc(rc, temp_rc);
}
return rc;
}
/*!
* \internal
* \brief Output an XML element or comment in a formatted way
*
* \param[in,out] out Output object
* \param[in,out] buffer Where to build output strings
* \param[in] prefix String to prepend to every line of output
* \param[in] data XML node to log
* \param[in] depth Current indentation level
* \param[in] options Group of \p pcmk__xml_fmt_options flags
*
* \return Standard Pacemaker return code
*
* \note This is a recursive helper function for \p pcmk__xml_show().
* \note This currently produces output only for text-like output objects.
* \note \p buffer may be overwritten many times. The caller is responsible for
* freeing it using \p g_string_free() but should not rely on its
* contents.
*/
static int
show_xml_node(pcmk__output_t *out, GString *buffer, const char *prefix,
const xmlNode *data, int depth, uint32_t options)
{
switch (data->type) {
case XML_COMMENT_NODE:
return show_xml_comment(out, data, depth, options);
case XML_ELEMENT_NODE:
return show_xml_element(out, buffer, prefix, data, depth, options);
default:
return pcmk_rc_no_output;
}
}
/*!
* \internal
* \brief Output an XML element or comment in a formatted way
*
* \param[in,out] out Output object
* \param[in] prefix String to prepend to every line of output
* \param[in] data XML node to output
* \param[in] depth Current nesting level
* \param[in] options Group of \p pcmk__xml_fmt_options flags
*
* \return Standard Pacemaker return code
*
* \note This currently produces output only for text-like output objects.
*/
int
pcmk__xml_show(pcmk__output_t *out, const char *prefix, const xmlNode *data,
int depth, uint32_t options)
{
int rc = pcmk_rc_no_output;
GString *buffer = NULL;
pcmk__assert(out != NULL);
CRM_CHECK(depth >= 0, depth = 0);
if (data == NULL) {
return rc;
}
/* Allocate a buffer once, for show_xml_node() to truncate and reuse in
* recursive calls
*/
buffer = g_string_sized_new(1024);
rc = show_xml_node(out, buffer, prefix, data, depth, options);
g_string_free(buffer, TRUE);
return rc;
}
/*!
* \internal
* \brief Output XML portions that have been marked as changed
*
* \param[in,out] out Output object
* \param[in] data XML node to output
* \param[in] depth Current indentation level
* \param[in] options Group of \p pcmk__xml_fmt_options flags
*
* \note This is a recursive helper for \p pcmk__xml_show_changes(), showing
* changes to \p data and its children.
* \note This currently produces output only for text-like output objects.
*/
static int
show_xml_changes_recursive(pcmk__output_t *out, const xmlNode *data, int depth,
uint32_t options)
{
/* @COMPAT: When log_data_element() is removed, we can remove the options
* argument here and instead hard-code pcmk__xml_log_pretty.
*/
xml_node_private_t *nodepriv = (xml_node_private_t *) data->_private;
int rc = pcmk_rc_no_output;
int temp_rc = pcmk_rc_no_output;
+ if (nodepriv == NULL) {
+ return pcmk_rc_no_output;
+ }
+
if (pcmk_all_flags_set(nodepriv->flags, pcmk__xf_dirty|pcmk__xf_created)) {
// Newly created
return pcmk__xml_show(out, PCMK__XML_PREFIX_CREATED, data, depth,
options
|pcmk__xml_fmt_open
|pcmk__xml_fmt_children
|pcmk__xml_fmt_close);
}
if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
// Modified or moved
bool pretty = pcmk_is_set(options, pcmk__xml_fmt_pretty);
int spaces = pretty? (2 * depth) : 0;
const char *prefix = PCMK__XML_PREFIX_MODIFIED;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
prefix = PCMK__XML_PREFIX_MOVED;
}
// Log opening tag
rc = pcmk__xml_show(out, prefix, data, depth,
options|pcmk__xml_fmt_open);
// Log changes to attributes
for (const xmlAttr *attr = pcmk__xe_first_attr(data); attr != NULL;
attr = attr->next) {
const char *name = (const char *) attr->name;
nodepriv = attr->_private;
if (pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) {
const char *value = pcmk__xml_attr_value(attr);
temp_rc = out->info(out, "%s %*s @%s=%s",
PCMK__XML_PREFIX_DELETED, spaces, "", name,
value);
} else if (pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) {
const char *value = pcmk__xml_attr_value(attr);
if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) {
prefix = PCMK__XML_PREFIX_CREATED;
} else if (pcmk_is_set(nodepriv->flags, pcmk__xf_modified)) {
prefix = PCMK__XML_PREFIX_MODIFIED;
} else if (pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) {
prefix = PCMK__XML_PREFIX_MOVED;
} else {
prefix = PCMK__XML_PREFIX_MODIFIED;
}
temp_rc = out->info(out, "%s %*s @%s=%s",
prefix, spaces, "", name, value);
}
rc = pcmk__output_select_rc(rc, temp_rc);
}
// Log changes to children
for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
child = pcmk__xml_next(child)) {
temp_rc = show_xml_changes_recursive(out, child, depth + 1,
options);
rc = pcmk__output_select_rc(rc, temp_rc);
}
// Log closing tag
temp_rc = pcmk__xml_show(out, PCMK__XML_PREFIX_MODIFIED, data, depth,
options|pcmk__xml_fmt_close);
return pcmk__output_select_rc(rc, temp_rc);
}
// This node hasn't changed, but check its children
for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL;
child = pcmk__xml_next(child)) {
temp_rc = show_xml_changes_recursive(out, child, depth + 1, options);
rc = pcmk__output_select_rc(rc, temp_rc);
}
return rc;
}
/*!
* \internal
* \brief Output changes to an XML node and any children
*
* \param[in,out] out Output object
* \param[in] xml XML node to output
*
* \return Standard Pacemaker return code
*
* \note This currently produces output only for text-like output objects.
*/
int
pcmk__xml_show_changes(pcmk__output_t *out, const xmlNode *xml)
{
xml_doc_private_t *docpriv = NULL;
int rc = pcmk_rc_no_output;
int temp_rc = pcmk_rc_no_output;
pcmk__assert((out != NULL) && (xml != NULL) && (xml->doc != NULL));
docpriv = xml->doc->_private;
if (!pcmk_is_set(docpriv->flags, pcmk__xf_dirty)) {
return rc;
}
for (const GList *iter = docpriv->deleted_objs; iter != NULL;
iter = iter->next) {
const pcmk__deleted_xml_t *deleted_obj = iter->data;
if (deleted_obj->position >= 0) {
temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s (%d)",
deleted_obj->path, deleted_obj->position);
} else {
temp_rc = out->info(out, PCMK__XML_PREFIX_DELETED " %s",
deleted_obj->path);
}
rc = pcmk__output_select_rc(rc, temp_rc);
}
temp_rc = show_xml_changes_recursive(out, xml, 0, pcmk__xml_fmt_pretty);
return pcmk__output_select_rc(rc, temp_rc);
}
diff --git a/lib/common/xml_element.c b/lib/common/xml_element.c
index 3b97e0f37c..1875efd50a 100644
--- a/lib/common/xml_element.c
+++ b/lib/common/xml_element.c
@@ -1,1603 +1,1612 @@
/*
* Copyright 2004-2025 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 // va_start(), etc.
#include // uint32_t
#include // NULL, etc.
#include // free(), etc.
#include // strchr(), etc.
#include // time_t, etc.
#include // xmlNode, etc.
#include // xmlValidateNameValue()
#include // xmlChar
#include
#include // crm_xml_add(), etc.
#include // pcmk_rc_ok, etc.
#include
#include "crmcommon_private.h"
/*!
* \internal
* \brief Find first XML child element matching given criteria
*
* \param[in] parent XML element to search (can be \c NULL)
* \param[in] node_name If not \c NULL, only match children of this type
* \param[in] attr_n If not \c NULL, only match children with an attribute
* of this name.
* \param[in] attr_v If \p attr_n and this are not NULL, only match children
* with an attribute named \p attr_n and this value
*
* \return Matching XML child element, or \c NULL if none found
*/
xmlNode *
pcmk__xe_first_child(const xmlNode *parent, const char *node_name,
const char *attr_n, const char *attr_v)
{
xmlNode *child = NULL;
CRM_CHECK((attr_v == NULL) || (attr_n != NULL), return NULL);
if (parent == NULL) {
return NULL;
}
child = parent->children;
while ((child != NULL) && (child->type != XML_ELEMENT_NODE)) {
child = child->next;
}
for (; child != NULL; child = pcmk__xe_next(child, NULL)) {
const char *value = NULL;
if ((node_name != NULL) && !pcmk__xe_is(child, node_name)) {
// Node name mismatch
continue;
}
if (attr_n == NULL) {
// No attribute match needed
return child;
}
value = crm_element_value(child, attr_n);
if ((attr_v == NULL) && (value != NULL)) {
// attr_v == NULL: Attribute attr_n must be set (to any value)
return child;
}
if ((attr_v != NULL) && (pcmk__str_eq(value, attr_v, pcmk__str_none))) {
// attr_v != NULL: Attribute attr_n must be set to value attr_v
return child;
}
}
if (attr_n == NULL) {
crm_trace("%s XML has no child element of %s type",
(const char *) parent->name, pcmk__s(node_name, "any"));
} else {
crm_trace("%s XML has no child element of %s type with %s='%s'",
(const char *) parent->name, pcmk__s(node_name, "any"),
attr_n, attr_v);
}
return NULL;
}
/*!
* \internal
* \brief Return next sibling element of an XML element
*
* \param[in] xml XML element to check
* \param[in] element_name If not NULL, get next sibling with this element name
*
* \return Next desired sibling of \p xml (or NULL if none)
*/
xmlNode *
pcmk__xe_next(const xmlNode *xml, const char *element_name)
{
for (xmlNode *next = (xml == NULL)? NULL : xml->next;
next != NULL; next = next->next) {
if ((next->type == XML_ELEMENT_NODE)
&& ((element_name == NULL) || pcmk__xe_is(next, element_name))) {
return next;
}
}
return NULL;
}
/*!
* \internal
* \brief Parse an integer score from an XML attribute
*
* \param[in] xml XML element with attribute to parse
* \param[in] name Name of attribute to parse
* \param[out] score Where to store parsed score (can be NULL to
* just validate)
* \param[in] default_score What to return if the attribute value is not
* present or invalid
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_get_score(const xmlNode *xml, const char *name, int *score,
int default_score)
{
const char *value = NULL;
CRM_CHECK((xml != NULL) && (name != NULL), return EINVAL);
value = crm_element_value(xml, name);
return pcmk_parse_score(value, score, default_score);
}
/*!
* \internal
* \brief Set an XML attribute, expanding \c ++ and \c += where appropriate
*
* If \p target already has an attribute named \p name set to an integer value
* and \p value is an addition assignment expression on \p name, then expand
* \p value to an integer and set attribute \p name to the expanded value in
* \p target.
*
* Otherwise, set attribute \p name on \p target using the literal \p value.
*
* The original attribute value in \p target and the number in an assignment
* expression in \p value are parsed and added as scores (that is, their values
* are capped at \c INFINITY and \c -INFINITY). For more details, refer to
* \c pcmk_parse_score().
*
* For example, suppose \p target has an attribute named \c "X" with value
* \c "5", and that \p name is \c "X".
* * If \p value is \c "X++", the new value of \c "X" in \p target is \c "6".
* * If \p value is \c "X+=3", the new value of \c "X" in \p target is \c "8".
* * If \p value is \c "val", the new value of \c "X" in \p target is \c "val".
* * If \p value is \c "Y++", the new value of \c "X" in \p target is \c "Y++".
*
* \param[in,out] target XML node whose attribute to set
* \param[in] name Name of the attribute to set
* \param[in] value New value of attribute to set (if NULL, initial value
* will be left unchanged)
*
* \return Standard Pacemaker return code (specifically, \c EINVAL on invalid
* argument, or \c pcmk_rc_ok otherwise)
*/
int
pcmk__xe_set_score(xmlNode *target, const char *name, const char *value)
{
const char *old_value = NULL;
CRM_CHECK((target != NULL) && (name != NULL), return EINVAL);
if (value == NULL) {
// @TODO Maybe instead delete the attribute or set it to 0
return pcmk_rc_ok;
}
old_value = crm_element_value(target, name);
// If no previous value, skip to default case and set the value unexpanded.
if (old_value != NULL) {
const char *n = name;
const char *v = value;
// Stop at first character that differs between name and value
for (; (*n == *v) && (*n != '\0'); n++, v++);
// If value begins with name followed by a "++" or "+="
if ((*n == '\0')
&& (*v++ == '+')
&& ((*v == '+') || (*v == '='))) {
int add = 1;
int old_value_i = 0;
int rc = pcmk_rc_ok;
// If we're expanding ourselves, no previous value was set; use 0
if (old_value != value) {
rc = pcmk_parse_score(old_value, &old_value_i, 0);
if (rc != pcmk_rc_ok) {
// @TODO This is inconsistent with old_value==NULL
crm_trace("Using 0 before incrementing %s because '%s' "
"is not a score", name, old_value);
}
}
/* value="X++": new value of X is old_value + 1
* value="X+=Y": new value of X is old_value + Y (for some number Y)
*/
if (*v != '+') {
rc = pcmk_parse_score(++v, &add, 0);
if (rc != pcmk_rc_ok) {
// @TODO We should probably skip expansion instead
crm_trace("Not incrementing %s because '%s' does not have "
"a valid increment", name, value);
}
}
crm_xml_add_int(target, name, pcmk__add_scores(old_value_i, add));
return pcmk_rc_ok;
}
}
// Default case: set the attribute unexpanded (with value treated literally)
if (old_value != value) {
crm_xml_add(target, name, value);
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Copy XML attributes from a source element to a target element
*
* This is similar to \c xmlCopyPropList() except that attributes are marked
* as dirty for change tracking purposes.
*
* \param[in,out] target XML element to receive copied attributes from \p src
* \param[in] src XML element whose attributes to copy to \p target
* \param[in] flags Group of enum pcmk__xa_flags
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_copy_attrs(xmlNode *target, const xmlNode *src, uint32_t flags)
{
CRM_CHECK((src != NULL) && (target != NULL), return EINVAL);
for (xmlAttr *attr = pcmk__xe_first_attr(src); attr != NULL;
attr = attr->next) {
const char *name = (const char *) attr->name;
const char *value = pcmk__xml_attr_value(attr);
if (pcmk_is_set(flags, pcmk__xaf_no_overwrite)
&& (crm_element_value(target, name) != NULL)) {
continue;
}
if (pcmk_is_set(flags, pcmk__xaf_score_update)) {
pcmk__xe_set_score(target, name, value);
} else {
crm_xml_add(target, name, value);
}
}
return pcmk_rc_ok;
}
/*!
* \internal
* \brief Compare two XML attributes by name
*
* \param[in] a First XML attribute to compare
* \param[in] b Second XML attribute to compare
*
* \retval negative \c a->name is \c NULL or comes before \c b->name
* lexicographically
* \retval 0 \c a->name and \c b->name are equal
* \retval positive \c b->name is \c NULL or comes before \c a->name
* lexicographically
*/
static gint
compare_xml_attr(gconstpointer a, gconstpointer b)
{
const xmlAttr *attr_a = a;
const xmlAttr *attr_b = b;
return pcmk__strcmp((const char *) attr_a->name,
(const char *) attr_b->name, pcmk__str_none);
}
/*!
* \internal
* \brief Sort an XML element's attributes by name
*
* This does not consider ACLs and does not mark the attributes as deleted or
* dirty. Upon return, all attributes still exist and are set to the same values
* as before the call. The only thing that may change is the order of the
* attribute list.
*
* \param[in,out] xml XML element whose attributes to sort
*/
void
pcmk__xe_sort_attrs(xmlNode *xml)
{
GSList *attr_list = NULL;
for (xmlAttr *iter = pcmk__xe_first_attr(xml); iter != NULL;
iter = iter->next) {
attr_list = g_slist_prepend(attr_list, iter);
}
attr_list = g_slist_sort(attr_list, compare_xml_attr);
for (GSList *iter = attr_list; iter != NULL; iter = iter->next) {
xmlNode *attr = iter->data;
xmlUnlinkNode(attr);
xmlAddChild(xml, attr);
}
g_slist_free(attr_list);
}
/*!
* \internal
* \brief Remove a named attribute from an XML element
*
* \param[in,out] element XML element to remove an attribute from
* \param[in] name Name of attribute to remove
*/
void
pcmk__xe_remove_attr(xmlNode *element, const char *name)
{
if (name != NULL) {
pcmk__xa_remove(xmlHasProp(element, (const xmlChar *) name), false);
}
}
/*!
* \internal
* \brief Remove a named attribute from an XML element
*
* This is a wrapper for \c pcmk__xe_remove_attr() for use with
* \c pcmk__xml_tree_foreach().
*
* \param[in,out] xml XML element to remove an attribute from
* \param[in] user_data Name of attribute to remove
*
* \return \c true (to continue traversing the tree)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
bool
pcmk__xe_remove_attr_cb(xmlNode *xml, void *user_data)
{
const char *name = user_data;
pcmk__xe_remove_attr(xml, name);
return true;
}
/*!
* \internal
* \brief Remove an XML element's attributes that match some criteria
*
* \param[in,out] element XML element to modify
* \param[in] force If \c true, remove matching attributes immediately,
* ignoring ACLs and change tracking
* \param[in] match If not NULL, only remove attributes for which
* this function returns true
* \param[in,out] user_data Data to pass to \p match
*/
void
pcmk__xe_remove_matching_attrs(xmlNode *element, bool force,
bool (*match)(xmlAttrPtr, void *),
void *user_data)
{
xmlAttrPtr next = NULL;
for (xmlAttrPtr a = pcmk__xe_first_attr(element); a != NULL; a = next) {
next = a->next; // Grab now because attribute might get removed
if ((match == NULL) || match(a, user_data)) {
if (pcmk__xa_remove(a, force) != pcmk_rc_ok) {
return;
}
}
}
}
/*!
* \internal
* \brief Create a new XML element under a given parent
*
* \param[in,out] parent XML element that will be the new element's parent
* (\c NULL to create a new XML document with the new
* node as root)
* \param[in] name Name of new element
*
* \return Newly created XML element (guaranteed not to be \c NULL)
*/
xmlNode *
pcmk__xe_create(xmlNode *parent, const char *name)
{
xmlNode *node = NULL;
pcmk__assert(!pcmk__str_empty(name));
if (parent == NULL) {
xmlDoc *doc = pcmk__xml_new_doc();
node = xmlNewDocRawNode(doc, NULL, (const xmlChar *) name, NULL);
pcmk__mem_assert(node);
xmlDocSetRootElement(doc, node);
} else {
node = xmlNewChild(parent, NULL, (const xmlChar *) name, NULL);
pcmk__mem_assert(node);
}
pcmk__xml_new_private_data(node);
return node;
}
/*!
* \internal
* \brief Set a formatted string as an XML node's content
*
* \param[in,out] node Node whose content to set
* \param[in] format printf(3)-style format string
* \param[in] ... Arguments for \p format
*
* \note This function escapes special characters. \c xmlNodeSetContent() does
* not.
*/
G_GNUC_PRINTF(2, 3)
void
pcmk__xe_set_content(xmlNode *node, const char *format, ...)
{
if (node != NULL) {
const char *content = NULL;
char *buf = NULL;
/* xmlNodeSetContent() frees node->children and replaces it with new
* text. If this function is called for a node that already has a non-
* text child, it's a bug.
*/
CRM_CHECK((node->children == NULL)
|| (node->children->type == XML_TEXT_NODE),
return);
if (strchr(format, '%') == NULL) {
// Nothing to format
content = format;
} else {
va_list ap;
va_start(ap, format);
if (pcmk__str_eq(format, "%s", pcmk__str_none)) {
// No need to make a copy
content = va_arg(ap, const char *);
} else {
pcmk__assert(vasprintf(&buf, format, ap) >= 0);
content = buf;
}
va_end(ap);
}
xmlNodeSetContent(node, (const xmlChar *) content);
free(buf);
}
}
/*!
* \internal
* \brief Set a formatted string as an XML element's ID
*
* If the formatted string would not be a valid ID, it's first sanitized by
* \c pcmk__xml_sanitize_id().
*
* \param[in,out] node Node whose ID to set
* \param[in] format printf(3)-style format string
* \param[in] ... Arguments for \p format
*/
G_GNUC_PRINTF(2, 3)
void
pcmk__xe_set_id(xmlNode *node, const char *format, ...)
{
char *id = NULL;
va_list ap;
pcmk__assert(!pcmk__str_empty(format));
if (node == NULL) {
return;
}
va_start(ap, format);
pcmk__assert(vasprintf(&id, format, ap) >= 0);
va_end(ap);
if (!xmlValidateNameValue((const xmlChar *) id)) {
pcmk__xml_sanitize_id(id);
}
crm_xml_add(node, PCMK_XA_ID, id);
free(id);
}
/*!
* \internal
* \brief Add a "last written" attribute to an XML element, set to current time
*
* \param[in,out] xe XML element to add attribute to
*
* \return Value that was set, or NULL on error
*/
const char *
pcmk__xe_add_last_written(xmlNode *xe)
{
char *now_s = pcmk__epoch2str(NULL, 0);
const char *result = NULL;
result = crm_xml_add(xe, PCMK_XA_CIB_LAST_WRITTEN,
pcmk__s(now_s, "Could not determine current time"));
free(now_s);
return result;
}
/*!
* \internal
* \brief Merge one XML tree into another
*
* Here, "merge" means:
* 1. Copy attribute values from \p update to the target, overwriting in case of
* conflict.
* 2. Descend through \p update and the target in parallel. At each level, for
* each child of \p update, look for a matching child of the target.
* a. For each child, if a match is found, go to step 1, recursively merging
* the child of \p update into the child of the target.
* b. Otherwise, copy the child of \p update as a child of the target.
*
* A match is defined as the first child of the same type within the target,
* with:
* * the \c PCMK_XA_ID attribute matching, if set in \p update; otherwise,
* * the \c PCMK_XA_ID_REF attribute matching, if set in \p update
*
* This function does not delete any elements or attributes from the target. It
* may add elements or overwrite attributes, as described above.
*
* \param[in,out] parent If \p target is NULL and this is not, add or update
* child of this XML node that matches \p update
* \param[in,out] target If not NULL, update this XML
* \param[in] update Make the desired XML match this (must not be \c NULL)
* \param[in] flags Group of enum pcmk__xa_flags
*
* \note At least one of \p parent and \p target must be non-NULL.
* \note This function is recursive. For the top-level call, \p parent is
* \c NULL and \p target is not \c NULL. For recursive calls, \p target is
* \c NULL and \p parent is not \c NULL.
*/
static void
update_xe(xmlNode *parent, xmlNode *target, xmlNode *update, uint32_t flags)
{
// @TODO Try to refactor further, possibly using pcmk__xml_tree_foreach()
const char *update_name = NULL;
const char *update_id_attr = NULL;
const char *update_id_val = NULL;
char *trace_s = NULL;
crm_log_xml_trace(update, "update");
crm_log_xml_trace(target, "target");
CRM_CHECK(update != NULL, goto done);
if (update->type == XML_COMMENT_NODE) {
pcmk__xc_update(parent, target, update);
goto done;
}
update_name = (const char *) update->name;
CRM_CHECK(update_name != NULL, goto done);
CRM_CHECK((target != NULL) || (parent != NULL), goto done);
update_id_val = pcmk__xe_id(update);
if (update_id_val != NULL) {
update_id_attr = PCMK_XA_ID;
} else {
update_id_val = crm_element_value(update, PCMK_XA_ID_REF);
if (update_id_val != NULL) {
update_id_attr = PCMK_XA_ID_REF;
}
}
pcmk__if_tracing(
{
if (update_id_attr != NULL) {
trace_s = crm_strdup_printf("<%s %s=%s/>",
update_name, update_id_attr,
update_id_val);
} else {
trace_s = crm_strdup_printf("<%s/>", update_name);
}
},
{}
);
if (target == NULL) {
// Recursive call
target = pcmk__xe_first_child(parent, update_name, update_id_attr,
update_id_val);
}
if (target == NULL) {
// Recursive call with no existing matching child
target = pcmk__xe_create(parent, update_name);
crm_trace("Added %s", pcmk__s(trace_s, update_name));
} else {
// Either recursive call with match, or top-level call
crm_trace("Found node %s to update", pcmk__s(trace_s, update_name));
}
CRM_CHECK(pcmk__xe_is(target, (const char *) update->name), return);
pcmk__xe_copy_attrs(target, update, flags);
for (xmlNode *child = pcmk__xml_first_child(update); child != NULL;
child = pcmk__xml_next(child)) {
crm_trace("Updating child of %s", pcmk__s(trace_s, update_name));
update_xe(target, NULL, child, flags);
}
crm_trace("Finished with %s", pcmk__s(trace_s, update_name));
done:
free(trace_s);
}
/*!
* \internal
* \brief Delete an XML subtree if it matches a search element
*
* A match is defined as follows:
* * \p xml and \p user_data are both element nodes of the same type.
* * If \p user_data has attributes set, \p xml has those attributes set to the
* same values. (\p xml may have additional attributes set to arbitrary
* values.)
*
* \param[in,out] xml XML subtree to delete upon match
* \param[in] user_data Search element
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was deleted)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
delete_xe_if_matching(xmlNode *xml, void *user_data)
{
xmlNode *search = user_data;
if (!pcmk__xe_is(search, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
for (const xmlAttr *attr = pcmk__xe_first_attr(search); attr != NULL;
attr = attr->next) {
const char *search_val = pcmk__xml_attr_value(attr);
const char *xml_val = crm_element_value(xml, (const char *) attr->name);
if (!pcmk__str_eq(search_val, xml_val, pcmk__str_casei)) {
// No match: an attr in xml doesn't match the attr in search
return true;
}
}
crm_log_xml_trace(xml, "delete-match");
crm_log_xml_trace(search, "delete-search");
pcmk__xml_free(xml);
// Found a match and deleted it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and delete the first matching element
*
* This function does not attempt to match the tree root (\p xml).
*
* A match with a node \c node is defined as follows:
* * \c node and \p search are both element nodes of the same type.
* * If \p search has attributes set, \c node has those attributes set to the
* same values. (\c node may have additional attributes set to arbitrary
* values.)
*
* \param[in,out] xml XML subtree to search
* \param[in] search Element to match against
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful deletion and an error code otherwise)
*/
int
pcmk__xe_delete_match(xmlNode *xml, xmlNode *search)
{
// See @COMPAT comment in pcmk__xe_replace_match()
CRM_CHECK((xml != NULL) && (search != NULL), return EINVAL);
for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
xml = pcmk__xe_next(xml, NULL)) {
if (!pcmk__xml_tree_foreach(xml, delete_xe_if_matching, search)) {
// Found and deleted an element
return pcmk_rc_ok;
}
}
// No match found in this subtree
return ENXIO;
}
/*!
* \internal
* \brief Replace one XML node with a copy of another XML node
*
* This function handles change tracking and applies ACLs.
*
* \param[in,out] old XML node to replace
* \param[in] new XML node to copy as replacement for \p old
*
* \note This frees \p old.
*/
static void
replace_node(xmlNode *old, xmlNode *new)
{
// Pass old for its doc; it won't remain the parent of new
new = pcmk__xml_copy(old, new);
old = xmlReplaceNode(old, new);
// old == NULL means memory allocation error
pcmk__assert(old != NULL);
// May be unnecessary but avoids slight changes to some test outputs
pcmk__xml_tree_foreach(new, pcmk__xml_reset_node_flags, NULL);
if (pcmk__xml_doc_all_flags_set(new->doc, pcmk__xf_tracking)) {
// Replaced sections may have included relevant ACLs
pcmk__apply_acl(new);
}
pcmk__xml_mark_changes(old, new);
pcmk__xml_free_node(old);
}
/*!
* \internal
* \brief Replace one XML subtree with a copy of another if the two match
*
* A match is defined as follows:
* * \p xml and \p user_data are both element nodes of the same type.
* * If \p user_data has the \c PCMK_XA_ID attribute set, then \p xml has
* \c PCMK_XA_ID set to the same value.
*
* \param[in,out] xml XML subtree to replace with \p user_data upon match
* \param[in] user_data XML to replace \p xml with a copy of upon match
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was replaced by \p user_data)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
replace_xe_if_matching(xmlNode *xml, void *user_data)
{
xmlNode *replace = user_data;
const char *xml_id = NULL;
const char *replace_id = NULL;
xml_id = pcmk__xe_id(xml);
replace_id = pcmk__xe_id(replace);
if (!pcmk__xe_is(replace, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if ((replace_id != NULL)
&& !pcmk__str_eq(replace_id, xml_id, pcmk__str_none)) {
// No match: ID was provided in replace and doesn't match xml's ID
return true;
}
crm_log_xml_trace(xml, "replace-match");
crm_log_xml_trace(replace, "replace-with");
replace_node(xml, replace);
// Found a match and replaced it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and replace the first matching element
*
* This function does not attempt to match the tree root (\p xml).
*
* A match with a node \c node is defined as follows:
* * \c node and \p replace are both element nodes of the same type.
* * If \p replace has the \c PCMK_XA_ID attribute set, then \c node has
* \c PCMK_XA_ID set to the same value.
*
* \param[in,out] xml XML tree to search
* \param[in] replace XML to replace a matching element with a copy of
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful replacement and an error code otherwise)
*/
int
pcmk__xe_replace_match(xmlNode *xml, xmlNode *replace)
{
/* @COMPAT Some of this behavior (like not matching the tree root, which is
* allowed by pcmk__xe_update_match()) is questionable for general use but
* required for backward compatibility by cib_process_replace() and
* cib_process_delete(). Behavior can change at a major version release if
* desired.
*/
CRM_CHECK((xml != NULL) && (replace != NULL), return EINVAL);
for (xml = pcmk__xe_first_child(xml, NULL, NULL, NULL); xml != NULL;
xml = pcmk__xe_next(xml, NULL)) {
if (!pcmk__xml_tree_foreach(xml, replace_xe_if_matching, replace)) {
// Found and replaced an element
return pcmk_rc_ok;
}
}
// No match found in this subtree
return ENXIO;
}
//! User data for \c update_xe_if_matching()
struct update_data {
xmlNode *update; //!< Update source
uint32_t flags; //!< Group of enum pcmk__xa_flags
};
/*!
* \internal
* \brief Update one XML subtree with another if the two match
*
* "Update" means to merge a source subtree into a target subtree (see
* \c update_xe()).
*
* A match is defined as follows:
* * \p xml and \p user_data->update are both element nodes of the same type.
* * \p xml and \p user_data->update have the same \c PCMK_XA_ID attribute
* value, or \c PCMK_XA_ID is unset in both
*
* \param[in,out] xml XML subtree to update with \p user_data->update
* upon match
* \param[in] user_data struct update_data object
*
* \return \c true to continue traversing the tree, or \c false to stop (because
* \p xml was updated by \p user_data->update)
*
* \note This is compatible with \c pcmk__xml_tree_foreach().
*/
static bool
update_xe_if_matching(xmlNode *xml, void *user_data)
{
struct update_data *data = user_data;
xmlNode *update = data->update;
if (!pcmk__xe_is(update, (const char *) xml->name)) {
// No match: either not both elements, or different element types
return true;
}
if (!pcmk__str_eq(pcmk__xe_id(xml), pcmk__xe_id(update), pcmk__str_none)) {
// No match: ID mismatch
return true;
}
crm_log_xml_trace(xml, "update-match");
crm_log_xml_trace(update, "update-with");
update_xe(NULL, xml, update, data->flags);
// Found a match and replaced it; stop traversing tree
return false;
}
/*!
* \internal
* \brief Search an XML tree depth-first and update the first matching element
*
* "Update" means to merge a source subtree into a target subtree (see
* \c update_xe()).
*
* A match with a node \c node is defined as follows:
* * \c node and \p update are both element nodes of the same type.
* * \c node and \p update have the same \c PCMK_XA_ID attribute value, or
* \c PCMK_XA_ID is unset in both
*
* \param[in,out] xml XML tree to search
* \param[in] update XML to update a matching element with
* \param[in] flags Group of enum pcmk__xa_flags
*
* \return Standard Pacemaker return code (specifically, \c pcmk_rc_ok on
* successful update and an error code otherwise)
*/
int
pcmk__xe_update_match(xmlNode *xml, xmlNode *update, uint32_t flags)
{
/* @COMPAT In pcmk__xe_delete_match() and pcmk__xe_replace_match(), we
* compare IDs only if the equivalent of the update argument has an ID.
* Here, we're stricter: we consider it a mismatch if only one element has
* an ID attribute, or if both elements have IDs but they don't match.
*
* Perhaps we should align the behavior at a major version release.
*/
struct update_data data = {
.update = update,
.flags = flags,
};
CRM_CHECK((xml != NULL) && (update != NULL), return EINVAL);
if (!pcmk__xml_tree_foreach(xml, update_xe_if_matching, &data)) {
// Found and updated an element
return pcmk_rc_ok;
}
// No match found in this subtree
return ENXIO;
}
void
pcmk__xe_set_propv(xmlNodePtr node, va_list pairs)
{
while (true) {
const char *name, *value;
name = va_arg(pairs, const char *);
if (name == NULL) {
return;
}
value = va_arg(pairs, const char *);
if (value != NULL) {
crm_xml_add(node, name, value);
}
}
}
void
pcmk__xe_set_props(xmlNodePtr node, ...)
{
va_list pairs;
va_start(pairs, node);
pcmk__xe_set_propv(node, pairs);
va_end(pairs);
}
int
pcmk__xe_foreach_child(xmlNode *xml, const char *child_element_name,
int (*handler)(xmlNode *xml, void *userdata),
void *userdata)
{
xmlNode *children = (xml? xml->children : NULL);
pcmk__assert(handler != NULL);
for (xmlNode *node = children; node != NULL; node = node->next) {
if ((node->type == XML_ELEMENT_NODE)
&& ((child_element_name == NULL)
|| pcmk__xe_is(node, child_element_name))) {
int rc = handler(node, userdata);
if (rc != pcmk_rc_ok) {
return rc;
}
}
}
return pcmk_rc_ok;
}
// XML attribute handling
/*!
* \brief Create an XML attribute with specified name and value
*
* \param[in,out] node XML node to modify
* \param[in] name Attribute name to set
* \param[in] value Attribute value to set
*
* \return New value on success, \c NULL otherwise
* \note This does nothing if node, name, or value are \c NULL or empty.
*/
const char *
crm_xml_add(xmlNode *node, const char *name, const char *value)
{
// @TODO Replace with internal function that returns the new attribute
bool dirty = FALSE;
xmlAttr *attr = NULL;
CRM_CHECK(node != NULL, return NULL);
CRM_CHECK(name != NULL, return NULL);
if (value == NULL) {
return NULL;
}
if (pcmk__xml_doc_all_flags_set(node->doc, pcmk__xf_tracking)) {
const char *old = crm_element_value(node, name);
if (old == NULL || value == NULL || strcmp(old, value) != 0) {
dirty = TRUE;
}
}
if (dirty && (pcmk__check_acl(node, name, pcmk__xf_acl_create) == FALSE)) {
crm_trace("Cannot add %s=%s to %s", name, value, node->name);
return NULL;
}
attr = xmlSetProp(node, (const xmlChar *) name, (const xmlChar *) value);
/* If the attribute already exists, this does nothing. Attribute values
* don't get private data.
*/
pcmk__xml_new_private_data((xmlNode *) attr);
if (dirty) {
pcmk__mark_xml_attr_dirty(attr);
}
CRM_CHECK(attr && attr->children && attr->children->content, return NULL);
return (char *)attr->children->content;
}
/*!
* \brief Create an XML attribute with specified name and integer value
*
* This is like \c crm_xml_add() but taking an integer value.
*
* \param[in,out] node XML node to modify
* \param[in] name Attribute name to set
* \param[in] value Attribute value to set
*
* \return New value as string on success, \c NULL otherwise
* \note This does nothing if node or name are \c NULL or empty.
*/
const char *
crm_xml_add_int(xmlNode *node, const char *name, int value)
{
char *number = pcmk__itoa(value);
const char *added = crm_xml_add(node, name, number);
free(number);
return added;
}
/*!
* \brief Create an XML attribute with specified name and unsigned value
*
* This is like \c crm_xml_add() but taking a guint value.
*
* \param[in,out] node XML node to modify
* \param[in] name Attribute name to set
* \param[in] ms Attribute value to set
*
* \return New value as string on success, \c NULL otherwise
* \note This does nothing if node or name are \c NULL or empty.
*/
const char *
crm_xml_add_ms(xmlNode *node, const char *name, guint ms)
{
char *number = crm_strdup_printf("%u", ms);
const char *added = crm_xml_add(node, name, number);
free(number);
return added;
}
/*!
* \brief Create an XML attribute with specified name and long long int value
*
* This is like \c crm_xml_add() but taking a long long int value. It is a
* useful equivalent for defined types like time_t, etc.
*
* \param[in,out] xml XML node to modify
* \param[in] name Attribute name to set
* \param[in] value Attribute value to set
*
* \return New value as string on success, \c NULL otherwise
* \note This does nothing if \p xml or \p name is \c NULL or empty.
*/
const char *
crm_xml_add_ll(xmlNode *xml, const char *name, long long value)
{
char *str = crm_strdup_printf("%lld", value);
const char *result = crm_xml_add(xml, name, str);
free(str);
return result;
}
/*!
* \brief Create XML attributes for seconds and microseconds
*
* This is like \c crm_xml_add() but taking a struct timeval.
*
* \param[in,out] xml XML node to modify
* \param[in] name_sec Name of XML attribute for seconds
* \param[in] name_usec Name of XML attribute for microseconds (or NULL)
* \param[in] value Time value to set
*
* \return New seconds value as string on success, \c NULL otherwise
* \note This does nothing if xml, name_sec, or value is \c NULL.
*/
const char *
crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec,
const struct timeval *value)
{
const char *added = NULL;
if (xml && name_sec && value) {
added = crm_xml_add_ll(xml, name_sec, (long long) value->tv_sec);
if (added && name_usec) {
// Any error is ignored (we successfully added seconds)
crm_xml_add_ll(xml, name_usec, (long long) value->tv_usec);
}
}
return added;
}
/*!
* \brief Retrieve the value of an XML attribute
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
*
* \return Value of specified attribute (may be \c NULL)
*/
const char *
crm_element_value(const xmlNode *data, const char *name)
{
xmlAttr *attr = NULL;
if (data == NULL) {
crm_err("Couldn't find %s in NULL", name ? name : "");
CRM_LOG_ASSERT(data != NULL);
return NULL;
} else if (name == NULL) {
crm_err("Couldn't find NULL in %s", data->name);
return NULL;
}
attr = xmlHasProp(data, (const xmlChar *) name);
if (!attr || !attr->children) {
return NULL;
}
return (const char *) attr->children->content;
}
/*!
* \brief Retrieve the integer value of an XML attribute
*
* This is like \c crm_element_value() but getting the value as an integer.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store element value
*
* \return 0 on success, -1 otherwise
*/
int
crm_element_value_int(const xmlNode *data, const char *name, int *dest)
{
const char *value = NULL;
CRM_CHECK(dest != NULL, return -1);
value = crm_element_value(data, name);
if (value) {
long long value_ll;
int rc = pcmk__scan_ll(value, &value_ll, 0LL);
*dest = PCMK__PARSE_INT_DEFAULT;
if (rc != pcmk_rc_ok) {
crm_warn("Using default for %s "
"because '%s' is not a valid integer: %s",
name, value, pcmk_rc_str(rc));
} else if ((value_ll < INT_MIN) || (value_ll > INT_MAX)) {
crm_warn("Using default for %s because '%s' is out of range",
name, value);
} else {
*dest = (int) value_ll;
return 0;
}
}
return -1;
}
/*!
* \internal
* \brief Retrieve a flag group from an XML attribute value
*
* This is like \c crm_element_value() except getting the value as a 32-bit
* unsigned integer.
*
* \param[in] xml XML node to check
* \param[in] name Attribute name to check (must not be NULL)
* \param[out] dest Where to store flags (may be NULL to just
* validate type)
* \param[in] default_value What to use for missing or invalid value
*
* \return Standard Pacemaker return code
*/
int
pcmk__xe_get_flags(const xmlNode *xml, const char *name, uint32_t *dest,
uint32_t default_value)
{
const char *value = NULL;
long long value_ll = 0LL;
int rc = pcmk_rc_ok;
if (dest != NULL) {
*dest = default_value;
}
if (name == NULL) {
return EINVAL;
}
if (xml == NULL) {
return pcmk_rc_ok;
}
value = crm_element_value(xml, name);
if (value == NULL) {
return pcmk_rc_ok;
}
rc = pcmk__scan_ll(value, &value_ll, default_value);
if ((value_ll < 0) || (value_ll > UINT32_MAX)) {
value_ll = default_value;
if (rc == pcmk_rc_ok) {
rc = pcmk_rc_bad_input;
}
}
if (dest != NULL) {
*dest = (uint32_t) value_ll;
}
return rc;
}
/*!
* \brief Retrieve the long long integer value of an XML attribute
*
* This is like \c crm_element_value() but getting the value as a long long int.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store element value
*
* \return 0 on success, -1 otherwise
*/
int
crm_element_value_ll(const xmlNode *data, const char *name, long long *dest)
{
const char *value = NULL;
CRM_CHECK(dest != NULL, return -1);
value = crm_element_value(data, name);
if (value != NULL) {
int rc = pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT);
if (rc == pcmk_rc_ok) {
return 0;
}
crm_warn("Using default for %s "
"because '%s' is not a valid integer: %s",
name, value, pcmk_rc_str(rc));
}
return -1;
}
/*!
* \brief Retrieve the millisecond value of an XML attribute
*
* This is like \c crm_element_value() but returning the value as a guint.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store attribute value
*
* \return \c pcmk_ok on success, -1 otherwise
*/
int
crm_element_value_ms(const xmlNode *data, const char *name, guint *dest)
{
const char *value = NULL;
long long value_ll;
int rc = pcmk_rc_ok;
CRM_CHECK(dest != NULL, return -1);
*dest = 0;
value = crm_element_value(data, name);
rc = pcmk__scan_ll(value, &value_ll, 0LL);
if (rc != pcmk_rc_ok) {
crm_warn("Using default for %s "
"because '%s' is not valid milliseconds: %s",
name, value, pcmk_rc_str(rc));
return -1;
}
if ((value_ll < 0) || (value_ll > G_MAXUINT)) {
crm_warn("Using default for %s because '%s' is out of range",
name, value);
return -1;
}
*dest = (guint) value_ll;
return pcmk_ok;
}
/*!
* \brief Retrieve the seconds-since-epoch value of an XML attribute
*
* This is like \c crm_element_value() but returning the value as a time_t.
*
* \param[in] xml XML node to check
* \param[in] name Attribute name to check
* \param[out] dest Where to store attribute value
*
* \return \c pcmk_ok on success, -1 otherwise
*/
int
crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest)
{
long long value_ll = 0;
if (crm_element_value_ll(xml, name, &value_ll) < 0) {
return -1;
}
/* Unfortunately, we can't do any bounds checking, since time_t has neither
* standardized bounds nor constants defined for them.
*/
*dest = (time_t) value_ll;
return pcmk_ok;
}
/*!
* \brief Retrieve the value of XML second/microsecond attributes as time
*
* This is like \c crm_element_value() but returning value as a struct timeval.
*
* \param[in] xml XML to parse
* \param[in] name_sec Name of XML attribute for seconds
* \param[in] name_usec Name of XML attribute for microseconds
* \param[out] dest Where to store result
*
* \return \c pcmk_ok on success, -errno on error
* \note Values default to 0 if XML or XML attribute does not exist
*/
int
crm_element_value_timeval(const xmlNode *xml, const char *name_sec,
const char *name_usec, struct timeval *dest)
{
long long value_i = 0;
CRM_CHECK(dest != NULL, return -EINVAL);
dest->tv_sec = 0;
dest->tv_usec = 0;
if (xml == NULL) {
return pcmk_ok;
}
/* Unfortunately, we can't do any bounds checking, since there are no
* constants provided for the bounds of time_t and suseconds_t, and
* calculating them isn't worth the effort. If there are XML values
* beyond the native sizes, there will probably be worse problems anyway.
*/
// Parse seconds
errno = 0;
if (crm_element_value_ll(xml, name_sec, &value_i) < 0) {
return -errno;
}
dest->tv_sec = (time_t) value_i;
// Parse microseconds
if (crm_element_value_ll(xml, name_usec, &value_i) < 0) {
return -errno;
}
dest->tv_usec = (suseconds_t) value_i;
return pcmk_ok;
}
/*!
* \internal
* \brief Get a date/time object from an XML attribute value
*
* \param[in] xml XML with attribute to parse (from CIB)
* \param[in] attr Name of attribute to parse
* \param[out] t Where to create date/time object
* (\p *t must be NULL initially)
*
* \return Standard Pacemaker return code
* \note The caller is responsible for freeing \p *t using crm_time_free().
*/
int
pcmk__xe_get_datetime(const xmlNode *xml, const char *attr, crm_time_t **t)
{
const char *value = NULL;
if ((t == NULL) || (*t != NULL) || (xml == NULL) || (attr == NULL)) {
return EINVAL;
}
value = crm_element_value(xml, attr);
if (value != NULL) {
*t = crm_time_new(value);
if (*t == NULL) {
return pcmk_rc_unpack_error;
}
}
return pcmk_rc_ok;
}
/*!
* \brief Retrieve a copy of the value of an XML attribute
*
* This is like \c crm_element_value() but allocating new memory for the result.
*
* \param[in] data XML node to check
* \param[in] name Attribute name to check
*
* \return Value of specified attribute (may be \c NULL)
* \note The caller is responsible for freeing the result.
*/
char *
crm_element_value_copy(const xmlNode *data, const char *name)
{
return pcmk__str_copy(crm_element_value(data, name));
}
/*!
* \internal
* \brief Add a boolean attribute to an XML node.
*
* \param[in,out] node XML node to add attributes to
* \param[in] name XML attribute to create
* \param[in] value Value to give to the attribute
*/
void
pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value)
{
crm_xml_add(node, name, pcmk__btoa(value));
}
/*!
* \internal
* \brief Extract a boolean attribute's value from an XML element, with
* error checking
*
* \param[in] node XML node to get attribute from
* \param[in] name XML attribute to get
* \param[out] value Destination for the value of the attribute
*
* \return EINVAL if \p name or \p value are NULL, ENODATA if \p node is
* NULL or the attribute does not exist, pcmk_rc_unknown_format
* if the attribute is not a boolean, and pcmk_rc_ok otherwise.
*
* \note \p value only has any meaning if the return value is pcmk_rc_ok.
*/
int
pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value)
{
const char *xml_value = NULL;
int ret, rc;
if (node == NULL) {
return ENODATA;
} else if (name == NULL || value == NULL) {
return EINVAL;
}
xml_value = crm_element_value(node, name);
if (xml_value == NULL) {
return ENODATA;
}
rc = crm_str_to_boolean(xml_value, &ret);
if (rc == 1) {
*value = ret;
return pcmk_rc_ok;
} else {
return pcmk_rc_bad_input;
}
}
/*!
* \internal
* \brief Extract a boolean attribute's value from an XML element
*
* \param[in] node XML node to get attribute from
* \param[in] name XML attribute to get
*
* \return True if the given \p name is an attribute on \p node and has
* the value \c PCMK_VALUE_TRUE, False in all other cases
*/
bool
pcmk__xe_attr_is_true(const xmlNode *node, const char *name)
{
bool value = false;
int rc;
rc = pcmk__xe_get_bool_attr(node, name, &value);
return rc == pcmk_rc_ok && value == true;
}
// Deprecated functions kept only for backward API compatibility
// LCOV_EXCL_START
#include // gboolean, GSList
#include // pcmk_xml_attrs2nvpairs(), etc.
#include // crm_xml_sanitize_id()
#include
xmlNode *
expand_idref(xmlNode *input, xmlNode *top)
{
return pcmk__xe_resolve_idref(input, top);
}
void
crm_xml_set_id(xmlNode *xml, const char *format, ...)
{
va_list ap;
int len = 0;
char *id = NULL;
/* equivalent to crm_strdup_printf() */
va_start(ap, format);
len = vasprintf(&id, format, ap);
va_end(ap);
pcmk__assert(len > 0);
crm_xml_sanitize_id(id);
crm_xml_add(xml, PCMK_XA_ID, id);
free(id);
}
xmlNode *
sorted_xml(xmlNode *input, xmlNode *parent, gboolean recursive)
{
xmlNode *child = NULL;
GSList *nvpairs = NULL;
xmlNode *result = NULL;
CRM_CHECK(input != NULL, return NULL);
result = pcmk__xe_create(parent, (const char *) input->name);
nvpairs = pcmk_xml_attrs2nvpairs(input);
nvpairs = pcmk_sort_nvpairs(nvpairs);
pcmk_nvpairs2xml_attrs(nvpairs, result);
pcmk_free_nvpairs(nvpairs);
for (child = pcmk__xe_first_child(input, NULL, NULL, NULL); child != NULL;
child = pcmk__xe_next(child, NULL)) {
if (recursive) {
sorted_xml(child, result, recursive);
} else {
pcmk__xml_copy(result, child);
}
}
return result;
}
+const char *
+crm_copy_xml_element(const xmlNode *obj1, xmlNode *obj2, const char *element)
+{
+ const char *value = crm_element_value(obj1, element);
+
+ crm_xml_add(obj2, element, value);
+ return value;
+}
+
// LCOV_EXCL_STOP
// End deprecated API
diff --git a/tools/crm_diff.c b/tools/crm_diff.c
index 173e87f5ce..239afc4c0d 100644
--- a/tools/crm_diff.c
+++ b/tools/crm_diff.c
@@ -1,342 +1,376 @@
/*
* Copyright 2005-2025 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
-
-#define SUMMARY "Compare two Pacemaker configurations (in XML format) to produce a custom diff-like output, " \
- "or apply such an output as a patch"
+#include // bool, true
+#include // NULL, printf(), etc.
+#include // free()
+
+#include // GOption, etc.
+#include // xmlNode
+
+#include // xml_{create,apply}_patchset()
+
+#define SUMMARY "Compare two Pacemaker configurations (in XML format) to " \
+ "produce a custom diff-like output, or apply such an output " \
+ "as a patch"
+#define INDENT " "
struct {
- gboolean apply;
+ gchar *source_file;
+ gchar *target_file;
+ gchar *source_string;
+ gchar *target_string;
+ bool patch;
gboolean as_cib;
gboolean no_version;
- gboolean raw_original;
- gboolean raw_new;
- gboolean use_stdin;
- char *xml_file_original;
- char *xml_file_new;
+ gboolean use_stdin; //!< \deprecated
} options;
-gboolean new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
-gboolean original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
-gboolean patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error);
+static gboolean
+patch_cb(const gchar *option_name, const gchar *optarg, gpointer data,
+ GError **error)
+{
+ options.patch = true;
+ g_free(options.target_file);
+ options.target_file = g_strdup(optarg);
+ return TRUE;
+}
+/* @COMPAT Use last-one-wins for original/new/patch input sources
+ *
+ * @COMPAT Precedence is --original-string > --stdin > --original. --stdin is
+ * now deprecated and hidden, so we don't mention it in the help text.
+ */
static GOptionEntry original_xml_entries[] = {
- { "original", 'o', 0, G_OPTION_ARG_STRING, &options.xml_file_original,
- "XML is contained in the named file",
+ { "original", 'o', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+ &options.source_file,
+ "XML is contained in the named file. Currently --original-string\n"
+ INDENT "overrides this. In a future release, the last one specified\n"
+ INDENT "will be used.",
"FILE" },
- { "original-string", 'O', 0, G_OPTION_ARG_CALLBACK, original_string_cb,
- "XML is contained in the supplied string",
+ { "original-string", 'O', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+ &options.source_string,
+ "XML is contained in the supplied string. Currently this takes\n"
+ INDENT "precedence over --original. In a future release, the last one\n"
+ INDENT "specified will be used.",
"STRING" },
{ NULL }
};
+/* @COMPAT Precedence is --original-string > --stdin > --original. --stdin is
+ * now deprecated and hidden, so we don't mention it in the help text.
+ */
static GOptionEntry operation_entries[] = {
- { "new", 'n', 0, G_OPTION_ARG_STRING, &options.xml_file_new,
- "Compare the original XML to the contents of the named file",
+ { "new", 'n', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &options.target_file,
+ "Compare the original XML to the contents of the named file. Currently\n"
+ INDENT "--new-string overrides this. In a future release, the last one\n"
+ INDENT "specified will be used.",
"FILE" },
- { "new-string", 'N', 0, G_OPTION_ARG_CALLBACK, new_string_cb,
- "Compare the original XML with the contents of the supplied string",
+ { "new-string", 'N', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+ &options.target_string,
+ "Compare the original XML with the contents of the supplied string.\n"
+ INDENT "Currently this takes precedence over --patch and --new. In a\n"
+ INDENT "future release, the last one specified will be used.",
"STRING" },
- { "patch", 'p', 0, G_OPTION_ARG_CALLBACK, patch_cb,
- "Patch the original XML with the contents of the named file",
+ { "patch", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, patch_cb,
+ "Patch the original XML with the contents of the named file. Currently\n"
+ INDENT "--new-string and (if specified later) --new override the input\n"
+ INDENT "source specified here. In a future release, the last one\n"
+ INDENT "specified will be used. Note: even if this input source is\n"
+ INDENT "overridden, the input source will be applied as a patch to the\n"
+ INDENT "original XML.",
"FILE" },
{ NULL }
};
static GOptionEntry addl_entries[] = {
/* @FIXME Revisit --cib and --no-version and consider deprecating or
* renaming --cib, as well as improving the help text. --cib does things
* other than include version details. Also, even with --cib, we call
* xml_create_patchset() with manage_versions=false in generate_patch(). So
* we don't update the target versions in the patchset, and we don't drop
* the versions from the patchset unless --no-version is given.
*/
- { "cib", 'c', 0, G_OPTION_ARG_NONE, &options.as_cib,
- "Compare/patch the inputs as a CIB (includes versions details)",
+ { "cib", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.as_cib,
+ "Compare/patch the inputs as a CIB (includes version details)",
NULL },
- { "stdin", 's', 0, G_OPTION_ARG_NONE, &options.use_stdin,
- "",
+ { "no-version", 'u', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE,
+ &options.no_version,
+ "Generate the difference without version details",
NULL },
- { "no-version", 'u', 0, G_OPTION_ARG_NONE, &options.no_version,
- "Generate the difference without versions details",
+
+ // @COMPAT Deprecated
+ { "stdin", 's', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &options.use_stdin,
+ "Get the original XML and new (or patch) XML from stdin. Currently\n"
+ INDENT "--original-string and --new-string override this for original\n"
+ INDENT "and new/patch XML, respectively. In a future release, the last\n"
+ INDENT "one specified will be used.",
NULL },
{ NULL }
};
-gboolean
-new_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
- options.raw_new = TRUE;
- pcmk__str_update(&options.xml_file_new, optarg);
- return TRUE;
-}
-
-gboolean
-original_string_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
- options.raw_original = TRUE;
- pcmk__str_update(&options.xml_file_original, optarg);
- return TRUE;
-}
-
-gboolean
-patch_cb(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) {
- options.apply = TRUE;
- pcmk__str_update(&options.xml_file_new, optarg);
- return TRUE;
-}
-
-static void
-print_patch(xmlNode *patch)
-{
- GString *buffer = g_string_sized_new(1024);
-
- pcmk__xml_string(patch, pcmk__xml_fmt_pretty, buffer, 0);
-
- printf("%s", buffer->str);
- g_string_free(buffer, TRUE);
- fflush(stdout);
-}
-
-// \return Standard Pacemaker return code
-static int
-apply_patch(xmlNode *input, xmlNode *patch, gboolean as_cib)
-{
- xmlNode *output = pcmk__xml_copy(NULL, input);
- int rc = xml_apply_patchset(output, patch, as_cib);
-
- rc = pcmk_legacy2rc(rc);
- if (rc != pcmk_rc_ok) {
- fprintf(stderr, "Could not apply patch: %s\n", pcmk_rc_str(rc));
- pcmk__xml_free(output);
- return rc;
- }
-
- if (output != NULL) {
- char *buffer;
-
- print_patch(output);
-
- buffer = pcmk__digest_xml(output, true);
- crm_trace("Digest: %s", pcmk__s(buffer, "\n"));
- free(buffer);
- pcmk__xml_free(output);
- }
- return pcmk_rc_ok;
-}
-
-static void
-log_patch_cib_versions(xmlNode *patch)
-{
- int add[] = { 0, 0, 0 };
- int del[] = { 0, 0, 0 };
-
- const char *fmt = NULL;
- const char *digest = NULL;
-
- pcmk__xml_patchset_versions(patch, del, add);
- fmt = crm_element_value(patch, PCMK_XA_FORMAT);
- digest = crm_element_value(patch, PCMK__XA_DIGEST);
-
- if (add[2] != del[2] || add[1] != del[1] || add[0] != del[0]) {
- crm_info("Patch: --- %d.%d.%d %s", del[0], del[1], del[2], fmt);
- crm_info("Patch: +++ %d.%d.%d %s", add[0], add[1], add[2], digest);
- }
-}
-
-// \return Standard Pacemaker return code
+/*!
+ * \internal
+ * \brief Create an XML patchset from the given source and target XML trees
+ *
+ * \param[in,out] out Output object
+ * \param[in,out] source Source XML
+ * \param[in,out] target Target XML
+ * \param[in] as_cib If \c true, treat the XML trees as CIBs. In
+ * particular, ignore attribute position changes,
+ * include the target digest in the patchset, and log
+ * the source and target CIB versions.
+ * \param[in] no_version If \c true, ignore changes to the CIB version
+ * (must be \c false if \p as_cib is \c true)
+ *
+ * \return Standard Pacemaker return code
+ */
static int
-generate_patch(xmlNode *object_original, xmlNode *object_new, const char *xml_file_new,
- gboolean as_cib, gboolean no_version)
+generate_patch(pcmk__output_t *out, xmlNode *source, xmlNode *target,
+ bool as_cib, bool no_version)
{
- const char *vfields[] = {
+ static const char *const vfields[] = {
PCMK_XA_ADMIN_EPOCH,
PCMK_XA_EPOCH,
PCMK_XA_NUM_UPDATES,
};
- xmlNode *output = NULL;
+ xmlNode *patchset = NULL;
+ GString *buffer = NULL;
- /* If we're ignoring the version, make the version information
- * identical, so it isn't detected as a change. */
- if (no_version) {
- int lpc;
+ // Currently impossible; just a reminder for when we move to libpacemaker
+ pcmk__assert(!as_cib || !no_version);
- for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) {
- crm_copy_xml_element(object_original, object_new, vfields[lpc]);
+ /* If we're ignoring the version, make the version information identical, so
+ * it isn't detected as a change.
+ */
+ if (no_version) {
+ for (int i = 0; i < PCMK__NELEM(vfields); i++) {
+ crm_xml_add(target, vfields[i],
+ crm_element_value(source, vfields[i]));
}
}
if (as_cib) {
- pcmk__xml_doc_set_flags(object_new->doc, pcmk__xf_ignore_attr_pos);
+ pcmk__xml_doc_set_flags(target->doc, pcmk__xf_ignore_attr_pos);
}
- pcmk__xml_mark_changes(object_original, object_new);
- crm_log_xml_debug(object_new, (xml_file_new? xml_file_new: "target"));
+ pcmk__xml_mark_changes(source, target);
+ crm_log_xml_debug(target, "target");
- output = xml_create_patchset(0, object_original, object_new, NULL, FALSE);
+ patchset = xml_create_patchset(0, source, target, NULL, false);
- pcmk__log_xml_changes(LOG_INFO, object_new);
- pcmk__xml_commit_changes(object_new->doc);
+ pcmk__log_xml_changes(LOG_INFO, target);
+ pcmk__xml_commit_changes(target->doc);
- if (output == NULL) {
+ if (patchset == NULL) {
return pcmk_rc_ok; // No changes
}
if (as_cib) {
- pcmk__xml_patchset_add_digest(output, object_new);
- log_patch_cib_versions(output);
+ pcmk__xml_patchset_add_digest(patchset, target);
} else if (no_version) {
- pcmk__xml_free(pcmk__xe_first_child(output, PCMK_XE_VERSION, NULL,
+ pcmk__xml_free(pcmk__xe_first_child(patchset, PCMK_XE_VERSION, NULL,
NULL));
}
- pcmk__log_xml_patchset(LOG_NOTICE, output);
- print_patch(output);
- pcmk__xml_free(output);
+ pcmk__log_xml_patchset(LOG_NOTICE, patchset);
+
+ buffer = g_string_sized_new(1024);
+ pcmk__xml_string(patchset, pcmk__xml_fmt_pretty, buffer, 0);
+ out->output_xml(out, PCMK_XE_PATCHSET, buffer->str);
+
+ pcmk__xml_free(patchset);
+ g_string_free(buffer, TRUE);
/* pcmk_rc_error means there's a non-empty diff.
* @COMPAT Choose a more descriptive return code, like one that maps to
* CRM_EX_DIGEST?
*/
return pcmk_rc_error;
}
+static const pcmk__supported_format_t formats[] = {
+ PCMK__SUPPORTED_FORMAT_NONE,
+ PCMK__SUPPORTED_FORMAT_TEXT,
+ PCMK__SUPPORTED_FORMAT_XML,
+
+ { NULL, NULL, NULL }
+};
+
static GOptionContext *
-build_arg_context(pcmk__common_args_t *args) {
+build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
+{
GOptionContext *context = NULL;
const char *description = "Examples:\n\n"
"Obtain the two different configuration files by running cibadmin on the two cluster setups to compare:\n\n"
"\t# cibadmin --query > cib-old.xml\n\n"
"\t# cibadmin --query > cib-new.xml\n\n"
"Calculate and save the difference between the two files:\n\n"
"\t# crm_diff --original cib-old.xml --new cib-new.xml > patch.xml\n\n"
"Apply the patch to the original file:\n\n"
"\t# crm_diff --original cib-old.xml --patch patch.xml > updated.xml\n\n"
"Apply the patch to the running cluster:\n\n"
"\t# cibadmin --patch -x patch.xml\n";
- context = pcmk__build_arg_context(args, NULL, NULL, NULL);
+ context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
g_option_context_set_description(context, description);
pcmk__add_arg_group(context, "xml", "Original XML:",
"Show original XML options", original_xml_entries);
pcmk__add_arg_group(context, "operation", "Operation:",
"Show operation options", operation_entries);
pcmk__add_arg_group(context, "additional", "Additional Options:",
"Show additional options", addl_entries);
return context;
}
int
main(int argc, char **argv)
{
- xmlNode *object_original = NULL;
- xmlNode *object_new = NULL;
-
crm_exit_t exit_code = CRM_EX_OK;
+ int rc = pcmk_rc_ok;
+
+ xmlNode *source = NULL;
+ xmlNode *target = 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, "nopNO");
- GOptionContext *context = build_arg_context(args);
+ GOptionContext *context = build_arg_context(args, &output_group);
- int rc = pcmk_rc_ok;
+ 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_diff", args->verbosity);
- if (args->version) {
- g_strfreev(processed_args);
- pcmk__free_arg_context(context);
- /* FIXME: When crm_diff is converted to use formatted output, this can go. */
- pcmk__cli_help('v');
+ rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
+ if (rc != pcmk_rc_ok) {
+ exit_code = CRM_EX_ERROR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Error creating output format %s: %s", args->output_ty,
+ pcmk_rc_str(rc));
+ goto done;
}
- if (options.apply && options.no_version) {
- fprintf(stderr, "warning: -u/--no-version ignored with -p/--patch\n");
- } else if (options.as_cib && options.no_version) {
- fprintf(stderr, "error: -u/--no-version incompatible with -c/--cib\n");
- exit_code = CRM_EX_USAGE;
+ if (args->version) {
+ out->version(out, false);
goto done;
}
- if (options.raw_original) {
- object_original = pcmk__xml_parse(options.xml_file_original);
+ if (options.no_version) {
+ if (options.as_cib) {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "-u/--no-version incompatible with -c/--cib");
+ goto done;
+ }
+ if (options.patch) {
+ out->err(out, "Warning: -u/--no-version ignored with -p/--patch");
+ }
+ }
+
+ if (options.source_string != NULL) {
+ source = pcmk__xml_parse(options.source_string);
} else if (options.use_stdin) {
- fprintf(stderr, "Input first XML fragment:");
- object_original = pcmk__xml_read(NULL);
+ source = pcmk__xml_read(NULL);
+
+ } else if (options.source_file != NULL) {
+ source = pcmk__xml_read(options.source_file);
- } else if (options.xml_file_original != NULL) {
- object_original = pcmk__xml_read(options.xml_file_original);
+ } else {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Either --original or --original-string must be specified");
+ goto done;
}
- if (options.raw_new) {
- object_new = pcmk__xml_parse(options.xml_file_new);
+ if (options.target_string != NULL) {
+ target = pcmk__xml_parse(options.target_string);
} else if (options.use_stdin) {
- fprintf(stderr, "Input second XML fragment:");
- object_new = pcmk__xml_read(NULL);
+ target = pcmk__xml_read(NULL);
+
+ } else if (options.target_file != NULL) {
+ target = pcmk__xml_read(options.target_file);
- } else if (options.xml_file_new != NULL) {
- object_new = pcmk__xml_read(options.xml_file_new);
+ } else {
+ exit_code = CRM_EX_USAGE;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Either --new, --new-string, or --patch must be specified");
+ goto done;
}
- if (object_original == NULL) {
- fprintf(stderr, "Could not parse the first XML fragment\n");
+ if (source == NULL) {
exit_code = CRM_EX_DATAERR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Failed to parse original XML");
goto done;
}
- if (object_new == NULL) {
- fprintf(stderr, "Could not parse the second XML fragment\n");
+ if (target == NULL) {
exit_code = CRM_EX_DATAERR;
+ g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
+ "Failed to parse %s XML", (options.patch? "patch" : "new"));
goto done;
}
- if (options.apply) {
- rc = apply_patch(object_original, object_new, options.as_cib);
+ if (options.patch) {
+ rc = xml_apply_patchset(source, target, options.as_cib);
+ rc = pcmk_legacy2rc(rc);
+ if (rc != pcmk_rc_ok) {
+ g_set_error(&error, PCMK__RC_ERROR, rc,
+ "Could not apply patch: %s", pcmk_rc_str(rc));
+ } else {
+ GString *buffer = g_string_sized_new(1024);
+
+ pcmk__xml_string(source, pcmk__xml_fmt_pretty, buffer, 0);
+ out->output_xml(out, PCMK_XE_UPDATED, buffer->str);
+ g_string_free(buffer, TRUE);
+ }
+
} else {
- rc = generate_patch(object_original, object_new, options.xml_file_new, options.as_cib, options.no_version);
+ rc = generate_patch(out, source, target, options.as_cib,
+ options.no_version);
}
exit_code = pcmk_rc2exitc(rc);
done:
g_strfreev(processed_args);
pcmk__free_arg_context(context);
- free(options.xml_file_original);
- free(options.xml_file_new);
- pcmk__xml_free(object_original);
- pcmk__xml_free(object_new);
- pcmk__output_and_clear_error(&error, NULL);
+ g_free(options.source_file);
+ g_free(options.target_file);
+ g_free(options.source_string);
+ g_free(options.target_string);
+ pcmk__xml_free(source);
+ pcmk__xml_free(target);
+
+ pcmk__output_and_clear_error(&error, out);
+
+ if (out != NULL) {
+ out->finish(out, exit_code, true, NULL);
+ pcmk__output_free(out);
+ }
crm_exit(exit_code);
}
diff --git a/xml/Makefile.am b/xml/Makefile.am
index dcbd9db054..5b218a9148 100644
--- a/xml/Makefile.am
+++ b/xml/Makefile.am
@@ -1,287 +1,288 @@
#
# Copyright 2004-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
# This source code is licensed under the GNU General Public License version 2
# or later (GPLv2+) WITHOUT ANY WARRANTY.
#
include $(top_srcdir)/mk/common.mk
noarch_pkgconfig_DATA = $(builddir)/pacemaker-schemas.pc
# Pacemaker has 3 schemas: the CIB schema, the API schema (for command-line
# tool XML output), and a legacy schema for crm_mon --as-xml.
#
# See README.md for details on updating CIB schema files (API is similar)
# The CIB and crm_mon schemas are installed directly in PCMK_SCHEMA_DIR
# for historical reasons, while the API schema is installed in a subdirectory.
APIdir = $(PCMK_SCHEMA_DIR)/api
CIBdir = $(PCMK_SCHEMA_DIR)
MONdir = $(PCMK_SCHEMA_DIR)
basexsltdir = $(PCMK_SCHEMA_DIR)/base
dist_basexslt_DATA = $(srcdir)/base/access-render-2.xsl
# Extract a sorted list of available numeric schema versions
# from filenames like NAME-MAJOR[.MINOR][.MINOR-MINOR].rng
numeric_versions = $(shell ls -1 $(1) \
| sed -n -e 's/^.*-\([0-9][0-9.]*\).rng$$/\1/p' \
| sort -u -t. -k 1,1n -k 2,2n -k 3,3n)
version_pairs = $(join \
$(1),$(addprefix \
-,$(wordlist \
2,$(words $(1)),$(1) \
) \
) \
)
version_pairs_last = $(wordlist \
$(words \
$(wordlist \
2,$(1),$(2) \
) \
),$(1),$(2) \
)
# NOTE: All files in API_request_base, CIB_cfg_base, API_base, and CIB_base
# need to start with a unique prefix. These variables all get iterated over
# and globbed, and two files starting with the same prefix will cause
# problems.
# Names of API schemas that form the choices for pacemaker-result content
API_request_base = command-output \
crm_attribute \
+ crm_diff \
crm_error \
crm_mon \
crm_node \
crm_resource \
crm_rule \
crm_shadow \
crm_simulate \
crm_ticket \
crmadmin \
digests \
iso8601 \
pacemakerd \
stonith_admin \
version
# Names of CIB schemas that form the choices for cib/configuration content
CIB_cfg_base = options \
nodes \
resources \
constraints \
fencing \
acls \
tags \
alerts
# Names of all schemas (including top level and those included by others)
API_base = $(API_request_base) \
any-element \
failure \
fence-event \
generic-list \
instruction \
item \
node-attrs \
node-history \
nodes \
ocf-ra \
options \
patchset \
resources \
status \
subprocess-output \
ticket
CIB_base = cib \
$(CIB_cfg_base) \
status \
score \
rule \
nvset
# Static schema files and transforms (only CIB has transforms)
#
# This is more complicated than it should be due to the need to support
# VPATH builds and "make distcheck". We need the absolute paths for reliable
# substitution back and forth, and relative paths for distributed files.
API_abs_files = $(foreach base,$(API_base),$(wildcard $(abs_srcdir)/api/$(base)-*.rng))
CIB_abs_files = $(foreach base,$(CIB_base),$(wildcard $(abs_srcdir)/$(base).rng $(abs_srcdir)/$(base)-*.rng))
CIB_abs_xsl = $(abs_srcdir)/upgrade-1.3-0.xsl \
$(wildcard $(abs_srcdir)/upgrade-2.10-[0-2].xsl) \
$(wildcard $(abs_srcdir)/upgrade-3.10-*.xsl)
MON_abs_files = $(abs_srcdir)/crm_mon.rng
API_files = $(foreach base,$(API_base),$(wildcard $(srcdir)/api/$(base)-*.rng))
CIB_files = $(foreach base,$(CIB_base),$(wildcard $(srcdir)/$(base).rng $(srcdir)/$(base)-*.rng))
CIB_xsl = $(srcdir)/upgrade-1.3-0.xsl \
$(wildcard $(srcdir)/upgrade-2.10-[0-2].xsl) \
$(wildcard $(srcdir)/upgrade-3.10-*.xsl)
MON_files = $(srcdir)/crm_mon.rng
# Sorted lists of all schema versions
API_versions = $(call numeric_versions,${API_files})
CIB_versions = $(call numeric_versions,${CIB_files})
MON_versions = $(call numeric_versions,$(wildcard $(srcdir)/api/crm_mon*.rng))
# The highest numeric schema version
API_max ?= $(lastword $(API_versions))
CIB_max ?= $(lastword $(CIB_versions))
MON_max ?= $(lastword $(MON_versions))
# Build tree locations of static schema files and transforms (for VPATH builds)
API_build_copies = $(foreach f,$(API_abs_files),$(subst $(abs_srcdir),$(abs_builddir),$(f)))
CIB_build_copies = $(foreach f,$(CIB_abs_files) $(CIB_abs_xsl),$(subst $(abs_srcdir),$(abs_builddir),$(f)))
MON_build_copies = $(foreach f,$(MON_abs_files),$(subst $(abs_srcdir),$(abs_builddir),$(f)))
# Dynamically generated schema files
API_generated = api/api-result.rng $(foreach base,$(API_versions),api/api-result-$(base).rng)
CIB_generated = pacemaker.rng \
$(foreach base,$(CIB_versions),pacemaker-$(base).rng) \
versions.rng
MON_generated = crm_mon.rng
CIB_version_pairs = $(call version_pairs,${CIB_versions})
CIB_version_pairs_cnt = $(words ${CIB_version_pairs})
CIB_version_pairs_last = $(call version_pairs_last,${CIB_version_pairs_cnt},${CIB_version_pairs})
dist_API_DATA = $(API_files)
dist_CIB_DATA = $(CIB_files) \
$(CIB_xsl)
nodist_API_DATA = $(API_generated)
nodist_CIB_DATA = $(CIB_generated)
nodist_MON_DATA = $(MON_generated)
EXTRA_DIST = README.md \
cibtr-2.rng \
context-of.xsl \
rng-helper \
ocf-meta2man.xsl \
upgrade-detail.xsl \
xslt_cibtr-2.rng
.PHONY: cib-versions
cib-versions:
@echo "Max: $(CIB_max)"
@echo "Available: $(CIB_versions)"
.PHONY: api-versions
api-versions:
@echo "Max: $(API_max)"
@echo "Available: $(API_versions)"
# Dynamically generated top-level API schema
api/api-result.rng: api/api-result-$(API_max).rng
$(AM_V_at)$(MKDIR_P) api # might not exist in VPATH build
$(AM_V_SCHEMA)cp $(top_builddir)/xml/$< $@
api/api-result-%.rng: $(API_build_copies) rng-helper Makefile.am
$(AM_V_SCHEMA)$(builddir)/rng-helper build_api_rng "$@" "$*" \
$(API_request_base)
crm_mon.rng: api/crm_mon-$(MON_max).rng
$(AM_V_at)echo '' > $@
$(AM_V_at)echo '> $@
$(AM_V_at)echo ' datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_SCHEMA)echo '' >> $@
# Dynamically generated top-level CIB schema
pacemaker.rng: pacemaker-$(CIB_max).rng
$(AM_V_SCHEMA)cp $(top_builddir)/xml/$< $@
pacemaker-%.rng: $(CIB_build_copies) rng-helper Makefile.am
$(AM_V_SCHEMA)$(builddir)/rng-helper build_cib_rng "$@" "$*" \
$(CIB_cfg_base)
# Dynamically generated CIB schema listing all pacemaker versions
#
# @COMPAT "none" is deprecated since 2.1.8
versions.rng: pacemaker-$(CIB_max).rng Makefile.am
$(AM_V_at)echo '' > $@
$(AM_V_at)echo '' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' none' >> $@
$(AM_V_at)for rng in $(CIB_versions); do echo " pacemaker-$$rng" >> $@; done
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_at)echo ' ' >> $@
$(AM_V_SCHEMA)echo '' >> $@
schemas:
@if [ -z "$$SCHEMAS" ]; then \
ls *.rng; \
ls *.rng | sort -V | awk 'gsub("-[0-9.]+.rng", ""){if (last != $$0) {print; last=$$0} }'; \
printf "\nusage: make schemas SCHEMAS=\"\" [NEW_VERSION=\"\"]\n \
\nNot specifying NEW_VERSION will increase the last field of the newest version of the schema(s).\n"; \
else \
if [ -z "$$NEW_VERSION" ]; then \
OLD_VERSION=$$(ls *[0-9].rng | awk -F'-' 'gsub(".rng$$", "") {print $$NF}' | sort -Vu | tail -1); \
lf=$$(echo "$$OLD_VERSION" | awk -F"." '{print $$NF}'); \
! echo "$$lf" | grep -q "^[[:digit:]]\+$$" && echo "Unable to detect version. Use NEW_VERSION= to specify version." && exit 1; \
lf=$$((lf+1)); \
NEW_VERSION=$$(echo "$$OLD_VERSION" | sed "s/[0-9]\+$$/$$lf/"); \
fi; \
for schema in $$SCHEMAS; do \
old_schema=$$(ls $$schema-[0-9]*.rng | sort -V | tail -1); \
new_schema=$$schema-$$NEW_VERSION.rng; \
echo "Copying $$old_schema to $$new_schema"; \
cp -n "$$old_schema" "$$new_schema"; \
done \
fi
.PHONY: diff
diff: rng-helper
@echo "# Comparing changes in + since $(CIB_max)"
@$(builddir)/rng-helper diff ${CIB_version_pairs_last}
.PHONY: fulldiff
fulldiff: rng-helper
@echo "# Comparing all changes across all the subsequent increments"
@$(builddir)/rng-helper diff ${CIB_version_pairs}
CLEANFILES = $(API_generated) \
$(CIB_generated) \
$(MON_generated)
# Remove pacemaker schema files generated by *any* source version. This allows
# "make -C xml clean" to have the desired effect when checking out an earlier
# revision in a source tree.
.PHONY: clean-local
clean-local:
if [ "x$(srcdir)" != "x$(builddir)" ]; then \
rm -f $(API_build_copies) $(CIB_build_copies) $(MON_build_copies); \
fi
rm -f $(builddir)/pacemaker-[0-9]*.[0-9]*.rng
# Enable ability to use $@ in prerequisite
.SECONDEXPANSION:
# For VPATH builds, copy the static schema files into the build tree
$(API_build_copies) $(CIB_build_copies) $(MON_build_copies): $$(subst $(abs_builddir),$(srcdir),$$(@))
$(AM_V_GEN)if [ "x$(srcdir)" != "x$(builddir)" ]; then \
$(MKDIR_P) "$(dir $(@))"; \
cp "$(<)" "$(@)"; \
fi
diff --git a/xml/api/crm_diff-2.39.rng b/xml/api/crm_diff-2.39.rng
new file mode 100644
index 0000000000..296539c24b
--- /dev/null
+++ b/xml/api/crm_diff-2.39.rng
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+