From cca213a598a2bbdd2a3bb2d6136afbfa214af5ce Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 14 Apr 2026 14:14:38 +0500 Subject: [PATCH 1/3] feat: generate sandstone charm --- .../storage/backends/sandstone/__init__.py | 4 + .../storage/backends/sandstone/backend.py | 91 +++++++++++++++++++ .../unit/sunbeam/storage/backends/conftest.py | 10 +- .../sunbeam/storage/backends/test_common.py | 10 +- .../storage/backends/test_sandstone.py | 58 ++++++++++++ 5 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/sandstone/backend.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py diff --git a/sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py b/sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py new file mode 100644 index 000000000..7025dc63e --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""SandStone backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py b/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py new file mode 100644 index 000000000..ab2335b86 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Sds 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 + +LOG = logging.getLogger(__name__) +console = Console() + + +class Protocol(StrEnum): + """Enumeration of valid protocol types.""" + + ISCSI = "iscsi" + + +class SandstoneConfig(StorageBackendConfig): + """Configuration model for Sds 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 + + # Optional backend configuration + default_sandstone_target_ips: Annotated[ + str | None, + Field(description="SandStone default target ip."), + ] = None + sandstone_pool: Annotated[ + str | None, + Field(description="SandStone storage pool resource name."), + ] = None + initiator_assign_sandstone_target_ip: Annotated[ + str | None, + Field(description="Support initiator assign target with assign ip."), + ] = None + + +class SandstoneBackend(StorageBackendBase): + """Sds iSCSI backend implementation.""" + + backend_type = "sandstone" + display_name = "Sds iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-sandstone" + + @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 SandstoneConfig diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 80d3d8ef5..33a4ceb0e 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.sandstone.backend import SandstoneBackend @pytest.fixture @@ -29,19 +30,26 @@ def dellsc_backend(): return DellSCBackend() +@pytest.fixture +def sandstone_backend(): + """Provide a SandStone backend instance.""" + return SandstoneBackend() + + @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", "sandstone", "dellpowerstore"]) def any_backend(request): """Parametrized fixture that provides each backend type.""" backends = { "hitachi": HitachiBackend(), "purestorage": PureStorageBackend(), "dellsc": DellSCBackend(), + "sandstone": SandstoneBackend(), "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..024ed7683 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, sandstone_backend, dellpowerstore_backend ): """Test that all backends have unique type identifiers.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, + dellsc_backend, sandstone_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, sandstone_backend, dellpowerstore_backend ): """Test that all backends have unique charm names.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, + dellsc_backend, sandstone_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"), + ("sandstone", "sandstone"), ("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"), + ("sandstone", "cinder-volume-sandstone"), ("dellpowerstore", "cinder-volume-dellpowerstore"), ], ) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py new file mode 100644 index 000000000..f973b8d8a --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for SandStone backend.""" + +import pytest +from pydantic import ValidationError + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestSandstoneBackend(BaseBackendTests): + """Tests for SandStone backend.""" + + @pytest.fixture + def backend(self, sandstone_backend): + """Provide SandStone backend instance.""" + return sandstone_backend + + def test_backend_type_is_sandstone(self, backend): + """Test that backend type is 'sandstone'.""" + assert backend.backend_type == "sandstone" + + def test_charm_name_is_sandstone_charm(self, backend): + """Test that charm name is cinder-volume-sandstone.""" + assert backend.charm_name == "cinder-volume-sandstone" + + def test_config_has_required_fields(self, backend): + """Test that SandStone config has required fields.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "protocol"): + assert field in fields, f"Required field {field} not found in config" + + +class TestSandstoneConfigValidation: + """Test SandStone config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, sandstone_backend): + """Test that protocol rejects values other than iscsi.""" + config_class = sandstone_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "protocol": "fc", + } + ) + + def test_protocol_accepts_iscsi(self, sandstone_backend): + """Test that protocol accepts iscsi.""" + config_class = sandstone_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "protocol": "iscsi", + } + ) + assert config.protocol == "iscsi" From a113a12ef040dd37f15563ef0a1e238507ff3299 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 14 Apr 2026 15:22:19 +0500 Subject: [PATCH 2/3] fix: clarify backend naming and config descriptions --- .../sunbeam/storage/backends/sandstone/__init__.py | 2 +- .../sunbeam/storage/backends/sandstone/backend.py | 13 +++++++------ .../tests/unit/sunbeam/storage/backends/conftest.py | 2 +- .../unit/sunbeam/storage/backends/test_sandstone.py | 10 +++++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py b/sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py index 7025dc63e..fcfca5589 100644 --- a/sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py +++ b/sunbeam-python/sunbeam/storage/backends/sandstone/__init__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2026 - Canonical Ltd # SPDX-License-Identifier: Apache-2.0 -"""SandStone backend for Sunbeam storage.""" +"""Sandstone backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py b/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py index ab2335b86..6386c551b 100644 --- a/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 - Canonical Ltd # SPDX-License-Identifier: Apache-2.0 -"""Sds iSCSI backend implementation using base step classes.""" +"""Sandstone iSCSI backend implementation using base step classes.""" import logging from enum import StrEnum @@ -24,7 +24,7 @@ class Protocol(StrEnum): class SandstoneConfig(StorageBackendConfig): - """Configuration model for Sds iSCSI backend. + """Configuration model for Sandstone iSCSI backend. This model includes ALL configuration options for the backend. Additional configuration can be managed dynamically through the charm. @@ -34,6 +34,7 @@ class SandstoneConfig(StorageBackendConfig): san_ip: Annotated[ str, Field(description="Storage array management IP address or hostname.") ] + # Optional connection configuration protocol: Annotated[ Protocol | None, Field(description="Protocol selector: iscsi."), @@ -42,20 +43,20 @@ class SandstoneConfig(StorageBackendConfig): # Optional backend configuration default_sandstone_target_ips: Annotated[ str | None, - Field(description="SandStone default target ip."), + Field(description="Sandstone default target IPs."), ] = None sandstone_pool: Annotated[ str | None, - Field(description="SandStone storage pool resource name."), + Field(description="Sandstone storage pool resource name."), ] = None initiator_assign_sandstone_target_ip: Annotated[ str | None, - Field(description="Support initiator assign target with assign ip."), + Field(description="Support assigning a target IP to an initiator."), ] = None class SandstoneBackend(StorageBackendBase): - """Sds iSCSI backend implementation.""" + """Sandstone iSCSI backend implementation.""" backend_type = "sandstone" display_name = "Sds iSCSI" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 33a4ceb0e..25a73f502 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py @@ -32,7 +32,7 @@ def dellsc_backend(): @pytest.fixture def sandstone_backend(): - """Provide a SandStone backend instance.""" + """Provide a Sandstone backend instance.""" return SandstoneBackend() diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py index f973b8d8a..47cc739ad 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 - Canonical Ltd # SPDX-License-Identifier: Apache-2.0 -"""Tests for SandStone backend.""" +"""Tests for Sandstone backend.""" import pytest from pydantic import ValidationError @@ -10,11 +10,11 @@ class TestSandstoneBackend(BaseBackendTests): - """Tests for SandStone backend.""" + """Tests for Sandstone backend.""" @pytest.fixture def backend(self, sandstone_backend): - """Provide SandStone backend instance.""" + """Provide Sandstone backend instance.""" return sandstone_backend def test_backend_type_is_sandstone(self, backend): @@ -26,14 +26,14 @@ def test_charm_name_is_sandstone_charm(self, backend): assert backend.charm_name == "cinder-volume-sandstone" def test_config_has_required_fields(self, backend): - """Test that SandStone config has required fields.""" + """Test that Sandstone config has required fields.""" fields = backend.config_type().model_fields for field in ("san_ip", "protocol"): assert field in fields, f"Required field {field} not found in config" class TestSandstoneConfigValidation: - """Test SandStone config validation behavior.""" + """Test Sandstone config validation behavior.""" def test_protocol_rejects_invalid_value(self, sandstone_backend): """Test that protocol rejects values other than iscsi.""" From ab32973c4fd9384145d8a5738aebc331849fc131 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 21 Apr 2026 16:20:43 +0500 Subject: [PATCH 3/3] fix: test/lint issues resolve --- .../unit/sunbeam/storage/backends/conftest.py | 4 +++- .../sunbeam/storage/backends/test_common.py | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 25a73f502..df2c05cdb 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py @@ -42,7 +42,9 @@ def dellpowerstore_backend(): return DellPowerstoreBackend() -@pytest.fixture(params=["hitachi", "purestorage", "dellsc", "sandstone", "dellpowerstore"]) +@pytest.fixture( + params=["hitachi", "purestorage", "dellsc", "sandstone", "dellpowerstore"] +) def any_backend(request): """Parametrized fixture that provides each backend type.""" backends = { 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 024ed7683..b94923640 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, sandstone_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + sandstone_backend, + dellpowerstore_backend, ): """Test that all backends have unique type identifiers.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, sandstone_backend, + dellsc_backend, + sandstone_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, sandstone_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + sandstone_backend, + dellpowerstore_backend, ): """Test that all backends have unique charm names.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, sandstone_backend, + dellsc_backend, + sandstone_backend, dellpowerstore_backend, ] charm_names = [b.charm_name for b in backends]