Add Netapp volume backend support STX-O storage classes

This change allows Kubernetes PVCs StorageClasses to not depend on Ceph,
and it will allow for multiple storage classes to coexist. The system
will pick the highest priority storage backend available to create the
persistent volumes where the data is stored.

Test Plan:
[PASS] build all packages and STX-O tarball.
[PASS] Apply successfully on host ceph only deployments.
[PASS] Apply successfully on rook ceph only deployments.
[PASS] Apply successfully on host ceph and NetApp-nfs deployments.
[PASS] Apply successfully when priority list is updated.
[PASS] Verify that persistent volumes are created correctly.

This change also updates the openstack lifecycle validation to allow
uploading STX-O to be applied without ceph. However, STX-O has an
automatic installation feature that installs required dependencies. It
auto installs rook-ceph despite it being removed. Hence, this scenario
was not tested

Story: 2011281
Task: 53121

Change-Id: Idea57a4ee8a362bcaea60dd873715cb57f7e8e77
Signed-off-by: Johnny Chia <johnny.chialung@windriver.com>
This commit is contained in:
jchialun
2025-10-29 11:27:39 -05:00
parent a311d4c391
commit 70800344d0
11 changed files with 262 additions and 5 deletions

View File

@@ -0,0 +1,57 @@
From cdad5b37e60f36d9b88e0e2ec79c666fffbcfae8 Mon Sep 17 00:00:00 2001
From: jchialun <johnny.chialung@windriver.com>
Date: Wed, 19 Nov 2025 14:56:26 -0500
Subject: [PATCH 1/1] Add volume storage class priorities
Added volume_storage_class_priority list to values.yaml file to enable
external applications to update the value. This list represents the
priority order for creation of the Kubernetes PVCs StorageClasses for
MariaDB and RabbitMQ. The highest available storage class will be
selected as the k8s volume storageClass
Signed-off-by: Johnny Chia <johnny.chialung@windriver.com>
---
mariadb/values.yaml | 8 ++++++++
rabbitmq/values.yaml | 7 +++++++
2 files changed, 15 insertions(+)
diff --git a/mariadb/values.yaml b/mariadb/values.yaml
index 9eb81231..66740a2c 100644
--- a/mariadb/values.yaml
+++ b/mariadb/values.yaml
@@ -740,6 +740,14 @@ network_policy:
# Set helm3_hook: false in case helm2 is used.
helm3_hook: true
+storage_conf:
+ volume_storage_class_priority:
+ - "ceph"
+ - "netapp-nfs"
+ - "netapp-iscsi"
+ - "netapp-fc"
+
+
manifests:
certificates: false
configmap_bin: true
diff --git a/rabbitmq/values.yaml b/rabbitmq/values.yaml
index fbb98414..99b2833c 100644
--- a/rabbitmq/values.yaml
+++ b/rabbitmq/values.yaml
@@ -441,6 +441,13 @@ io_thread_pool:
enabled: false
size: 64
+storage_conf:
+ volume_storage_class_priority:
+ - "ceph"
+ - "netapp-nfs"
+ - "netapp-iscsi"
+ - "netapp-fc"
+
manifests:
certificates: false
configmap_bin: true
--
2.43.0

View File

@@ -22,3 +22,4 @@
0022-Update-ipFamilyPolicy-to-support-DualStack.patch
0023-Update-libvirt-cgroup-controllers-initiation.patch
0024-Add-cluster-host-ip-env-var-to-libvirt.patch
0025-Add-volume-storage-class-priorities.patch

View File

@@ -154,6 +154,9 @@ NETAPP_STORAGECLASS_NAME = "csi.trident.netapp.io"
NETAPP_NFS_BACKEND_NAME = 'netapp-nfs'
NETAPP_ISCSI_BACKEND_NAME = 'netapp-iscsi'
NETAPP_FC_BACKEND_NAME = 'netapp-fc'
BACKEND_DEFAULT_STORAGE_CLASS = "general"
BACKEND_TYPE_NETAPP_NFS = "ontap-nas"
BACKEND_TYPE_NETAPP_ISCSI = "ontap-san"
# Storage backends overrides
OVERRIDE_STORAGE_BACKENDS = "storage_conf.storage_backends"

View File

