Repository: openstack-k8s-operators/ci-framework
Pull request #2887: Create bmh compute replace validator
Author: login: drosenfe
Labels: name: lgtm
name: Ready For Review
Created: 2025-04-09T17:07:07Z | Merged: 2025-04-28T12:42:25Z

Add a validator that verifies a faulty bmh compute node may be replaced


Diff:
diff --git a/roles/validations/defaults/main.yml b/roles/validations/defaults/main.yml
index 9657c2d35..f33253a41 100644
--- a/roles/validations/defaults/main.yml
+++ b/roles/validations/defaults/main.yml
@@ -51,3 +51,9 @@ cifmw_validations_xml_status_file_dir: "{{ cifmw_validations_basedir }}/tests/va
 cifmw_validations_edpm_scale_down_hostname: compute-2.ctlplane.example.com
 cifmw_validations_edpm_scale_down_nodename: edpm-compute-2
 cifmw_validations_timeout: 100
+
+# variables needed for bmh compute replacement
+cifmw_validations_bmh_replace_leaf_label: leaf0-1
+cifmw_validations_bmh_spare_leaf_label: leaf0-0
+cifmw_validations_bmh_spare_nodename: edpm-compute-0-0
+cifmw_validations_bmh_spare_hostname: edpm-compute-0-0.ctlplane.openstack.lab
diff --git a/roles/validations/tasks/edpm/bmh_compute_replace.yml b/roles/validations/tasks/edpm/bmh_compute_replace.yml
new file mode 100644
index 000000000..2947c2756
--- /dev/null
+++ b/roles/validations/tasks/edpm/bmh_compute_replace.yml
@@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} -o jsonpath='{.spec.consumerRef.name}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} -o jsonpath='{.status.provisioning.state}'
+  register: bmh_nodes_before_scale_down
+  failed_when: bmh_nodes_before_scale_down.stdout != "provisioned"
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} -f value -c ID
+  register: remove_compute_service_id
+
+- name: Delete compute service for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service delete {{ remove_compute_service_id.stdout }}
+
+- name: Patch nodeset to remove node
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} osdpns/"{{ bmh_nodeset_name.stdout | trim}}" --type=json --patch '[{ "op": "remove", "path": "/spec/nodes/{{ cifmw_validations_bmh_spare_nodename }}" }]'
+
+- name: Wait for nodeset to be SetupReady again
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=SetupReady
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Patch spare bmh node to change its label to match label of node being replaced
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} bmh/"{{ cifmw_validations_bmh_spare_leaf_label }}" --type=json --patch '[{ "op": "replace", "path": "/metadata/labels/nodeName", "value": "{{ cifmw_validations_bmh_replace_leaf_label }}" }]'
+
+- name: Verify bmh node being used as spare is available after changing label
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} -o jsonpath='{.status.provisioning.state}'
+  register: bmh_nodes_after_scale_down
+  until: bmh_nodes_after_scale_down.stdout == "available"
+  retries: 20
+  delay: 20
+
+- name: Create openstackdataplanedeployment to deploy the scaledown
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: |
+      oc apply -f - <<EOF
+      apiVersion: dataplane.openstack.org/v1beta1
+      kind: OpenStackDataPlaneDeployment
+      metadata:
+        name: edpm-scaledown
+        namespace: {{ cifmw_validations_namespace }}
+      spec:
+        nodeSets:
+          - "{{ bmh_nodeset_name.stdout | trim }}"
+        servicesOverride:
+          - ssh-known-hosts
+      EOF
+
+- name: Wait for deployment to be complete
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait openstackdataplanedeployment edpm-scaledown
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=ready
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Delete faulty baremetal node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} delete bmh {{ cifmw_validations_bmh_replace_leaf_label }}
+
+- name: Wait for nodeset to be SetupReady
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=SetupReady
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Create openstackdataplanedeployment to deploy the compute replacement
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: |
+      oc apply -f - <<EOF
+      apiVersion: dataplane.openstack.org/v1beta1
+      kind: OpenStackDataPlaneDeployment
+      metadata:
+        name: edpm-compute-replacement
+        namespace: {{ cifmw_validations_namespace }}
+      spec:
+        nodeSets:
+          - "{{ bmh_nodeset_name.stdout | trim }}"
+        servicesOverride:
+          - ssh-known-hosts
+      EOF
+
+- name: Wait for deployment to be complete
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait openstackdataplanedeployment edpm-compute-replacement
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=ready
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Wait for nodeset to be Ready
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=Ready
+      --timeout={{ cifmw_validations_timeout }}m


