From e33fbe79f47198085a0b9e01811357d4dcce671a Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 14 Apr 2026 15:45:47 +0500 Subject: [PATCH 1/3] feat: generate zadara charm --- .../storage/backends/zadara/__init__.py | 4 + .../storage/backends/zadara/backend.py | 122 ++++++++++++++++++ .../unit/sunbeam/storage/backends/conftest.py | 10 +- .../sunbeam/storage/backends/test_common.py | 10 +- .../sunbeam/storage/backends/test_zadara.py | 70 ++++++++++ 5 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 sunbeam-python/sunbeam/storage/backends/zadara/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/zadara/backend.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py diff --git a/sunbeam-python/sunbeam/storage/backends/zadara/__init__.py b/sunbeam-python/sunbeam/storage/backends/zadara/__init__.py new file mode 100644 index 000000000..af3d47d39 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/zadara/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Zadara backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/zadara/backend.py b/sunbeam-python/sunbeam/storage/backends/zadara/backend.py new file mode 100644 index 000000000..754dbd86d --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/zadara/backend.py @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 +# ruff: noqa: E501 + +"""ZadaraVPSA iSCSI backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated + +from pydantic import Field +from rich.console import Console + +from sunbeam.core.manifest import StorageBackendConfig +from sunbeam.storage.base import StorageBackendBase +from sunbeam.storage.models import SecretDictField + +LOG = logging.getLogger(__name__) +console = Console() + + +class Protocol(StrEnum): + """Enumeration of valid protocol types.""" + + ISCSI = "iscsi" + + +class ZadaraConfig(StorageBackendConfig): + """Configuration model for ZadaraVPSA iSCSI backend. + + This model includes ALL configuration options for the backend. + Additional configuration can be managed dynamically through the charm. + """ + + # Mandatory connection parameters + san_ip: Annotated[ + str, Field(description="Storage array management IP address or hostname.") + ] + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: iscsi."), + ] = None + zadara_access_key: Annotated[ + str, + Field(description="VPSA access key"), + SecretDictField(field="zadara-access-key"), + ] + + # Optional backend configuration + zadara_vpsa_host: Annotated[ + str | None, Field(description="VPSA - Management Host name or IP address") + ] = None + zadara_vpsa_port: Annotated[int | None, Field(description="VPSA - Port number")] = ( + None + ) + zadara_vpsa_use_ssl: Annotated[ + bool | None, Field(description="VPSA - Use SSL connection") + ] = None + zadara_ssl_cert_verify: Annotated[ + bool | None, + Field( + description="If set to True the http client will validate the SSL certificate of the VPSA endpoint." + ), + ] = None + zadara_vpsa_poolname: Annotated[ + str | None, Field(description="VPSA - Storage Pool assigned for volumes") + ] = None + zadara_vol_encrypt: Annotated[ + bool | None, Field(description="VPSA - Default encryption policy for volumes.") + ] = None + zadara_gen3_vol_dedupe: Annotated[ + bool | None, Field(description="VPSA - Enable deduplication for volumes.") + ] = None + zadara_gen3_vol_compress: Annotated[ + bool | None, Field(description="VPSA - Enable compression for volumes.") + ] = None + zadara_default_snap_policy: Annotated[ + bool | None, Field(description="VPSA - Attach snapshot policy for volumes.") + ] = None + zadara_use_iser: Annotated[ + bool | None, Field(description="VPSA - Use ISER instead of iSCSI") + ] = None + zadara_vol_name_template: Annotated[ + str | None, Field(description="VPSA - Default template for VPSA volume names") + ] = None + + +class ZadaraBackend(StorageBackendBase): + """ZadaraVPSA iSCSI backend implementation.""" + + backend_type = "zadara" + display_name = "ZadaraVPSA iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-zadara" + + @property + def charm_channel(self) -> str: + """Return the default charm channel.""" + return "latest/edge" + + @property + def charm_revision(self) -> str | None: + """Return a pinned charm revision, if any.""" + return None + + @property + def charm_base(self) -> str: + """Return the target base for this charm.""" + return "ubuntu@24.04" + + @property + def supports_ha(self) -> bool: + """Whether this backend supports HA deployments.""" + return False + + def config_type(self) -> type[StorageBackendConfig]: + """Return the configuration model type for this backend.""" + return ZadaraConfig diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 80d3d8ef5..29790f0d7 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py @@ -9,6 +9,7 @@ from sunbeam.storage.backends.dellsc.backend import DellSCBackend from sunbeam.storage.backends.hitachi.backend import HitachiBackend from sunbeam.storage.backends.purestorage.backend import PureStorageBackend +from sunbeam.storage.backends.zadara.backend import ZadaraBackend @pytest.fixture @@ -29,19 +30,26 @@ def dellsc_backend(): return DellSCBackend() +@pytest.fixture +def zadara_backend(): + """Provide a Zadara backend instance.""" + return ZadaraBackend() + + @pytest.fixture def dellpowerstore_backend(): """Provide a Dell PowerStore backend instance.""" return DellPowerstoreBackend() -@pytest.fixture(params=["hitachi", "purestorage", "dellsc", "dellpowerstore"]) +@pytest.fixture(params=["hitachi", "purestorage", "dellsc", "zadara", "dellpowerstore"]) def any_backend(request): """Parametrized fixture that provides each backend type.""" backends = { "hitachi": HitachiBackend(), "purestorage": PureStorageBackend(), "dellsc": DellSCBackend(), + "zadara": ZadaraBackend(), "dellpowerstore": DellPowerstoreBackend(), } return backends[request.param] diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py index def51a0e8..ba9f13bf2 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py @@ -157,13 +157,13 @@ def backend(self, any_backend): def test_all_backends_have_unique_types( - hitachi_backend, purestorage_backend, dellsc_backend, dellpowerstore_backend + hitachi_backend, purestorage_backend, dellsc_backend, zadara_backend, dellpowerstore_backend ): """Test that all backends have unique type identifiers.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, + dellsc_backend, zadara_backend, dellpowerstore_backend, ] types = [b.backend_type for b in backends] @@ -173,13 +173,13 @@ def test_all_backends_have_unique_types( def test_all_backends_have_unique_charm_names( - hitachi_backend, purestorage_backend, dellsc_backend, dellpowerstore_backend + hitachi_backend, purestorage_backend, dellsc_backend, zadara_backend, dellpowerstore_backend ): """Test that all backends have unique charm names.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, + dellsc_backend, zadara_backend, dellpowerstore_backend, ] charm_names = [b.charm_name for b in backends] @@ -196,6 +196,7 @@ def test_all_backends_have_unique_charm_names( ("hitachi", "hitachi"), ("purestorage", "purestorage"), ("dellsc", "dellsc"), + ("zadara", "zadara"), ("dellpowerstore", "dellpowerstore"), ], ) @@ -211,6 +212,7 @@ def test_backend_types_match_expected(any_backend, backend_type, expected_type): ("hitachi", "cinder-volume-hitachi"), ("purestorage", "cinder-volume-purestorage"), ("dellsc", "cinder-volume-dellsc"), + ("zadara", "cinder-volume-zadara"), ("dellpowerstore", "cinder-volume-dellpowerstore"), ], ) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py new file mode 100644 index 000000000..2bfd6c898 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Zadara backend.""" + +import pytest +from pydantic import ValidationError + +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestZadaraBackend(BaseBackendTests): + """Tests for Zadara backend.""" + + @pytest.fixture + def backend(self, zadara_backend): + """Provide Zadara backend instance.""" + return zadara_backend + + def test_backend_type_is_zadara(self, backend): + """Test that backend type is 'zadara'.""" + assert backend.backend_type == "zadara" + + def test_charm_name_is_zadara_charm(self, backend): + """Test that charm name is cinder-volume-zadara.""" + assert backend.charm_name == "cinder-volume-zadara" + + def test_config_has_required_fields(self, backend): + """Test that Zadara config has required fields.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "protocol", "zadara_access_key"): + assert field in fields, f"Required field {field} not found in config" + + def test_sensitive_fields_are_marked_secret(self, backend): + """Test that access key field is marked as secret.""" + config_class = backend.config_type() + field = config_class.model_fields.get("zadara_access_key") + assert field is not None + assert any(isinstance(m, SecretDictField) for m in field.metadata), ( + "zadara_access_key should be marked as secret" + ) + + +class TestZadaraConfigValidation: + """Test Zadara config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, zadara_backend): + """Test that protocol rejects values other than iscsi.""" + config_class = zadara_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "zadara-access-key": "secret", + "protocol": "fc", + } + ) + + def test_protocol_accepts_iscsi(self, zadara_backend): + """Test that protocol accepts iscsi.""" + config_class = zadara_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "zadara-access-key": "secret", + "protocol": "iscsi", + } + ) + assert config.protocol == "iscsi" From fcbd8c123bced6e7b3eb93f709345e0d3b1504fc Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Thu, 16 Apr 2026 16:59:30 +0500 Subject: [PATCH 2/3] fix: add Zadara VPSA iSCSI storage backend --- sunbeam-python/sunbeam/storage/backends/zadara/backend.py | 6 ++++-- .../tests/unit/sunbeam/storage/backends/test_zadara.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sunbeam-python/sunbeam/storage/backends/zadara/backend.py b/sunbeam-python/sunbeam/storage/backends/zadara/backend.py index 754dbd86d..1508cf499 100644 --- a/sunbeam-python/sunbeam/storage/backends/zadara/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/zadara/backend.py @@ -1,6 +1,5 @@ # SPDX-FileCopyrightText: 2026 - Canonical Ltd # SPDX-License-Identifier: Apache-2.0 -# ruff: noqa: E501 """ZadaraVPSA iSCSI backend implementation using base step classes.""" @@ -59,7 +58,10 @@ class ZadaraConfig(StorageBackendConfig): zadara_ssl_cert_verify: Annotated[ bool | None, Field( - description="If set to True the http client will validate the SSL certificate of the VPSA endpoint." + description=( + "If set to True the http client will validate" + " the SSL certificate of the VPSA endpoint." + ) ), ] = None zadara_vpsa_poolname: Annotated[ diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py index 2bfd6c898..ffad46974 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py @@ -26,11 +26,11 @@ def test_charm_name_is_zadara_charm(self, backend): """Test that charm name is cinder-volume-zadara.""" assert backend.charm_name == "cinder-volume-zadara" - def test_config_has_required_fields(self, backend): - """Test that Zadara config has required fields.""" + def test_config_has_expected_fields(self, backend): + """Test that Zadara config exposes the expected fields.""" fields = backend.config_type().model_fields for field in ("san_ip", "protocol", "zadara_access_key"): - assert field in fields, f"Required field {field} not found in config" + assert field in fields, f"Expected field {field} not found in config" def test_sensitive_fields_are_marked_secret(self, backend): """Test that access key field is marked as secret.""" From f0cd835bcd6a97e398de6b6fd8ef3075c5f6d9a9 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 21 Apr 2026 17:14:33 +0500 Subject: [PATCH 3/3] fix: test/lint issues resolve --- .../sunbeam/storage/backends/test_common.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py index ba9f13bf2..f25bebcb2 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py @@ -157,13 +157,18 @@ def backend(self, any_backend): def test_all_backends_have_unique_types( - hitachi_backend, purestorage_backend, dellsc_backend, zadara_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + zadara_backend, + dellpowerstore_backend, ): """Test that all backends have unique type identifiers.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, zadara_backend, + dellsc_backend, + zadara_backend, dellpowerstore_backend, ] types = [b.backend_type for b in backends] @@ -173,13 +178,18 @@ def test_all_backends_have_unique_types( def test_all_backends_have_unique_charm_names( - hitachi_backend, purestorage_backend, dellsc_backend, zadara_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + zadara_backend, + dellpowerstore_backend, ): """Test that all backends have unique charm names.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, zadara_backend, + dellsc_backend, + zadara_backend, dellpowerstore_backend, ] charm_names = [b.charm_name for b in backends]