diff --git a/cts/cts-scheduler.in b/cts/cts-scheduler.in
index 066c39b2375..ff9ea63cb63 100644
--- a/cts/cts-scheduler.in
+++ b/cts/cts-scheduler.in
@@ -639,6 +639,7 @@ TESTS = [
[ "no_quorum_demote", "Promotable demotion and primitive stop with no-quorum-policy=\"demote\"" ],
[ "no-promote-on-unrunnable-guest", "Don't select bundle instance for promotion when container can't run" ],
[ "leftover-pending-monitor", "Prevent a leftover pending monitor from causing unexpected stop of other instances" ],
+ [ "promotable-colocation-with-node-attribute", "Promote dependent clone on nodes belonging to a site that has a primary clone promoted" ],
],
[
[ "history-1", "Correctly parse stateful-1 resource state" ],
@@ -747,6 +748,12 @@ TESTS = [
"Verify ordered sets work with symmetrical=true" ],
[ "clbz5007-promotable-colocation",
"Verify use of colocation scores other than INFINITY and -INFINITY work on multi-state resources" ],
+ [ "promotable-self-colocation-with-node-attribute",
+ "Group promotions on a single site via self-colocation with node attribute" ],
+ [ "promotable-self-colocation-with-node-attribute-2",
+ "Group promotions on siteB (not siteA) via self-colocation with node attribute" ],
+ [ "promotable-self-colocation-with-node-attribute-3",
+ "Correct wrong-site promotions via self-colocation with node attribute" ],
[ "bug-5038", "Prevent restart of anonymous clones when clone-max decreases" ],
[ "bug-5025-1", "Automatically clean up failcount after resource config change with reload" ],
[ "bug-5025-2", "Make sure clear failcount action isn't set when config does not change" ],
diff --git a/cts/scheduler/dot/promotable-colocation-with-node-attribute.dot b/cts/scheduler/dot/promotable-colocation-with-node-attribute.dot
new file mode 100644
index 00000000000..89d066f893c
--- /dev/null
+++ b/cts/scheduler/dot/promotable-colocation-with-node-attribute.dot
@@ -0,0 +1,28 @@
+ digraph "g" {
+"dependent-clone_demote_0" -> "dependent-clone_demoted_0" [ style = bold]
+"dependent-clone_demote_0" -> "dependent-rsc_demote_0 node3" [ style = bold]
+"dependent-clone_demote_0" [ style=bold color="green" fontcolor="orange"]
+"dependent-clone_demoted_0" -> "dependent-clone_promote_0" [ style = bold]
+"dependent-clone_demoted_0" [ style=bold color="green" fontcolor="orange"]
+"dependent-clone_promote_0" -> "dependent-rsc_promote_0 node1" [ style = bold]
+"dependent-clone_promote_0" -> "dependent-rsc_promote_0 node2" [ style = bold]
+"dependent-clone_promote_0" [ style=bold color="green" fontcolor="orange"]
+"dependent-clone_promoted_0" [ style=bold color="green" fontcolor="orange"]
+"dependent-rsc_demote_0 node3" -> "dependent-clone_demoted_0" [ style = bold]
+"dependent-rsc_demote_0 node3" -> "dependent-rsc_monitor_11000 node3" [ style = bold]
+"dependent-rsc_demote_0 node3" [ style=bold color="green" fontcolor="black"]
+"dependent-rsc_monitor_10000 node1" [ style=bold color="green" fontcolor="black"]
+"dependent-rsc_monitor_10000 node2" [ style=bold color="green" fontcolor="black"]
+"dependent-rsc_monitor_11000 node3" [ style=bold color="green" fontcolor="black"]
+"dependent-rsc_monitor_11000 node4" [ style=bold color="green" fontcolor="black"]
+"dependent-rsc_promote_0 node1" -> "dependent-clone_promoted_0" [ style = bold]
+"dependent-rsc_promote_0 node1" -> "dependent-rsc_monitor_10000 node1" [ style = bold]
+"dependent-rsc_promote_0 node1" [ style=bold color="green" fontcolor="black"]
+"dependent-rsc_promote_0 node2" -> "dependent-clone_promoted_0" [ style = bold]
+"dependent-rsc_promote_0 node2" -> "dependent-rsc_monitor_10000 node2" [ style = bold]
+"dependent-rsc_promote_0 node2" [ style=bold color="green" fontcolor="black"]
+"primary-rsc_monitor_10000 node1" [ style=bold color="green" fontcolor="black"]
+"primary-rsc_monitor_11000 node2" [ style=bold color="green" fontcolor="black"]
+"primary-rsc_monitor_11000 node3" [ style=bold color="green" fontcolor="black"]
+"primary-rsc_monitor_11000 node4" [ style=bold color="green" fontcolor="black"]
+}
diff --git a/cts/scheduler/dot/promotable-self-colocation-with-node-attribute-2.dot b/cts/scheduler/dot/promotable-self-colocation-with-node-attribute-2.dot
new file mode 100644
index 00000000000..72003b9a82e
--- /dev/null
+++ b/cts/scheduler/dot/promotable-self-colocation-with-node-attribute-2.dot
@@ -0,0 +1,16 @@
+ digraph "g" {
+"site-clone_promote_0" -> "site-rsc_promote_0 node3" [ style = bold]
+"site-clone_promote_0" -> "site-rsc_promote_0 node4" [ style = bold]
+"site-clone_promote_0" [ style=bold color="green" fontcolor="orange"]
+"site-clone_promoted_0" [ style=bold color="green" fontcolor="orange"]
+"site-rsc_monitor_10000 node3" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_10000 node4" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_11000 node1" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_11000 node2" [ style=bold color="green" fontcolor="black"]
+"site-rsc_promote_0 node3" -> "site-clone_promoted_0" [ style = bold]
+"site-rsc_promote_0 node3" -> "site-rsc_monitor_10000 node3" [ style = bold]
+"site-rsc_promote_0 node3" [ style=bold color="green" fontcolor="black"]
+"site-rsc_promote_0 node4" -> "site-clone_promoted_0" [ style = bold]
+"site-rsc_promote_0 node4" -> "site-rsc_monitor_10000 node4" [ style = bold]
+"site-rsc_promote_0 node4" [ style=bold color="green" fontcolor="black"]
+}
diff --git a/cts/scheduler/dot/promotable-self-colocation-with-node-attribute-3.dot b/cts/scheduler/dot/promotable-self-colocation-with-node-attribute-3.dot
new file mode 100644
index 00000000000..24c52ef753c
--- /dev/null
+++ b/cts/scheduler/dot/promotable-self-colocation-with-node-attribute-3.dot
@@ -0,0 +1,28 @@
+ digraph "g" {
+"site-clone_demote_0" -> "site-clone_demoted_0" [ style = bold]
+"site-clone_demote_0" -> "site-rsc_demote_0 node3" [ style = bold]
+"site-clone_demote_0" -> "site-rsc_demote_0 node4" [ style = bold]
+"site-clone_demote_0" [ style=bold color="green" fontcolor="orange"]
+"site-clone_demoted_0" -> "site-clone_promote_0" [ style = bold]
+"site-clone_demoted_0" [ style=bold color="green" fontcolor="orange"]
+"site-clone_promote_0" -> "site-rsc_promote_0 node1" [ style = bold]
+"site-clone_promote_0" -> "site-rsc_promote_0 node2" [ style = bold]
+"site-clone_promote_0" [ style=bold color="green" fontcolor="orange"]
+"site-clone_promoted_0" [ style=bold color="green" fontcolor="orange"]
+"site-rsc_demote_0 node3" -> "site-clone_demoted_0" [ style = bold]
+"site-rsc_demote_0 node3" -> "site-rsc_monitor_11000 node3" [ style = bold]
+"site-rsc_demote_0 node3" [ style=bold color="green" fontcolor="black"]
+"site-rsc_demote_0 node4" -> "site-clone_demoted_0" [ style = bold]
+"site-rsc_demote_0 node4" -> "site-rsc_monitor_11000 node4" [ style = bold]
+"site-rsc_demote_0 node4" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_10000 node1" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_10000 node2" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_11000 node3" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_11000 node4" [ style=bold color="green" fontcolor="black"]
+"site-rsc_promote_0 node1" -> "site-clone_promoted_0" [ style = bold]
+"site-rsc_promote_0 node1" -> "site-rsc_monitor_10000 node1" [ style = bold]
+"site-rsc_promote_0 node1" [ style=bold color="green" fontcolor="black"]
+"site-rsc_promote_0 node2" -> "site-clone_promoted_0" [ style = bold]
+"site-rsc_promote_0 node2" -> "site-rsc_monitor_10000 node2" [ style = bold]
+"site-rsc_promote_0 node2" [ style=bold color="green" fontcolor="black"]
+}
diff --git a/cts/scheduler/dot/promotable-self-colocation-with-node-attribute.dot b/cts/scheduler/dot/promotable-self-colocation-with-node-attribute.dot
new file mode 100644
index 00000000000..b5685f2697f
--- /dev/null
+++ b/cts/scheduler/dot/promotable-self-colocation-with-node-attribute.dot
@@ -0,0 +1,16 @@
+ digraph "g" {
+"site-clone_promote_0" -> "site-rsc_promote_0 node1" [ style = bold]
+"site-clone_promote_0" -> "site-rsc_promote_0 node2" [ style = bold]
+"site-clone_promote_0" [ style=bold color="green" fontcolor="orange"]
+"site-clone_promoted_0" [ style=bold color="green" fontcolor="orange"]
+"site-rsc_monitor_10000 node1" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_10000 node2" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_11000 node3" [ style=bold color="green" fontcolor="black"]
+"site-rsc_monitor_11000 node4" [ style=bold color="green" fontcolor="black"]
+"site-rsc_promote_0 node1" -> "site-clone_promoted_0" [ style = bold]
+"site-rsc_promote_0 node1" -> "site-rsc_monitor_10000 node1" [ style = bold]
+"site-rsc_promote_0 node1" [ style=bold color="green" fontcolor="black"]
+"site-rsc_promote_0 node2" -> "site-clone_promoted_0" [ style = bold]
+"site-rsc_promote_0 node2" -> "site-rsc_monitor_10000 node2" [ style = bold]
+"site-rsc_promote_0 node2" [ style=bold color="green" fontcolor="black"]
+}
diff --git a/cts/scheduler/exp/promotable-colocation-with-node-attribute.exp b/cts/scheduler/exp/promotable-colocation-with-node-attribute.exp
new file mode 100644
index 00000000000..d3de3a8844b
--- /dev/null
+++ b/cts/scheduler/exp/promotable-colocation-with-node-attribute.exp
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cts/scheduler/exp/promotable-self-colocation-with-node-attribute-2.exp b/cts/scheduler/exp/promotable-self-colocation-with-node-attribute-2.exp
new file mode 100644
index 00000000000..a9bed76ec72
--- /dev/null
+++ b/cts/scheduler/exp/promotable-self-colocation-with-node-attribute-2.exp
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cts/scheduler/exp/promotable-self-colocation-with-node-attribute-3.exp b/cts/scheduler/exp/promotable-self-colocation-with-node-attribute-3.exp
new file mode 100644
index 00000000000..c2c75295f60
--- /dev/null
+++ b/cts/scheduler/exp/promotable-self-colocation-with-node-attribute-3.exp
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cts/scheduler/exp/promotable-self-colocation-with-node-attribute.exp b/cts/scheduler/exp/promotable-self-colocation-with-node-attribute.exp
new file mode 100644
index 00000000000..ccdae1f5483
--- /dev/null
+++ b/cts/scheduler/exp/promotable-self-colocation-with-node-attribute.exp
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cts/scheduler/scores/promotable-colocation-with-node-attribute.scores b/cts/scheduler/scores/promotable-colocation-with-node-attribute.scores
new file mode 100644
index 00000000000..023ee779107
--- /dev/null
+++ b/cts/scheduler/scores/promotable-colocation-with-node-attribute.scores
@@ -0,0 +1,81 @@
+
+dependent-rsc:0 promotion score on node1: INFINITY
+dependent-rsc:1 promotion score on node2: INFINITY
+dependent-rsc:2 promotion score on node3: -INFINITY
+dependent-rsc:3 promotion score on node4: -INFINITY
+pcmk__clone_assign: dependent-clone allocation score on node1: 0
+pcmk__clone_assign: dependent-clone allocation score on node2: 0
+pcmk__clone_assign: dependent-clone allocation score on node3: 0
+pcmk__clone_assign: dependent-clone allocation score on node4: 0
+pcmk__clone_assign: dependent-rsc:0 allocation score on node1: 6
+pcmk__clone_assign: dependent-rsc:0 allocation score on node2: 0
+pcmk__clone_assign: dependent-rsc:0 allocation score on node3: 0
+pcmk__clone_assign: dependent-rsc:0 allocation score on node4: 0
+pcmk__clone_assign: dependent-rsc:1 allocation score on node1: 0
+pcmk__clone_assign: dependent-rsc:1 allocation score on node2: 6
+pcmk__clone_assign: dependent-rsc:1 allocation score on node3: 0
+pcmk__clone_assign: dependent-rsc:1 allocation score on node4: 0
+pcmk__clone_assign: dependent-rsc:2 allocation score on node1: 0
+pcmk__clone_assign: dependent-rsc:2 allocation score on node2: 0
+pcmk__clone_assign: dependent-rsc:2 allocation score on node3: 11
+pcmk__clone_assign: dependent-rsc:2 allocation score on node4: 0
+pcmk__clone_assign: dependent-rsc:3 allocation score on node1: 0
+pcmk__clone_assign: dependent-rsc:3 allocation score on node2: 0
+pcmk__clone_assign: dependent-rsc:3 allocation score on node3: 0
+pcmk__clone_assign: dependent-rsc:3 allocation score on node4: 6
+pcmk__clone_assign: primary-clone allocation score on node1: 0
+pcmk__clone_assign: primary-clone allocation score on node2: 0
+pcmk__clone_assign: primary-clone allocation score on node3: 0
+pcmk__clone_assign: primary-clone allocation score on node4: 0
+pcmk__clone_assign: primary-rsc:0 allocation score on node1: 11
+pcmk__clone_assign: primary-rsc:0 allocation score on node2: 0
+pcmk__clone_assign: primary-rsc:0 allocation score on node3: 0
+pcmk__clone_assign: primary-rsc:0 allocation score on node4: 0
+pcmk__clone_assign: primary-rsc:1 allocation score on node1: 0
+pcmk__clone_assign: primary-rsc:1 allocation score on node2: 6
+pcmk__clone_assign: primary-rsc:1 allocation score on node3: 0
+pcmk__clone_assign: primary-rsc:1 allocation score on node4: 0
+pcmk__clone_assign: primary-rsc:2 allocation score on node1: 0
+pcmk__clone_assign: primary-rsc:2 allocation score on node2: 0
+pcmk__clone_assign: primary-rsc:2 allocation score on node3: 6
+pcmk__clone_assign: primary-rsc:2 allocation score on node4: 0
+pcmk__clone_assign: primary-rsc:3 allocation score on node1: 0
+pcmk__clone_assign: primary-rsc:3 allocation score on node2: 0
+pcmk__clone_assign: primary-rsc:3 allocation score on node3: 0
+pcmk__clone_assign: primary-rsc:3 allocation score on node4: 6
+pcmk__primitive_assign: dependent-rsc:0 allocation score on node1: 6
+pcmk__primitive_assign: dependent-rsc:0 allocation score on node2: 0
+pcmk__primitive_assign: dependent-rsc:0 allocation score on node3: -INFINITY
+pcmk__primitive_assign: dependent-rsc:0 allocation score on node4: 0
+pcmk__primitive_assign: dependent-rsc:1 allocation score on node1: -INFINITY
+pcmk__primitive_assign: dependent-rsc:1 allocation score on node2: 6
+pcmk__primitive_assign: dependent-rsc:1 allocation score on node3: -INFINITY
+pcmk__primitive_assign: dependent-rsc:1 allocation score on node4: 0
+pcmk__primitive_assign: dependent-rsc:2 allocation score on node1: 0
+pcmk__primitive_assign: dependent-rsc:2 allocation score on node2: 0
+pcmk__primitive_assign: dependent-rsc:2 allocation score on node3: 11
+pcmk__primitive_assign: dependent-rsc:2 allocation score on node4: 0
+pcmk__primitive_assign: dependent-rsc:3 allocation score on node1: -INFINITY
+pcmk__primitive_assign: dependent-rsc:3 allocation score on node2: -INFINITY
+pcmk__primitive_assign: dependent-rsc:3 allocation score on node3: -INFINITY
+pcmk__primitive_assign: dependent-rsc:3 allocation score on node4: 6
+pcmk__primitive_assign: primary-rsc:0 allocation score on node1: 11
+pcmk__primitive_assign: primary-rsc:0 allocation score on node2: 0
+pcmk__primitive_assign: primary-rsc:0 allocation score on node3: 0
+pcmk__primitive_assign: primary-rsc:0 allocation score on node4: 0
+pcmk__primitive_assign: primary-rsc:1 allocation score on node1: -INFINITY
+pcmk__primitive_assign: primary-rsc:1 allocation score on node2: 6
+pcmk__primitive_assign: primary-rsc:1 allocation score on node3: 0
+pcmk__primitive_assign: primary-rsc:1 allocation score on node4: 0
+pcmk__primitive_assign: primary-rsc:2 allocation score on node1: -INFINITY
+pcmk__primitive_assign: primary-rsc:2 allocation score on node2: -INFINITY
+pcmk__primitive_assign: primary-rsc:2 allocation score on node3: 6
+pcmk__primitive_assign: primary-rsc:2 allocation score on node4: 0
+pcmk__primitive_assign: primary-rsc:3 allocation score on node1: -INFINITY
+pcmk__primitive_assign: primary-rsc:3 allocation score on node2: -INFINITY
+pcmk__primitive_assign: primary-rsc:3 allocation score on node3: -INFINITY
+pcmk__primitive_assign: primary-rsc:3 allocation score on node4: 6
+primary-rsc:0 promotion score on node1: 10
+primary-rsc:1 promotion score on node2: 5
+primary-rsc:2 promotion score on node3: 5
+primary-rsc:3 promotion score on node4: 5
diff --git a/cts/scheduler/scores/promotable-self-colocation-with-node-attribute-2.scores b/cts/scheduler/scores/promotable-self-colocation-with-node-attribute-2.scores
new file mode 100644
index 00000000000..27475c0d823
--- /dev/null
+++ b/cts/scheduler/scores/promotable-self-colocation-with-node-attribute-2.scores
@@ -0,0 +1,41 @@
+
+pcmk__clone_assign: site-clone allocation score on node1: 0
+pcmk__clone_assign: site-clone allocation score on node2: 0
+pcmk__clone_assign: site-clone allocation score on node3: 0
+pcmk__clone_assign: site-clone allocation score on node4: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node1: 7
+pcmk__clone_assign: site-rsc:0 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node2: 8
+pcmk__clone_assign: site-rsc:1 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node3: 11
+pcmk__clone_assign: site-rsc:2 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node4: 9
+pcmk__primitive_assign: site-rsc:0 allocation score on node1: 7
+pcmk__primitive_assign: site-rsc:0 allocation score on node2: -INFINITY
+pcmk__primitive_assign: site-rsc:0 allocation score on node3: -INFINITY
+pcmk__primitive_assign: site-rsc:0 allocation score on node4: -INFINITY
+pcmk__primitive_assign: site-rsc:1 allocation score on node1: 0
+pcmk__primitive_assign: site-rsc:1 allocation score on node2: 8
+pcmk__primitive_assign: site-rsc:1 allocation score on node3: -INFINITY
+pcmk__primitive_assign: site-rsc:1 allocation score on node4: -INFINITY
+pcmk__primitive_assign: site-rsc:2 allocation score on node1: 0
+pcmk__primitive_assign: site-rsc:2 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:2 allocation score on node3: 11
+pcmk__primitive_assign: site-rsc:2 allocation score on node4: 0
+pcmk__primitive_assign: site-rsc:3 allocation score on node1: 0
+pcmk__primitive_assign: site-rsc:3 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:3 allocation score on node3: -INFINITY
+pcmk__primitive_assign: site-rsc:3 allocation score on node4: 9
+site-rsc:0 promotion score on node1: -INFINITY
+site-rsc:1 promotion score on node2: -INFINITY
+site-rsc:2 promotion score on node3: 10
+site-rsc:3 promotion score on node4: 8
diff --git a/cts/scheduler/scores/promotable-self-colocation-with-node-attribute-3.scores b/cts/scheduler/scores/promotable-self-colocation-with-node-attribute-3.scores
new file mode 100644
index 00000000000..26027b4053b
--- /dev/null
+++ b/cts/scheduler/scores/promotable-self-colocation-with-node-attribute-3.scores
@@ -0,0 +1,41 @@
+
+pcmk__clone_assign: site-clone allocation score on node1: 0
+pcmk__clone_assign: site-clone allocation score on node2: 0
+pcmk__clone_assign: site-clone allocation score on node3: 0
+pcmk__clone_assign: site-clone allocation score on node4: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node1: 11
+pcmk__clone_assign: site-rsc:0 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node2: 7
+pcmk__clone_assign: site-rsc:1 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node3: 9
+pcmk__clone_assign: site-rsc:2 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node4: 8
+pcmk__primitive_assign: site-rsc:0 allocation score on node1: 11
+pcmk__primitive_assign: site-rsc:0 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:0 allocation score on node3: 0
+pcmk__primitive_assign: site-rsc:0 allocation score on node4: 0
+pcmk__primitive_assign: site-rsc:1 allocation score on node1: -INFINITY
+pcmk__primitive_assign: site-rsc:1 allocation score on node2: 7
+pcmk__primitive_assign: site-rsc:1 allocation score on node3: -INFINITY
+pcmk__primitive_assign: site-rsc:1 allocation score on node4: -INFINITY
+pcmk__primitive_assign: site-rsc:2 allocation score on node1: -INFINITY
+pcmk__primitive_assign: site-rsc:2 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:2 allocation score on node3: 9
+pcmk__primitive_assign: site-rsc:2 allocation score on node4: 0
+pcmk__primitive_assign: site-rsc:3 allocation score on node1: -INFINITY
+pcmk__primitive_assign: site-rsc:3 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:3 allocation score on node3: -INFINITY
+pcmk__primitive_assign: site-rsc:3 allocation score on node4: 8
+site-rsc:0 promotion score on node1: 10
+site-rsc:1 promotion score on node2: 6
+site-rsc:2 promotion score on node3: -INFINITY
+site-rsc:3 promotion score on node4: -INFINITY
diff --git a/cts/scheduler/scores/promotable-self-colocation-with-node-attribute.scores b/cts/scheduler/scores/promotable-self-colocation-with-node-attribute.scores
new file mode 100644
index 00000000000..26027b4053b
--- /dev/null
+++ b/cts/scheduler/scores/promotable-self-colocation-with-node-attribute.scores
@@ -0,0 +1,41 @@
+
+pcmk__clone_assign: site-clone allocation score on node1: 0
+pcmk__clone_assign: site-clone allocation score on node2: 0
+pcmk__clone_assign: site-clone allocation score on node3: 0
+pcmk__clone_assign: site-clone allocation score on node4: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node1: 11
+pcmk__clone_assign: site-rsc:0 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:0 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node2: 7
+pcmk__clone_assign: site-rsc:1 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:1 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:2 allocation score on node3: 9
+pcmk__clone_assign: site-rsc:2 allocation score on node4: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node1: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node2: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node3: 0
+pcmk__clone_assign: site-rsc:3 allocation score on node4: 8
+pcmk__primitive_assign: site-rsc:0 allocation score on node1: 11
+pcmk__primitive_assign: site-rsc:0 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:0 allocation score on node3: 0
+pcmk__primitive_assign: site-rsc:0 allocation score on node4: 0
+pcmk__primitive_assign: site-rsc:1 allocation score on node1: -INFINITY
+pcmk__primitive_assign: site-rsc:1 allocation score on node2: 7
+pcmk__primitive_assign: site-rsc:1 allocation score on node3: -INFINITY
+pcmk__primitive_assign: site-rsc:1 allocation score on node4: -INFINITY
+pcmk__primitive_assign: site-rsc:2 allocation score on node1: -INFINITY
+pcmk__primitive_assign: site-rsc:2 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:2 allocation score on node3: 9
+pcmk__primitive_assign: site-rsc:2 allocation score on node4: 0
+pcmk__primitive_assign: site-rsc:3 allocation score on node1: -INFINITY
+pcmk__primitive_assign: site-rsc:3 allocation score on node2: 0
+pcmk__primitive_assign: site-rsc:3 allocation score on node3: -INFINITY
+pcmk__primitive_assign: site-rsc:3 allocation score on node4: 8
+site-rsc:0 promotion score on node1: 10
+site-rsc:1 promotion score on node2: 6
+site-rsc:2 promotion score on node3: -INFINITY
+site-rsc:3 promotion score on node4: -INFINITY
diff --git a/cts/scheduler/summary/promotable-colocation-with-node-attribute.summary b/cts/scheduler/summary/promotable-colocation-with-node-attribute.summary
new file mode 100644
index 00000000000..30e81c8f371
--- /dev/null
+++ b/cts/scheduler/summary/promotable-colocation-with-node-attribute.summary
@@ -0,0 +1,45 @@
+Current cluster status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: primary-clone [primary-rsc] (promotable):
+ * Promoted: [ node1 ]
+ * Unpromoted: [ node2 node3 node4 ]
+ * Clone Set: dependent-clone [dependent-rsc] (promotable):
+ * Promoted: [ node3 ]
+ * Unpromoted: [ node1 node2 node4 ]
+
+Transition Summary:
+ * Promote dependent-rsc:0 ( Unpromoted -> Promoted node1 )
+ * Promote dependent-rsc:1 ( Unpromoted -> Promoted node2 )
+ * Demote dependent-rsc:2 ( Promoted -> Unpromoted node3 )
+
+Executing Cluster Transition:
+ * Resource action: primary-rsc monitor=10000 on node1
+ * Resource action: primary-rsc monitor=11000 on node2
+ * Resource action: primary-rsc monitor=11000 on node3
+ * Resource action: primary-rsc monitor=11000 on node4
+ * Resource action: dependent-rsc monitor=11000 on node4
+ * Pseudo action: dependent-clone_demote_0
+ * Resource action: dependent-rsc demote on node3
+ * Pseudo action: dependent-clone_demoted_0
+ * Pseudo action: dependent-clone_promote_0
+ * Resource action: dependent-rsc promote on node1
+ * Resource action: dependent-rsc promote on node2
+ * Resource action: dependent-rsc monitor=11000 on node3
+ * Pseudo action: dependent-clone_promoted_0
+ * Resource action: dependent-rsc monitor=10000 on node1
+ * Resource action: dependent-rsc monitor=10000 on node2
+
+Revised Cluster Status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: primary-clone [primary-rsc] (promotable):
+ * Promoted: [ node1 ]
+ * Unpromoted: [ node2 node3 node4 ]
+ * Clone Set: dependent-clone [dependent-rsc] (promotable):
+ * Promoted: [ node1 node2 ]
+ * Unpromoted: [ node3 node4 ]
diff --git a/cts/scheduler/summary/promotable-self-colocation-with-node-attribute-2.summary b/cts/scheduler/summary/promotable-self-colocation-with-node-attribute-2.summary
new file mode 100644
index 00000000000..8085bba0baa
--- /dev/null
+++ b/cts/scheduler/summary/promotable-self-colocation-with-node-attribute-2.summary
@@ -0,0 +1,30 @@
+Current cluster status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: site-clone [site-rsc] (promotable):
+ * Unpromoted: [ node1 node2 node3 node4 ]
+
+Transition Summary:
+ * Promote site-rsc:2 ( Unpromoted -> Promoted node3 )
+ * Promote site-rsc:3 ( Unpromoted -> Promoted node4 )
+
+Executing Cluster Transition:
+ * Resource action: site-rsc monitor=11000 on node1
+ * Resource action: site-rsc monitor=11000 on node2
+ * Pseudo action: site-clone_promote_0
+ * Resource action: site-rsc promote on node3
+ * Resource action: site-rsc promote on node4
+ * Pseudo action: site-clone_promoted_0
+ * Resource action: site-rsc monitor=10000 on node3
+ * Resource action: site-rsc monitor=10000 on node4
+
+Revised Cluster Status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: site-clone [site-rsc] (promotable):
+ * Promoted: [ node3 node4 ]
+ * Unpromoted: [ node1 node2 ]
diff --git a/cts/scheduler/summary/promotable-self-colocation-with-node-attribute-3.summary b/cts/scheduler/summary/promotable-self-colocation-with-node-attribute-3.summary
new file mode 100644
index 00000000000..4efbf725eb1
--- /dev/null
+++ b/cts/scheduler/summary/promotable-self-colocation-with-node-attribute-3.summary
@@ -0,0 +1,37 @@
+Current cluster status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: site-clone [site-rsc] (promotable):
+ * Promoted: [ node3 node4 ]
+ * Unpromoted: [ node1 node2 ]
+
+Transition Summary:
+ * Promote site-rsc:0 ( Unpromoted -> Promoted node1 )
+ * Promote site-rsc:1 ( Unpromoted -> Promoted node2 )
+ * Demote site-rsc:2 ( Promoted -> Unpromoted node3 )
+ * Demote site-rsc:3 ( Promoted -> Unpromoted node4 )
+
+Executing Cluster Transition:
+ * Pseudo action: site-clone_demote_0
+ * Resource action: site-rsc demote on node3
+ * Resource action: site-rsc demote on node4
+ * Pseudo action: site-clone_demoted_0
+ * Pseudo action: site-clone_promote_0
+ * Resource action: site-rsc promote on node1
+ * Resource action: site-rsc promote on node2
+ * Resource action: site-rsc monitor=11000 on node3
+ * Resource action: site-rsc monitor=11000 on node4
+ * Pseudo action: site-clone_promoted_0
+ * Resource action: site-rsc monitor=10000 on node1
+ * Resource action: site-rsc monitor=10000 on node2
+
+Revised Cluster Status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: site-clone [site-rsc] (promotable):
+ * Promoted: [ node1 node2 ]
+ * Unpromoted: [ node3 node4 ]
diff --git a/cts/scheduler/summary/promotable-self-colocation-with-node-attribute.summary b/cts/scheduler/summary/promotable-self-colocation-with-node-attribute.summary
new file mode 100644
index 00000000000..c5d37e062a6
--- /dev/null
+++ b/cts/scheduler/summary/promotable-self-colocation-with-node-attribute.summary
@@ -0,0 +1,30 @@
+Current cluster status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: site-clone [site-rsc] (promotable):
+ * Unpromoted: [ node1 node2 node3 node4 ]
+
+Transition Summary:
+ * Promote site-rsc:0 ( Unpromoted -> Promoted node1 )
+ * Promote site-rsc:1 ( Unpromoted -> Promoted node2 )
+
+Executing Cluster Transition:
+ * Resource action: site-rsc monitor=11000 on node3
+ * Resource action: site-rsc monitor=11000 on node4
+ * Pseudo action: site-clone_promote_0
+ * Resource action: site-rsc promote on node1
+ * Resource action: site-rsc promote on node2
+ * Pseudo action: site-clone_promoted_0
+ * Resource action: site-rsc monitor=10000 on node1
+ * Resource action: site-rsc monitor=10000 on node2
+
+Revised Cluster Status:
+ * Node List:
+ * Online: [ node1 node2 node3 node4 ]
+
+ * Full List of Resources:
+ * Clone Set: site-clone [site-rsc] (promotable):
+ * Promoted: [ node1 node2 ]
+ * Unpromoted: [ node3 node4 ]
diff --git a/cts/scheduler/xml/promotable-colocation-with-node-attribute.xml b/cts/scheduler/xml/promotable-colocation-with-node-attribute.xml
new file mode 100644
index 00000000000..328cfbc901c
--- /dev/null
+++ b/cts/scheduler/xml/promotable-colocation-with-node-attribute.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cts/scheduler/xml/promotable-self-colocation-with-node-attribute-2.xml b/cts/scheduler/xml/promotable-self-colocation-with-node-attribute-2.xml
new file mode 100644
index 00000000000..383f75abaf3
--- /dev/null
+++ b/cts/scheduler/xml/promotable-self-colocation-with-node-attribute-2.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cts/scheduler/xml/promotable-self-colocation-with-node-attribute-3.xml b/cts/scheduler/xml/promotable-self-colocation-with-node-attribute-3.xml
new file mode 100644
index 00000000000..f2503b3a45e
--- /dev/null
+++ b/cts/scheduler/xml/promotable-self-colocation-with-node-attribute-3.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cts/scheduler/xml/promotable-self-colocation-with-node-attribute.xml b/cts/scheduler/xml/promotable-self-colocation-with-node-attribute.xml
new file mode 100644
index 00000000000..a35ab18f157
--- /dev/null
+++ b/cts/scheduler/xml/promotable-self-colocation-with-node-attribute.xml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/pacemaker/libpacemaker_private.h b/lib/pacemaker/libpacemaker_private.h
index 9e8a02ef0f6..e68c4256cd3 100644
--- a/lib/pacemaker/libpacemaker_private.h
+++ b/lib/pacemaker/libpacemaker_private.h
@@ -949,13 +949,14 @@ void pcmk__create_instance_actions(pcmk_resource_t *rsc, GList *instances);
G_GNUC_INTERNAL
bool pcmk__instance_matches(const pcmk_resource_t *instance,
const pcmk_node_t *node, enum rsc_role_e role,
- bool current);
+ bool current, const char *node_attribute);
G_GNUC_INTERNAL
pcmk_resource_t *pcmk__find_compatible_instance(const pcmk_resource_t *match_rsc,
const pcmk_resource_t *rsc,
enum rsc_role_e role,
- bool current);
+ bool current,
+ const char *node_attribute);
G_GNUC_INTERNAL
uint32_t pcmk__instance_update_ordered_actions(pcmk_action_t *first,
diff --git a/lib/pacemaker/pcmk_sched_bundle.c b/lib/pacemaker/pcmk_sched_bundle.c
index f1accf24474..ddb9aa58e61 100644
--- a/lib/pacemaker/pcmk_sched_bundle.c
+++ b/lib/pacemaker/pcmk_sched_bundle.c
@@ -376,7 +376,7 @@ match_replica_container(const pcmk__bundle_replica_t *replica, void *user_data)
struct match_data *match_data = user_data;
if (pcmk__instance_matches(replica->container, match_data->node,
- pcmk_role_unknown, false)) {
+ pcmk_role_unknown, false, NULL)) {
match_data->container = replica->container;
return false; // Match found, don't bother searching further replicas
}
diff --git a/lib/pacemaker/pcmk_sched_clone.c b/lib/pacemaker/pcmk_sched_clone.c
index 12604a6e836..a46c4bd341a 100644
--- a/lib/pacemaker/pcmk_sched_clone.c
+++ b/lib/pacemaker/pcmk_sched_clone.c
@@ -296,7 +296,7 @@ pcmk__clone_apply_coloc_score(pcmk_resource_t *dependent,
primary_instance = pcmk__find_compatible_instance(dependent, primary,
pcmk_role_unknown,
- false);
+ false, NULL);
if (primary_instance != NULL) {
pcmk__rsc_debug(primary, "Interleaving %s with %s",
dependent->id, primary_instance->id);
diff --git a/lib/pacemaker/pcmk_sched_instances.c b/lib/pacemaker/pcmk_sched_instances.c
index b231c0c5fb0..db315f42347 100644
--- a/lib/pacemaker/pcmk_sched_instances.c
+++ b/lib/pacemaker/pcmk_sched_instances.c
@@ -1063,18 +1063,22 @@ free_instance_list(const pcmk_resource_t *rsc, GList *list)
* \internal
* \brief Check whether an instance is compatible with a role and node
*
- * \param[in] instance Clone instance or bundle replica container
- * \param[in] node Instance must match this node
- * \param[in] role If not pcmk_role_unknown, instance must match this role
- * \param[in] current If true, compare instance's original node and role,
- * otherwise compare assigned next node and role
+ * \param[in] instance Clone instance or bundle replica container
+ * \param[in] node Instance must match this node
+ * \param[in] role If not pcmk_role_unknown, instance must match this role
+ * \param[in] current If true, compare instance's original node and role,
+ * otherwise compare assigned next node and role
+ * \param[in] node_attribute If not NULL, instance's node must have the same value
+ * for this attribute as \p node (instead of requiring
+ * the exact same node)
*
* \return true if \p instance is compatible with \p node and \p role,
* otherwise false
*/
bool
pcmk__instance_matches(const pcmk_resource_t *instance, const pcmk_node_t *node,
- enum rsc_role_e role, bool current)
+ enum rsc_role_e role, bool current,
+ const char *node_attribute)
{
pcmk_node_t *instance_node = NULL;
@@ -1101,7 +1105,25 @@ pcmk__instance_matches(const pcmk_resource_t *instance, const pcmk_node_t *node,
return false;
}
- if (!pcmk__same_node(instance_node, node)) {
+ if (node_attribute != NULL) {
+ // Compare by node attribute value instead of node identity
+ const char *instance_value = pcmk__colocation_node_attr(instance_node,
+ node_attribute,
+ instance);
+ const char *target_value = pcmk__colocation_node_attr(node,
+ node_attribute,
+ instance);
+
+ if (!pcmk__str_eq(instance_value, target_value, pcmk__str_casei)) {
+ pcmk__rsc_trace(instance,
+ "%s is not a compatible instance "
+ "(instance has %s=%s, target node has %s=%s)",
+ instance->id, node_attribute,
+ pcmk__s(instance_value, ""),
+ node_attribute, pcmk__s(target_value, ""));
+ return false;
+ }
+ } else if (!pcmk__same_node(instance_node, node)) {
pcmk__rsc_trace(instance,
"%s is not a compatible instance "
"(assigned to %s not %s)",
@@ -1120,12 +1142,14 @@ pcmk__instance_matches(const pcmk_resource_t *instance, const pcmk_node_t *node,
* \internal
* \brief Find an instance that matches a given resource by node and role
*
- * \param[in] match_rsc Resource that instance must match (for logging only)
- * \param[in] rsc Clone or bundle resource to check for matching instance
- * \param[in] node Instance must match this node
- * \param[in] role If not pcmk_role_unknown, instance must match this role
- * \param[in] current If true, compare instance's original node and role,
- * otherwise compare assigned next node and role
+ * \param[in] match_rsc Resource that instance must match (for logging only)
+ * \param[in] rsc Clone or bundle resource to check for matching instance
+ * \param[in] node Instance must match this node
+ * \param[in] role If not pcmk_role_unknown, instance must match this role
+ * \param[in] current If true, compare instance's original node and role,
+ * otherwise compare assigned next node and role
+ * \param[in] node_attribute If not NULL, match instances by this node attribute
+ * instead of by node identity
*
* \return \p rsc instance matching \p node and \p role if any, otherwise NULL
*/
@@ -1133,7 +1157,7 @@ static pcmk_resource_t *
find_compatible_instance_on_node(const pcmk_resource_t *match_rsc,
const pcmk_resource_t *rsc,
const pcmk_node_t *node, enum rsc_role_e role,
- bool current)
+ bool current, const char *node_attribute)
{
GList *instances = NULL;
@@ -1141,7 +1165,8 @@ find_compatible_instance_on_node(const pcmk_resource_t *match_rsc,
for (GList *iter = instances; iter != NULL; iter = iter->next) {
pcmk_resource_t *instance = (pcmk_resource_t *) iter->data;
- if (pcmk__instance_matches(instance, node, role, current)) {
+ if (pcmk__instance_matches(instance, node, role, current,
+ node_attribute)) {
pcmk__rsc_trace(match_rsc,
"Found %s %s instance %s compatible with %s on %s",
display_role(role), rsc->id, instance->id,
@@ -1163,11 +1188,13 @@ find_compatible_instance_on_node(const pcmk_resource_t *match_rsc,
* \internal
* \brief Find a clone instance or bundle container compatible with a resource
*
- * \param[in] match_rsc Resource that instance must match
- * \param[in] rsc Clone or bundle resource to check for matching instance
- * \param[in] role If not pcmk_role_unknown, instance must match this role
- * \param[in] current If true, compare instance's original node and role,
- * otherwise compare assigned next node and role
+ * \param[in] match_rsc Resource that instance must match
+ * \param[in] rsc Clone or bundle resource to check for matching instance
+ * \param[in] role If not pcmk_role_unknown, instance must match this role
+ * \param[in] current If true, compare instance's original node and role,
+ * otherwise compare assigned next node and role
+ * \param[in] node_attribute If not NULL, match instances by this node attribute
+ * instead of by node identity
*
* \return Compatible (by \p role and \p match_rsc location) instance of \p rsc
* if any, otherwise NULL
@@ -1175,7 +1202,7 @@ find_compatible_instance_on_node(const pcmk_resource_t *match_rsc,
pcmk_resource_t *
pcmk__find_compatible_instance(const pcmk_resource_t *match_rsc,
const pcmk_resource_t *rsc, enum rsc_role_e role,
- bool current)
+ bool current, const char *node_attribute)
{
pcmk_resource_t *instance = NULL;
GList *nodes = NULL;
@@ -1185,7 +1212,7 @@ pcmk__find_compatible_instance(const pcmk_resource_t *match_rsc,
node = match_rsc->fns->location(match_rsc, NULL, current);
if (node != NULL) {
return find_compatible_instance_on_node(match_rsc, rsc, node, role,
- current);
+ current, node_attribute);
}
// Otherwise check for an instance matching any of match_rsc's allowed nodes
@@ -1195,7 +1222,8 @@ pcmk__find_compatible_instance(const pcmk_resource_t *match_rsc,
iter = iter->next) {
instance = find_compatible_instance_on_node(match_rsc, rsc,
(pcmk_node_t *) iter->data,
- role, current);
+ role, current,
+ node_attribute);
}
if (instance == NULL) {
@@ -1402,7 +1430,7 @@ update_interleaved_actions(pcmk_action_t *first, pcmk_action_t *then,
first_instance = pcmk__find_compatible_instance(then_instance,
first->rsc,
pcmk_role_unknown,
- current);
+ current, NULL);
if (first_instance == NULL) { // No instance can be interleaved
if (unassign_if_mandatory(first, then, then_instance, type,
diff --git a/lib/pacemaker/pcmk_sched_probes.c b/lib/pacemaker/pcmk_sched_probes.c
index 56820f173c9..a18a29c0b39 100644
--- a/lib/pacemaker/pcmk_sched_probes.c
+++ b/lib/pacemaker/pcmk_sched_probes.c
@@ -600,7 +600,7 @@ add_restart_orderings_for_probe(pcmk_action_t *probe, pcmk_action_t *after)
compatible_rsc = pcmk__find_compatible_instance(probe->rsc,
after->rsc,
pcmk_role_unknown,
- false);
+ false, NULL);
}
}
diff --git a/lib/pacemaker/pcmk_sched_promotable.c b/lib/pacemaker/pcmk_sched_promotable.c
index 852fcab3bea..87954ea828f 100644
--- a/lib/pacemaker/pcmk_sched_promotable.c
+++ b/lib/pacemaker/pcmk_sched_promotable.c
@@ -340,6 +340,121 @@ add_sort_index_to_node_score(gpointer data, gpointer user_data)
pcmk__node_name(node), pcmk_readable_score(node->weight));
}
+/*!
+ * \internal
+ * \brief Apply self-colocation to group promotion by node attribute
+ *
+ * When a promotable clone colocates its own promoted role with itself
+ * (i.e. \c rsc and \c with-rsc reference the same promotable clone, both with
+ * the Promoted role), group promoted instances by the colocation's node
+ * attribute value, so that all promoted instances will be on nodes sharing the
+ * same value for that attribute.
+ *
+ * This allows multi-site clusters to ensure all promoted instances are on a
+ * single site without requiring a separate leading primitive resource.
+ *
+ * \param[in,out] clone Promotable clone
+ * \param[in] colocation Self-colocation constraint
+ */
+static void
+apply_site_colocation(pcmk_resource_t *clone,
+ const pcmk__colocation_t *colocation)
+{
+ const char *attr = colocation->node_attribute;
+ GHashTableIter iter;
+ pcmk_node_t *node = NULL;
+ GHashTable *site_scores = NULL;
+ const char *best_site = NULL;
+ int best_score = -PCMK_SCORE_INFINITY;
+ gpointer key = NULL;
+ gpointer val = NULL;
+
+ pcmk__rsc_trace(clone,
+ "Applying self-colocation %s to group %s promotion "
+ "by node attribute %s",
+ colocation->id, clone->id, attr);
+
+ // Compute aggregate promotion score per unique attribute value
+ site_scores = pcmk__strikey_table(NULL, NULL);
+
+ g_hash_table_iter_init(&iter, clone->allowed_nodes);
+ while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
+ const char *value = pcmk__colocation_node_attr(node, attr, clone);
+ gpointer lookup = NULL;
+ int current = 0;
+ int new_score = 0;
+
+ if (value == NULL) {
+ continue;
+ }
+ if (node->weight <= -PCMK_SCORE_INFINITY) {
+ continue; // Don't let banned nodes drag down their site
+ }
+
+ lookup = g_hash_table_lookup(site_scores, value);
+ current = GPOINTER_TO_INT(lookup);
+ new_score = pcmk__add_scores(current, node->weight);
+ g_hash_table_insert(site_scores, (gpointer) value,
+ GINT_TO_POINTER(new_score));
+ }
+
+ // Find the site with the highest aggregate score
+ g_hash_table_iter_init(&iter, site_scores);
+ while (g_hash_table_iter_next(&iter, &key, &val)) {
+ int score = GPOINTER_TO_INT(val);
+
+ pcmk__rsc_trace(clone, "Site %s=%s aggregate promotion score: %s",
+ attr, (const char *) key,
+ pcmk_readable_score(score));
+ if (score > best_score) {
+ best_score = score;
+ best_site = (const char *) key;
+ }
+ }
+
+ g_hash_table_destroy(site_scores);
+
+ if (best_site == NULL) {
+ pcmk__rsc_trace(clone,
+ "No eligible site found for self-colocation %s",
+ colocation->id);
+ return;
+ }
+
+ pcmk__rsc_debug(clone,
+ "Self-colocation %s favors promoting %s "
+ "on site %s=%s (score %s)",
+ colocation->id, clone->id, attr, best_site,
+ pcmk_readable_score(best_score));
+
+ // Apply preference for the winning site
+ g_hash_table_iter_init(&iter, clone->allowed_nodes);
+ while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) {
+ const char *value = pcmk__colocation_node_attr(node, attr, clone);
+ bool matches = pcmk__str_eq(value, best_site, pcmk__str_casei);
+
+ if (!matches && (colocation->score >= PCMK_SCORE_INFINITY)) {
+ node->weight = -PCMK_SCORE_INFINITY;
+ pcmk__rsc_trace(clone,
+ "Banned %s from promotion of %s "
+ "(not on preferred site %s=%s)",
+ pcmk__node_name(node), clone->id,
+ attr, best_site);
+
+ } else if (matches && (colocation->score > 0)
+ && (colocation->score < PCMK_SCORE_INFINITY)) {
+ node->weight = pcmk__add_scores(colocation->score,
+ node->weight);
+ pcmk__rsc_trace(clone,
+ "Added %s to %s score on %s for site colocation "
+ "(now %s)",
+ pcmk_readable_score(colocation->score),
+ clone->id, pcmk__node_name(node),
+ pcmk_readable_score(node->weight));
+ }
+ }
+}
+
/*!
* \internal
* \brief Apply colocation to primary's node scores if for promoted role
@@ -362,6 +477,14 @@ apply_coloc_to_primary(gpointer data, gpointer user_data)
return;
}
+ // Self-colocation: group promotion by node attribute value
+ if (dependent == clone) {
+ if (colocation->score > 0) {
+ apply_site_colocation(clone, colocation);
+ }
+ return;
+ }
+
pcmk__rsc_trace(clone, "Applying colocation %s (%s with promoted %s) @%s",
colocation->id, colocation->dependent->id,
colocation->primary->id,
@@ -1274,7 +1397,8 @@ pcmk__update_promotable_dependent_priority(const pcmk_resource_t *primary,
// Look for a primary instance where dependent will be
primary_instance = pcmk__find_compatible_instance(dependent, primary,
colocation->primary_role,
- false);
+ false,
+ colocation->node_attribute);
if (primary_instance != NULL) {
// Add primary instance's priority to dependent's