Reviews:
user: login: bshephar
body: Looks good. I just added a couple of suggestions that might be a bit more reliable than checking stdout for strings. My suggestions just focus on getting the specific information you want from the initial compared to make the conditionals a bit more simplified.

My thoughts are that it will probably result in the jobs being a bit more reliable.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T22:43:31Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T13:36:44Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:06:56Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:00:35Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:10:17Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T13:45:27Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:01:16Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:04:33Z
user: login: bshephar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T08:47:37Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T13:25:53Z
user: login: bshephar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T04:15:16Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T20:11:50Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T20:12:06Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T20:12:19Z
user: login: drosenfe
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T20:12:49Z
user: login: bshephar
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T22:19:59Z
user: login: frenzyfriday
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T11:10:01Z

Review comments:
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: The awk is ok, but you can also do this with just `oc`:
```
$ oc get bmh --no-headers -o custom-columns=":metadata.name"
bshephar-edpm-compute-01
bshephar-edpm-compute-02
bshephar-edpm-compute-03
```
created_at: 2025-04-13T22:17:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: ```suggestion
      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} -o jsonpath='{.status.provisioning.state}'
  register: bmh_nodes_before_scale_down
  failed_when: bmh_nodes_before_scale_down != "provisioned"
```

I think that just makes the `failed_when` conditional a little more reliable. Just in case the stdout is something unexpected that just happens to container `provisioned`.
created_at: 2025-04-13T22:25:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,3 +51,9 @@ cifmw_validations_xml_status_file_dir: "{{ cifmw_validations_basedir }}/tests/va
 cifmw_validations_edpm_scale_down_hostname: compute-2.ctlplane.example.com
 cifmw_validations_edpm_scale_down_nodename: edpm-compute-2
 cifmw_validations_timeout: 100
+
+# variables needed for bmh compute replacement
+cifmw_validations_bmh_replace_leaf_label: leaf0-1
+cifmw_validations_bmh_spare_leaf_label: leaf0-0
path: roles/validations/defaults/main.yml
user: login: bshephar
body: Is this actually a `label`? Or is it the node name? For example, we typically use `app: openstack` as a label:
```
[m3@osp-df-3 beta]$ oc get bmh -o yaml bshephar-edpm-compute-01 | yq --yaml-output .metadata.labels
app: openstack
openstack-edpm-ipam-osbms-hostname: edpm-compute-0
openstackbaremetalset.openstack.org/name: openstack-edpm-ipam
openstackbaremetalset.openstack.org/namespace: openstack
openstackbaremetalset.openstack.org/uid: c80bb9b7-a04c-4407-bba2-965ce7db1984
```

So, to list nodes using the label we could:
```
[m3@osp-df-3 beta]$ oc get bmh -l app=openstack
NAME                       STATE         CONSUMER               ONLINE   ERROR   AGE
bshephar-edpm-compute-01   provisioned   openstack-edpm-ipam    true             72d
bshephar-edpm-compute-02   available                            false            73d
bshephar-edpm-compute-03   provisioned   openstack-edpm-bootc   true             70d
```

But looking at the `oc get bmh` commands used in this PR below, it seems like we're not searching for the nodes using a label selector using `-l`. If this is indeed the name, maybe `cifmw_validations_bmh_spare_leaf_nodename` ?
created_at: 2025-04-13T22:29:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: ```suggestion
      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} -f value -c ID
```

That will just give you the value:
```
oc rsh openstackclient openstack network agent list --host edpm-compute-1.ctlplane.example.com -f value -c ID
945d481a-20c4-45d3-8c92-3b2c04ba628f
```
created_at: 2025-04-13T22:34:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: ```suggestion
      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} -f value -c ID
```

Same for this one:

