From 3bd16f0d99a75eb3fc6144e4034e9e0f665732d2 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 14 Apr 2026 15:34:08 +0500 Subject: [PATCH 1/4] feat: generate yadro charm --- .../storage/backends/yadro/__init__.py | 4 + .../sunbeam/storage/backends/yadro/backend.py | 124 ++++++++++++++++++ .../unit/sunbeam/storage/backends/conftest.py | 10 +- .../sunbeam/storage/backends/test_common.py | 10 +- .../sunbeam/storage/backends/test_yadro.py | 58 ++++++++ 5 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 sunbeam-python/sunbeam/storage/backends/yadro/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/yadro/backend.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py diff --git a/sunbeam-python/sunbeam/storage/backends/yadro/__init__.py b/sunbeam-python/sunbeam/storage/backends/yadro/__init__.py new file mode 100644 index 000000000..42bc478c2 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/yadro/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Yadro backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/yadro/backend.py b/sunbeam-python/sunbeam/storage/backends/yadro/backend.py new file mode 100644 index 000000000..89ce25812 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/yadro/backend.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tatlin FCVolume 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.""" + + FC = "fc" + ISCSI = "iscsi" + + +class YadroConfig(StorageBackendConfig): + """Configuration model for Tatlin FCVolume backend. + + This model includes ALL configuration options for the backend. + Additional configuration can be managed dynamically through the charm. + """ + + # Mandatory connection parameters (required: true in spec) + san_ip: Annotated[ + str, Field(description="Storage array management IP address or hostname.") + ] + + # Optional backend configuration (required: false in spec) + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + pool_name: Annotated[ + str | None, + Field(description="storage pool name"), + ] = None + api_port: Annotated[ + int | None, + Field(description="Port to use to access the Tatlin API"), + ] = None + export_ports: Annotated[ + str | None, + Field(description="Ports to export Tatlin resource through"), + ] = None + host_group: Annotated[ + str | None, + Field(description="Tatlin host group name"), + ] = None + max_resource_count: Annotated[ + int | None, + Field(description="Max resource count allowed for Tatlin"), + ] = None + pool_max_resource_count: Annotated[ + int | None, + Field(description="Max resource count allowed for single pool"), + ] = None + tat_api_retry_count: Annotated[ + int | None, + Field(description="Number of retry on Tatlin API"), + ] = None + auth_method: Annotated[ + str | None, + Field(description="Authentication method for iSCSI (CHAP)"), + ] = None + lba_format: Annotated[ + str | None, + Field(description="LBA Format for new volume"), + ] = None + wait_retry_count: Annotated[ + int | None, + Field(description="Number of checks for a lengthy operation to finish"), + ] = None + wait_interval: Annotated[ + int | None, + Field(description="Wait number of seconds before re-checking"), + ] = None + + +class YadroBackend(StorageBackendBase): + """Tatlin FCVolume backend implementation.""" + + backend_type = "yadro" + display_name = "Tatlin FCVolume" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-yadro" + + @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 True + + def config_type(self) -> type[StorageBackendConfig]: + """Return the configuration model type for this backend.""" + return YadroConfig diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 80d3d8ef5..edfc89ba2 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.yadro.backend import YadroBackend @pytest.fixture @@ -29,19 +30,26 @@ def dellsc_backend(): return DellSCBackend() +@pytest.fixture +def yadro_backend(): + """Provide a Yadro backend instance.""" + return YadroBackend() + + @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", "yadro", "dellpowerstore"]) def any_backend(request): """Parametrized fixture that provides each backend type.""" backends = { "hitachi": HitachiBackend(), "purestorage": PureStorageBackend(), "dellsc": DellSCBackend(), + "yadro": YadroBackend(), "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..63a14234d 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, yadro_backend, dellpowerstore_backend ): """Test that all backends have unique type identifiers.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, + dellsc_backend, yadro_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, yadro_backend, dellpowerstore_backend ): """Test that all backends have unique charm names.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, + dellsc_backend, yadro_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"), + ("yadro", "yadro"), ("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"), + ("yadro", "cinder-volume-yadro"), ("dellpowerstore", "cinder-volume-dellpowerstore"), ], ) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py new file mode 100644 index 000000000..da410f078 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Yadro backend.""" + +import pytest +from pydantic import ValidationError + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestYadroBackend(BaseBackendTests): + """Tests for Yadro backend.""" + + @pytest.fixture + def backend(self, yadro_backend): + """Provide Yadro backend instance.""" + return yadro_backend + + def test_backend_type_is_yadro(self, backend): + """Test that backend type is 'yadro'.""" + assert backend.backend_type == "yadro" + + def test_charm_name_is_yadro_charm(self, backend): + """Test that charm name is cinder-volume-yadro.""" + assert backend.charm_name == "cinder-volume-yadro" + + def test_config_has_required_fields(self, backend): + """Test that Yadro 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 TestYadroConfigValidation: + """Test Yadro config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, yadro_backend): + """Test that protocol rejects values other than fc/iscsi.""" + config_class = yadro_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "protocol": "nvme", + } + ) + + def test_protocol_accepts_fc(self, yadro_backend): + """Test that protocol accepts fc.""" + config_class = yadro_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "protocol": "fc", + } + ) + assert config.protocol == "fc" From c5023c08149ef35d7b1e3d325d7cf254761652cd Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Thu, 16 Apr 2026 16:31:43 +0500 Subject: [PATCH 2/4] fix: add Yadro Tatlin FCVolume storage backend --- .../tests/unit/sunbeam/storage/backends/test_yadro.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py index da410f078..81c767fca 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py @@ -13,9 +13,11 @@ class TestYadroBackend(BaseBackendTests): """Tests for Yadro backend.""" @pytest.fixture - def backend(self, yadro_backend): + def backend(self): """Provide Yadro backend instance.""" - return yadro_backend + # This fixture must be injected via pytest's fixture dependency injection + # If yadro_backend is needed, inject it as a parameter to the test methods instead + raise NotImplementedError("Use the yadro_backend fixture directly in test methods") def test_backend_type_is_yadro(self, backend): """Test that backend type is 'yadro'.""" From b62d74735045c8022c1d235854d61fc3bed4d092 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Thu, 16 Apr 2026 17:19:22 +0500 Subject: [PATCH 3/4] fix: address review comments on Yadro backend --- .../sunbeam/storage/backends/yadro/backend.py | 40 +++++++++---------- .../sunbeam/storage/backends/test_yadro.py | 6 +-- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/sunbeam-python/sunbeam/storage/backends/yadro/backend.py b/sunbeam-python/sunbeam/storage/backends/yadro/backend.py index 89ce25812..0a4bb39db 100644 --- a/sunbeam-python/sunbeam/storage/backends/yadro/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/yadro/backend.py @@ -1,21 +1,16 @@ # SPDX-FileCopyrightText: 2026 - Canonical Ltd # SPDX-License-Identifier: Apache-2.0 -"""Tatlin FCVolume backend implementation using base step classes.""" +"""Yadro storage backend implementation.""" -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.""" @@ -25,49 +20,50 @@ class Protocol(StrEnum): class YadroConfig(StorageBackendConfig): - """Configuration model for Tatlin FCVolume backend. + """Configuration model for the Yadro storage backend. - This model includes ALL configuration options for the backend. - Additional configuration can be managed dynamically through the charm. + This model includes the configuration options defined directly for this + backend. Additional configuration can be managed dynamically through the + charm. """ - # Mandatory connection parameters (required: true in spec) + # Required connection parameter. san_ip: Annotated[ str, Field(description="Storage array management IP address or hostname.") ] - # Optional backend configuration (required: false in spec) + # Optional backend configuration parameters. protocol: Annotated[ Protocol | None, Field(description="Protocol selector: fc, iscsi."), ] = None pool_name: Annotated[ str | None, - Field(description="storage pool name"), + Field(description="Storage pool name."), ] = None api_port: Annotated[ int | None, - Field(description="Port to use to access the Tatlin API"), + Field(description="Port used to access the storage API."), ] = None export_ports: Annotated[ str | None, - Field(description="Ports to export Tatlin resource through"), + Field(description="Ports used to export storage resources."), ] = None host_group: Annotated[ str | None, - Field(description="Tatlin host group name"), + Field(description="Host group name."), ] = None max_resource_count: Annotated[ int | None, - Field(description="Max resource count allowed for Tatlin"), + Field(description="Maximum number of resources allowed."), ] = None pool_max_resource_count: Annotated[ int | None, - Field(description="Max resource count allowed for single pool"), + Field(description="Maximum number of resources allowed for a single pool."), ] = None tat_api_retry_count: Annotated[ int | None, - Field(description="Number of retry on Tatlin API"), + Field(description="Number of retries for storage API operations."), ] = None auth_method: Annotated[ str | None, @@ -75,20 +71,20 @@ class YadroConfig(StorageBackendConfig): ] = None lba_format: Annotated[ str | None, - Field(description="LBA Format for new volume"), + Field(description="LBA format for new volumes."), ] = None wait_retry_count: Annotated[ int | None, - Field(description="Number of checks for a lengthy operation to finish"), + Field(description="Number of checks for a lengthy operation to finish."), ] = None wait_interval: Annotated[ int | None, - Field(description="Wait number of seconds before re-checking"), + Field(description="Number of seconds to wait before re-checking."), ] = None class YadroBackend(StorageBackendBase): - """Tatlin FCVolume backend implementation.""" + """Yadro storage backend implementation.""" backend_type = "yadro" display_name = "Tatlin FCVolume" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py index 81c767fca..da410f078 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py @@ -13,11 +13,9 @@ class TestYadroBackend(BaseBackendTests): """Tests for Yadro backend.""" @pytest.fixture - def backend(self): + def backend(self, yadro_backend): """Provide Yadro backend instance.""" - # This fixture must be injected via pytest's fixture dependency injection - # If yadro_backend is needed, inject it as a parameter to the test methods instead - raise NotImplementedError("Use the yadro_backend fixture directly in test methods") + return yadro_backend def test_backend_type_is_yadro(self, backend): """Test that backend type is 'yadro'.""" From 37b6919fce2f6c9a33edfddf4dfd330d04205088 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 21 Apr 2026 17:10:07 +0500 Subject: [PATCH 4/4] 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 63a14234d..72ccb2d36 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, yadro_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + yadro_backend, + dellpowerstore_backend, ): """Test that all backends have unique type identifiers.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, yadro_backend, + dellsc_backend, + yadro_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, yadro_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + yadro_backend, + dellpowerstore_backend, ): """Test that all backends have unique charm names.""" backends = [ hitachi_backend, purestorage_backend, - dellsc_backend, yadro_backend, + dellsc_backend, + yadro_backend, dellpowerstore_backend, ] charm_names = [b.charm_name for b in backends]