diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml
index 7c648df07abcf..c24a4aab33455 100644
--- a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml
+++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp4.22.yaml
@@ -23,10 +23,6 @@ base_images:
name: "2.17"
namespace: acm-qe
tag: multicluster-observability-operator-opp
- ocs-ci-tests:
- name: ocs-ci-container
- namespace: ci
- tag: stable
tests-private:
name: tests-private
namespace: ci
@@ -90,8 +86,6 @@ tests:
FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP
FIREWATCH_FAIL_WITH_TEST_FAILURES: "true"
IGNORE_SECONDARY_POLICIES: "true"
- MAP_TESTS: "true"
- ODF_VERSION_MAJOR_MINOR: "4.21"
OPERATORS: |
[
{"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.16", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"}
@@ -125,7 +119,7 @@ tests:
- ref: acm-tests-clc-create
- ref: acm-fetch-managed-clusters
- ref: acm-opp-app
- - ref: interop-tests-ocs-tests
+ - ref: interop-opp-odf-health
- ref: quay-tests-quay-interop-test
- ref: acm-tests-observability
- ref: acm-tests-grc
@@ -150,8 +144,6 @@ tests:
cluster_profile: vsphere-connected-2
env:
COMPUTE_NODE_REPLICAS: "6"
- DISABLE_ENVIRONMENT_CHECKER: "true"
- DR__RP__CR_COMP_NAME: lp-interop--OPP
FIREWATCH_CONFIG_FILE_PATH: https://raw.githubusercontent.com/CSPI-QE/cspi-utils/refs/heads/main/firewatch-base-configs/opp/lp-interop-vsphere.json
FIREWATCH_DEFAULT_JIRA_ADDITIONAL_LABELS: '["4.22-lp","opp-vsphere-lp","opp-lp"]'
FIREWATCH_DEFAULT_JIRA_ASSIGNEE: ftan@redhat.com
@@ -159,8 +151,6 @@ tests:
FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP
FIREWATCH_FAIL_WITH_TEST_FAILURES: "true"
IGNORE_SECONDARY_POLICIES: "true"
- MAP_TESTS: "true"
- ODF_VERSION_MAJOR_MINOR: "4.21"
OPENSHIFT_REQUIRED_CORES: "72"
OPENSHIFT_REQUIRED_MEMORY: "288"
OPERATORS: |
@@ -184,7 +174,7 @@ tests:
- ref: acm-policies-openshift-plus-setup
- ref: acm-policies-openshift-plus
- chain: cucushift-installer-check-cluster-health
- - ref: interop-tests-ocs-tests
+ - ref: interop-opp-odf-health
- ref: acm-tests-observability
- ref: acm-opp-app
- ref: openshift-extended-test
diff --git a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml
index 2efc0cc8ee622..610fc395e85fb 100644
--- a/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml
+++ b/ci-operator/config/stolostron/policy-collection/stolostron-policy-collection-main__ocp5.0.yaml
@@ -23,10 +23,6 @@ base_images:
name: "2.17"
namespace: acm-qe
tag: multicluster-observability-operator-opp
- ocs-ci-tests:
- name: ocs-ci-container
- namespace: ci
- tag: stable
tests-private:
name: tests-private
namespace: ci
@@ -89,8 +85,6 @@ tests:
FIREWATCH_DEFAULT_JIRA_EPIC: INTEROP-9323
FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP
FIREWATCH_FAIL_WITH_TEST_FAILURES: "true"
- MAP_TESTS: "true"
- ODF_VERSION_MAJOR_MINOR: "5.0"
OPERATORS: |
[
{"name": "advanced-cluster-management", "source": "redhat-operators", "channel": "release-2.17", "install_namespace": "ocm", "target_namespaces": "ocm", "operator_group": "acm-operator-group"}
@@ -123,7 +117,7 @@ tests:
- ref: acm-tests-clc-create
- ref: acm-fetch-managed-clusters
- ref: acm-opp-app
- - ref: interop-tests-ocs-tests
+ - ref: interop-opp-odf-health
- ref: quay-tests-quay-interop-test
- ref: acm-tests-observability
- ref: acm-tests-grc
@@ -148,16 +142,12 @@ tests:
cluster_profile: vsphere-connected-2
env:
COMPUTE_NODE_REPLICAS: "6"
- DISABLE_ENVIRONMENT_CHECKER: "true"
- DR__RP__CR_COMP_NAME: lp-interop--OPP
FIREWATCH_CONFIG_FILE_PATH: https://raw.githubusercontent.com/CSPI-QE/cspi-utils/refs/heads/main/firewatch-base-configs/opp/lp-interop-vsphere.json
FIREWATCH_DEFAULT_JIRA_ADDITIONAL_LABELS: '["5.0-lp","opp-vsphere-lp","opp-lp"]'
FIREWATCH_DEFAULT_JIRA_ASSIGNEE: mpruitt@redhat.com
FIREWATCH_DEFAULT_JIRA_EPIC: INTEROP-9323
FIREWATCH_DEFAULT_JIRA_PROJECT: LPINTEROP
FIREWATCH_FAIL_WITH_TEST_FAILURES: "true"
- MAP_TESTS: "true"
- ODF_VERSION_MAJOR_MINOR: "5.0"
OPENSHIFT_REQUIRED_CORES: "72"
OPENSHIFT_REQUIRED_MEMORY: "288"
OPERATORS: |
@@ -180,7 +170,7 @@ tests:
- ref: acm-policies-openshift-plus-setup
- ref: acm-policies-openshift-plus
- chain: cucushift-installer-check-cluster-health
- - ref: interop-tests-ocs-tests
+ - ref: interop-opp-odf-health
- ref: acm-tests-observability
- ref: acm-opp-app
- ref: openshift-extended-test
diff --git a/ci-operator/step-registry/interop/opp/odf-health/OWNERS b/ci-operator/step-registry/interop/opp/odf-health/OWNERS
new file mode 100644
index 0000000000000..41d144d3728a2
--- /dev/null
+++ b/ci-operator/step-registry/interop/opp/odf-health/OWNERS
@@ -0,0 +1,3 @@
+approvers: &owners
+- cspi-qe-ocp-lp
+reviewers: *owners
diff --git a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh
new file mode 100755
index 0000000000000..156a7f9b28caa
--- /dev/null
+++ b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-commands.sh
@@ -0,0 +1,525 @@
+#!/bin/bash
+set -euo pipefail
+shopt -s inherit_errexit
+
+# ---------------------------------------------------------------------------
+# ODF Health Check (7-point gate)
+#
+# Validates that ODF is healthy and functional in the OPP interop cluster.
+# Replaces the former interop-tests-ocs-tests step which ran single-product
+# ODF acceptance tests unrelated to cross-product interop.
+#
+# Produces JUnit XML consumed by Prow / Sippy / TestGrid.
+# ---------------------------------------------------------------------------
+
+ODF_NAMESPACE="${ODF_NAMESPACE:-openshift-storage}"
+NOOBAA_S3_TIMEOUT="${NOOBAA_S3_TIMEOUT:-30}"
+
+typeset junitFile="${ARTIFACT_DIR}/junit_odf_health.xml"
+
+typeset -a tcNamesArr=()
+typeset -a tcResultsArr=()
+typeset -a tcMessagesArr=()
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+function AddResult () {
+ typeset name="${1:-}"; (($#)) && shift
+ typeset result="${1:-}"; (($#)) && shift
+ typeset message="${1:-}"; (($#)) && shift
+ tcNamesArr+=("${name}")
+ tcResultsArr+=("${result}")
+ tcMessagesArr+=("${message}")
+ true
+}
+
+function XmlEscape () {
+ typeset text="${1:-}"; (($#)) && shift
+ text="${text//&/&}"
+ text="${text//<}"
+ text="${text//>/>}"
+ text="${text//\"/"}"
+ text="${text//\'/'}"
+ printf '%s' "${text}"
+ true
+}
+
+function WriteJunit () {
+ typeset -i total=${#tcNamesArr[@]}
+ typeset -i failCount=0
+ typeset -i skipCount=0
+ typeset r=""
+ for r in "${tcResultsArr[@]}"; do
+ if [[ "${r}" == "fail" ]]; then
+ (( failCount++ )) || true
+ elif [[ "${r}" == "skip" ]]; then
+ (( skipCount++ )) || true
+ fi
+ done
+
+ {
+ echo ''
+ echo ""
+ typeset -i i=0
+ for i in "${!tcNamesArr[@]}"; do
+ typeset name=""
+ name="$(XmlEscape "${tcNamesArr[$i]}")"
+ echo " "
+ if [[ "${tcResultsArr[$i]}" == "fail" ]]; then
+ typeset msg=""
+ msg="$(XmlEscape "${tcMessagesArr[$i]}")"
+ echo " "
+ elif [[ "${tcResultsArr[$i]}" == "skip" ]]; then
+ typeset msg=""
+ msg="$(XmlEscape "${tcMessagesArr[$i]}")"
+ echo " "
+ fi
+ echo " "
+ done
+ echo ""
+ } > "${junitFile}"
+ : "JUnit XML written to ${junitFile}"
+}
+
+# shellcheck disable=SC2317
+function CollectExitArtifacts () {
+ : "Collecting ODF diagnostics..."
+ oc get csv -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/odf-csvs.yaml" || true
+ oc get storagecluster -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/storagecluster.yaml" || true
+ oc get cephcluster -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/cephcluster.yaml" || true
+ oc get noobaa -n "${ODF_NAMESPACE}" -o yaml > "${ARTIFACT_DIR}/noobaa.yaml" || true
+ oc get sc -o yaml > "${ARTIFACT_DIR}/storageclasses.yaml" || true
+}
+
+trap CollectExitArtifacts EXIT
+
+# ---------------------------------------------------------------------------
+# Check 1: ODF Operator CSV in Succeeded phase
+# ---------------------------------------------------------------------------
+
+function CheckOdfCsv () {
+ : "=== Check 1: ODF Operator CSV ==="
+
+ typeset csvPhase=""
+ if ! csvPhase="$(oc get csv -n "${ODF_NAMESPACE}" -o json | python3 -c "
+import sys,json,re; d=json.load(sys.stdin)
+m=[i for i in d.get('items',[]) if re.match(r'^(odf-|ocs-)operator',i['metadata']['name'])]
+print((m[0].get('status',{}).get('phase','NotFound')) if m else 'NotFound')
+")"; then
+ AddResult "odf-csv-phase" "fail" "Failed to query ODF CSVs in ${ODF_NAMESPACE}"
+ return
+ fi
+
+ if [[ "${csvPhase}" == "Succeeded" ]]; then
+ : "PASS: ODF CSV phase=Succeeded"
+ AddResult "odf-csv-phase" "pass"
+ elif [[ "${csvPhase}" == "NotFound" ]]; then
+ AddResult "odf-csv-phase" "fail" "No ODF/OCS operator CSV found in ${ODF_NAMESPACE}"
+ else
+ AddResult "odf-csv-phase" "fail" "ODF CSV phase=${csvPhase} (expected Succeeded)"
+ fi
+ true
+}
+
+# ---------------------------------------------------------------------------
+# Check 2: StorageCluster phase == Ready
+# ---------------------------------------------------------------------------
+
+function CheckStorageCluster () {
+ : "=== Check 2: StorageCluster Ready ==="
+
+ typeset scPhase=""
+ if ! scPhase="$(oc get storagecluster -n "${ODF_NAMESPACE}" -o json | python3 -c "
+import sys,json; d=json.load(sys.stdin)
+print(d['items'][0]['status'].get('phase','NotFound') if d.get('items') else 'NotFound')
+")"; then
+ AddResult "storagecluster-ready" "fail" "Failed to query StorageCluster"
+ return
+ fi
+
+ if [[ "${scPhase}" == "Ready" ]]; then
+ : "PASS: StorageCluster phase=Ready"
+ AddResult "storagecluster-ready" "pass"
+ elif [[ "${scPhase}" == "NotFound" ]]; then
+ AddResult "storagecluster-ready" "fail" "No StorageCluster found in ${ODF_NAMESPACE}"
+ else
+ AddResult "storagecluster-ready" "fail" "StorageCluster phase=${scPhase} (expected Ready)"
+ fi
+ true
+}
+
+# ---------------------------------------------------------------------------
+# Check 3: CephCluster health == HEALTH_OK or HEALTH_WARN
+# ---------------------------------------------------------------------------
+
+function CheckCephCluster () {
+ : "=== Check 3: CephCluster health ==="
+
+ typeset cephHealth=""
+ if ! cephHealth="$(oc get cephcluster -n "${ODF_NAMESPACE}" -o json | python3 -c "
+import sys,json; d=json.load(sys.stdin)
+print(d['items'][0].get('status',{}).get('ceph',{}).get('health','NotFound') if d.get('items') else 'NotFound')
+")"; then
+ AddResult "cephcluster-health" "fail" "Failed to query CephCluster"
+ return
+ fi
+
+ if [[ "${cephHealth}" == "HEALTH_OK" || "${cephHealth}" == "HEALTH_WARN" ]]; then
+ : "PASS: CephCluster health=${cephHealth}"
+ AddResult "cephcluster-health" "pass"
+ elif [[ "${cephHealth}" == "NotFound" ]]; then
+ AddResult "cephcluster-health" "fail" "No CephCluster found in ${ODF_NAMESPACE}"
+ else
+ AddResult "cephcluster-health" "fail" "CephCluster health=${cephHealth} (expected HEALTH_OK or HEALTH_WARN)"
+ fi
+ true
+}
+
+# ---------------------------------------------------------------------------
+# Check 4: Default StorageClasses available (ceph-rbd, cephfs)
+# ---------------------------------------------------------------------------
+
+function CheckStorageClasses () {
+ : "=== Check 4: StorageClasses ==="
+ typeset failMsg=""
+ typeset scName=""
+
+ for scName in ocs-storagecluster-ceph-rbd ocs-storagecluster-cephfs; do
+ if ! oc get sc "${scName}" -o name 2>/dev/null; then
+ if [[ -n "${failMsg}" ]]; then
+ failMsg="${failMsg}; StorageClass ${scName} not found"
+ else
+ failMsg="StorageClass ${scName} not found"
+ fi
+ else
+ : "PASS: StorageClass ${scName} exists"
+ fi
+ done
+
+ if [[ -z "${failMsg}" ]]; then
+ AddResult "storageclasses-available" "pass"
+ else
+ AddResult "storageclasses-available" "fail" "${failMsg}"
+ fi
+ true
+}
+
+# ---------------------------------------------------------------------------
+# Check 5: PVC provisionable (create, bind, delete)
+# ---------------------------------------------------------------------------
+
+function CheckPvcProvision () {
+ : "=== Check 5: PVC provisioning (RBD + CephFS) ==="
+
+ typeset -a scTests=("ocs-storagecluster-ceph-rbd" "ocs-storagecluster-cephfs")
+ typeset -a scModes=("ReadWriteOnce" "ReadWriteMany")
+ typeset -i idx=0
+
+ for idx in "${!scTests[@]}"; do
+ typeset scName="${scTests[$idx]}"
+ typeset accessMode="${scModes[$idx]}"
+ typeset testId="pvc-provision-${scName##*-}"
+ typeset pvcName="odf-health-${scName##*-}-${RANDOM}"
+ typeset pvcYaml
+ pvcYaml=$(cat </dev/null || echo "")"
+ if [[ "${phase}" == "Bound" ]]; then
+ break
+ fi
+ sleep 5
+ (( elapsed += 5 )) || true
+ done
+
+ oc delete pvc "${pvcName}" -n "${ODF_NAMESPACE}" --wait=false 2>/dev/null || true
+
+ if [[ "${phase}" == "Bound" ]]; then
+ : "PASS: ${scName} PVC bound in ${elapsed}s"
+ AddResult "${testId}" "pass"
+ else
+ AddResult "${testId}" "fail" "${scName} PVC did not bind within ${maxWait}s (phase=${phase:-unknown})"
+ fi
+ done
+ true
+}
+
+# ---------------------------------------------------------------------------
+# Check 6: NooBaa system Ready + S3 put/get/delete functional check
+# ---------------------------------------------------------------------------
+
+function CheckNoobaa () {
+ : "=== Check 6: NooBaa S3 functional ==="
+
+ typeset nbPhase=""
+ if ! nbPhase="$(oc get noobaa -n "${ODF_NAMESPACE}" -o json | python3 -c "
+import sys,json; d=json.load(sys.stdin)
+print(d['items'][0]['status'].get('phase','NotFound') if d.get('items') else 'NotFound')
+")"; then
+ AddResult "noobaa-s3-functional" "fail" "Failed to query NooBaa"
+ return
+ fi
+
+ if [[ "${nbPhase}" == "NotFound" ]]; then
+ AddResult "noobaa-s3-functional" "fail" "No NooBaa system found in ${ODF_NAMESPACE}"
+ return
+ fi
+
+ if [[ "${nbPhase}" != "Ready" ]]; then
+ AddResult "noobaa-s3-functional" "fail" "NooBaa phase=${nbPhase} (expected Ready)"
+ return
+ fi
+
+ : "NooBaa phase=Ready, creating OBC for S3 functional check..."
+
+ typeset obcName="odf-health-obc-${RANDOM}"
+ typeset obcYaml
+ obcYaml=$(cat </dev/null || echo "")"
+ if [[ "${obcPhase}" == "Bound" ]]; then
+ break
+ fi
+ sleep 5
+ (( elapsed += 5 )) || true
+ done
+
+ if [[ "${obcPhase}" != "Bound" ]]; then
+ oc delete obc "${obcName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true
+ AddResult "noobaa-s3-functional" "fail" "OBC did not bind within ${maxWait}s (phase=${obcPhase:-unknown})"
+ return
+ fi
+
+ typeset bucketName=""
+ bucketName="$(oc get obc "${obcName}" -n "${ODF_NAMESPACE}" -o jsonpath='{.spec.bucketName}' 2>/dev/null)" || true
+ if [[ -z "${bucketName}" ]]; then
+ oc delete obc "${obcName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true
+ AddResult "noobaa-s3-functional" "fail" "OBC bound but bucket name is empty"
+ return
+ fi
+ typeset secretRef=""
+ secretRef="$(oc get obc "${obcName}" -n "${ODF_NAMESPACE}" -o jsonpath='{.spec.secretName}' 2>/dev/null)"
+ if [[ -z "${secretRef}" ]]; then
+ secretRef="${obcName}"
+ fi
+
+ typeset s3Endpoint=""
+ s3Endpoint="$(oc get noobaa -n "${ODF_NAMESPACE}" -o json | python3 -c "
+import sys,json; d=json.load(sys.stdin)
+v=d['items'][0].get('status',{}).get('services',{}).get('serviceS3',{}).get('internalDNS',[])
+print(v[0] if v else '')
+")" || true
+ if [[ -z "${s3Endpoint}" ]]; then
+ s3Endpoint="https://s3.${ODF_NAMESPACE}.svc:443"
+ fi
+
+ typeset testKey="health-check-${RANDOM}"
+ typeset testData=""
+ testData="odf-health-$(date +%s)"
+
+ typeset podName="odf-health-s3-check-${RANDOM}"
+ typeset podManifest=""
+ podManifest=$(cat </dev/null && \
+ RETRIEVED=\$(aws --endpoint-url "\${S3_ENDPOINT}" --no-verify-ssl s3 cp "s3://\${BUCKET_NAME}/\${TEST_KEY}" - 2>/dev/null) && \
+ aws --endpoint-url "\${S3_ENDPOINT}" --no-verify-ssl s3 rm "s3://\${BUCKET_NAME}/\${TEST_KEY}" 2>/dev/null && \
+ if [ "\${RETRIEVED}" = "\${TEST_DATA}" ]; then echo "S3_CHECK_PASS"; else echo "S3_CHECK_FAIL: data mismatch"; fi
+ activeDeadlineSeconds: ${NOOBAA_S3_TIMEOUT}
+EOF
+)
+
+ typeset s3Result=""
+ typeset -i podWait=$(( NOOBAA_S3_TIMEOUT + 60 ))
+ if echo "${podManifest}" | oc apply -f -; then
+ if ! oc wait pod "${podName}" -n "${ODF_NAMESPACE}" \
+ --for=jsonpath='{.status.phase}'=Succeeded \
+ --timeout="${podWait}s" 2>/dev/null; then
+ : "Pod did not succeed within ${podWait}s, checking logs anyway"
+ fi
+ s3Result="$(oc logs "${podName}" -n "${ODF_NAMESPACE}" 2>/dev/null || echo "")"
+ fi
+
+ oc delete pod "${podName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true
+ oc delete obc "${obcName}" -n "${ODF_NAMESPACE}" --ignore-not-found=true --wait=false 2>/dev/null || true
+
+ if echo "${s3Result}" | grep -q "S3_CHECK_PASS"; then
+ : "PASS: NooBaa S3 put/get/delete cycle succeeded"
+ AddResult "noobaa-s3-functional" "pass"
+ else
+ typeset s3Msg="NooBaa S3 functional check failed"
+ if echo "${s3Result}" | grep -q "S3_CHECK_FAIL"; then
+ s3Msg="NooBaa S3: $(echo "${s3Result}" | grep "S3_CHECK_FAIL")"
+ fi
+ AddResult "noobaa-s3-functional" "fail" "${s3Msg}"
+ fi
+ true
+}
+
+# ---------------------------------------------------------------------------
+# Check 7: Ceph overall health (via toolbox or CephCluster status)
+# ---------------------------------------------------------------------------
+
+function CheckCephHealth () {
+ : "=== Check 7: Ceph health detail ==="
+
+ typeset cephDetail=""
+ if ! cephDetail="$(oc get cephcluster -n "${ODF_NAMESPACE}" -o json | python3 -c "
+import sys,json; d=json.load(sys.stdin)
+details=d['items'][0].get('status',{}).get('ceph',{}).get('details',{}) if d.get('items') else {}
+for k,v in details.items():
+ if v.get('severity')!='HEALTH_OK': print(f\"{k}: {v.get('message','unknown')}\")
+")"; then
+ AddResult "ceph-health-detail" "fail" "Failed to query CephCluster details"
+ return
+ fi
+
+ typeset cephHealth=""
+ cephHealth="$(oc get cephcluster -n "${ODF_NAMESPACE}" -o json | python3 -c "
+import sys,json; d=json.load(sys.stdin)
+print(d['items'][0].get('status',{}).get('ceph',{}).get('health','unknown') if d.get('items') else 'unknown')
+")" || true
+
+ if [[ "${cephHealth}" == "HEALTH_OK" ]]; then
+ : "PASS: Ceph health=HEALTH_OK"
+ AddResult "ceph-health-detail" "pass"
+ elif [[ "${cephHealth}" == "HEALTH_WARN" ]]; then
+ : "PASS (warn): Ceph health=HEALTH_WARN: ${cephDetail}"
+ AddResult "ceph-health-detail" "pass"
+ else
+ AddResult "ceph-health-detail" "fail" "Ceph health=${cephHealth}: ${cephDetail}"
+ fi
+ true
+}
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+function Main () {
+ if [[ -f "${SHARED_DIR}/kubeconfig" ]]; then
+ export KUBECONFIG="${SHARED_DIR}/kubeconfig"
+ fi
+
+ : "ODF Health Check (7-point gate) starting"
+ : "Namespace: ${ODF_NAMESPACE}"
+ : "Artifacts dir: ${ARTIFACT_DIR}"
+
+ CheckOdfCsv || true
+ CheckStorageCluster || true
+ CheckCephCluster || true
+ CheckStorageClasses || true
+ CheckPvcProvision || true
+ CheckNoobaa || true
+ CheckCephHealth || true
+
+ WriteJunit
+
+ typeset -i hasAnyFail=0
+ typeset r=""
+ for r in "${tcResultsArr[@]}"; do
+ if [[ "${r}" == "fail" ]]; then
+ hasAnyFail=1
+ break
+ fi
+ done
+
+ if (( hasAnyFail )); then
+ : "ODF Health Check: SOME CHECKS FAILED"
+ exit 1
+ fi
+
+ : "ODF Health Check: ALL PASSED"
+ exit 0
+}
+
+Main "$@"
diff --git a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.metadata.json b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.metadata.json
new file mode 100644
index 0000000000000..4db42591de65c
--- /dev/null
+++ b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.metadata.json
@@ -0,0 +1,11 @@
+{
+ "path": "interop/opp/odf-health/interop-opp-odf-health-ref.yaml",
+ "owners": {
+ "approvers": [
+ "cspi-qe-ocp-lp"
+ ],
+ "reviewers": [
+ "cspi-qe-ocp-lp"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.yaml b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.yaml
new file mode 100644
index 0000000000000..9f8c8a5523239
--- /dev/null
+++ b/ci-operator/step-registry/interop/opp/odf-health/interop-opp-odf-health-ref.yaml
@@ -0,0 +1,25 @@
+ref:
+ as: interop-opp-odf-health
+ from: cli
+ grace_period: 30s
+ commands: interop-opp-odf-health-commands.sh
+ timeout: 8m
+ env:
+ - name: ODF_NAMESPACE
+ default: "openshift-storage"
+ documentation: Namespace where ODF is installed
+ - name: NOOBAA_S3_TIMEOUT
+ default: "30"
+ documentation: Timeout in seconds for the NooBaa S3 functional check pod
+ resources:
+ requests:
+ cpu: 100m
+ memory: 200Mi
+ documentation: |-
+ Validates ODF health in the OPP interop cluster via a 7-point gate:
+ ODF Operator CSV phase, StorageCluster readiness, CephCluster health,
+ default StorageClass availability, PVC provisioning, NooBaa system
+ readiness with S3 functional check, and Ceph overall health.
+ Replaces the former interop-tests-ocs-tests step which ran 28
+ single-product acceptance tests with a 3h timeout.
+ Produces JUnit XML for Prow / Sippy / TestGrid consumption.