```
$ oc rsh openstackclient openstack compute service list --host edpm-compute-1.ctlplane.example.com -f value -c ID
3e29ed72-c314-4c21-bfc4-93b7afda69c0
```
created_at: 2025-04-13T22:35:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
+  register: remove_compute_service_id
+
+- name: Delete compute service for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service delete {{ remove_compute_service_id.stdout }}
+
+- name: Patch nodeset to remove node
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} osdpns/"{{ bmh_nodeset_name.stdout | trim}}" --type=json --patch '[{ "op": "remove", "path": "/spec/nodes/{{ cifmw_validations_bmh_spare_nodename }}" }]'
+
+- name: Wait for nodeset to be SetupReady again
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=SetupReady
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Patch spare bmh node to change its label to match label of node being replaced
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} bmh/"{{ cifmw_validations_bmh_spare_leaf_label }}" --type=json --patch '[{ "op": "replace", "path": "/metadata/labels/nodeName", "value": "{{ cifmw_validations_bmh_replace_leaf_label }}" }]'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: Ahh, right, so it is the nodeName, but it's also being used as the label.
created_at: 2025-04-13T22:37:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
+  register: remove_compute_service_id
+
+- name: Delete compute service for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service delete {{ remove_compute_service_id.stdout }}
+
+- name: Patch nodeset to remove node
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} osdpns/"{{ bmh_nodeset_name.stdout | trim}}" --type=json --patch '[{ "op": "remove", "path": "/spec/nodes/{{ cifmw_validations_bmh_spare_nodename }}" }]'
+
+- name: Wait for nodeset to be SetupReady again
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=SetupReady
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Patch spare bmh node to change its label to match label of node being replaced
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} bmh/"{{ cifmw_validations_bmh_spare_leaf_label }}" --type=json --patch '[{ "op": "replace", "path": "/metadata/labels/nodeName", "value": "{{ cifmw_validations_bmh_replace_leaf_label }}" }]'
+
+- name: Verify bmh node being used as spare is available after changing label
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_after_scale_down
+  until: '"available" in bmh_nodes_after_scale_down.stdout'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: ```suggestion
      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} -o jsonpath='{.status.provisioning.state}'
  register: bmh_nodes_after_scale_down
  until: bmh_nodes_after_scale_down == "available"
```
created_at: 2025-04-13T22:40:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,3 +51,9 @@ cifmw_validations_xml_status_file_dir: "{{ cifmw_validations_basedir }}/tests/va
 cifmw_validations_edpm_scale_down_hostname: compute-2.ctlplane.example.com
 cifmw_validations_edpm_scale_down_nodename: edpm-compute-2
 cifmw_validations_timeout: 100
+
+# variables needed for bmh compute replacement
+cifmw_validations_bmh_replace_leaf_label: leaf0-1
+cifmw_validations_bmh_spare_leaf_label: leaf0-0
path: roles/validations/defaults/main.yml
user: login: drosenfe
body: I got label from this review: https://github.com/openstack-k8s-operators/architecture/pull/434/files   It does look like nodeName it is what its called and label is for the whole code block.
created_at: 2025-04-14T13:36:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,3 +51,9 @@ cifmw_validations_xml_status_file_dir: "{{ cifmw_validations_basedir }}/tests/va
 cifmw_validations_edpm_scale_down_hostname: compute-2.ctlplane.example.com
 cifmw_validations_edpm_scale_down_nodename: edpm-compute-2
 cifmw_validations_timeout: 100
+
+# variables needed for bmh compute replacement
+cifmw_validations_bmh_replace_leaf_label: leaf0-1
+cifmw_validations_bmh_spare_leaf_label: leaf0-0
path: roles/validations/defaults/main.yml
user: login: drosenfe
body: The above review also adds a bmhLabelSelector value which is the nodeName. I also see the comment below about this. I'm not certain what I should do. I can name the variable as you would like. Should I go with the suggested: cifmw_validations_bmh_spare_leaf_nodename or leave as it is?
created_at: 2025-04-14T14:06:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: The output of the current command with the awk is:

oc -n openstack get bmh leaf0-1 --no-headers | awk '{print $3}'
nodeset-0