@@ -10,6 +10,8 @@ from sysinv.helm import common
from k8sapp_openstack.common import constants as app_constants
from k8sapp_openstack.helm import openstack
from k8sapp_openstack.utils import get_available_volume_backends
from k8sapp_openstack.utils import get_storage_backends_priority_list
from k8sapp_openstack.utils import is_ipv4
@@ -25,6 +27,16 @@ class MariadbHelm(openstack.OpenstackBaseHelm):
return self._num_provisioned_controllers()
def get_overrides(self, namespace=None):
available_backend = get_available_volume_backends()
default_priority_list = get_storage_backends_priority_list(app_constants.HELM_CHART_MARIADB)
priority_storage_class = app_constants.BACKEND_DEFAULT_STORAGE_CLASS
for priority in default_priority_list:
if available_backend.get(priority, ""):
priority_storage_class = available_backend.get(priority)
break
overrides = {
common.HELM_NS_OPENSTACK: {
'pod': {
@@ -36,6 +48,13 @@ class MariadbHelm(openstack.OpenstackBaseHelm):
'endpoints': self._get_endpoints_overrides(),
'manifests': {
'config_ipv6': not is_ipv4()
},
'volume': {
'class_name': priority_storage_class,
'backup': {
'class_name': priority_storage_class,
}
}
}
}

View File

@@ -1,5 +1,5 @@
#
# Copyright (c) 2019-2020 Wind River Systems, Inc.
# Copyright (c) 2019-2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@@ -9,6 +9,8 @@ from sysinv.helm import common
from k8sapp_openstack.common import constants as app_constants
from k8sapp_openstack.helm import openstack
from k8sapp_openstack.utils import get_available_volume_backends
from k8sapp_openstack.utils import get_storage_backends_priority_list
class RabbitmqHelm(openstack.OpenstackBaseHelm):
@@ -27,6 +29,15 @@ class RabbitmqHelm(openstack.OpenstackBaseHelm):
elif io_thread_pool_size > 1024:
io_thread_pool_size = 1024
available_backend = get_available_volume_backends()
default_priority_list = get_storage_backends_priority_list(app_constants.HELM_CHART_RABBITMQ)
priority_storage_class = app_constants.BACKEND_DEFAULT_STORAGE_CLASS
for priority in default_priority_list:
if available_backend.get(priority, ""):
priority_storage_class = available_backend.get(priority)
break
overrides = {
common.HELM_NS_OPENSTACK: {
'pod': {
@@ -56,6 +67,9 @@ class RabbitmqHelm(openstack.OpenstackBaseHelm):
'endpoints': self._get_endpoints_overrides(),
'manifests': {
'config_ipv6': self._is_ipv6_cluster_service()
},
'volume': {
'class_name': priority_storage_class,
}
}
}

View File

@@ -431,8 +431,16 @@ class OpenstackAppLifecycleOperator(base.AppLifecycleOperator):
rook_ceph_available, _ = app_utils.is_ceph_backend_available(
ceph_type=constants.SB_TYPE_CEPH_ROOK
)
netapp_backends_available = app_utils.check_netapp_backends()
netapp_nfs_available = netapp_backends_available.get("nfs", False)
netapp_iscsi_available = netapp_backends_available.get("iscsi", False)
netapp_fc_available = netapp_backends_available.get("fc", False)
status = f"ceph_available={ceph_available}, " \
f"rook_ceph_available={rook_ceph_available}"
f"rook_ceph_available={rook_ceph_available}, " \
f"netapp_nfs_available={netapp_nfs_available}, " \
f"netapp_iscsi_available={netapp_iscsi_available}, " \
f"netapp_fc_available={netapp_fc_available}"
if rook_ceph_available:
rook_api_available = app_utils.is_rook_ceph_api_available()
fsid_available = app_utils.get_ceph_fsid() is not None
@@ -444,6 +452,13 @@ class OpenstackAppLifecycleOperator(base.AppLifecycleOperator):
backend_available = fsid_available
status += f", fsid_available={fsid_available}"
if netapp_nfs_available:
backend_available = True
elif netapp_iscsi_available:
backend_available = True
elif netapp_fc_available:
backend_available = True
if not backend_available:
err_msg = "No storage backends available and ready for openstack " \
f"deployment. status: {status}"

View File

@@ -33,6 +33,8 @@ class OpenstackAppLifecycleOperatorTest(dbbase.BaseHostTestCase):
constants.SB_TYPE_CEPH):
return ceph_type == constants.SB_TYPE_CEPH, ""
@mock.patch('k8sapp_openstack.utils.check_netapp_backends',
return_value={"nfs": False, "iscsi": False, "fc": False})
@mock.patch('k8sapp_openstack.utils.is_rook_ceph_api_available',
return_value=True)
@mock.patch('k8sapp_openstack.utils.get_ceph_fsid',
@@ -42,7 +44,8 @@ class OpenstackAppLifecycleOperatorTest(dbbase.BaseHostTestCase):
self,
mock_is_ceph_backend_available,
mock_get_ceph_fsid,
mock_is_rook_ceph_api_available
mock_is_rook_ceph_api_available,
mock_check_netapp_backends
):
""" Test _semantic_check_storage_backend_available for rook ceph
backend, api and fsid available.
@@ -51,16 +54,20 @@ class OpenstackAppLifecycleOperatorTest(dbbase.BaseHostTestCase):
self._rook_ceph_backend_available
self.lifecycle._semantic_check_storage_backend_available()
mock_is_ceph_backend_available.assert_called()
mock_check_netapp_backends.assert_called()
mock_get_ceph_fsid.assert_called()
mock_is_rook_ceph_api_available.assert_called()
@mock.patch('k8sapp_openstack.utils.check_netapp_backends',
return_value={"nfs": False, "iscsi": False, "fc": False})
@mock.patch('k8sapp_openstack.utils.get_ceph_fsid',
return_value='aa8c8da0-47de-4fad-8b5d-2c06be236fc8')
@mock.patch('k8sapp_openstack.utils.is_ceph_backend_available')
def test_semantic_check_storage_backend_available_ceph(
self,
mock_is_ceph_backend_available,
mock_get_ceph_fsid
mock_get_ceph_fsid,
mock_check_netapp_backends
):
""" Test _semantic_check_storage_backend_available for host ceph
backend and fsid available.
@@ -69,14 +76,38 @@ class OpenstackAppLifecycleOperatorTest(dbbase.BaseHostTestCase):
self._ceph_backend_available
self.lifecycle._semantic_check_storage_backend_available()
mock_is_ceph_backend_available.assert_called()
mock_check_netapp_backends.assert_called()
mock_get_ceph_fsid.assert_called()
@mock.patch('k8sapp_openstack.utils.check_netapp_backends',
return_value={"nfs": True, "iscsi": False, "fc": False})
@mock.patch('k8sapp_openstack.utils.get_ceph_fsid', return_value=None)
@mock.patch('k8sapp_openstack.utils.is_ceph_backend_available')
def test_semantic_check_storage_backend_available_netapp_nfs(
self,
mock_is_ceph_backend_available,
mock_get_ceph_fsid,
mock_check_netapp_backends,
):
""" Test _semantic_check_storage_backend_available for netapp backend
backend available.
"""
mock_is_ceph_backend_available.side_effect = \
self._ceph_backend_available
self.lifecycle._semantic_check_storage_backend_available()
mock_is_ceph_backend_available.assert_called()
mock_check_netapp_backends.assert_called()
mock_get_ceph_fsid.assert_called()
@mock.patch('k8sapp_openstack.utils.check_netapp_backends',
return_value={"nfs": False, "iscsi": False, "fc": False})
@mock.patch('k8sapp_openstack.utils.get_ceph_fsid', return_value=None)
@mock.patch('k8sapp_openstack.utils.is_ceph_backend_available')
def test_semantic_check_storage_backend_available_fsid_unavailable(
self,
mock_is_ceph_backend_available,
mock_get_ceph_fsid
mock_get_ceph_fsid,
mock_check_netapp_backends,
):
""" Test _semantic_check_storage_backend_available for host ceph
available and fsid unavailable.
@@ -90,7 +121,10 @@ class OpenstackAppLifecycleOperatorTest(dbbase.BaseHostTestCase):
else:
self.fail("LifecycleSemanticCheckException was not raised")
mock_get_ceph_fsid.assert_called()
mock_check_netapp_backends.assert_called()
@mock.patch('k8sapp_openstack.utils.check_netapp_backends',
return_value={"nfs": False, "iscsi": False, "fc": False})
@mock.patch('k8sapp_openstack.utils.get_ceph_fsid', return_value=None)
@mock.patch('k8sapp_openstack.utils.is_ceph_backend_available',
side_effect=[(False, ""), (False, "")])

View File

@@ -10,6 +10,7 @@ import subprocess
import mock
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import kubernetes
from sysinv.tests.db import base as dbbase
from k8sapp_openstack import utils as app_utils
@@ -1616,3 +1617,26 @@ class UtilsTest(dbbase.ControllerHostTestCase):
backends_map = app_utils.check_netapp_backends()
assert backends_map["nfs"]
@mock.patch("k8sapp_openstack.utils.subprocess.run")
def test_get_netapp_storage_class_name(self, mock_run):
""" Tests if get_netapp_storage_class_name returns a valid
storage class name for netapp.
"""
mock_process = mock.MagicMock()
mock_process.stdout = "netapp-nas-backend ontap-nas"
mock_process.stderr = ""
mock_process.check_returncode.return_value = None
mock_run.return_value = mock_process
result = app_utils.get_netapp_storage_class_name(app_constants.BACKEND_TYPE_NETAPP_NFS)
assert result == "netapp-nas-backend"
mock_run.assert_called_once_with(
args=["kubectl", "--kubeconfig", kubernetes.KUBERNETES_ADMIN_CONF,
"get", "sc", "-o", "custom-columns=NAME:.metadata.name,TYPE:.parameters.backendType"],
capture_output=True,
text=True,
check=True,
shell=False
)

View File

@@ -1671,6 +1671,84 @@ def get_server_list() -> str:
return ""
def get_available_volume_backends() -> dict:
"""
Searches for all available backends volume available.
Returns:
dict[string, string]: A dictionary containing the backend volumes with corresponding
storage class name.
"""
rook_ceph = is_ceph_backend_available(
ceph_type=constants.SB_TYPE_CEPH_ROOK
)
netapp_backend = check_netapp_backends()
available_volume_backends = {
"ceph": app_constants.BACKEND_DEFAULT_STORAGE_CLASS if rook_ceph else "",
"netapp-nfs":
get_netapp_storage_class_name(
app_constants.BACKEND_TYPE_NETAPP_NFS
) if netapp_backend.get("nfs", False) else "",
"netapp-iscsi":
get_netapp_storage_class_name(
app_constants.BACKEND_TYPE_NETAPP_ISCSI
) if netapp_backend.get("iscsi", False) else "",
"netapp-fc": "",
}
return available_volume_backends
def get_netapp_storage_class_name(backend_type) -> str:
"""
Check for the storage class name for NetApp backends based on a backend-type.
Returns:
str: A string indicating the backends class_name
Example: "netapp-nas-backend"
"""
class_name = ""
try:
cmd = [
"kubectl", "--kubeconfig", kubernetes.KUBERNETES_ADMIN_CONF,
"get", "sc", "-o", "custom-columns=NAME:.metadata.name,TYPE:.parameters.backendType"
]
storage_classes_info = subprocess.run(
args=cmd,
capture_output=True,
text=True,
check=True,
shell=False)
if not storage_classes_info.stdout:
return class_name
# Need to manually search for the backend type and parse the string
lines = storage_classes_info.stdout.splitlines()
for line in lines:
if backend_type in line:
class_name = line.split(" ")[0]
return class_name
except KubeApiException as e:
LOG.error(f"Failed to get kubectl sc: {e}")
return class_name
except subprocess.CalledProcessError as e:
LOG.error(
"kubectl command did not return successful return code: "
f"{e.returncode}. Error message was: {e.output}"
)
return class_name
except subprocess.TimeoutExpired as e:
LOG.error(f"kubectl command timed out: {e}")
return class_name
except Exception as e:
LOG.error(f"Unexpected error while fetching NetApp backends: {e}")
return class_name
def is_dex_enabled() -> bool:
"""
Determine whether DEX integration is enabled in Keystone overrides.

View File

@@ -533,6 +533,12 @@ monitoring:
enabled: false
mysqld_exporter:
scrape: true
storage_conf:
volume_storage_class_priority:
- "ceph"
- "netapp-nfs"
- "netapp-iscsi"
- "netapp-fc"
secrets:
identity:
admin: keystone-admin-user

View File

@@ -425,6 +425,12 @@ helm3_hook: true
io_thread_pool:
enabled: false
size: 64
storage_conf:
volume_storage_class_priority:
- "ceph"
- "netapp-nfs"
- "netapp-iscsi"
- "netapp-fc"
manifests:
certificates: false
configmap_bin: true