From a38808aae86beacda496537319195cbfd7e6d2c2 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 14 Apr 2026 15:05:51 +0500 Subject: [PATCH 1/3] feat: generate toyouacs5000 charm --- .../storage/backends/toyouacs5000/__init__.py | 4 + .../storage/backends/toyouacs5000/backend.py | 118 ++++++++++++++++++ .../unit/sunbeam/storage/backends/conftest.py | 10 +- .../sunbeam/storage/backends/test_common.py | 16 ++- .../storage/backends/test_toyouacs5000.py | 73 +++++++++++ 5 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 sunbeam-python/sunbeam/storage/backends/toyouacs5000/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py diff --git a/sunbeam-python/sunbeam/storage/backends/toyouacs5000/__init__.py b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/__init__.py new file mode 100644 index 000000000..b994e3755 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Toyou ACS5000 backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py new file mode 100644 index 000000000..05c4c0cd7 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 +# ruff: noqa: E501 + +"""Vendor 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.""" + + FC = "fc" + ISCSI = "iscsi" + + +class Toyouacs5000Config(StorageBackendConfig): + """Configuration model for Acs5000 FC 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="IP address of SAN controller")] + san_login: Annotated[ + str, + Field(description="Username for SAN controller"), + SecretDictField(field="san-login"), + ] + san_password: Annotated[ + str, + Field(description="Password for SAN controller"), + SecretDictField(field="san-password"), + ] + + # Optional backend configuration (required: false in spec) + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + + # Storage management options + acs5000_volpool_name: Annotated[ + str | None, + Field( + description="Comma separated list of storage system storage pools for volumes." + ), + ] = None + acs5000_copy_interval: Annotated[ + int | None, + Field( + description="When volume copy task is going on,refresh volume status interval" + ), + ] = None + acs5000_multiattach: Annotated[ + bool | None, + Field( + description="Enable to allow volumes attaching to multiple hosts with no limit." + ), + ] = None + san_thin_provision: Annotated[ + bool | None, + Field(description="Use thin provisioning for SAN volumes?"), + ] = None + use_multipath_for_image_xfer: Annotated[ + bool | None, + Field(description="Enable multipathing for image transfer operations."), + ] = None + + +class Toyouacs5000Backend(StorageBackendBase): + """Acs5000 FC backend implementation.""" + + backend_type = "toyouacs5000" + display_name = "Acs5000 FC" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-toyouacs5000" + + @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 Toyouacs5000Config diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 80d3d8ef5..4e60311ad 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.toyouacs5000.backend import Toyouacs5000Backend @pytest.fixture @@ -29,19 +30,26 @@ def dellsc_backend(): return DellSCBackend() +@pytest.fixture +def toyouacs5000_backend(): + """Provide a Toyou ACS5000 backend instance.""" + return Toyouacs5000Backend() + + @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", "toyouacs5000", "dellpowerstore"]) def any_backend(request): """Parametrized fixture that provides each backend type.""" backends = { "hitachi": HitachiBackend(), "purestorage": PureStorageBackend(), "dellsc": DellSCBackend(), + "toyouacs5000": Toyouacs5000Backend(), "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..1486cf723 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, dellpowerstore_backend + hitachi_backend, purestorage_backend, dellsc_backend, toyouacs5000_backend, dellpowerstore_backend ): """Test that all backends have unique type identifiers.""" backends = [ + hitachi_backend, + purestorage_backend, + dellsc_backend, + toyouacs5000_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, dellpowerstore_backend + hitachi_backend, purestorage_backend, dellsc_backend, toyouacs5000_backend, dellpowerstore_backend ): """Test that all backends have unique charm names.""" backends = [ + hitachi_backend, + purestorage_backend, + dellsc_backend, + toyouacs5000_backend, + , dellpowerstore_backend, ] charm_names = [b.charm_name for b in backends] @@ -196,6 +206,7 @@ def test_all_backends_have_unique_charm_names( ("hitachi", "hitachi"), ("purestorage", "purestorage"), ("dellsc", "dellsc"), + ("toyouacs5000", "toyouacs5000"), ("dellpowerstore", "dellpowerstore"), ], ) @@ -211,6 +222,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"), + ("toyouacs5000", "cinder-volume-toyouacs5000"), ("dellpowerstore", "cinder-volume-dellpowerstore"), ], ) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py new file mode 100644 index 000000000..ccc53d46e --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Toyou ACS5000 backend.""" + +import pytest +from pydantic import ValidationError + +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestToyouacs5000Backend(BaseBackendTests): + """Tests for Toyou ACS5000 backend.""" + + @pytest.fixture + def backend(self, toyouacs5000_backend): + """Provide Toyou ACS5000 backend instance.""" + return toyouacs5000_backend + + def test_backend_type_is_toyouacs5000(self, backend): + """Test that backend type is 'toyouacs5000'.""" + assert backend.backend_type == "toyouacs5000" + + def test_charm_name_is_toyouacs5000_charm(self, backend): + """Test that charm name is cinder-volume-toyouacs5000.""" + assert backend.charm_name == "cinder-volume-toyouacs5000" + + def test_config_has_required_fields(self, backend): + """Test that Toyou ACS5000 config has required fields.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "protocol", "san_login", "san_password"): + assert field in fields, f"Required field {field} not found in config" + + def test_sensitive_fields_are_marked_secret(self, backend): + """Test that SAN credentials are marked as secret.""" + config_class = backend.config_type() + for field_name in ("san_login", "san_password"): + field = config_class.model_fields.get(field_name) + assert field is not None + assert any(isinstance(m, SecretDictField) for m in field.metadata), ( + f"{field_name} should be marked as secret" + ) + + +class TestToyouacs5000ConfigValidation: + """Test Toyou ACS5000 config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, toyouacs5000_backend): + """Test that protocol rejects values other than fc/iscsi.""" + config_class = toyouacs5000_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "protocol": "nvme", + } + ) + + def test_protocol_accepts_fc(self, toyouacs5000_backend): + """Test that protocol accepts fc.""" + config_class = toyouacs5000_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "protocol": "fc", + } + ) + assert config.protocol == "fc" From b8e753c4033cd696ee9f303e71d4a05c67cee0b7 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 14 Apr 2026 18:16:03 +0500 Subject: [PATCH 2/3] fix: refine Toyou ACS5000 backend lint and config tests --- .../storage/backends/toyouacs5000/backend.py | 20 +++++++++---------- .../sunbeam/storage/backends/test_common.py | 8 -------- .../storage/backends/test_toyouacs5000.py | 18 ++++++++++++++--- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py index 05c4c0cd7..daf5cca63 100644 --- a/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py @@ -1,7 +1,5 @@ # SPDX-FileCopyrightText: 2026 - Canonical Ltd # SPDX-License-Identifier: Apache-2.0 -# ruff: noqa: E501 - """Vendor backend implementation using base step classes.""" import logging @@ -27,7 +25,7 @@ class Protocol(StrEnum): class Toyouacs5000Config(StorageBackendConfig): - """Configuration model for Acs5000 FC backend. + """Configuration model for Acs5000 backend. This model includes ALL configuration options for the backend. Additional configuration can be managed dynamically through the charm. @@ -56,20 +54,22 @@ class Toyouacs5000Config(StorageBackendConfig): acs5000_volpool_name: Annotated[ str | None, Field( - description="Comma separated list of storage system storage pools for volumes." + description=( + "Comma-separated list of storage system storage pools for volumes." + ) ), ] = None acs5000_copy_interval: Annotated[ int | None, Field( - description="When volume copy task is going on,refresh volume status interval" + description=( + "When volume copy task is going on, refresh volume status interval" + ) ), ] = None acs5000_multiattach: Annotated[ bool | None, - Field( - description="Enable to allow volumes attaching to multiple hosts with no limit." - ), + Field(description="Enable multi-attach for volumes with no host limit."), ] = None san_thin_provision: Annotated[ bool | None, @@ -82,10 +82,10 @@ class Toyouacs5000Config(StorageBackendConfig): class Toyouacs5000Backend(StorageBackendBase): - """Acs5000 FC backend implementation.""" + """Acs5000 backend implementation.""" backend_type = "toyouacs5000" - display_name = "Acs5000 FC" + display_name = "Acs5000 FC/iSCSI" generally_available = True @property 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 1486cf723..13c1714ec 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py @@ -161,14 +161,10 @@ def test_all_backends_have_unique_types( ): """Test that all backends have unique type identifiers.""" backends = [ - hitachi_backend, - purestorage_backend, - dellsc_backend, toyouacs5000_backend, - , dellpowerstore_backend, ] types = [b.backend_type for b in backends] @@ -182,14 +178,10 @@ def test_all_backends_have_unique_charm_names( ): """Test that all backends have unique charm names.""" backends = [ - hitachi_backend, - purestorage_backend, - dellsc_backend, toyouacs5000_backend, - , dellpowerstore_backend, ] charm_names = [b.charm_name for b in backends] diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py index ccc53d46e..e339a4050 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py @@ -26,11 +26,23 @@ def test_charm_name_is_toyouacs5000_charm(self, backend): """Test that charm name is cinder-volume-toyouacs5000.""" assert backend.charm_name == "cinder-volume-toyouacs5000" - def test_config_has_required_fields(self, backend): - """Test that Toyou ACS5000 config has required fields.""" + def test_display_name_is_protocol_agnostic(self, backend): + """Test that display name reflects FC and iSCSI support.""" + assert backend.display_name == "Acs5000 FC/iSCSI" + + def test_config_has_expected_fields(self, backend): + """Test that Toyou ACS5000 config exposes expected fields.""" fields = backend.config_type().model_fields for field in ("san_ip", "protocol", "san_login", "san_password"): - 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_config_enforces_required_fields(self, backend): + """Test that mandatory Toyou ACS5000 config fields are required.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "san_login", "san_password"): + assert fields[field].is_required(), ( + f"Field {field} should be required in config" + ) def test_sensitive_fields_are_marked_secret(self, backend): """Test that SAN credentials are marked as secret.""" From 3b3b417e2f2c551986fa5870e79ff33a275ff287 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 21 Apr 2026 16:45:11 +0500 Subject: [PATCH 3/3] fix: test/lint issues resolve --- .../tests/unit/sunbeam/storage/backends/conftest.py | 4 +++- .../unit/sunbeam/storage/backends/test_common.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 4e60311ad..cb5064275 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", "toyouacs5000", "dellpowerstore"]) +@pytest.fixture( + params=["hitachi", "purestorage", "dellsc", "toyouacs5000", "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 13c1714ec..182eed1c1 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py @@ -157,7 +157,11 @@ def backend(self, any_backend): def test_all_backends_have_unique_types( - hitachi_backend, purestorage_backend, dellsc_backend, toyouacs5000_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + toyouacs5000_backend, + dellpowerstore_backend, ): """Test that all backends have unique type identifiers.""" backends = [ @@ -174,7 +178,11 @@ def test_all_backends_have_unique_types( def test_all_backends_have_unique_charm_names( - hitachi_backend, purestorage_backend, dellsc_backend, toyouacs5000_backend, dellpowerstore_backend + hitachi_backend, + purestorage_backend, + dellsc_backend, + toyouacs5000_backend, + dellpowerstore_backend, ): """Test that all backends have unique charm names.""" backends = [