i.e the validator needs the nodeset name and not the compute list. There could be a better option than the awk, but I'm not finding it. Tried this since the nodeset is in the CONSUMER column:

oc get bmh leaf0-1--no-headers -o custom-columns=":metadata.consumer"
but it returned none

If there is a better way to get the nodeset name when there are multiple nodesets then I could change it.
created_at: 2025-04-15T13:00:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: I'll try it. From command line it looks like it should work:
oc -n openstack get bmh leaf0-1 -o jsonpath='{.status.provisioning.state}'
provisioned
created_at: 2025-04-15T13:10:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: The network agent list output can contain both "OVN Controller agent" and "OVN Metadata agent" rows. When they are both present two IDS are returned e.g.:

oc -n openstack rsh openstackclient openstack network agent list --host edpm-compute-0-1.ctlplane.openstack.lab -f value -c ID
8dd6a928-5c30-4bdc-90cd-6e44db608d7a
2b2ca4c6-2a90-53f9-af29-1c27fede26f4

I tried using the --agent-type option in the command, but "OVN Controller agent" and "OVN Metadata agent" aren't allowable options. I'm open to modifying the command, but it needs to be something that will only return one id.
created_at: 2025-04-15T13:45:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: Using -f value -c ID only returns one id so it should be OK.
created_at: 2025-04-15T14:01:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
+  register: remove_compute_service_id
+
+- name: Delete compute service for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service delete {{ remove_compute_service_id.stdout }}
+
+- name: Patch nodeset to remove node
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} osdpns/"{{ bmh_nodeset_name.stdout | trim}}" --type=json --patch '[{ "op": "remove", "path": "/spec/nodes/{{ cifmw_validations_bmh_spare_nodename }}" }]'
+
+- name: Wait for nodeset to be SetupReady again
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=SetupReady
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Patch spare bmh node to change its label to match label of node being replaced
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} bmh/"{{ cifmw_validations_bmh_spare_leaf_label }}" --type=json --patch '[{ "op": "replace", "path": "/metadata/labels/nodeName", "value": "{{ cifmw_validations_bmh_replace_leaf_label }}" }]'
+
+- name: Verify bmh node being used as spare is available after changing label
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_after_scale_down
+  until: '"available" in bmh_nodes_after_scale_down.stdout'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: I'll try it. From command line looks like it should work.
created_at: 2025-04-15T14:04:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,3 +51,9 @@ cifmw_validations_xml_status_file_dir: "{{ cifmw_validations_basedir }}/tests/va
 cifmw_validations_edpm_scale_down_hostname: compute-2.ctlplane.example.com
 cifmw_validations_edpm_scale_down_nodename: edpm-compute-2
 cifmw_validations_timeout: 100
+
+# variables needed for bmh compute replacement
+cifmw_validations_bmh_replace_leaf_label: leaf0-1
+cifmw_validations_bmh_spare_leaf_label: leaf0-0
path: roles/validations/defaults/main.yml
user: login: bshephar
body: I think we can probably leave it as is. As long as it makes sense to you, and your use case, then I think it's fine.
created_at: 2025-04-16T06:29:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: Ah right, fair enough. Just leave this one as is then I think.
created_at: 2025-04-16T08:47:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
+  register: remove_compute_service_id
+
+- name: Delete compute service for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service delete {{ remove_compute_service_id.stdout }}
+
+- name: Patch nodeset to remove node
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} osdpns/"{{ bmh_nodeset_name.stdout | trim}}" --type=json --patch '[{ "op": "remove", "path": "/spec/nodes/{{ cifmw_validations_bmh_spare_nodename }}" }]'
+
+- name: Wait for nodeset to be SetupReady again
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=SetupReady
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Patch spare bmh node to change its label to match label of node being replaced
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} bmh/"{{ cifmw_validations_bmh_spare_leaf_label }}" --type=json --patch '[{ "op": "replace", "path": "/metadata/labels/nodeName", "value": "{{ cifmw_validations_bmh_replace_leaf_label }}" }]'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: Similar to other comment on label vs name. Leaving names the same with same reason.
created_at: 2025-04-16T13:25:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: bshephar
body: Ahh right, ok. That makes sense:
```
oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label } -o jsonpath='{.spec.consumerRef.name}'
```
created_at: 2025-04-17T04:15:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: done
created_at: 2025-04-17T20:11:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: done
created_at: 2025-04-17T20:12:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
+
+- name: Get compute service list
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list
+  register: compute_service_list_out
+  until: '"{{ cifmw_validations_bmh_spare_hostname }}" in compute_service_list_out.stdout'
+
+- name: Disable nova-compute for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service set {{ cifmw_validations_bmh_spare_hostname }} nova-compute --disable
+  register: compute_service_set_out
+  until: '"Failed" not in compute_service_set_out.stdout'
+
+- name: Get ovn controller id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "OVN Controller agent"  | awk '{print $2}'
+  register: remove_ovn_id
+
+- name: Delete network agent for compute being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack network agent delete {{ remove_ovn_id.stdout }}
+
+- name: Get compute service id of host to be removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service list --host {{ cifmw_validations_bmh_spare_hostname }} | grep "nova-compute"  | awk '{print $2}'
+  register: remove_compute_service_id
+
+- name: Delete compute service for node being removed
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} rsh openstackclient openstack compute service delete {{ remove_compute_service_id.stdout }}
+
+- name: Patch nodeset to remove node
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} osdpns/"{{ bmh_nodeset_name.stdout | trim}}" --type=json --patch '[{ "op": "remove", "path": "/spec/nodes/{{ cifmw_validations_bmh_spare_nodename }}" }]'
+
+- name: Wait for nodeset to be SetupReady again
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc wait osdpns "{{ bmh_nodeset_name.stdout | trim }}"
+      --namespace={{ cifmw_validations_namespace }}
+      --for=condition=SetupReady
+      --timeout={{ cifmw_validations_timeout }}m
+
+- name: Patch spare bmh node to change its label to match label of node being replaced
+  environment:
+    KUBECONFIG: "{{ cifmw_openshift_kubeconfig }}"
+    PATH: "{{ cifmw_path }}"
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc patch -n {{ cifmw_validations_namespace }} bmh/"{{ cifmw_validations_bmh_spare_leaf_label }}" --type=json --patch '[{ "op": "replace", "path": "/metadata/labels/nodeName", "value": "{{ cifmw_validations_bmh_replace_leaf_label }}" }]'
+
+- name: Verify bmh node being used as spare is available after changing label
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_after_scale_down
+  until: '"available" in bmh_nodes_after_scale_down.stdout'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: done
created_at: 2025-04-17T20:12:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,196 @@
+# This job tests the functionality of the openstack-operator to replace a
+# bmh compute node.
+#
+# This job was created to satisfy:
+# https://issues.redhat.com/browse/OSPRH-15061
+
+- name: Get name of nodeset containing bmh node to be used as spare node
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }} --no-headers | awk '{print $3}'
+  register: bmh_nodeset_name
+
+- name: Verify bmh node being used as spare is provisioned before scale down
+  cifmw.general.ci_script:
+    output_dir: "{{ cifmw_validations_basedir }}/artifacts"
+    script: >-
+      oc -n {{ cifmw_validations_namespace }} get bmh {{ cifmw_validations_bmh_spare_leaf_label }}
+  register: bmh_nodes_before_scale_down
+  failed_when: '"provisioned" not in bmh_nodes_before_scale_down.stdout'
path: roles/validations/tasks/edpm/bmh_compute_replace.yml
user: login: drosenfe
body: done
created_at: 2025-04-17T20:12:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: openshift-ci[bot]
created_at: 2025-04-09T17:07:14Z
body: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:


The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openstack-k8s-operators%2Fci-framework).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/openstack-k8s-operators/ci-framework/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: CONTRIBUTOR
user: login: drosenfe
created_at: 2025-04-23T19:40:14Z
body: @danpawlik   This is for a validator that is specific to edpm. The DFG has approved it. Need a second approver. Anyway you could do that?
author_association: CONTRIBUTOR
user: login: bshephar
created_at: 2025-04-25T21:43:56Z
body: /lgtm
author_association: CONTRIBUTOR