From aa218f16dacac9ab7f9d2bb78846935b73d710ed Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 26 May 2026 14:08:44 +0500 Subject: [PATCH 1/2] feat: add Cinder volume storage backends and tests Add 32 storage backend implementations for Cinder volume drivers including __init__.py, backend.py, test files, conftest fixtures, and test_common entries. Signed-off-by: Ahmad Hassan --- .../storage/backends/datacore/__init__.py | 4 + .../storage/backends/datacore/backend.py | 126 ++++++ .../storage/backends/datera/__init__.py | 4 + .../storage/backends/datera/backend.py | 141 ++++++ .../storage/backends/dellpowermax/__init__.py | 4 + .../storage/backends/dellpowermax/backend.py | 218 +++++++++ .../backends/dellpowerstore/backend.py | 1 - .../backends/dellpowervault/__init__.py | 4 + .../backends/dellpowervault/backend.py | 97 ++++ .../backends/fujitsueternusdx/__init__.py | 4 + .../backends/fujitsueternusdx/backend.py | 97 ++++ .../storage/backends/hpexp/__init__.py | 4 + .../sunbeam/storage/backends/hpexp/backend.py | 343 ++++++++++++++ .../backends/ibmflashsystemcommon/__init__.py | 4 + .../backends/ibmflashsystemcommon/backend.py | 104 +++++ .../backends/ibmflashsystemiscsi/__init__.py | 4 + .../backends/ibmflashsystemiscsi/backend.py | 84 ++++ .../storage/backends/ibmgpfs/__init__.py | 4 + .../storage/backends/ibmgpfs/backend.py | 155 +++++++ .../backends/ibmibmstorage/__init__.py | 4 + .../storage/backends/ibmibmstorage/backend.py | 161 +++++++ .../backends/ibmstorwizesvc/__init__.py | 4 + .../backends/ibmstorwizesvc/backend.py | 307 +++++++++++++ .../backends/inspuras13000/__init__.py | 4 + .../storage/backends/inspuras13000/backend.py | 108 +++++ .../backends/inspurinstorage/__init__.py | 4 + .../backends/inspurinstorage/backend.py | 182 ++++++++ .../storage/backends/kaminario/__init__.py | 4 + .../storage/backends/kaminario/backend.py | 84 ++++ .../storage/backends/linstor/__init__.py | 4 + .../storage/backends/linstor/backend.py | 109 +++++ .../storage/backends/macrosan/__init__.py | 4 + .../storage/backends/macrosan/backend.py | 196 ++++++++ .../sunbeam/storage/backends/necv/__init__.py | 4 + .../sunbeam/storage/backends/necv/backend.py | 298 +++++++++++++ .../storage/backends/netapp/__init__.py | 4 + .../storage/backends/netapp/backend.py | 417 ++++++++++++++++++ .../storage/backends/nexenta/__init__.py | 4 + .../storage/backends/nexenta/backend.py | 332 ++++++++++++++ .../storage/backends/nimble/__init__.py | 4 + .../storage/backends/nimble/backend.py | 113 +++++ .../storage/backends/opene/__init__.py | 4 + .../sunbeam/storage/backends/opene/backend.py | 101 +++++ .../storage/backends/prophetstor/__init__.py | 4 + .../storage/backends/prophetstor/backend.py | 90 ++++ .../sunbeam/storage/backends/qnap/__init__.py | 4 + .../sunbeam/storage/backends/qnap/backend.py | 114 +++++ .../storage/backends/sandstone/__init__.py | 4 + .../storage/backends/sandstone/backend.py | 92 ++++ .../storage/backends/solidfire/__init__.py | 4 + .../storage/backends/solidfire/backend.py | 163 +++++++ .../sunbeam/storage/backends/stx/__init__.py | 4 + .../sunbeam/storage/backends/stx/backend.py | 99 +++++ .../storage/backends/synology/__init__.py | 4 + .../storage/backends/synology/backend.py | 125 ++++++ .../storage/backends/toyouacs5000/__init__.py | 4 + .../storage/backends/toyouacs5000/backend.py | 118 +++++ .../backends/veritasaccess/__init__.py | 4 + .../storage/backends/veritasaccess/backend.py | 88 ++++ .../storage/backends/yadro/__init__.py | 4 + .../sunbeam/storage/backends/yadro/backend.py | 120 +++++ .../storage/backends/zadara/__init__.py | 4 + .../storage/backends/zadara/backend.py | 124 ++++++ .../unit/sunbeam/storage/backends/conftest.py | 283 ++++++++++++ .../sunbeam/storage/backends/test_common.py | 190 +++++++- .../sunbeam/storage/backends/test_datacore.py | 37 ++ .../sunbeam/storage/backends/test_datera.py | 79 ++++ .../storage/backends/test_dellpowermax.py | 86 ++++ .../storage/backends/test_dellpowerstore.py | 11 +- .../storage/backends/test_dellpowervault.py | 37 ++ .../storage/backends/test_fujitsueternusdx.py | 77 ++++ .../sunbeam/storage/backends/test_hpexp.py | 58 +++ .../backends/test_ibmflashsystemcommon.py | 73 +++ .../backends/test_ibmflashsystemiscsi.py | 68 +++ .../sunbeam/storage/backends/test_ibmgpfs.py | 90 ++++ .../storage/backends/test_ibmibmstorage.py | 25 ++ .../storage/backends/test_ibmstorwizesvc.py | 25 ++ .../storage/backends/test_inspuras13000.py | 25 ++ .../storage/backends/test_inspurinstorage.py | 25 ++ .../storage/backends/test_kaminario.py | 25 ++ .../sunbeam/storage/backends/test_linstor.py | 25 ++ .../sunbeam/storage/backends/test_macrosan.py | 25 ++ .../sunbeam/storage/backends/test_necv.py | 25 ++ .../sunbeam/storage/backends/test_netapp.py | 25 ++ .../sunbeam/storage/backends/test_nexenta.py | 25 ++ .../sunbeam/storage/backends/test_nimble.py | 100 +++++ .../sunbeam/storage/backends/test_opene.py | 25 ++ .../storage/backends/test_prophetstor.py | 25 ++ .../sunbeam/storage/backends/test_qnap.py | 25 ++ .../storage/backends/test_sandstone.py | 25 ++ .../storage/backends/test_solidfire.py | 103 +++++ .../unit/sunbeam/storage/backends/test_stx.py | 25 ++ .../sunbeam/storage/backends/test_synology.py | 25 ++ .../storage/backends/test_toyouacs5000.py | 25 ++ .../storage/backends/test_veritasaccess.py | 25 ++ .../sunbeam/storage/backends/test_yadro.py | 25 ++ .../sunbeam/storage/backends/test_zadara.py | 25 ++ 97 files changed, 6812 insertions(+), 11 deletions(-) create mode 100644 sunbeam-python/sunbeam/storage/backends/datacore/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/datacore/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/datera/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/datera/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/dellpowermax/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/dellpowervault/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/hpexp/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/hpexp/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmgpfs/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmibmstorage/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/inspuras13000/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/inspurinstorage/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/kaminario/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/kaminario/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/linstor/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/linstor/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/macrosan/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/macrosan/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/necv/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/necv/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/netapp/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/netapp/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/nexenta/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/nexenta/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/nimble/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/nimble/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/opene/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/opene/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/prophetstor/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/qnap/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/qnap/backend.py 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/sunbeam/storage/backends/solidfire/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/solidfire/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/stx/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/stx/backend.py create mode 100644 sunbeam-python/sunbeam/storage/backends/synology/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/synology/backend.py 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/sunbeam/storage/backends/veritasaccess/__init__.py create mode 100644 sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py 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/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_datacore.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_datera.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowermax.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowervault.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_fujitsueternusdx.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_hpexp.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemcommon.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemiscsi.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmgpfs.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmibmstorage.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmstorwizesvc.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspuras13000.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspurinstorage.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_kaminario.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_linstor.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_macrosan.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_necv.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_netapp.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_nexenta.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_nimble.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_opene.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_prophetstor.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_qnap.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_solidfire.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_stx.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_synology.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_veritasaccess.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py create mode 100644 sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py diff --git a/sunbeam-python/sunbeam/storage/backends/datacore/__init__.py b/sunbeam-python/sunbeam/storage/backends/datacore/__init__.py new file mode 100644 index 000000000..61be540c2 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/datacore/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""DataCore storage backend for Sunbeam.""" diff --git a/sunbeam-python/sunbeam/storage/backends/datacore/backend.py b/sunbeam-python/sunbeam/storage/backends/datacore/backend.py new file mode 100644 index 000000000..985c97e3c --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/datacore/backend.py @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""DataCore backend implementation using base step classes.""" + +import logging +from typing import Annotated, Literal + +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 DatacoreConfig(StorageBackendConfig): + """Configuration model for DataCore SANsymphony 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="IP address or hostname of the DataCore management endpoint"), + ] + san_login: Annotated[ + str, + Field(description="Username for DataCore management authentication"), + SecretDictField(field="san-login"), + ] + san_password: Annotated[ + str, + Field(description="Password for DataCore management authentication"), + SecretDictField(field="san-password"), + ] + + # Protocol selection + protocol: Annotated[ + Literal["iscsi", "fc"] | None, + Field(description="Front-end protocol used by DataCore (iscsi or fc)"), + ] = None + + # DataCore-specific options + datacore_disk_pools: Annotated[ + str | None, + Field( + description="Comma-separated list of DataCore disk pools to use for virtual disk creation" # noqa: E501 + ), + ] = None + datacore_disk_type: Annotated[ + Literal["single", "mirrored"] | None, + Field( + description="Virtual disk type: single or mirrored (mirrored requires two DataCore servers)" # noqa: E501 + ), + ] = None + datacore_storage_profile: Annotated[ + str | None, + Field( + description="Storage profile for virtual disk (Critical, High, Normal, Low, Archive)" # noqa: E501 + ), + ] = None + datacore_api_timeout: Annotated[ + int | None, + Field(description="Timeout in seconds for DataCore API calls"), + ] = None + datacore_disk_failed_delay: Annotated[ + int | None, + Field( + description="Timeout in seconds to wait for a virtual disk to leave Failed state" # noqa: E501 + ), + ] = None + + # iSCSI-specific options + datacore_iscsi_unallowed_targets: Annotated[ + str | None, + Field( + description="Comma-separated list of iSCSI targets that cannot be used for volume attachment" # noqa: E501 + ), + ] = None + use_chap_auth: Annotated[ + bool | None, + Field(description="Enable CHAP authentication for iSCSI targets"), + ] = None + + +class DatacoreBackend(StorageBackendBase): + """DataCore backend implementation.""" + + backend_type = "datacore" + display_name = "DataCore" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-datacore" + + @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 DatacoreConfig diff --git a/sunbeam-python/sunbeam/storage/backends/datera/__init__.py b/sunbeam-python/sunbeam/storage/backends/datera/__init__.py new file mode 100644 index 000000000..2e97a8557 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/datera/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Datera backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/datera/backend.py b/sunbeam-python/sunbeam/storage/backends/datera/backend.py new file mode 100644 index 000000000..d84af2089 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/datera/backend.py @@ -0,0 +1,141 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Datera backend implementation using base step classes.""" + +import logging +from typing import Annotated, Literal + +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 DateraConfig(StorageBackendConfig): + """Configuration model for Datera 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="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 + protocol: Annotated[ + Literal["iscsi"], + Field(description="Protocol selector: iscsi."), + ] = "iscsi" + datera_ldap_server: Annotated[ + str | None, Field(description="LDAP authentication server") + ] = None + datera_503_timeout: Annotated[ + int | None, Field(description="Timeout for HTTP 503 retry messages") + ] = None + datera_503_interval: Annotated[ + int | None, Field(description="Interval between 503 retries") + ] = None + datera_debug: Annotated[ + bool | None, Field(description="True to set function arg and return logging") + ] = None + datera_debug_replica_count_override: Annotated[ + bool | None, + Field( + description="ONLY FOR DEBUG/TESTING PURPOSES True to set replica_count to 1" + ), + ] = None + datera_tenant_id: Annotated[ + str | None, + Field( + description="If set to 'Map' --> OpenStack project ID will be mapped implicitly to Datera tenant ID" # noqa: E501 + ), + ] = None + datera_enable_image_cache: Annotated[ + bool | None, + Field(description="Set to True to enable Datera backend image caching"), + ] = None + datera_image_cache_volume_type_id: Annotated[ + str | None, Field(description="Cinder volume type id to use for cached volumes") + ] = None + datera_disable_profiler: Annotated[ + bool | None, + Field(description="Set to True to disable profiling in the Datera driver"), + ] = None + datera_disable_extended_metadata: Annotated[ + bool | None, + Field( + description="Set to True to disable sending additional metadata to the Datera backend" # noqa: E501 + ), + ] = None + datera_disable_template_override: Annotated[ + bool | None, + Field( + description="Set to True to disable automatic template override of the size attribute when creating from a template" # noqa: E501 + ), + ] = None + datera_volume_type_defaults: Annotated[ + str | None, + Field( + description="Settings here will be used as volume-type defaults if the volume-type setting is not provided." # noqa: E501 + ), + ] = 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 DateraBackend(StorageBackendBase): + """Datera backend implementation.""" + + backend_type = "datera" + display_name = "Datera" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-datera" + + @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 DateraConfig diff --git a/sunbeam-python/sunbeam/storage/backends/dellpowermax/__init__.py b/sunbeam-python/sunbeam/storage/backends/dellpowermax/__init__.py new file mode 100644 index 000000000..562684583 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/dellpowermax/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Dell PowerMax backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py b/sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py new file mode 100644 index 000000000..9cd79e36f --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py @@ -0,0 +1,218 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Dell PowerMax backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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 DellpowermaxConfig(StorageBackendConfig): + """Configuration model for Dell PowerMax 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="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 + protocol: Annotated[ + Literal["fc", "iscsi"] | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + interval: Annotated[ + int | None, + Field( + description="Use this value to specify length of the interval in seconds." + ), + ] = None + retries: Annotated[ + int | None, Field(description="Use this value to specify number of retries.") + ] = None + initiator_check: Annotated[ + bool | None, Field(description="Use this value to enable the initiator_check.") + ] = None + vmax_workload: Annotated[ + str | None, + Field( + description="Workload, setting this as an extra spec in pool_name is preferable." # noqa: E501 + ), + ] = None + u4p_failover_timeout: Annotated[ + int | None, + Field( + description="How long to wait for the server to send data before giving up." + ), + ] = None + u4p_failover_retries: Annotated[ + int | None, + Field( + description="The maximum number of retries each connection should attempt." + ), + ] = None + u4p_failover_backoff_factor: Annotated[ + int | None, + Field( + description="A backoff factor to apply between attempts after the second try." # noqa: E501 + ), + ] = None + u4p_failover_autofailback: Annotated[ + bool | None, + Field( + description="If the driver should automatically failback to the primary instance of Unisphere." # noqa: E501 + ), + ] = None + u4p_failover_target: Annotated[ + str | None, Field(description="Dictionary of Unisphere failover target info.") + ] = None + powermax_array: Annotated[ + str | None, Field(description="Serial number of the array to connect to.") + ] = None + powermax_srp: Annotated[ + str | None, + Field(description="Storage resource pool on array to use for provisioning."), + ] = None + powermax_service_level: Annotated[ + str | None, Field(description="Service level to use for provisioning storage.") + ] = None + powermax_port_groups: Annotated[ + str | None, + Field( + description="List of port groups containing frontend ports configured prior for server connection." # noqa: E501 + ), + ] = None + powermax_array_tag_list: Annotated[ + str | None, Field(description="List of user assigned name for storage array.") + ] = None + powermax_short_host_name_template: Annotated[ + str | None, Field(description="User defined override for short host name.") + ] = None + powermax_port_group_name_template: Annotated[ + str | None, Field(description="User defined override for port group name.") + ] = None + load_balance: Annotated[ + bool | None, + Field(description="Enable/disable load balancing for a PowerMax backend."), + ] = None + load_balance_real_time: Annotated[ + bool | None, + Field( + description="Enable/disable real-time performance metrics for Port level load balancing for a PowerMax backend." # noqa: E501 + ), + ] = None + load_data_format: Annotated[ + str | None, + Field( + description="Performance data format, not applicable for real-time metrics." + ), + ] = None + load_look_back: Annotated[ + int | None, + Field( + description="How far in minutes to look back for diagnostic performance metrics in load calculation." # noqa: E501 + ), + ] = None + load_look_back_real_time: Annotated[ + int | None, + Field( + description="How far in minutes to look back for real-time performance metrics in load calculation." # noqa: E501 + ), + ] = None + port_group_load_metric: Annotated[ + str | None, Field(description="Metric used for port group load calculation.") + ] = None + port_load_metric: Annotated[ + str | None, Field(description="Metric used for port load calculation.") + ] = None + rest_api_connect_timeout: Annotated[ + int | None, + Field( + description="Use this value to specify connect timeout value (in seconds) for rest call." # noqa: E501 + ), + ] = None + rest_api_read_timeout: Annotated[ + int | None, + Field( + description="Use this value to specify read timeout value (in seconds) for rest call." # noqa: E501 + ), + ] = None + snapvx_unlink_symforce: Annotated[ + bool | None, + Field( + description="Enable SnapVx unlink symforce, which forces the operation to execute when normally it is rejected." # noqa: E501 + ), + ] = 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 DellpowermaxBackend(StorageBackendBase): + """Dell PowerMax backend implementation.""" + + backend_type = "dellpowermax" + display_name = "Dell PowerMax" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-dellpowermax" + + @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 DellpowermaxConfig diff --git a/sunbeam-python/sunbeam/storage/backends/dellpowerstore/backend.py b/sunbeam-python/sunbeam/storage/backends/dellpowerstore/backend.py index 0fa25924e..4ca2a6f77 100644 --- a/sunbeam-python/sunbeam/storage/backends/dellpowerstore/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/dellpowerstore/backend.py @@ -28,7 +28,6 @@ class DellPowerstoreConfig(StorageBackendConfig): san_ip: Annotated[ str, Field(description="Dell PowerStore management IP"), - SecretDictField(field="san-ip"), ] san_login: Annotated[ str, diff --git a/sunbeam-python/sunbeam/storage/backends/dellpowervault/__init__.py b/sunbeam-python/sunbeam/storage/backends/dellpowervault/__init__.py new file mode 100644 index 000000000..dc1a1fa20 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/dellpowervault/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Dell PowerVault backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py b/sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py new file mode 100644 index 000000000..5074cc34b --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Dell PowerVault 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 DellPowerVaultConfig(StorageBackendConfig): + """Configuration model for Dell PowerVault 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, + Field(description="Protocol selector: fc, iscsi."), + ] + + # Optional backend configuration + driver_ssl_cert: Annotated[ + str | None, + Field( + description="PEM-encoded SSL certificate for HTTPS connections to the storage array." # noqa: E501 + ), + ] = None + + pvme_pool_name: Annotated[ + str | None, + Field(description="Pool or Vdisk name to use for volume creation."), + ] = None + + pvme_iscsi_ips: Annotated[ + str | None, + Field(description="List of comma-separated target iSCSI IP addresses."), + ] = None + + +class DellPowerVaultBackend(StorageBackendBase): + """Dell PowerVault backend implementation.""" + + backend_type = "dellpowervault" + display_name = "Dell PowerVault" + generally_available = False + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-dellpowervault" + + @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 DellPowerVaultConfig diff --git a/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/__init__.py b/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/__init__.py new file mode 100644 index 000000000..090d829d5 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Fujitsu ETERNUS DX backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py b/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py new file mode 100644 index 000000000..01ca412ad --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py @@ -0,0 +1,97 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""FJDX FC backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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" + + +class FujitsueternusdxConfig(StorageBackendConfig): + """Configuration model for FJDX FC 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") + ] + fujitsu_passwordless: Annotated[ + bool, + Field(description="Whether to use SSH key authentication to connect."), + ] = False + + # Optional backend configuration + protocol: Annotated[ + Literal["fc"] | None, + Field(description="Protocol selector: fc."), + ] = None + cinder_eternus_config_file: Annotated[ + str | None, + Field(description="Config file for cinder eternus_dx volume driver."), + ] = None + fujitsu_private_key_path: Annotated[ + str | None, + Field( + description="Filename of private key for ETERNUS CLI when SSH key authentication is enabled." # noqa: E501 + ), + ] = None + fujitsu_use_cli_copy: Annotated[ + bool | None, + Field(description="If True use CLI command to create snapshot."), + ] = None + + +class FujitsueternusdxBackend(StorageBackendBase): + """FJDX FC backend implementation.""" + + backend_type = "fujitsueternusdx" + display_name = "FJDX FC" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-fujitsueternusdx" + + @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 FujitsueternusdxConfig diff --git a/sunbeam-python/sunbeam/storage/backends/hpexp/__init__.py b/sunbeam-python/sunbeam/storage/backends/hpexp/__init__.py new file mode 100644 index 000000000..77533447f --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/hpexp/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""HPE XP backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/hpexp/backend.py b/sunbeam-python/sunbeam/storage/backends/hpexp/backend.py new file mode 100644 index 000000000..92e082b2d --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/hpexp/backend.py @@ -0,0 +1,343 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""HPE XP 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 HpexpConfig(StorageBackendConfig): + """Configuration model for HPE XP 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") + ] + + # Optional backend configuration + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + + hpexp_storage_id: Annotated[ + str | None, + Field(description="Product number of the storage system."), + ] = None + + hpexp_pools: Annotated[ + str | None, + Field(description="Pool number[s] or pool name[s] of the THP pool."), + ] = None + + hpexp_snap_pool: Annotated[ + str | None, + Field(description="Pool number or pool name of the snapshot pool."), + ] = None + + hpexp_ldev_range: Annotated[ + str | None, + Field( + description=( + "Range of the LDEV numbers in the format of " + "'xxxx-yyyy' that can be used by the driver." + ) + ), + ] = None + + hpexp_target_ports: Annotated[ + str | None, + Field( + description=( + "IDs of the storage ports used to attach volumes " + "to the controller node." + ) + ), + ] = None + + hpexp_compute_target_ports: Annotated[ + str | None, + Field( + description=( + "IDs of the storage ports used to attach volumes to compute nodes." + ) + ), + ] = None + + hpexp_group_create: Annotated[ + bool | None, + Field( + description=( + "If True, the driver will create host groups or iSCSI " + "targets on storage ports as needed." + ) + ), + ] = None + + hpexp_group_delete: Annotated[ + bool | None, + Field( + description=( + "If True, the driver will delete host groups or iSCSI " + "targets on storage ports as needed." + ) + ), + ] = None + + hpexp_copy_speed: Annotated[ + int | None, + Field( + description=( + "Copy speed of storage system. 1 or 2 indicates low speed, " + "3 indicates middle speed, and a value between 4 and 15 " + "indicates high speed." + ) + ), + ] = None + + hpexp_copy_check_interval: Annotated[ + int | None, + Field(description="Interval in seconds to check copy"), + ] = None + + hpexp_async_copy_check_interval: Annotated[ + int | None, + Field(description="Interval in seconds to check copy asynchronously"), + ] = None + + hpexp_manage_drs_volumes: Annotated[ + bool | None, + Field( + description=( + "If true, the driver will create a driver managed vClone " + "parent for each non-cloned DRS volume it creates." + ) + ), + ] = None + + hpexp_rest_disable_io_wait: Annotated[ + bool | None, + Field( + description=( + "It may take some time to detach volume after I/O. " + "This option will allow detaching volume to complete " + "immediately." + ) + ), + ] = None + + hpexp_rest_tcp_keepalive: Annotated[ + bool | None, + Field(description="Enables or disables use of REST API tcp keepalive"), + ] = None + + hpexp_discard_zero_page: Annotated[ + bool | None, + Field(description="Enable or disable zero page reclamation in a THP V-VOL."), + ] = None + + hpexp_lun_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds for adding a LUN to complete."), + ] = None + + hpexp_lun_retry_interval: Annotated[ + int | None, + Field(description="Retry interval in seconds for REST API adding a LUN."), + ] = None + + hpexp_restore_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for the restore operation to complete." + ) + ), + ] = None + + hpexp_state_transition_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for a volume transition to complete." + ) + ), + ] = None + + hpexp_lock_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds for storage to be unlocked."), + ] = None + + hpexp_rest_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for REST API execution to complete." + ) + ), + ] = None + + hpexp_extend_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for a volume extension to complete." + ) + ), + ] = None + + hpexp_exec_retry_interval: Annotated[ + int | None, + Field(description="Retry interval in seconds for REST API execution."), + ] = None + + hpexp_rest_connect_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for REST API connection to complete." + ) + ), + ] = None + + hpexp_rest_job_api_response_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds for a response from REST API."), + ] = None + + hpexp_rest_get_api_response_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for a response " + "against GET method of REST API." + ) + ), + ] = None + + hpexp_rest_server_busy_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds when REST API returns busy."), + ] = None + + hpexp_rest_keep_session_loop_interval: Annotated[ + int | None, + Field(description="Loop interval in seconds for keeping REST API session."), + ] = None + + hpexp_rest_another_ldev_mapped_retry_timeout: Annotated[ + int | None, + Field( + description="Retry time in seconds when new LUN allocation request fails." + ), + ] = None + + hpexp_rest_tcp_keepidle: Annotated[ + int | None, + Field( + description="Wait time in seconds for sending a first TCP keepalive packet." + ), + ] = None + + hpexp_rest_tcp_keepintvl: Annotated[ + int | None, + Field( + description="Interval of transmissions in seconds for TCP keepalive packet." + ), + ] = None + + hpexp_rest_tcp_keepcnt: Annotated[ + int | None, + Field(description="Maximum number of transmissions for TCP keepalive packet."), + ] = None + + hpexp_host_mode_options: Annotated[ + str | None, + Field(description="Host mode option for host group or iSCSI target."), + ] = None + + hpexp_rest_use_object_caching: Annotated[ + bool | None, + Field( + description=( + "Set True to enable object caching of certain " + "REST objects for better performance." + ) + ), + ] = None + + hpexp_rest_max_request_workers: Annotated[ + int | None, + Field(description="The maximum number of workers for concurrent requests."), + ] = None + + hpexp_zoning_request: Annotated[ + bool | None, + Field( + description=( + "If True, the driver will configure FC zoning between " + "the server and the storage system provided that FC " + "zoning manager is enabled." + ) + ), + ] = None + + +class HpexpBackend(StorageBackendBase): + """HPE XP backend implementation.""" + + backend_type = "hpexp" + display_name = "HPE XP" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-hpexp" + + @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 HpexpConfig diff --git a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/__init__.py b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/__init__.py new file mode 100644 index 000000000..e41e0e9fa --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""IBM FlashSystem Common backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py new file mode 100644 index 000000000..8ccdd7efa --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Ibmflashsystemcommon backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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" + + +class IbmflashsystemcommonConfig(StorageBackendConfig): + """Configuration model for Ibmflashsystemcommon 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="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 + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc."), + ] = None + flashsystem_connection_protocol: Annotated[ + Literal["FC"] | None, + Field(description="Connection protocol should be FC. (Default is FC.)"), + ] = None + flashsystem_multihostmap_enabled: Annotated[ + bool | None, + Field(description="Allows vdisk to multi host mapping. (Default is True)"), + ] = 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 IbmflashsystemcommonBackend(StorageBackendBase): + """Ibmflashsystemcommon backend implementation.""" + + backend_type = "ibmflashsystemcommon" + display_name = "IBM FlashSystem Common" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-ibmflashsystemcommon" + + @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 IbmflashsystemcommonConfig diff --git a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/__init__.py b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/__init__.py new file mode 100644 index 000000000..e4782f24f --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""IBM FlashSystem iSCSI backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py new file mode 100644 index 000000000..520557736 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""FlashSystem 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 IbmflashsystemiscsiConfig(StorageBackendConfig): + """Configuration model for FlashSystem iSCSI backend. + + This model includes the essential static configuration options for + the backend. Additional backend configuration can be managed + dynamically through the charm. + """ + + # Mandatory connection parameters + san_ip: Annotated[ + str, Field(description="Storage array management IP address or hostname.") + ] + + # Optional backend configuration + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: iscsi."), + ] = None + flashsystem_iscsi_portid: Annotated[ + int | None, + Field(description="Default iSCSI Port ID of FlashSystem. (Default port is 0.)"), # noqa: E501 + ] = None + + +class IbmflashsystemiscsiBackend(StorageBackendBase): + """FlashSystem iSCSI backend implementation.""" + + backend_type = "ibmflashsystemiscsi" + display_name = "FlashSystem iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-ibmflashsystemiscsi" + + @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 IbmflashsystemiscsiConfig diff --git a/sunbeam-python/sunbeam/storage/backends/ibmgpfs/__init__.py b/sunbeam-python/sunbeam/storage/backends/ibmgpfs/__init__.py new file mode 100644 index 000000000..bac1bd7df --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmgpfs/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""IBM GPFS backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py new file mode 100644 index 000000000..38ca6a6e3 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py @@ -0,0 +1,155 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""GPFS backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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 ImagesShareMode(StrEnum): + """Enumeration of valid image share modes.""" + + COPY = "copy" + COPY_ON_WRITE = "copy_on_write" + + +class IbmgpfsConfig(StorageBackendConfig): + """Configuration model for GPFS backend. + + This model includes ALL configuration options for the backend. + Additional configuration can be managed dynamically through the charm. + """ + + # Optional backend configuration + protocol: Annotated[ + Literal["iscsi"] | None, + Field(description="Protocol selector: iscsi."), + ] = None + gpfs_mount_point_base: Annotated[ + str | None, + Field( + description="Specifies the path of the GPFS directory where Block Storage volume and snapshot files are stored." # noqa: E501 + ), + ] = None + gpfs_images_dir: Annotated[ + str | None, + Field( + description="Specifies the path of the Image service repository in GPFS. Leave undefined if not storing images in GPFS." # noqa: E501 + ), + ] = None + gpfs_images_share_mode: Annotated[ + ImagesShareMode | None, + Field(description="Specifies the type of image copy to be used."), + ] = None + gpfs_max_clone_depth: Annotated[ + int | None, + Field( + description="Specifies an upper limit on the number of indirections required to reach a specific block due to snapshots or clones." # noqa: E501 + ), + ] = None + gpfs_sparse_volumes: Annotated[ + bool | None, + Field( + description="Specifies that volumes are created as sparse files which initially consume no space." # noqa: E501 + ), + ] = None + gpfs_storage_pool: Annotated[ + str | None, + Field(description="Specifies the storage pool that volumes are assigned to."), + ] = None + gpfs_hosts: Annotated[ + str | None, + Field( + description="Comma-separated list of IP address or hostnames of GPFS nodes." + ), + ] = None + gpfs_user_login: Annotated[str, Field(description="Username for GPFS nodes.")] + gpfs_user_password: Annotated[ + str, + Field(description="Password for GPFS node user."), + SecretDictField(field="gpfs-user-password"), + ] + gpfs_private_key: Annotated[ + str | None, + Field(description="Filename of private key to use for SSH authentication."), + ] = None + gpfs_ssh_port: Annotated[int | None, Field(description="SSH port to use.")] = None + gpfs_hosts_key_file: Annotated[ + str | None, + Field( + description="File containing SSH host keys for the gpfs nodes with which driver needs to communicate." # noqa: E501 + ), + ] = None + gpfs_strict_host_key_policy: Annotated[ + bool | None, + Field( + description="Option to enable strict gpfs host key checking while connecting to gpfs nodes." # noqa: E501 + ), + ] = None + san_thin_provision: Annotated[ + bool | None, Field(description="Use thin provisioning for SAN volumes?") + ] = None + 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"), + ] + use_multipath_for_image_xfer: Annotated[ + bool | None, + Field(description="Enable multipathing for image transfer operations."), + ] = None + + +class IbmgpfsBackend(StorageBackendBase): + """GPFS backend implementation.""" + + backend_type = "ibmgpfs" + display_name = "GPFS" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-ibmgpfs" + + @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 IbmgpfsConfig diff --git a/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/__init__.py b/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/__init__.py new file mode 100644 index 000000000..f451cbcb4 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""IBMStorage backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py new file mode 100644 index 000000000..032f496c5 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""IBMStorage backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +from pydantic import Field, model_validator +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 ConnectionType(StrEnum): + """Enumeration of valid connection types.""" + + FIBRE_CHANNEL = "fibre_channel" + ISCSI = "iscsi" + + +class Chap(StrEnum): + """Enumeration of CHAP authentication modes.""" + + DISABLED = "disabled" + ENABLED = "enabled" + + +class IbmibmstorageConfig(StorageBackendConfig): + """Configuration model for IBMStorage 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="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 + protocol: Annotated[ + Literal["fc", "iscsi"] | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + ds8k_devadd_unitadd_mapping: Annotated[ + str | None, + Field(description="Mapping between IODevice address and unit address."), + ] = None + ds8k_ssid_prefix: Annotated[ + str | None, + Field(description="Set the first two digits of SSID."), + ] = "FF" + lss_range_for_cg: Annotated[ + str | None, + Field(description="Reserve LSSs for consistency group."), + ] = None + ds8k_host_type: Annotated[ + str | None, + Field( + description=( + 'DS8K host type identifier. Use "auto" for automatic host ' + "type selection, or provide a value supported by the array." + ) + ), + ] = "auto" + proxy: Annotated[ + str | None, + Field(description="Proxy driver that connects to the IBM Storage Array"), + ] = "cinder.volume.drivers.ibm.ibm_storage.proxy.IBMStorageProxy" + connection_type: Annotated[ + ConnectionType | None, + Field(description="Connection type to the IBM Storage Array"), + ] = None + chap: Annotated[ + Chap | None, + Field( + description="CHAP authentication mode, effective only for iscsi (disabled|enabled)" # noqa: E501 + ), + ] = None + management_ips: Annotated[ + str | None, + Field(description="List of Management IP addresses (separated by commas)"), + ] = None + san_thin_provision: Annotated[ + bool | None, + Field(description="Use thin provisioning for SAN volumes?"), + ] = True + use_multipath_for_image_xfer: Annotated[ + bool | None, + Field(description="Enable multipathing for image transfer operations."), + ] = True + + @model_validator(mode="after") + def validate_protocol_connection_consistency(self): + """Ensure protocol and connection_type do not conflict when both set.""" + if self.protocol is None or self.connection_type is None: + return self + + expected = ( + ConnectionType.FIBRE_CHANNEL + if self.protocol == "fc" + else ConnectionType.ISCSI + ) + if self.connection_type != expected: + raise ValueError( + "protocol and connection_type must be consistent " + "(fc<->fibre_channel, iscsi<->iscsi)" + ) + return self + + +class IbmibmstorageBackend(StorageBackendBase): + """IBMStorage backend implementation.""" + + backend_type = "ibmibmstorage" + display_name = "IBMStorage" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-ibmibmstorage" + + @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 IbmibmstorageConfig diff --git a/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/__init__.py b/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/__init__.py new file mode 100644 index 000000000..fa430f206 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""IBM Storwize SVC backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py new file mode 100644 index 000000000..e90cbe130 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py @@ -0,0 +1,307 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Storwize SVC 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 IbmstorwizesvcConfig(StorageBackendConfig): + """Configuration model for Storwize SVC 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="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 + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + storwize_svc_volpool_name: Annotated[ + str | None, + Field( + description=( + "Comma-separated list of storage system storage pools for volumes." + ) + ), + ] = None + storwize_svc_vol_rsize: Annotated[ + int | None, + Field( + description=( + "Storage system space-efficiency parameter for volumes (percentage)" + ) + ), + ] = None + storwize_svc_vol_warning: Annotated[ + int | None, + Field( + description=( + "Storage system threshold for volume capacity warnings (percentage)" + ) + ), + ] = None + storwize_svc_vol_autoexpand: Annotated[ + bool | None, + Field( + description="Storage system autoexpand parameter for volumes (True/False)" + ), + ] = None + storwize_svc_vol_grainsize: Annotated[ + int | None, + Field( + description=( + "Storage system grain size parameter for volumes (8/32/64/128/256)" + ) + ), + ] = None + storwize_svc_vol_compression: Annotated[ + bool | None, + Field(description="Storage system compression option for volumes"), + ] = None + storwize_svc_vol_easytier: Annotated[ + bool | None, + Field(description="Enable Easy Tier for volumes"), + ] = None + storwize_svc_vol_iogrp: Annotated[ + str | None, + Field( + description=( + "The I/O group in which to allocate volumes. " + "It can be a comma-separated list." + ) + ), + ] = None + storwize_svc_flashcopy_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum number of seconds to wait for FlashCopy to be prepared." + ) + ), + ] = None + storwize_svc_allow_tenant_qos: Annotated[ + bool | None, + Field(description="Allow tenants to specify QOS on create"), + ] = None + storwize_svc_stretched_cluster_partner: Annotated[ + str | None, + Field( + description=( + "If operating in stretched cluster mode, specify " + "the name of the pool in which mirrored copies are stored." + ) + ), + ] = None + storwize_san_secondary_ip: Annotated[ + str | None, + Field( + description=( + "Specifies secondary management IP or hostname " + "to be used if san_ip is invalid or inaccessible." + ) + ), + ] = None + storwize_svc_vol_nofmtdisk: Annotated[ + bool | None, + Field( + description="Specifies that the volume not be formatted during creation." + ), + ] = None + storwize_svc_flashcopy_rate: Annotated[ + int | None, + Field( + description=( + "Specifies the Storwize FlashCopy copy rate to " + "be used when creating a full volume copy." + ) + ), + ] = None + storwize_svc_clean_rate: Annotated[ + int | None, + Field(description="Specifies the Storwize cleaning rate for the mapping."), + ] = None + storwize_svc_mirror_pool: Annotated[ + str | None, + Field( + description=( + "Specifies the name of the pool in which mirrored copy is stored." + ) + ), + ] = None + storwize_svc_aux_mirror_pool: Annotated[ + str | None, + Field( + description=( + "Specifies the name of the pool in which mirrored " + "copy is stored for aux volume." + ) + ), + ] = None + storwize_portset: Annotated[ + str | None, + Field( + description=( + "Specifies the name of the portset in which the host is to be created." + ) + ), + ] = None + storwize_svc_src_child_pool: Annotated[ + str | None, + Field( + description=( + "Specifies the source child pool for global " + "mirror source change volume storage." + ) + ), + ] = None + storwize_svc_target_child_pool: Annotated[ + str | None, + Field( + description=( + "Specifies the target child pool for global mirror " + "auxiliary change volume storage." + ) + ), + ] = None + storwize_peer_pool: Annotated[ + str | None, + Field( + description=( + "Specifies the peer pool for hyperswap volume; " + "the peer pool must exist on the other site." + ) + ), + ] = None + storwize_preferred_host_site: Annotated[ + str | None, + Field( + description=( + "Specifies site information for host. One WWPN " + "or multiple WWPNs used in the host can be specified." + ) + ), + ] = None + cycle_period_seconds: Annotated[ + int | None, + Field( + description=( + "Defines an optional cycle period that applies to " + "Global Mirror relationships with cycling mode multi." + ) + ), + ] = None + storwize_svc_retain_aux_volume: Annotated[ + bool | None, + Field( + description=( + "Enable or disable retaining aux volume on " + "secondary storage when deleting the primary volume." + ) + ), + ] = None + migrate_from_flashcopy: Annotated[ + bool | None, + Field( + description=( + "Allow or prevent volumes with legacy FlashCopy " + "mappings from joining volume group features." + ) + ), + ] = None + storwize_svc_multipath_enabled: Annotated[ + bool | None, + Field( + description=( + "Connect with multipath (FC only; iSCSI " + "multipath is controlled by Nova)." + ) + ), + ] = None + storwize_svc_iscsi_chap_enabled: Annotated[ + bool | None, + Field( + description=( + "Configure CHAP authentication for iSCSI " + "connections (default: enabled)." + ) + ), + ] = 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 IbmstorwizesvcBackend(StorageBackendBase): + """Storwize SVC backend implementation.""" + + backend_type = "ibmstorwizesvc" + display_name = "IBM Storwize SVC" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-ibmstorwizesvc" + + @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 IbmstorwizesvcConfig diff --git a/sunbeam-python/sunbeam/storage/backends/inspuras13000/__init__.py b/sunbeam-python/sunbeam/storage/backends/inspuras13000/__init__.py new file mode 100644 index 000000000..d8b4b858e --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/inspuras13000/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Inspur AS13000 backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py b/sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py new file mode 100644 index 000000000..861ee567c --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""AS13000 backend implementation using base step classes.""" + +import logging +from typing import Annotated, Literal + +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 Inspuras13000Config(StorageBackendConfig): + """Configuration model for AS13000 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="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"), + ] + as13000_token_available_time: Annotated[ + int | None, + Field( + alias="as13000-token-available-time", + description="The effective time of token validity in seconds.", + ), + ] = None + + # Optional backend configuration + protocol: Annotated[ + Literal["iscsi"] | None, + Field(description="Protocol selector: iscsi."), + ] = None + as13000_ipsan_pools: Annotated[ + str | None, + Field( + description="The Storage Pools Cinder should use, a comma separated list." + ), + ] = None + as13000_meta_pool: Annotated[ + str | None, + Field( + description="The pool which is used as a meta pool when creating a volume." + ), + ] = 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 Inspuras13000Backend(StorageBackendBase): + """AS13000 backend implementation.""" + + backend_type = "inspuras13000" + display_name = "AS13000" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-inspuras13000" + + @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 Inspuras13000Config diff --git a/sunbeam-python/sunbeam/storage/backends/inspurinstorage/__init__.py b/sunbeam-python/sunbeam/storage/backends/inspurinstorage/__init__.py new file mode 100644 index 000000000..88c7f4719 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/inspurinstorage/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Inspur InStorage backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py b/sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py new file mode 100644 index 000000000..b5e3f9053 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py @@ -0,0 +1,182 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Inspur InStorageMCS 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 InspurinstorageConfig(StorageBackendConfig): + """Configuration model for Inspur InStorageMCS 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="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 + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + + instorage_mcs_vol_autoexpand: Annotated[ + bool | None, + Field( + description=("Storage system autoexpand parameter for volumes (True/False)") + ), + ] = None + instorage_mcs_vol_compression: Annotated[ + bool | None, Field(description="Storage system compression option for volumes") + ] = None + instorage_mcs_vol_intier: Annotated[ + bool | None, Field(description="Enable InTier for volumes") + ] = None + instorage_mcs_allow_tenant_qos: Annotated[ + bool | None, Field(description="Allow tenants to specify QOS on create") + ] = None + instorage_mcs_vol_grainsize: Annotated[ + int | None, + Field( + description=( + "Storage system grain size parameter for volumes (32/64/128/256)" + ) + ), + ] = None + instorage_mcs_vol_rsize: Annotated[ + int | None, + Field( + description=( + "Storage system space-efficiency parameter for volumes (percentage)" + ) + ), + ] = None + instorage_mcs_vol_warning: Annotated[ + int | None, + Field( + description=( + "Storage system threshold for volume capacity warnings (percentage)" + ) + ), + ] = None + instorage_mcs_localcopy_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum number of seconds to wait for LocalCopy to be prepared." + ) + ), + ] = None + instorage_mcs_localcopy_rate: Annotated[ + int | None, + Field( + description=( + "Specifies the InStorage LocalCopy copy rate " + "used when creating a full volume copy." + ) + ), + ] = None + instorage_mcs_vol_iogrp: Annotated[ + str | None, Field(description="The I/O group in which to allocate volumes.") + ] = None + instorage_san_secondary_ip: Annotated[ + str | None, + Field( + description=( + "Specifies secondary management IP or hostname " + "used if san_ip is invalid or inaccessible." + ) + ), + ] = None + instorage_mcs_volpool_name: Annotated[ + str | None, + Field( + description=( + "Comma-separated list of storage system storage pools for volumes." + ) + ), + ] = None + instorage_mcs_iscsi_chap_enabled: Annotated[ + bool | None, + Field( + description=( + "Configure CHAP authentication for iSCSI " + "connections (default: enabled)." + ) + ), + ] = 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 InspurinstorageBackend(StorageBackendBase): + """Inspur InStorageMCS backend implementation.""" + + backend_type = "inspurinstorage" + display_name = "InStorageMCS" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-inspurinstorage" + + @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 InspurinstorageConfig diff --git a/sunbeam-python/sunbeam/storage/backends/kaminario/__init__.py b/sunbeam-python/sunbeam/storage/backends/kaminario/__init__.py new file mode 100644 index 000000000..a0b814211 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/kaminario/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Kaminario backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/kaminario/backend.py b/sunbeam-python/sunbeam/storage/backends/kaminario/backend.py new file mode 100644 index 000000000..95f201703 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/kaminario/backend.py @@ -0,0 +1,84 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Kaminario iSCSI backend implementation using base step classes.""" + +import logging +from typing import Annotated, Literal + +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 KaminarioConfig(StorageBackendConfig): + """Configuration model for Kaminario 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") + ] + + # Optional backend configuration + protocol: Annotated[ + Literal["iscsi"] | None, + Field(description="Protocol selector: iscsi."), + ] = None + auto_calc_max_oversubscription_ratio: Annotated[ + bool | None, + Field( + description="K2 driver will calculate max_oversubscription_ratio on setting this option as True" # noqa: E501 + ), + ] = None + disable_discovery: Annotated[ + bool | None, + Field( + description="Disabling iSCSI discovery (sendtargets) for multipath connections on K2 driver" # noqa: E501 + ), + ] = None + + +class KaminarioBackend(StorageBackendBase): + """Kaminario iSCSI backend implementation.""" + + backend_type = "kaminario" + display_name = "Kaminario iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-kaminario" + + @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 KaminarioConfig diff --git a/sunbeam-python/sunbeam/storage/backends/linstor/__init__.py b/sunbeam-python/sunbeam/storage/backends/linstor/__init__.py new file mode 100644 index 000000000..20d74e88b --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/linstor/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""LINSTOR backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/linstor/backend.py b/sunbeam-python/sunbeam/storage/backends/linstor/backend.py new file mode 100644 index 000000000..245bd3e4d --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/linstor/backend.py @@ -0,0 +1,109 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""LINSTOR iSCSI backend implementation using base step classes.""" + +from typing import Annotated, Literal + +from pydantic import Field + +from sunbeam.core.manifest import StorageBackendConfig +from sunbeam.storage.base import StorageBackendBase + + +class LinstorConfig(StorageBackendConfig): + """Configuration model for LINSTOR 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.") + ] + + # Optional backend configuration + protocol: Annotated[ + Literal["iscsi"] | None, + Field(description="Protocol selector: iscsi."), + ] = None + linstor_default_volume_group_name: Annotated[ + str | None, + Field( + description=("Default volume group name for LINSTOR (not Cinder volume).") + ), + ] = None + linstor_default_uri: Annotated[ + str | None, + Field(description="Default storage URI for LINSTOR."), + ] = None + linstor_default_storage_pool_name: Annotated[ + str | None, + Field(description="Default Storage Pool name for LINSTOR."), + ] = None + linstor_volume_downsize_factor: Annotated[ + int | None, + Field(description="Default volume downscale size in KiB = 4 MiB."), + ] = None + linstor_default_blocksize: Annotated[ + int | None, + Field( + description=( + "Default block size for image restoration. " + "When using iSCSI transport, this option specifies " + "the block size." + ) + ), + ] = None + linstor_autoplace_count: Annotated[ + int | None, + Field( + description=( + "Autoplace replication count on volume deployment: " + "0=full cluster without autoplace, 1=single-node " + "without replication, >=2=replicated with autoplace." + ) + ), + ] = None + linstor_controller_diskless: Annotated[ + bool | None, + Field(description="True means Cinder node is a diskless LINSTOR node."), + ] = None + + +class LinstorBackend(StorageBackendBase): + """LINSTOR iSCSI backend implementation.""" + + backend_type = "linstor" + display_name = "LINSTOR iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-linstor" + + @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 LinstorConfig diff --git a/sunbeam-python/sunbeam/storage/backends/macrosan/__init__.py b/sunbeam-python/sunbeam/storage/backends/macrosan/__init__.py new file mode 100644 index 000000000..c6cfe6c28 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/macrosan/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""MacroSAN backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/macrosan/backend.py b/sunbeam-python/sunbeam/storage/backends/macrosan/backend.py new file mode 100644 index 000000000..72844f7fa --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/macrosan/backend.py @@ -0,0 +1,196 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""MacroSAN 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" + FC = "fc" + + +class MacrosanConfig(StorageBackendConfig): + """Configuration model for MacroSAN 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.") + ] + + macrosan_sdas_password: Annotated[ + str, + Field(description="MacroSAN sdas devices' password"), + SecretDictField(field="macrosan-sdas-password"), + ] + + macrosan_replication_password: Annotated[ + str, + Field(description="MacroSAN replication devices' password"), + SecretDictField(field="macrosan-replication-password"), + ] + + # Optional backend configuration + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: iscsi, fc."), + ] = None + + macrosan_sdas_ipaddrs: Annotated[ + str | None, + Field(description="MacroSAN sdas devices' ip addresses"), + ] = None + + macrosan_sdas_username: Annotated[ + str | None, + Field(description="MacroSAN sdas devices' username"), + ] = None + + macrosan_replication_ipaddrs: Annotated[ + str | None, + Field(description="MacroSAN replication devices' ip addresses"), + ] = None + + macrosan_replication_username: Annotated[ + str | None, + Field(description="MacroSAN replication devices' username"), + ] = None + + macrosan_replication_destination_ports: Annotated[ + str | None, + Field(description="Slave device"), + ] = None + + macrosan_pool: Annotated[ + str | None, + Field(description="Pool to use for volume creation"), + ] = None + + macrosan_thin_lun_extent_size: Annotated[ + int | None, + Field(description="Set the thin lun's extent size"), + ] = None + + macrosan_thin_lun_low_watermark: Annotated[ + int | None, + Field(description="Set the thin lun's low watermark"), + ] = None + + macrosan_thin_lun_high_watermark: Annotated[ + int | None, + Field(description="Set the thin lun's high watermark"), + ] = None + + macrosan_force_unmap_itl: Annotated[ + bool | None, + Field(description="Force disconnect while deleting volume"), + ] = None + + macrosan_snapshot_resource_ratio: Annotated[ + str | None, + Field(description="Set snapshot's resource ratio"), + ] = None + + macrosan_log_timing: Annotated[ + bool | None, + Field(description="Whether enable log timing"), + ] = None + + macrosan_fc_use_sp_port_nr: Annotated[ + int | None, + Field( + description=( + "The use_sp_port_nr parameter is the number of online FC ports " + "used when FC connection is established in switch non-all-pass " + "mode. The maximum is 4." + ) + ), + ] = None + + macrosan_fc_keep_mapped_ports: Annotated[ + bool | None, + Field( + description=( + "In FC connections, keep the configuration item " + "associated with the port." + ) + ), + ] = None + + macrosan_client: Annotated[ + str | None, + Field( + description=( + "MacroSAN iSCSI clients list. Configure one or more entries in " + "format: (host;client_name;sp1_iscsi_port;sp2_iscsi_port). " + "client_name supports [a-zA-Z0-9.-_:] up to 31 chars." + ) + ), + ] = None + + macrosan_client_default: Annotated[ + str | None, + Field( + description=( + "Default iSCSI connection port names used when " + "no host-specific information is available, e.g. " + "eth-1:0/eth-1:1;eth-2:0/eth-2:1." + ) + ), + ] = None + + +class MacrosanBackend(StorageBackendBase): + """MacroSAN backend implementation.""" + + backend_type = "macrosan" + display_name = "MacroSAN" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-macrosan" + + @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 MacrosanConfig diff --git a/sunbeam-python/sunbeam/storage/backends/necv/__init__.py b/sunbeam-python/sunbeam/storage/backends/necv/__init__.py new file mode 100644 index 000000000..c82953f5c --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/necv/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""NEC V backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/necv/backend.py b/sunbeam-python/sunbeam/storage/backends/necv/backend.py new file mode 100644 index 000000000..a225820b8 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/necv/backend.py @@ -0,0 +1,298 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""VStorage 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 NecvConfig(StorageBackendConfig): + """Configuration model for VStorage 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") + ] + + # Optional backend configuration + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + nec_v_storage_id: Annotated[ + str | None, Field(description="Product number of the storage system.") + ] = None + nec_v_pools: Annotated[ + str | None, Field(description="Pool number[s] or pool name[s] of the DP pool.") + ] = None + nec_v_snap_pool: Annotated[ + str | None, Field(description="Pool number or pool name of the snapshot pool.") + ] = None + nec_v_ldev_range: Annotated[ + str | None, + Field( + description=( + "Range of the LDEV numbers in the format of " + "'xxxx-yyyy' that can be used by the driver." + ) + ), + ] = None + nec_v_target_ports: Annotated[ + str | None, + Field( + description=( + "IDs of the storage ports used to attach volumes " + "to the controller node." + ) + ), + ] = None + nec_v_compute_target_ports: Annotated[ + str | None, + Field( + description=( + "IDs of the storage ports used to attach volumes to compute nodes." + ) + ), + ] = None + nec_v_group_create: Annotated[ + bool | None, + Field( + description=( + "If True, the driver will create host groups or iSCSI " + "targets on storage ports as needed." + ) + ), + ] = None + nec_v_group_delete: Annotated[ + bool | None, + Field( + description=( + "If True, the driver will delete host groups or iSCSI " + "targets on storage ports as needed." + ) + ), + ] = None + nec_v_copy_speed: Annotated[ + int | None, + Field( + description=( + "Copy speed of storage system. 1 or 2 indicates low speed, " + "3 indicates middle speed, and a value between 4 and 15 " + "indicates high speed." + ) + ), + ] = None + nec_v_copy_check_interval: Annotated[ + int | None, + Field( + description=( + "Interval in seconds to check copying status during a volume copy." + ) + ), + ] = None + nec_v_async_copy_check_interval: Annotated[ + int | None, + Field( + description=( + "Interval in seconds to check asynchronous copying status " + "during copy pair deletion or data restoration." + ) + ), + ] = None + nec_v_manage_drs_volumes: Annotated[ + bool | None, + Field( + description=( + "If true, the driver creates a driver-managed vClone " + "parent for each non-cloned DRS volume." + ) + ), + ] = None + nec_v_rest_disable_io_wait: Annotated[ + bool | None, + Field( + description=( + "It may take time to detach volume after I/O. This option " + "allows detaching volume to complete immediately." + ) + ), + ] = None + nec_v_rest_tcp_keepalive: Annotated[ + bool | None, + Field(description="Enables or disables use of REST API tcp keepalive"), + ] = None + nec_v_discard_zero_page: Annotated[ + bool | None, + Field(description="Enable or disable zero page reclamation in a DP-VOL."), + ] = None + nec_v_lun_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds for adding a LUN to complete."), + ] = None + nec_v_lun_retry_interval: Annotated[ + int | None, + Field(description="Retry interval in seconds for REST API adding a LUN."), + ] = None + nec_v_restore_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for the restore operation to complete." + ) + ), + ] = None + nec_v_state_transition_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for a volume transition to complete." + ) + ), + ] = None + nec_v_lock_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds for storage to be unlocked."), + ] = None + nec_v_rest_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for REST API execution to complete." + ) + ), + ] = None + nec_v_extend_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for a volume extension to complete." + ) + ), + ] = None + nec_v_exec_retry_interval: Annotated[ + int | None, + Field(description="Retry interval in seconds for REST API execution."), + ] = None + nec_v_rest_connect_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for REST API connection to complete." + ) + ), + ] = None + nec_v_rest_job_api_response_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds for a response from REST API."), + ] = None + nec_v_rest_get_api_response_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum wait time in seconds for a response " + "against GET method of REST API." + ) + ), + ] = None + nec_v_rest_server_busy_timeout: Annotated[ + int | None, + Field(description="Maximum wait time in seconds when REST API returns busy."), + ] = None + nec_v_rest_keep_session_loop_interval: Annotated[ + int | None, + Field(description="Loop interval in seconds for keeping REST API session."), + ] = None + nec_v_rest_another_ldev_mapped_retry_timeout: Annotated[ + int | None, + Field( + description="Retry time in seconds when new LUN allocation request fails." + ), + ] = None + nec_v_rest_tcp_keepidle: Annotated[ + int | None, + Field( + description="Wait time in seconds for sending a first TCP keepalive packet." + ), + ] = None + nec_v_rest_tcp_keepintvl: Annotated[ + int | None, + Field( + description="Interval of transmissions in seconds for TCP keepalive packet." + ), + ] = None + nec_v_rest_tcp_keepcnt: Annotated[ + int | None, + Field(description="Maximum number of transmissions for TCP keepalive packet."), + ] = None + nec_v_host_mode_options: Annotated[ + str | None, Field(description="Host mode option for host group or iSCSI target") + ] = None + nec_v_zoning_request: Annotated[ + bool | None, + Field( + description=( + "If True, the driver will configure FC zoning between " + "the server and storage system when FC zoning manager " + "is enabled." + ) + ), + ] = None + + +class NecvBackend(StorageBackendBase): + """VStorage backend implementation.""" + + backend_type = "necv" + display_name = "VStorage" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-necv" + + @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 NecvConfig diff --git a/sunbeam-python/sunbeam/storage/backends/netapp/__init__.py b/sunbeam-python/sunbeam/storage/backends/netapp/__init__.py new file mode 100644 index 000000000..12d9d1c64 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/netapp/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""NetApp backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/netapp/backend.py b/sunbeam-python/sunbeam/storage/backends/netapp/backend.py new file mode 100644 index 000000000..ca3b21f8d --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/netapp/backend.py @@ -0,0 +1,417 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""NetApp ONTAP backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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 Family(StrEnum): + """Enumeration of valid storage family types.""" + + ONTAP_CLUSTER = "ontap_cluster" + + +class TransportType(StrEnum): + """Enumeration of valid transport types.""" + + HTTP = "http" + HTTPS = "https" + + +class LunSpaceReservation(StrEnum): + """Enumeration of valid LUN space reservation options.""" + + ENABLED = "enabled" + DISABLED = "disabled" + + +class NetAppConfig(StorageBackendConfig): + """Configuration model for NetApp ONTAP 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 selection + protocol: Annotated[ + Literal["iscsi", "nvme"], + Field(description="Protocol selector: iscsi, nvme."), + ] + + # Optional backend configuration + netapp_storage_family: Annotated[ + Family | None, + Field(description="The storage family type used on the storage system."), + ] = None + + netapp_storage_protocol: Annotated[ + Literal["iscsi", "fc", "nfs", "nvme"] | None, + Field( + description=( + "The storage protocol to be used on the data path with the " + "storage system." + ) + ), + ] = None + + netapp_server_hostname: Annotated[ + str | None, + Field( + description="The hostname (or IP address) for the storage system or proxy server." # noqa: E501 + ), + ] = None + + netapp_server_port: Annotated[ + int | None, + Field( + description=( + "The TCP port to use for communication with the storage system " + "or proxy server." + ) + ), + ] = None + + netapp_use_legacy_client: Annotated[ + bool | None, + Field( + description=( + "Select which ONTAP client to use for retrieving and modifying " + "data on the storage." + ) + ), + ] = None + + netapp_async_rest_timeout: Annotated[ + int | None, + Field( + description=( + "The maximum time in seconds to wait for completing a REST " + "asynchronous operation." + ) + ), + ] = None + + netapp_transport_type: Annotated[ + TransportType | None, + Field( + description=( + "The transport protocol used when communicating with the storage " + "system or proxy server." + ) + ), + ] = None + + netapp_ssl_cert_path: Annotated[ + str | None, + Field( + description=( + "The path to a CA_BUNDLE file or directory with certificates of " + "trusted CA." + ) + ), + ] = None + + netapp_login: Annotated[ + str | None, + Field( + description=( + "Administrative user account name used to access the storage " + "system or proxy server." + ) + ), + SecretDictField(field="netapp-login"), + ] = None + + netapp_password: Annotated[ + str | None, + Field( + description=( + "Password for the administrative user account specified in the " + "netapp_login option." + ) + ), + SecretDictField(field="netapp-password"), + ] = None + + netapp_private_key_file: Annotated[ + str | None, + Field( + description=( + "Absolute path to the file containing the private key " + "associated with the certificate." + ) + ), + SecretDictField(field="netapp-private-key-file"), + ] = None + + netapp_certificate_file: Annotated[ + str | None, + Field( + description="Absolute path to the file containing the digital certificate." + ), + SecretDictField(field="netapp-certificate-file"), + ] = None + + netapp_ca_certificate_file: Annotated[ + str | None, + Field( + description=( + "Absolute path to the file containing the public key " + "certificate of the trusted CA." + ) + ), + ] = None + + netapp_certificate_host_validation: Annotated[ + bool | None, + Field(description="Enable certificate verification for host validation."), + ] = None + + netapp_size_multiplier: Annotated[ + float | None, + Field( + description="Multiplier for requested volume size to ensure enough space." + ), + ] = None + + netapp_lun_space_reservation: Annotated[ + LunSpaceReservation | None, + Field( + description="Determines if storage space is reserved for LUN allocation." + ), + ] = None + + netapp_driver_reports_provisioned_capacity: Annotated[ + bool | None, + Field( + description=( + "Enable querying of storage system to calculate volumes " + "provisioned size." + ) + ), + ] = None + + netapp_vserver: Annotated[ + str | None, + Field( + description="Virtual storage server (Vserver) name on the storage cluster." + ), + ] = None + + netapp_disaggregated_platform: Annotated[ + bool | None, + Field(description="Enable ASA r2 workflows for NetApp disaggregated platform."), + ] = None + + netapp_nfs_image_cache_cleanup_interval: Annotated[ + int | None, + Field(description="Time in seconds between NFS image cache cleanup tasks."), + ] = None + + thres_avl_size_perc_start: Annotated[ + int | None, + Field( + description=( + "Percentage of available space for an NFS share to trigger " + "cache cleaning." + ) + ), + ] = None + + thres_avl_size_perc_stop: Annotated[ + int | None, + Field( + description=( + "Percentage of available space on an NFS share to stop cache cleaning." + ) + ), + ] = None + + expiry_thres_minutes: Annotated[ + int | None, + Field( + description=( + "Threshold for last access time for images in the NFS image cache." + ) + ), + ] = None + + netapp_lun_ostype: Annotated[ + str | None, + Field( + description=( + "Type of operating system that will access a LUN exported from " + "Data ONTAP." + ) + ), + ] = None + + netapp_namespace_ostype: Annotated[ + str | None, + Field( + description=( + "Type of operating system that will access a namespace exported " + "from Data ONTAP." + ) + ), + ] = None + + netapp_host_type: Annotated[ + str | None, + Field( + description="Type of operating system for all initiators that can access a LUN." # noqa: E501 + ), + ] = None + + netapp_pool_name_search_pattern: Annotated[ + str | None, + Field( + description=( + "Regular expression to restrict provisioning to specified pools." + ) + ), + ] = None + + netapp_lun_clone_busy_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum time to retry LUN clone operation when device busy " + "error occurs." + ) + ), + ] = None + + netapp_lun_clone_busy_interval: Annotated[ + int | None, + Field( + description=( + "Time interval to retry LUN clone operation when device busy " + "error occurs." + ) + ), + ] = None + + netapp_dedupe_cache_expiry_duration: Annotated[ + int | None, + Field( + description=( + "Time interval between updates of netapp_dedupe_used_percent " + "for ONTAP backend pools." + ) + ), + ] = None + + netapp_performance_cache_expiry_duration: Annotated[ + int | None, + Field( + description=( + "Time interval between updates of performance utilization for " + "ONTAP backend pools." + ) + ), + ] = None + + netapp_replication_aggregate_map: Annotated[ + str | None, + Field( + description=( + "Aggregate mapping between source and destination back ends " + "for replication." + ) + ), + ] = None + + netapp_snapmirror_quiesce_timeout: Annotated[ + int | None, + Field( + description=( + "Maximum time to wait for existing SnapMirror transfers to " + "complete before aborting." + ) + ), + ] = None + + netapp_replication_volume_online_timeout: Annotated[ + int | None, + Field( + description=( + "Time to wait for a replication volume create to complete and " + "go online." + ) + ), + ] = None + + netapp_replication_policy: Annotated[ + str | None, + Field( + description="Replication policy to be used while creating snapmirror relationship." # noqa: E501 + ), + ] = None + + netapp_api_trace_pattern: Annotated[ + str | None, + Field(description="Regular expression to limit the API tracing."), + ] = None + + netapp_migrate_volume_timeout: Annotated[ + int | None, + Field( + description="Time to wait for storage assisted volume migration to complete." # noqa: E501 + ), + ] = None + + +class NetAppBackend(StorageBackendBase): + """NetApp ONTAP backend implementation.""" + + backend_type = "netapp" + display_name = "NetApp ONTAP" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-netapp" + + @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 NetAppConfig diff --git a/sunbeam-python/sunbeam/storage/backends/nexenta/__init__.py b/sunbeam-python/sunbeam/storage/backends/nexenta/__init__.py new file mode 100644 index 000000000..5e310cfbb --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/nexenta/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Nexenta backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/nexenta/backend.py b/sunbeam-python/sunbeam/storage/backends/nexenta/backend.py new file mode 100644 index 000000000..403ac3e63 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/nexenta/backend.py @@ -0,0 +1,332 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Nexenta iSCSI backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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 RestProtocol(StrEnum): + """Enumeration of valid REST protocol types.""" + + HTTP = "http" + HTTPS = "https" + AUTO = "auto" + + +class DatasetCompression(StrEnum): + """Enumeration of valid dataset compression types.""" + + ON = "on" + OFF = "off" + GZIP = "gzip" + GZIP_1 = "gzip-1" + GZIP_2 = "gzip-2" + GZIP_3 = "gzip-3" + GZIP_4 = "gzip-4" + GZIP_5 = "gzip-5" + GZIP_6 = "gzip-6" + GZIP_7 = "gzip-7" + GZIP_8 = "gzip-8" + GZIP_9 = "gzip-9" + LZJB = "lzjb" + ZLE = "zle" + LZ4 = "lz4" + + +class DatasetDedup(StrEnum): + """Enumeration of valid dataset deduplication types.""" + + ON = "on" + OFF = "off" + SHA256 = "sha256" + VERIFY = "verify" + SHA256_VERIFY = "sha256, verify" + + +class NexentaConfig(StorageBackendConfig): + """Configuration model for Nexenta iSCSI backend. + + This model includes ALL configuration options for the backend. + Additional configuration can be managed dynamically through the charm. + """ + + san_ip: Annotated[ + str, Field(description="Storage array management IP address or hostname") + ] + protocol: Annotated[ + Literal["iscsi"] | None, + Field(description="Protocol selector: iscsi."), + ] = None + nexenta_rest_password: Annotated[ + str, + Field(description="Password to connect to NexentaEdge"), + SecretDictField(field="nexenta-rest-password"), + ] + nexenta_rest_protocol: Annotated[ + RestProtocol | None, + Field( + description="Use http or https for NexentaStor management REST API connection" # noqa: E501 + ), + ] = None + nexenta_nbd_symlinks_dir: Annotated[ + str | None, + Field( + description="NexentaEdge logical path of directory to store symbolic links to NBDs" # noqa: E501 + ), + ] = None + nexenta_rest_user: Annotated[ + str | None, + Field(description="User name to connect to NexentaEdge"), + ] = None + nexenta_lun_container: Annotated[ + str | None, + Field(description="NexentaEdge logical path of bucket for LUNs"), + ] = None + nexenta_iscsi_service: Annotated[ + str | None, + Field(description="NexentaEdge iSCSI service name"), + ] = None + nexenta_iops_limit: Annotated[ + int | None, + Field(description="NexentaEdge iSCSI LUN object IOPS limit"), + ] = None + nexenta_chunksize: Annotated[ + int | None, + Field(description="NexentaEdge iSCSI LUN object chunk size"), + ] = None + nexenta_replication_count: Annotated[ + int | None, + Field(description="NexentaEdge iSCSI LUN object replication count"), + ] = None + nexenta_host: Annotated[ + str | None, + Field(description="IP address of NexentaStor Appliance"), + ] = None + nexenta_rest_connect_timeout: Annotated[ + int | None, + Field( + description=( + "Specifies the time limit (in seconds), within which the " + "connection to NexentaStor management REST API server must be " + "established" + ) + ), + ] = 30 + nexenta_rest_read_timeout: Annotated[ + int | None, + Field( + description=( + "Specifies the time limit (in seconds), within which " + "NexentaStor management REST API server must send a response" + ) + ), + ] = 300 + nexenta_rest_backoff_factor: Annotated[ + float | None, + Field( + description=( + "Specifies the backoff factor to apply between connection " + "attempts to NexentaStor management REST API server" + ) + ), + ] = 0.5 + nexenta_rest_retry_count: Annotated[ + int | None, + Field( + description=( + "Specifies the number of times to repeat NexentaStor " + "management REST API call in case of connection errors and " + "NexentaStor appliance EBUSY or ENOENT errors" + ) + ), + ] = 3 + nexenta_use_https: Annotated[ + bool | None, + Field( + description=( + "Use HTTP secure protocol for NexentaStor management REST API " + "connections" + ) + ), + ] = True + nexenta_lu_writebackcache_disabled: Annotated[ + bool | None, + Field(description="Postponed write to backing store or not"), + ] = False + nexenta_iscsi_target_portal_groups: Annotated[ + str | None, + Field(description="NexentaStor target portal groups"), + ] = None + nexenta_iscsi_target_portals: Annotated[ + str | None, + Field( + description=( + "Comma separated list of portals for NexentaStor5, in format " + "of IP1:port1,IP2:port2. Port is optional, default=3260. " + "Example: 10.10.10.1:3267,10.10.1.2" + ) + ), + ] = None + nexenta_iscsi_target_host_group: Annotated[ + str | None, + Field(description="Group of hosts which are allowed to access volumes"), + ] = "all" + nexenta_iscsi_target_portal_port: Annotated[ + int | None, + Field(description="Nexenta appliance iSCSI target portal port"), + ] = 3260 + nexenta_luns_per_target: Annotated[ + int | None, + Field(description="Amount of LUNs per iSCSI target"), + ] = 100 + nexenta_volume: Annotated[ + str | None, + Field(description="NexentaStor pool name that holds all volumes"), + ] = "cinder" + nexenta_target_prefix: Annotated[ + str | None, + Field(description="iqn prefix for NexentaStor iSCSI targets"), + ] = "iqn.1986-03.com.sun:02:cinder" + nexenta_target_group_prefix: Annotated[ + str | None, + Field(description="Prefix for iSCSI target groups on NexentaStor"), + ] = "cinder" + nexenta_host_group_prefix: Annotated[ + str | None, + Field(description="Prefix for iSCSI host groups on NexentaStor"), + ] = "cinder" + nexenta_volume_group: Annotated[ + str | None, + Field(description="Volume group for NexentaStor5 iSCSI"), + ] = "iscsi" + nexenta_shares_config: Annotated[ + str | None, + Field(description="File with the list of available nfs shares"), + ] = "/etc/cinder/nfs_shares" + nexenta_mount_point_base: Annotated[ + str | None, + Field(description="Base directory that contains NFS share mount points"), + ] = "$state_path/mnt" + nexenta_sparsed_volumes: Annotated[ + bool | None, + Field( + description=( + "Enables or disables the creation of volumes as sparsed files " + "that take no space. If disabled (False), volume is created as " + "a regular file, which takes a long time." + ) + ), + ] = True + nexenta_qcow2_volumes: Annotated[ + bool | None, + Field(description="Create volumes as QCOW2 files rather than raw files"), + ] = False + nexenta_nms_cache_volroot: Annotated[ + bool | None, + Field( + description="If set True cache NexentaStor appliance volroot option value." + ), + ] = True + nexenta_dataset_compression: Annotated[ + DatasetCompression | None, + Field(description="Compression value for new ZFS folders."), + ] = None + nexenta_dataset_dedup: Annotated[ + DatasetDedup | None, + Field(description="Deduplication value for new ZFS folders."), + ] = None + nexenta_folder: Annotated[ + str | None, + Field(description="A folder where cinder created datasets will reside."), + ] = None + nexenta_dataset_description: Annotated[ + str | None, + Field(description="Human-readable description for the folder."), + ] = None + nexenta_blocksize: Annotated[ + int | None, + Field(description="Block size for datasets"), + ] = 4096 + nexenta_ns5_blocksize: Annotated[ + int | None, + Field(description="Block size for datasets"), + ] = 32 + nexenta_sparse: Annotated[ + bool | None, + Field(description="Enables or disables the creation of sparse datasets"), + ] = False + nexenta_origin_snapshot_template: Annotated[ + str | None, + Field(description="Template string to generate origin name of clone"), + ] = "origin-snapshot-%s" + nexenta_group_snapshot_template: Annotated[ + str | None, + Field(description="Template string to generate group snapshot name"), + ] = "group-snapshot-%s" + nexenta_rrmgr_compression: Annotated[ + int | None, + Field( + description=( + "Enable stream compression, level 1..9. 1 - gives best speed; " + "9 - gives best compression." + ) + ), + ] = 0 + nexenta_rrmgr_tcp_buf_size: Annotated[ + int | None, + Field(description="TCP Buffer size in KiloBytes."), + ] = 4096 + nexenta_rrmgr_connections: Annotated[ + int | None, + Field(description="Number of TCP connections."), + ] = 2 + + +class NexentaBackend(StorageBackendBase): + """Nexenta iSCSI backend implementation.""" + + backend_type = "nexenta" + display_name = "Nexenta iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-nexenta" + + @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 NexentaConfig diff --git a/sunbeam-python/sunbeam/storage/backends/nimble/__init__.py b/sunbeam-python/sunbeam/storage/backends/nimble/__init__.py new file mode 100644 index 000000000..7c9ad2569 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/nimble/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""HPE Nimble Storage backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/nimble/backend.py b/sunbeam-python/sunbeam/storage/backends/nimble/backend.py new file mode 100644 index 000000000..e9b4176c1 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/nimble/backend.py @@ -0,0 +1,113 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""HPE Nimble Storage 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" + FC = "fc" + + +class NimbleConfig(StorageBackendConfig): + """Configuration model for HPE Nimble Storage 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="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 + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: iscsi, fc."), + ] = None + nimble_pool_name: Annotated[ + str | None, + Field(description="Nimble Controller pool name"), + ] = None + nimble_subnet_label: Annotated[ + str | None, + Field(description="Nimble Subnet Label"), + ] = None + nimble_verify_certificate: Annotated[ + bool | None, + Field(description="Whether to verify Nimble SSL Certificate"), + ] = None + nimble_verify_cert_path: Annotated[ + str | None, + Field(description="Path to Nimble Array SSL certificate"), + ] = 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 NimbleBackend(StorageBackendBase): + """HPE Nimble Storage backend implementation.""" + + backend_type = "nimble" + display_name = "HPE Nimble Storage" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-nimble" + + @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 NimbleConfig diff --git a/sunbeam-python/sunbeam/storage/backends/opene/__init__.py b/sunbeam-python/sunbeam/storage/backends/opene/__init__.py new file mode 100644 index 000000000..eb04fdef5 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/opene/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Open-E Jovian backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/opene/backend.py b/sunbeam-python/sunbeam/storage/backends/opene/backend.py new file mode 100644 index 000000000..a7dc6c6c3 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/opene/backend.py @@ -0,0 +1,101 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Jovian iSCSI backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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 BlockSize(StrEnum): + """Enumeration of valid block sizes.""" + + K16 = "16K" + K32 = "32K" + K64 = "64K" + K128 = "128K" + K256 = "256K" + K512 = "512K" + M1 = "1M" + + +class OpeneConfig(StorageBackendConfig): + """Configuration model for Jovian iSCSI backend. + + This model includes ALL configuration options for the backend. + Additional configuration can be managed dynamically through the charm. + """ + + san_ip: Annotated[ + str, Field(description="Storage array management IP address or hostname.") + ] + protocol: Annotated[ + Literal["iscsi"] | None, + Field(description="Protocol selector: iscsi."), + ] = None + san_hosts: Annotated[ + str | None, Field(description="IP address of Open-E JovianDSS SA") + ] = None + jovian_recovery_delay: Annotated[ + int | None, Field(description="Time before HA cluster failure.") + ] = None + jovian_ignore_tpath: Annotated[ + str | None, Field(description="List of multipath ip addresses to ignore.") + ] = None + chap_password_len: Annotated[ + int, + Field(description="Length of the random string for CHAP password."), + ] + jovian_pool: Annotated[ + str | None, Field(description="JovianDSS pool that holds all cinder volumes") + ] = None + jovian_block_size: Annotated[ + BlockSize | None, Field(description="Block size for new volume") + ] = None + + +class OpeneBackend(StorageBackendBase): + """Jovian iSCSI backend implementation.""" + + backend_type = "opene" + display_name = "Jovian iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-opene" + + @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 OpeneConfig diff --git a/sunbeam-python/sunbeam/storage/backends/prophetstor/__init__.py b/sunbeam-python/sunbeam/storage/backends/prophetstor/__init__.py new file mode 100644 index 000000000..b53fdaa39 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/prophetstor/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""ProphetStor backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py b/sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py new file mode 100644 index 000000000..62494c916 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""ProphetStor DPL FC 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 ProphetStorConfig(StorageBackendConfig): + """Configuration model for ProphetStor DPL FC 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, + Field(description="Protocol selector: fc, iscsi."), + ] + + # Optional backend configuration + dpl_pool: Annotated[ + str | None, + Field(description="DPL pool uuid in which DPL volumes are stored."), + ] = None + + dpl_port: Annotated[ + int, + Field(description="DPL port number."), + ] = 8357 + + +class ProphetStorBackend(StorageBackendBase): + """ProphetStor DPL FC backend implementation.""" + + backend_type = "prophetstor" + display_name = "ProphetStor FC" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-prophetstor" + + @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 ProphetStorConfig diff --git a/sunbeam-python/sunbeam/storage/backends/qnap/__init__.py b/sunbeam-python/sunbeam/storage/backends/qnap/__init__.py new file mode 100644 index 000000000..3b45f6e91 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/qnap/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""QNAP backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/qnap/backend.py b/sunbeam-python/sunbeam/storage/backends/qnap/backend.py new file mode 100644 index 000000000..c33ac4b09 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/qnap/backend.py @@ -0,0 +1,114 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""QNAP Storage 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 QnapConfig(StorageBackendConfig): + """Configuration model for QNAP Storage 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="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"), + ] + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: fc, iscsi."), + ] = None + + # Optional backend configuration + qnap_management_url: Annotated[ + str | None, + Field( + description=( + "URL to manage the QNAP storage system. The driver does not " + "support IPv6 addresses in the URL." + ) + ), + ] = None + qnap_poolname: Annotated[ + str | None, + Field(description="The pool name in the QNAP Storage"), + ] = None + qnap_storage_protocol: Annotated[ + str | None, + Field(description="Communication protocol to access QNAP storage"), + ] = 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 QnapBackend(StorageBackendBase): + """QNAP Storage backend implementation.""" + + backend_type = "qnap" + display_name = "QNAP Storage" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-qnap" + + @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 QnapConfig 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..fcfca5589 --- /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..6386c551b --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Sandstone 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 Sandstone 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.") + ] + # Optional connection configuration + 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 IPs."), + ] = 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 assigning a target IP to an initiator."), + ] = None + + +class SandstoneBackend(StorageBackendBase): + """Sandstone 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/sunbeam/storage/backends/solidfire/__init__.py b/sunbeam-python/sunbeam/storage/backends/solidfire/__init__.py new file mode 100644 index 000000000..a2e79b7e6 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/solidfire/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""NetApp SolidFire backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/solidfire/backend.py b/sunbeam-python/sunbeam/storage/backends/solidfire/backend.py new file mode 100644 index 000000000..f380704f2 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/solidfire/backend.py @@ -0,0 +1,163 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""NetApp SolidFire backend implementation using base step classes.""" + +import logging +from enum import StrEnum +from typing import Annotated, Literal + +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 ProvisioningCalc(StrEnum): + """Enumeration of valid provisioning calculation types.""" + + MAX_PROVISIONED_SPACE = "maxProvisionedSpace" + USED_SPACE = "usedSpace" + + +class SolidFireConfig(StorageBackendConfig): + """Configuration model for NetApp SolidFire backend. + + Covers the SolidFire-related options used with this backend; additional + settings may be managed through the charm where supported. + """ + + # Mandatory connection parameters + 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 + protocol: Annotated[ + Literal["iscsi"] | None, + Field( + description="Front-end protocol (Cinder SolidFire driver uses iSCSI).", + ), + ] = None + sf_emulate_512: Annotated[ + bool | None, + Field(description="Set 512 byte emulation on volume creation."), + ] = None + sf_allow_tenant_qos: Annotated[ + bool | None, + Field(description="Allow tenants to specify QOS on create"), + ] = None + sf_account_prefix: Annotated[ + str | None, + Field(description="Create SolidFire accounts with this prefix."), + ] = None + sf_volume_prefix: Annotated[ + str | None, + Field(description="Create SolidFire volumes with this prefix."), + ] = None + sf_svip: Annotated[ + str | None, + Field(description="Overrides default cluster SVIP with the one specified."), + ] = None + sf_api_port: Annotated[ + int | None, + Field(description="SolidFire API port."), + ] = None + sf_enable_vag: Annotated[ + bool | None, + Field(description="Utilize volume access groups on a per-tenant basis."), + ] = None + sf_provisioning_calc: Annotated[ + ProvisioningCalc | None, + Field( + description="Change how SolidFire reports used space and provisioning calculations." # noqa: E501 + ), + ] = None + sf_cluster_pairing_timeout: Annotated[ + int | None, + Field( + description="Sets time in seconds to wait for clusters to complete pairing." # noqa: E501 + ), + ] = None + sf_volume_pairing_timeout: Annotated[ + int | None, + Field( + description="Sets time in seconds to wait for a migrating volume to complete pairing and sync." # noqa: E501 + ), + ] = None + sf_api_request_timeout: Annotated[ + int | None, + Field( + description=("Sets time in seconds to wait for an api request to complete.") # noqa: E501 + ), + ] = None + sf_volume_clone_timeout: Annotated[ + int | None, + Field( + description="Sets time in seconds to wait for a clone of a volume or snapshot to complete." # noqa: E501 + ), + ] = None + sf_volume_create_timeout: Annotated[ + int | None, + Field( + description="Sets time in seconds to wait for a create volume operation to complete." # noqa: E501 + ), + ] = 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 SolidFireBackend(StorageBackendBase): + """NetApp SolidFire backend implementation.""" + + backend_type = "solidfire" + display_name = "NetApp SolidFire" + generally_available = False + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-solidfire" + + @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 SolidFireConfig diff --git a/sunbeam-python/sunbeam/storage/backends/stx/__init__.py b/sunbeam-python/sunbeam/storage/backends/stx/__init__.py new file mode 100644 index 000000000..e1a5e14a1 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/stx/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Stx backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/stx/backend.py b/sunbeam-python/sunbeam/storage/backends/stx/backend.py new file mode 100644 index 000000000..0e3536bc2 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/stx/backend.py @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Stx 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 PoolType(StrEnum): + """Enumeration of valid pool types.""" + + LINEAR = "linear" + VIRTUAL = "virtual" + + +class Protocol(StrEnum): + """Enumeration of valid protocol types.""" + + ISCSI = "iscsi" + + +class StxConfig(StorageBackendConfig): + """Configuration model for Stx 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") + ] + # Optional connection configuration + protocol: Annotated[ + Protocol | None, + Field(description="Protocol selector: iscsi."), + ] = None + + # Optional backend configuration + seagate_pool_name: Annotated[ + str | None, + Field(description="Pool or vdisk name to use for volume creation"), + ] = "A" + seagate_pool_type: Annotated[ + PoolType | None, + Field(description="linear (for vdisk) or virtual (for virtual pool)"), + ] = None + seagate_iscsi_ips: Annotated[ + str | None, + Field(description="List of comma-separated target iSCSI IP addresses"), + ] = None + + +class StxBackend(StorageBackendBase): + """Stx backend implementation.""" + + backend_type = "stx" + display_name = "Stx" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-stx" + + @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 StxConfig diff --git a/sunbeam-python/sunbeam/storage/backends/synology/__init__.py b/sunbeam-python/sunbeam/storage/backends/synology/__init__.py new file mode 100644 index 000000000..8a9ecad28 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/synology/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Synology backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/synology/backend.py b/sunbeam-python/sunbeam/storage/backends/synology/backend.py new file mode 100644 index 000000000..d091572c7 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/synology/backend.py @@ -0,0 +1,125 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 +"""Synology 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 SynologyConfig(StorageBackendConfig): + """Configuration model for Synology 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 + + synology_password: Annotated[ + str, + Field(description="Password of administrator for logging in Synology storage."), + SecretDictField(field="synology-password"), + ] + + synology_one_time_pass: Annotated[ + str | None, + Field( + description=( + "One time password of administrator for logging in Synology " + "storage if OTP is enabled." + ) + ), + SecretDictField(field="synology-one-time-pass"), + ] = None + + # Optional backend configuration + synology_pool_name: Annotated[ + str | None, + Field(description="Volume on Synology storage to be used for creating lun."), + ] = None + + synology_admin_port: Annotated[ + int, + Field(description="Management port for Synology storage."), + ] = 5000 + + synology_username: Annotated[ + str, + Field(description="Administrator of Synology storage."), + ] = "admin" + + synology_ssl_verify: Annotated[ + bool, + Field(description="Verify SSL certificates when connecting over HTTPS."), + ] = True + + synology_device_id: Annotated[ + str | None, + Field( + description=( + "Device id for skip one time password check for logging in " + "Synology storage if OTP is enabled." + ) + ), + ] = None + + +class SynologyBackend(StorageBackendBase): + """Synology iSCSI backend implementation.""" + + backend_type = "synology" + display_name = "Synology iSCSI" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-synology" + + @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 SynologyConfig 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..daf5cca63 --- /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 +"""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 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 multi-attach for volumes with no host 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 backend implementation.""" + + backend_type = "toyouacs5000" + display_name = "Acs5000 FC/iSCSI" + 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/sunbeam/storage/backends/veritasaccess/__init__.py b/sunbeam-python/sunbeam/storage/backends/veritasaccess/__init__.py new file mode 100644 index 000000000..cd0cce88a --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/veritasaccess/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Veritas Access backend for Sunbeam storage.""" diff --git a/sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py b/sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py new file mode 100644 index 000000000..1e508c0f8 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Veritas Access 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 VeritasAccessConfig(StorageBackendConfig): + """Configuration model for Veritas Access 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 + vrts_lun_sparse: Annotated[ + bool | None, + Field(description="Create sparse LUN."), + ] = None + + vrts_target_config: Annotated[ + str | None, + Field(description="VA config file."), + ] = None + + +class VeritasAccessBackend(StorageBackendBase): + """Veritas Access backend implementation.""" + + backend_type = "veritasaccess" + display_name = "Veritas Access" + generally_available = True + + @property + def charm_name(self) -> str: + """Return the charm application name.""" + return "cinder-volume-veritasaccess" + + @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 VeritasAccessConfig 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..0a4bb39db --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/yadro/backend.py @@ -0,0 +1,120 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Yadro storage backend implementation.""" + +from enum import StrEnum +from typing import Annotated + +from pydantic import Field + +from sunbeam.core.manifest import StorageBackendConfig +from sunbeam.storage.base import StorageBackendBase + + +class Protocol(StrEnum): + """Enumeration of valid protocol types.""" + + FC = "fc" + ISCSI = "iscsi" + + +class YadroConfig(StorageBackendConfig): + """Configuration model for the Yadro storage backend. + + This model includes the configuration options defined directly for this + backend. Additional configuration can be managed dynamically through the + charm. + """ + + # Required connection parameter. + san_ip: Annotated[ + str, Field(description="Storage array management IP address or hostname.") + ] + + # 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."), + ] = None + api_port: Annotated[ + int | None, + Field(description="Port used to access the storage API."), + ] = None + export_ports: Annotated[ + str | None, + Field(description="Ports used to export storage resources."), + ] = None + host_group: Annotated[ + str | None, + Field(description="Host group name."), + ] = None + max_resource_count: Annotated[ + int | None, + Field(description="Maximum number of resources allowed."), + ] = None + pool_max_resource_count: Annotated[ + int | None, + Field(description="Maximum number of resources allowed for a single pool."), + ] = None + tat_api_retry_count: Annotated[ + int | None, + Field(description="Number of retries for storage API operations."), + ] = 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 volumes."), + ] = 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="Number of seconds to wait before re-checking."), + ] = None + + +class YadroBackend(StorageBackendBase): + """Yadro storage 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/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..1508cf499 --- /dev/null +++ b/sunbeam-python/sunbeam/storage/backends/zadara/backend.py @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""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 26a10630b..9abe74721 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py @@ -5,12 +5,47 @@ import pytest +from sunbeam.storage.backends.datacore.backend import DatacoreBackend +from sunbeam.storage.backends.datera.backend import DateraBackend +from sunbeam.storage.backends.dellpowermax.backend import DellpowermaxBackend from sunbeam.storage.backends.dellpowerstore.backend import DellPowerstoreBackend +from sunbeam.storage.backends.dellpowervault.backend import DellPowerVaultBackend from sunbeam.storage.backends.dellsc.backend import DellSCBackend +from sunbeam.storage.backends.fujitsueternusdx.backend import FujitsueternusdxBackend from sunbeam.storage.backends.hitachi.backend import HitachiBackend from sunbeam.storage.backends.hpe3par.backend import HPEthreeparBackend +from sunbeam.storage.backends.hpexp.backend import HpexpBackend +from sunbeam.storage.backends.ibmflashsystemcommon.backend import ( + IbmflashsystemcommonBackend, +) +from sunbeam.storage.backends.ibmflashsystemiscsi.backend import ( + IbmflashsystemiscsiBackend, +) +from sunbeam.storage.backends.ibmgpfs.backend import IbmgpfsBackend +from sunbeam.storage.backends.ibmibmstorage.backend import IbmibmstorageBackend +from sunbeam.storage.backends.ibmstorwizesvc.backend import IbmstorwizesvcBackend from sunbeam.storage.backends.infinidat.backend import InfinidatBackend +from sunbeam.storage.backends.inspuras13000.backend import Inspuras13000Backend +from sunbeam.storage.backends.inspurinstorage.backend import InspurinstorageBackend +from sunbeam.storage.backends.kaminario.backend import KaminarioBackend +from sunbeam.storage.backends.linstor.backend import LinstorBackend +from sunbeam.storage.backends.macrosan.backend import MacrosanBackend +from sunbeam.storage.backends.necv.backend import NecvBackend +from sunbeam.storage.backends.netapp.backend import NetAppBackend +from sunbeam.storage.backends.nexenta.backend import NexentaBackend +from sunbeam.storage.backends.nimble.backend import NimbleBackend +from sunbeam.storage.backends.opene.backend import OpeneBackend +from sunbeam.storage.backends.prophetstor.backend import ProphetStorBackend from sunbeam.storage.backends.purestorage.backend import PureStorageBackend +from sunbeam.storage.backends.qnap.backend import QnapBackend +from sunbeam.storage.backends.sandstone.backend import SandstoneBackend +from sunbeam.storage.backends.solidfire.backend import SolidFireBackend +from sunbeam.storage.backends.stx.backend import StxBackend +from sunbeam.storage.backends.synology.backend import SynologyBackend +from sunbeam.storage.backends.toyouacs5000.backend import Toyouacs5000Backend +from sunbeam.storage.backends.veritasaccess.backend import VeritasAccessBackend +from sunbeam.storage.backends.yadro.backend import YadroBackend +from sunbeam.storage.backends.zadara.backend import ZadaraBackend @pytest.fixture @@ -31,6 +66,186 @@ def dellsc_backend(): return DellSCBackend() +@pytest.fixture +def datacore_backend(): + """Provide a DataCore backend instance.""" + return DatacoreBackend() + + +@pytest.fixture +def datera_backend(): + """Provide a Datera backend instance.""" + return DateraBackend() + + +@pytest.fixture +def dellpowermax_backend(): + """Provide a Dell PowerMax backend instance.""" + return DellpowermaxBackend() + + +@pytest.fixture +def dellpowervault_backend(): + """Provide a Dell PowerVault backend instance.""" + return DellPowerVaultBackend() + + +@pytest.fixture +def fujitsueternusdx_backend(): + """Provide a Fujitsu ETERNUS DX backend instance.""" + return FujitsueternusdxBackend() + + +@pytest.fixture +def nimble_backend(): + """Provide an HPE Nimble backend instance.""" + return NimbleBackend() + + +@pytest.fixture +def hpexp_backend(): + """Provide an HPE XP backend instance.""" + return HpexpBackend() + + +@pytest.fixture +def ibmflashsystemcommon_backend(): + """Provide an IBM FlashSystem Common backend instance.""" + return IbmflashsystemcommonBackend() + + +@pytest.fixture +def ibmflashsystemiscsi_backend(): + """Provide an IBM FlashSystem iSCSI backend instance.""" + return IbmflashsystemiscsiBackend() + + +@pytest.fixture +def ibmgpfs_backend(): + """Provide an IBM GPFS backend instance.""" + return IbmgpfsBackend() + + +@pytest.fixture +def ibmibmstorage_backend(): + """Provide an IBM Storage backend instance.""" + return IbmibmstorageBackend() + + +@pytest.fixture +def ibmstorwizesvc_backend(): + """Provide an IBM Storwize SVC backend instance.""" + return IbmstorwizesvcBackend() + + +@pytest.fixture +def inspuras13000_backend(): + """Provide an Inspur AS13000 backend instance.""" + return Inspuras13000Backend() + + +@pytest.fixture +def inspurinstorage_backend(): + """Provide an Inspur InStorage backend instance.""" + return InspurinstorageBackend() + + +@pytest.fixture +def kaminario_backend(): + """Provide a Kaminario backend instance.""" + return KaminarioBackend() + + +@pytest.fixture +def linstor_backend(): + """Provide a LINSTOR backend instance.""" + return LinstorBackend() + + +@pytest.fixture +def macrosan_backend(): + """Provide a MacroSAN backend instance.""" + return MacrosanBackend() + + +@pytest.fixture +def necv_backend(): + """Provide an NEC V backend instance.""" + return NecvBackend() + + +@pytest.fixture +def netapp_backend(): + """Provide a NetApp backend instance.""" + return NetAppBackend() + + +@pytest.fixture +def nexenta_backend(): + """Provide a Nexenta backend instance.""" + return NexentaBackend() + + +@pytest.fixture +def opene_backend(): + """Provide an Open-E backend instance.""" + return OpeneBackend() + + +@pytest.fixture +def prophetstor_backend(): + """Provide a ProphetStor backend instance.""" + return ProphetStorBackend() + + +@pytest.fixture +def qnap_backend(): + """Provide a QNAP backend instance.""" + return QnapBackend() + + +@pytest.fixture +def sandstone_backend(): + """Provide a Sandstone backend instance.""" + return SandstoneBackend() + + +@pytest.fixture +def stx_backend(): + """Provide a Stx backend instance.""" + return StxBackend() + + +@pytest.fixture +def synology_backend(): + """Provide a Synology backend instance.""" + return SynologyBackend() + + +@pytest.fixture +def toyouacs5000_backend(): + """Provide a Toyou ACS5000 backend instance.""" + return Toyouacs5000Backend() + + +@pytest.fixture +def veritasaccess_backend(): + """Provide a Veritas Access backend instance.""" + return VeritasAccessBackend() + + +@pytest.fixture +def yadro_backend(): + """Provide a Yadro backend instance.""" + return YadroBackend() + + +@pytest.fixture +def zadara_backend(): + """Provide a Zadara backend instance.""" + return ZadaraBackend() + + @pytest.fixture def dellpowerstore_backend(): """Provide a Dell PowerStore backend instance.""" @@ -43,6 +258,12 @@ def infinidat_backend(): return InfinidatBackend() +@pytest.fixture +def solidfire_backend(): + """Provide a NetApp SolidFire backend instance.""" + return SolidFireBackend() + + @pytest.fixture def hpe3par_backend(): """Provide a HPE 3PAR Storage backend instance.""" @@ -54,7 +275,38 @@ def hpe3par_backend(): "hitachi", "purestorage", "dellsc", + "datacore", + "datera", + "dellpowermax", + "dellpowervault", + "fujitsueternusdx", + "hpexp", + "ibmflashsystemcommon", + "ibmflashsystemiscsi", + "ibmgpfs", + "nimble", + "ibmibmstorage", + "ibmstorwizesvc", + "inspuras13000", + "inspurinstorage", + "kaminario", + "linstor", + "macrosan", + "necv", + "netapp", + "nexenta", + "opene", + "prophetstor", + "qnap", + "sandstone", + "stx", + "synology", + "toyouacs5000", + "veritasaccess", + "yadro", + "zadara", "dellpowerstore", + "solidfire", "hpe3par", "infinidat", ] @@ -65,7 +317,38 @@ def any_backend(request): "hitachi": HitachiBackend(), "purestorage": PureStorageBackend(), "dellsc": DellSCBackend(), + "datacore": DatacoreBackend(), + "datera": DateraBackend(), + "dellpowermax": DellpowermaxBackend(), + "dellpowervault": DellPowerVaultBackend(), + "fujitsueternusdx": FujitsueternusdxBackend(), + "hpexp": HpexpBackend(), + "ibmflashsystemcommon": IbmflashsystemcommonBackend(), + "ibmflashsystemiscsi": IbmflashsystemiscsiBackend(), + "ibmgpfs": IbmgpfsBackend(), + "nimble": NimbleBackend(), + "ibmibmstorage": IbmibmstorageBackend(), + "ibmstorwizesvc": IbmstorwizesvcBackend(), + "inspuras13000": Inspuras13000Backend(), + "inspurinstorage": InspurinstorageBackend(), + "kaminario": KaminarioBackend(), + "linstor": LinstorBackend(), + "macrosan": MacrosanBackend(), + "necv": NecvBackend(), + "netapp": NetAppBackend(), + "nexenta": NexentaBackend(), + "opene": OpeneBackend(), + "prophetstor": ProphetStorBackend(), + "qnap": QnapBackend(), + "sandstone": SandstoneBackend(), + "stx": StxBackend(), + "synology": SynologyBackend(), + "toyouacs5000": Toyouacs5000Backend(), + "veritasaccess": VeritasAccessBackend(), + "yadro": YadroBackend(), + "zadara": ZadaraBackend(), "dellpowerstore": DellPowerstoreBackend(), + "solidfire": SolidFireBackend(), "hpe3par": HPEthreeparBackend(), "infinidat": InfinidatBackend(), } 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 8d8e0dead..6693f3504 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py @@ -160,16 +160,78 @@ def test_all_backends_have_unique_types( hitachi_backend, purestorage_backend, dellsc_backend, + datacore_backend, + datera_backend, + dellpowermax_backend, + dellpowervault_backend, + fujitsueternusdx_backend, + hpexp_backend, + ibmflashsystemcommon_backend, + ibmflashsystemiscsi_backend, + ibmgpfs_backend, + nimble_backend, + ibmibmstorage_backend, + ibmstorwizesvc_backend, + inspuras13000_backend, + inspurinstorage_backend, + kaminario_backend, + linstor_backend, + macrosan_backend, + necv_backend, + netapp_backend, + nexenta_backend, + opene_backend, + prophetstor_backend, + qnap_backend, + sandstone_backend, + stx_backend, + synology_backend, + toyouacs5000_backend, + veritasaccess_backend, + yadro_backend, + zadara_backend, dellpowerstore_backend, - infinidat_backend, + solidfire_backend, hpe3par_backend, + infinidat_backend, ): """Test that all backends have unique type identifiers.""" backends = [ hitachi_backend, purestorage_backend, dellsc_backend, + datacore_backend, + datera_backend, + dellpowermax_backend, + dellpowervault_backend, + fujitsueternusdx_backend, + hpexp_backend, + ibmflashsystemcommon_backend, + ibmflashsystemiscsi_backend, + ibmgpfs_backend, + nimble_backend, + ibmibmstorage_backend, + ibmstorwizesvc_backend, + inspuras13000_backend, + inspurinstorage_backend, + kaminario_backend, + linstor_backend, + macrosan_backend, + necv_backend, + netapp_backend, + nexenta_backend, + opene_backend, + prophetstor_backend, + qnap_backend, + sandstone_backend, + stx_backend, + synology_backend, + toyouacs5000_backend, + veritasaccess_backend, + yadro_backend, + zadara_backend, dellpowerstore_backend, + solidfire_backend, hpe3par_backend, infinidat_backend, ] @@ -183,16 +245,78 @@ def test_all_backends_have_unique_charm_names( hitachi_backend, purestorage_backend, dellsc_backend, + datacore_backend, + datera_backend, + dellpowermax_backend, + dellpowervault_backend, + fujitsueternusdx_backend, + hpexp_backend, + ibmflashsystemcommon_backend, + ibmflashsystemiscsi_backend, + ibmgpfs_backend, + nimble_backend, + ibmibmstorage_backend, + ibmstorwizesvc_backend, + inspuras13000_backend, + inspurinstorage_backend, + kaminario_backend, + linstor_backend, + macrosan_backend, + necv_backend, + netapp_backend, + nexenta_backend, + opene_backend, + prophetstor_backend, + qnap_backend, + sandstone_backend, + stx_backend, + synology_backend, + toyouacs5000_backend, + veritasaccess_backend, + yadro_backend, + zadara_backend, dellpowerstore_backend, - infinidat_backend, + solidfire_backend, hpe3par_backend, + infinidat_backend, ): """Test that all backends have unique charm names.""" backends = [ hitachi_backend, purestorage_backend, dellsc_backend, + datacore_backend, + datera_backend, + dellpowermax_backend, + dellpowervault_backend, + fujitsueternusdx_backend, + hpexp_backend, + ibmflashsystemcommon_backend, + ibmflashsystemiscsi_backend, + ibmgpfs_backend, + nimble_backend, + ibmibmstorage_backend, + ibmstorwizesvc_backend, + inspuras13000_backend, + inspurinstorage_backend, + kaminario_backend, + linstor_backend, + macrosan_backend, + necv_backend, + netapp_backend, + nexenta_backend, + opene_backend, + prophetstor_backend, + qnap_backend, + sandstone_backend, + stx_backend, + synology_backend, + toyouacs5000_backend, + veritasaccess_backend, + yadro_backend, + zadara_backend, dellpowerstore_backend, + solidfire_backend, hpe3par_backend, infinidat_backend, ] @@ -210,7 +334,38 @@ def test_all_backends_have_unique_charm_names( ("hitachi", "hitachi"), ("purestorage", "purestorage"), ("dellsc", "dellsc"), + ("datacore", "datacore"), + ("datera", "datera"), + ("dellpowermax", "dellpowermax"), + ("dellpowervault", "dellpowervault"), + ("fujitsueternusdx", "fujitsueternusdx"), + ("hpexp", "hpexp"), + ("ibmflashsystemcommon", "ibmflashsystemcommon"), + ("ibmflashsystemiscsi", "ibmflashsystemiscsi"), + ("ibmgpfs", "ibmgpfs"), + ("nimble", "nimble"), + ("ibmibmstorage", "ibmibmstorage"), + ("ibmstorwizesvc", "ibmstorwizesvc"), + ("inspuras13000", "inspuras13000"), + ("inspurinstorage", "inspurinstorage"), + ("kaminario", "kaminario"), + ("linstor", "linstor"), + ("macrosan", "macrosan"), + ("necv", "necv"), + ("netapp", "netapp"), + ("nexenta", "nexenta"), + ("opene", "opene"), + ("prophetstor", "prophetstor"), + ("qnap", "qnap"), + ("sandstone", "sandstone"), + ("stx", "stx"), + ("synology", "synology"), + ("toyouacs5000", "toyouacs5000"), + ("veritasaccess", "veritasaccess"), + ("yadro", "yadro"), + ("zadara", "zadara"), ("dellpowerstore", "dellpowerstore"), + ("solidfire", "solidfire"), ("hpe3par", "hpe3par"), ("infinidat", "infinidat"), ], @@ -227,7 +382,38 @@ def test_backend_types_match_expected(any_backend, backend_type, expected_type): ("hitachi", "cinder-volume-hitachi"), ("purestorage", "cinder-volume-purestorage"), ("dellsc", "cinder-volume-dellsc"), + ("datacore", "cinder-volume-datacore"), + ("datera", "cinder-volume-datera"), + ("dellpowermax", "cinder-volume-dellpowermax"), + ("dellpowervault", "cinder-volume-dellpowervault"), + ("fujitsueternusdx", "cinder-volume-fujitsueternusdx"), + ("hpexp", "cinder-volume-hpexp"), + ("ibmflashsystemcommon", "cinder-volume-ibmflashsystemcommon"), + ("ibmflashsystemiscsi", "cinder-volume-ibmflashsystemiscsi"), + ("ibmgpfs", "cinder-volume-ibmgpfs"), + ("nimble", "cinder-volume-nimble"), + ("ibmibmstorage", "cinder-volume-ibmibmstorage"), + ("ibmstorwizesvc", "cinder-volume-ibmstorwizesvc"), + ("inspuras13000", "cinder-volume-inspuras13000"), + ("inspurinstorage", "cinder-volume-inspurinstorage"), + ("kaminario", "cinder-volume-kaminario"), + ("linstor", "cinder-volume-linstor"), + ("macrosan", "cinder-volume-macrosan"), + ("necv", "cinder-volume-necv"), + ("netapp", "cinder-volume-netapp"), + ("nexenta", "cinder-volume-nexenta"), + ("opene", "cinder-volume-opene"), + ("prophetstor", "cinder-volume-prophetstor"), + ("qnap", "cinder-volume-qnap"), + ("sandstone", "cinder-volume-sandstone"), + ("stx", "cinder-volume-stx"), + ("synology", "cinder-volume-synology"), + ("toyouacs5000", "cinder-volume-toyouacs5000"), + ("veritasaccess", "cinder-volume-veritasaccess"), + ("yadro", "cinder-volume-yadro"), + ("zadara", "cinder-volume-zadara"), ("dellpowerstore", "cinder-volume-dellpowerstore"), + ("solidfire", "cinder-volume-solidfire"), ("hpe3par", "cinder-volume-hpe3par"), ("infinidat", "cinder-volume-infinidat"), ], diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_datacore.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_datacore.py new file mode 100644 index 000000000..3a769f7ee --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_datacore.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for DataCore storage backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestDatacoreBackend(BaseBackendTests): + """Tests for DataCore backend.""" + + @pytest.fixture + def backend(self, datacore_backend): + """Provide DataCore backend instance.""" + return datacore_backend + + def test_backend_type_is_datacore(self, backend): + """Test that backend type is datacore.""" + assert backend.backend_type == "datacore" + + def test_display_name_mentions_datacore(self, backend): + """Test that display name mentions DataCore.""" + assert "datacore" in backend.display_name.lower() + + def test_charm_name_is_datacore_charm(self, backend): + """Test that charm name is cinder-volume-datacore.""" + assert backend.charm_name == "cinder-volume-datacore" + + def test_datacore_config_has_required_contract_fields(self, backend): + """Test fields required by shared backend contract.""" + config_class = backend.config_type() + fields = config_class.model_fields + + assert "san_ip" in fields + assert "protocol" in fields diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_datera.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_datera.py new file mode 100644 index 000000000..aeb19e7d4 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_datera.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Datera backend.""" + +import pytest +from pydantic import ValidationError + +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestDateraBackend(BaseBackendTests): + """Tests for Datera backend.""" + + @pytest.fixture + def backend(self, datera_backend): + """Provide Datera backend instance.""" + return datera_backend + + def test_backend_type_is_datera(self, backend): + """Test that backend type is 'datera'.""" + assert backend.backend_type == "datera" + + def test_charm_name_is_datera_charm(self, backend): + """Test that charm name is cinder-volume-datera.""" + assert backend.charm_name == "cinder-volume-datera" + + def test_datera_config_has_expected_fields(self, backend): + """Test that Datera config defines all expected fields.""" + fields = backend.config_type().model_fields + expected_fields = ["san_ip", "san_login", "san_password", "protocol"] + for field in expected_fields: + assert field in fields, f"Expected field {field} not found in config" + + def test_datera_credentials_are_secrets(self, backend): + """Test that SAN login and password are marked as secrets.""" + 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 TestDateraConfigValidation: + """Test Datera config validation behavior.""" + + def test_requires_mandatory_fields(self, datera_backend): + """Test that required SAN connection fields are enforced.""" + config_class = datera_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate({}) + + def test_accepts_valid_minimal_config(self, datera_backend): + """Test that a minimal valid Datera config is accepted.""" + config_class = datera_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.10", + "san-login": "admin", + "san-password": "secret", + "protocol": "iscsi", + } + ) + assert config.protocol == "iscsi" + + def test_defaults_protocol_to_iscsi(self, datera_backend): + """Test that protocol defaults to iscsi when omitted.""" + config_class = datera_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.10", + "san-login": "admin", + "san-password": "secret", + } + ) + assert config.protocol == "iscsi" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowermax.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowermax.py new file mode 100644 index 000000000..a9e6dc014 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowermax.py @@ -0,0 +1,86 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Dell PowerMax backend.""" + +import pytest +from pydantic import ValidationError + +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestDellpowermaxBackend(BaseBackendTests): + """Tests for Dell PowerMax backend.""" + + @pytest.fixture + def backend(self, dellpowermax_backend): + """Provide Dell PowerMax backend instance.""" + return dellpowermax_backend + + def test_backend_type_is_dellpowermax(self, backend): + """Test that backend type is 'dellpowermax'.""" + assert backend.backend_type == "dellpowermax" + + def test_charm_name_is_dellpowermax_charm(self, backend): + """Test that charm name is cinder-volume-dellpowermax.""" + assert backend.charm_name == "cinder-volume-dellpowermax" + + def test_config_has_required_fields(self, backend): + """Test that Dell PowerMax config has required fields.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "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 TestDellpowermaxConfigValidation: + """Test Dell PowerMax config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, dellpowermax_backend): + """Test that protocol rejects values other than fc/iscsi.""" + config_class = dellpowermax_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, dellpowermax_backend): + """Test that protocol accepts fc.""" + config_class = dellpowermax_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" + + def test_protocol_accepts_iscsi(self, dellpowermax_backend): + """Test that protocol accepts iscsi.""" + config_class = dellpowermax_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "protocol": "iscsi", + } + ) + assert config.protocol == "iscsi" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowerstore.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowerstore.py index cf9828df1..1cd8bb040 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowerstore.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowerstore.py @@ -108,17 +108,12 @@ def test_dellpowerstore_san_credentials_are_secret(self, backend): ) assert has_secret_marker, "san_password should be marked as secret" - def test_dellpowerstore_network_is_secret(self, backend): - """Test that SAN IP is properly marked as secret.""" - from sunbeam.storage.models import SecretDictField - + def test_dellpowerstore_network_is_required(self, backend): + """Test that SAN IP is a required field.""" config_class = backend.config_type() ip_field = config_class.model_fields.get("san_ip") assert ip_field is not None - has_secret_marker = any( - isinstance(m, SecretDictField) for m in ip_field.metadata - ) - assert has_secret_marker, "san_ip should be marked as secret" + assert ip_field.is_required(), "san_ip should be a required field" def test_dellpowerstore_config_optional_fields_work(self, backend): """Test that optional fields can be omitted.""" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowervault.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowervault.py new file mode 100644 index 000000000..94910364e --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_dellpowervault.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Dell PowerVault backend.""" + +import pytest + +from sunbeam.storage.backends.dellpowervault.backend import Protocol +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestDellPowerVaultBackend(BaseBackendTests): + """Tests for Dell PowerVault backend.""" + + @pytest.fixture + def backend(self, dellpowervault_backend): + """Provide Dell PowerVault backend instance.""" + return dellpowervault_backend + + def test_backend_type_is_dellpowervault(self, backend): + """Test that backend type is 'dellpowervault'.""" + assert backend.backend_type == "dellpowervault" + + def test_display_name_mentions_powervault(self, backend): + """Test that display name mentions PowerVault.""" + assert "powervault" in backend.display_name.lower() + + def test_charm_name_is_dellpowervault_charm(self, backend): + """Test that charm name is cinder-volume-dellpowervault.""" + assert backend.charm_name == "cinder-volume-dellpowervault" + + def test_dellpowervault_protocol_uses_enum(self, backend): + """Test protocol field is typed using the Protocol enum.""" + config_class = backend.config_type() + protocol_field = config_class.model_fields.get("protocol") + assert protocol_field is not None + assert protocol_field.annotation is Protocol diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_fujitsueternusdx.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_fujitsueternusdx.py new file mode 100644 index 000000000..cb10f951f --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_fujitsueternusdx.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Fujitsu ETERNUS DX backend.""" + +import pytest +from pydantic import ValidationError + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestFujitsueternusdxBackend(BaseBackendTests): + """Tests for Fujitsu ETERNUS DX backend.""" + + @pytest.fixture + def backend(self, fujitsueternusdx_backend): + """Provide Fujitsu ETERNUS DX backend instance.""" + return fujitsueternusdx_backend + + def test_backend_type_is_fujitsueternusdx(self, backend): + """Test that backend type is 'fujitsueternusdx'.""" + assert backend.backend_type == "fujitsueternusdx" + + def test_charm_name_is_fujitsueternusdx_charm(self, backend): + """Test that charm name is cinder-volume-fujitsueternusdx.""" + assert backend.charm_name == "cinder-volume-fujitsueternusdx" + + def test_fujitsueternusdx_config_has_required_fields(self, backend): + """Test that Fujitsu ETERNUS DX config has all required fields.""" + fields = backend.config_type().model_fields + required_fields = ["san_ip", "fujitsu_passwordless", "protocol"] + for field in required_fields: + assert field in fields, f"Required field {field} not found in config" + + def test_fujitsu_passwordless_is_boolean_toggle(self, backend): + """Test that fujitsu_passwordless is a boolean toggle.""" + config_class = backend.config_type() + field = config_class.model_fields.get("fujitsu_passwordless") + assert field is not None + assert field.annotation is bool + + +class TestFujitsueternusdxConfigValidation: + """Test Fujitsu ETERNUS DX config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, fujitsueternusdx_backend): + """Test that protocol rejects values other than fc.""" + config_class = fujitsueternusdx_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "fujitsu-passwordless": True, + "protocol": "nvme", + } + ) + + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "fujitsu-passwordless": True, + "protocol": "iscsi", + } + ) + + def test_protocol_accepts_fc(self, fujitsueternusdx_backend): + """Test that protocol accepts fc.""" + config_class = fujitsueternusdx_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "fujitsu-passwordless": True, + "protocol": "fc", + } + ) + assert config.protocol == "fc" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_hpexp.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_hpexp.py new file mode 100644 index 000000000..ec7429918 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_hpexp.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for HPE XP backend.""" + +import pytest +from pydantic import ValidationError + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestHpexpBackend(BaseBackendTests): + """Tests for HPE XP backend.""" + + @pytest.fixture + def backend(self, hpexp_backend): + """Provide HPE XP backend instance.""" + return hpexp_backend + + def test_backend_type_is_hpexp(self, backend): + """Test that backend type is 'hpexp'.""" + assert backend.backend_type == "hpexp" + + def test_charm_name_is_hpexp_charm(self, backend): + """Test that charm name is cinder-volume-hpexp.""" + assert backend.charm_name == "cinder-volume-hpexp" + + def test_hpexp_config_has_required_fields(self, backend): + """Test that HPE XP 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 TestHpexpConfigValidation: + """Test HPE XP config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, hpexp_backend): + """Test that protocol rejects values other than fc/iscsi.""" + config_class = hpexp_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, hpexp_backend): + """Test that protocol accepts fc.""" + config_class = hpexp_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "protocol": "fc", + } + ) + assert config.protocol == "fc" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemcommon.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemcommon.py new file mode 100644 index 000000000..880bad192 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemcommon.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for IBM FlashSystem Common backend.""" + +import pytest +from pydantic import ValidationError + +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestIbmflashsystemcommonBackend(BaseBackendTests): + """Tests for IBM FlashSystem Common backend.""" + + @pytest.fixture + def backend(self, ibmflashsystemcommon_backend): + """Provide IBM FlashSystem Common backend instance.""" + return ibmflashsystemcommon_backend + + def test_backend_type_is_ibmflashsystemcommon(self, backend): + """Test that backend type is 'ibmflashsystemcommon'.""" + assert backend.backend_type == "ibmflashsystemcommon" + + def test_charm_name_is_ibmflashsystemcommon_charm(self, backend): + """Test that charm name is cinder-volume-ibmflashsystemcommon.""" + assert backend.charm_name == "cinder-volume-ibmflashsystemcommon" + + def test_config_has_expected_fields(self, backend): + """Test that IBM FlashSystem Common config exposes expected fields.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "san_login", "san_password", "protocol"): + assert field in fields, f"Expected field {field} not found in config" + + def test_san_credentials_are_secret(self, backend): + """Test that SAN login and password are marked as secrets.""" + 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 TestIbmflashsystemcommonConfigValidation: + """Test IBM FlashSystem Common config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, ibmflashsystemcommon_backend): + """Test that protocol rejects values other than fc.""" + config_class = ibmflashsystemcommon_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "protocol": "iscsi", + } + ) + + def test_protocol_accepts_fc(self, ibmflashsystemcommon_backend): + """Test that protocol accepts fc.""" + config_class = ibmflashsystemcommon_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" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemiscsi.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemiscsi.py new file mode 100644 index 000000000..f9bc6fa69 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmflashsystemiscsi.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for IBM FlashSystem iSCSI backend.""" + +import pytest +from pydantic import ValidationError + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestIbmflashsystemiscsiBackend(BaseBackendTests): + """Tests for IBM FlashSystem iSCSI backend.""" + + @pytest.fixture + def backend(self, ibmflashsystemiscsi_backend): + """Provide IBM FlashSystem iSCSI backend instance.""" + return ibmflashsystemiscsi_backend + + def test_backend_type_is_ibmflashsystemiscsi(self, backend): + """Test that backend type is 'ibmflashsystemiscsi'.""" + assert backend.backend_type == "ibmflashsystemiscsi" + + def test_charm_name_is_ibmflashsystemiscsi_charm(self, backend): + """Test that charm name is cinder-volume-ibmflashsystemiscsi.""" + assert backend.charm_name == "cinder-volume-ibmflashsystemiscsi" + + def test_config_has_expected_fields(self, backend): + """Test that IBM FlashSystem iSCSI config exposes expected fields.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "protocol"): + assert field in fields, f"Expected field {field} not found in config" + + +class TestIbmflashsystemiscsiConfigValidation: + """Test IBM FlashSystem iSCSI config validation behavior.""" + + def test_san_ip_is_required(self, ibmflashsystemiscsi_backend): + """Test that san-ip is required.""" + config_class = ibmflashsystemiscsi_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "protocol": "iscsi", + } + ) + + def test_protocol_rejects_invalid_value(self, ibmflashsystemiscsi_backend): + """Test that protocol rejects values other than iscsi.""" + config_class = ibmflashsystemiscsi_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, ibmflashsystemiscsi_backend): + """Test that protocol accepts iscsi.""" + config_class = ibmflashsystemiscsi_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "protocol": "iscsi", + } + ) + assert config.protocol == "iscsi" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmgpfs.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmgpfs.py new file mode 100644 index 000000000..4b675be82 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmgpfs.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for IBM GPFS backend.""" + +import pytest +from pydantic import ValidationError + +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestIbmgpfsBackend(BaseBackendTests): + """Tests for IBM GPFS backend.""" + + @pytest.fixture + def backend(self, ibmgpfs_backend): + """Provide IBM GPFS backend instance.""" + return ibmgpfs_backend + + def test_backend_type_is_ibmgpfs(self, backend): + """Test that backend type is 'ibmgpfs'.""" + assert backend.backend_type == "ibmgpfs" + + def test_charm_name_is_ibmgpfs_charm(self, backend): + """Test that charm name is cinder-volume-ibmgpfs.""" + assert backend.charm_name == "cinder-volume-ibmgpfs" + + def test_config_has_required_fields(self, backend): + """Test that IBM GPFS config has required fields.""" + fields = backend.config_type().model_fields + for field in ("san_ip", "san_login", "san_password", "protocol"): + assert field in fields, f"Required field {field} not found in config" + + def test_credentials_are_secret(self, backend): + """Test that configured credentials are marked as secrets.""" + config_class = backend.config_type() + for field_name in ("san_login", "san_password", "gpfs_user_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 TestIbmgpfsConfigValidation: + """Test IBM GPFS config validation behavior.""" + + def test_gpfs_login_is_required(self, ibmgpfs_backend): + """Test that gpfs-user-login is required.""" + config_class = ibmgpfs_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "gpfs-user-password": "secret", + "protocol": "iscsi", + } + ) + + def test_protocol_rejects_invalid_value(self, ibmgpfs_backend): + """Test that protocol rejects values other than iscsi.""" + config_class = ibmgpfs_backend.config_type() + with pytest.raises(ValidationError): + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "gpfs-user-password": "secret", + "protocol": "fc", + } + ) + + def test_protocol_accepts_iscsi(self, ibmgpfs_backend): + """Test that protocol accepts iscsi.""" + config_class = ibmgpfs_backend.config_type() + config = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "gpfs-user-login": "gpfs-admin", + "gpfs-user-password": "secret", + "protocol": "iscsi", + } + ) + assert config.protocol == "iscsi" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmibmstorage.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmibmstorage.py new file mode 100644 index 000000000..d691079cb --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmibmstorage.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for IBMStorage backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestIbmibmstorageBackend(BaseBackendTests): + """Tests for IBMStorage backend.""" + + @pytest.fixture + def backend(self, ibmibmstorage_backend): + """Provide IBMStorage backend instance.""" + return ibmibmstorage_backend + + def test_backend_type_is_ibmibmstorage(self, backend): + """Test that backend type is 'ibmibmstorage'.""" + assert backend.backend_type == "ibmibmstorage" + + def test_charm_name_is_ibmibmstorage_charm(self, backend): + """Test that charm name is cinder-volume-ibmibmstorage.""" + assert backend.charm_name == "cinder-volume-ibmibmstorage" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmstorwizesvc.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmstorwizesvc.py new file mode 100644 index 000000000..037dcbeb0 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_ibmstorwizesvc.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Storwize SVC backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestIbmstorwizesvcBackend(BaseBackendTests): + """Tests for Storwize SVC backend.""" + + @pytest.fixture + def backend(self, ibmstorwizesvc_backend): + """Provide Storwize SVC backend instance.""" + return ibmstorwizesvc_backend + + def test_backend_type_is_ibmstorwizesvc(self, backend): + """Test that backend type is 'ibmstorwizesvc'.""" + assert backend.backend_type == "ibmstorwizesvc" + + def test_charm_name_is_ibmstorwizesvc_charm(self, backend): + """Test that charm name is cinder-volume-ibmstorwizesvc.""" + assert backend.charm_name == "cinder-volume-ibmstorwizesvc" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspuras13000.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspuras13000.py new file mode 100644 index 000000000..6c4bcfa59 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspuras13000.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Inspur AS13000 backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestInspuras13000Backend(BaseBackendTests): + """Tests for Inspur AS13000 backend.""" + + @pytest.fixture + def backend(self, inspuras13000_backend): + """Provide Inspur AS13000 backend instance.""" + return inspuras13000_backend + + def test_backend_type_is_inspuras13000(self, backend): + """Test that backend type is 'inspuras13000'.""" + assert backend.backend_type == "inspuras13000" + + def test_charm_name_is_inspuras13000_charm(self, backend): + """Test that charm name is cinder-volume-inspuras13000.""" + assert backend.charm_name == "cinder-volume-inspuras13000" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspurinstorage.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspurinstorage.py new file mode 100644 index 000000000..48d80d3d3 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_inspurinstorage.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Inspur InStorage backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestInspurinstorageBackend(BaseBackendTests): + """Tests for Inspur InStorage backend.""" + + @pytest.fixture + def backend(self, inspurinstorage_backend): + """Provide Inspur InStorage backend instance.""" + return inspurinstorage_backend + + def test_backend_type_is_inspurinstorage(self, backend): + """Test that backend type is 'inspurinstorage'.""" + assert backend.backend_type == "inspurinstorage" + + def test_charm_name_is_inspurinstorage_charm(self, backend): + """Test that charm name is cinder-volume-inspurinstorage.""" + assert backend.charm_name == "cinder-volume-inspurinstorage" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_kaminario.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_kaminario.py new file mode 100644 index 000000000..a10660c13 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_kaminario.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Kaminario backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestKaminarioBackend(BaseBackendTests): + """Tests for Kaminario backend.""" + + @pytest.fixture + def backend(self, kaminario_backend): + """Provide Kaminario backend instance.""" + return kaminario_backend + + def test_backend_type_is_kaminario(self, backend): + """Test that backend type is 'kaminario'.""" + assert backend.backend_type == "kaminario" + + def test_charm_name_is_kaminario_charm(self, backend): + """Test that charm name is cinder-volume-kaminario.""" + assert backend.charm_name == "cinder-volume-kaminario" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_linstor.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_linstor.py new file mode 100644 index 000000000..d9f71175b --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_linstor.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for LINSTOR backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestLinstorBackend(BaseBackendTests): + """Tests for LINSTOR backend.""" + + @pytest.fixture + def backend(self, linstor_backend): + """Provide LINSTOR backend instance.""" + return linstor_backend + + def test_backend_type_is_linstor(self, backend): + """Test that backend type is 'linstor'.""" + assert backend.backend_type == "linstor" + + def test_charm_name_is_linstor_charm(self, backend): + """Test that charm name is cinder-volume-linstor.""" + assert backend.charm_name == "cinder-volume-linstor" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_macrosan.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_macrosan.py new file mode 100644 index 000000000..321ba8da7 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_macrosan.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for MacroSAN backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestMacrosanBackend(BaseBackendTests): + """Tests for MacroSAN backend.""" + + @pytest.fixture + def backend(self, macrosan_backend): + """Provide MacroSAN backend instance.""" + return macrosan_backend + + def test_backend_type_is_macrosan(self, backend): + """Test that backend type is 'macrosan'.""" + assert backend.backend_type == "macrosan" + + def test_charm_name_is_macrosan_charm(self, backend): + """Test that charm name is cinder-volume-macrosan.""" + assert backend.charm_name == "cinder-volume-macrosan" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_necv.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_necv.py new file mode 100644 index 000000000..6e2626928 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_necv.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for NEC V backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestNecvBackend(BaseBackendTests): + """Tests for NEC V backend.""" + + @pytest.fixture + def backend(self, necv_backend): + """Provide NEC V backend instance.""" + return necv_backend + + def test_backend_type_is_necv(self, backend): + """Test that backend type is 'necv'.""" + assert backend.backend_type == "necv" + + def test_charm_name_is_necv_charm(self, backend): + """Test that charm name is cinder-volume-necv.""" + assert backend.charm_name == "cinder-volume-necv" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_netapp.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_netapp.py new file mode 100644 index 000000000..abb202f9e --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_netapp.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for NetApp backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestNetAppBackend(BaseBackendTests): + """Tests for NetApp backend.""" + + @pytest.fixture + def backend(self, netapp_backend): + """Provide NetApp backend instance.""" + return netapp_backend + + def test_backend_type_is_netapp(self, backend): + """Test that backend type is 'netapp'.""" + assert backend.backend_type == "netapp" + + def test_charm_name_is_netapp_charm(self, backend): + """Test that charm name is cinder-volume-netapp.""" + assert backend.charm_name == "cinder-volume-netapp" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_nexenta.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_nexenta.py new file mode 100644 index 000000000..f2395f005 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_nexenta.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Nexenta backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestNexentaBackend(BaseBackendTests): + """Tests for Nexenta backend.""" + + @pytest.fixture + def backend(self, nexenta_backend): + """Provide Nexenta backend instance.""" + return nexenta_backend + + def test_backend_type_is_nexenta(self, backend): + """Test that backend type is 'nexenta'.""" + assert backend.backend_type == "nexenta" + + def test_charm_name_is_nexenta_charm(self, backend): + """Test that charm name is cinder-volume-nexenta.""" + assert backend.charm_name == "cinder-volume-nexenta" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_nimble.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_nimble.py new file mode 100644 index 000000000..cd20bb430 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_nimble.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for HPE Nimble storage backend.""" + +from typing import get_args, get_origin + +import pytest + +from sunbeam.storage.backends.nimble.backend import Protocol +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestNimbleBackend(BaseBackendTests): + """Tests for HPE Nimble backend.""" + + @pytest.fixture + def backend(self, nimble_backend): + """Provide Nimble backend instance.""" + return nimble_backend + + def test_backend_type_is_nimble(self, backend): + """Test that backend type is 'nimble'.""" + assert backend.backend_type == "nimble" + + def test_display_name_mentions_nimble(self, backend): + """Test that display name mentions Nimble.""" + assert "nimble" in backend.display_name.lower() + + def test_charm_name_is_nimble_charm(self, backend): + """Test that charm name is cinder-volume-nimble.""" + assert backend.charm_name == "cinder-volume-nimble" + + def test_nimble_config_has_required_fields(self, backend): + """Test that Nimble config has all required fields.""" + fields = backend.config_type().model_fields + required_fields = ["san_ip", "san_login", "san_password"] + for field in required_fields: + assert field in fields, f"Required field {field} not found in config" + + def test_nimble_protocol_uses_protocol_enum(self, backend): + """Test that protocol field uses Protocol enum as source of truth.""" + protocol_field = backend.config_type().model_fields["protocol"] + annotation = protocol_field.annotation + assert get_origin(annotation) is not None + assert Protocol in get_args(annotation) + + def test_nimble_san_credentials_are_secret(self, backend): + """Test that SAN credentials are properly marked as secrets.""" + fields = backend.config_type().model_fields + for field_name in ("san_login", "san_password"): + field = fields.get(field_name) + assert field is not None + has_secret_marker = any( + isinstance(meta, SecretDictField) for meta in field.metadata + ) + assert has_secret_marker, f"{field_name} should be marked as secret" + + +class TestNimbleConfigValidation: + """Test Nimble config validation behavior.""" + + def test_protocol_accepts_only_valid_values(self, nimble_backend): + """Test that protocol field accepts only iscsi or fc.""" + from pydantic import ValidationError + + config_class = nimble_backend.config_type() + + valid_config_iscsi = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "protocol": "iscsi", + } + ) + assert valid_config_iscsi.protocol == "iscsi" + + valid_config_fc = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "protocol": "fc", + } + ) + assert valid_config_fc.protocol == "fc" + + with pytest.raises(ValidationError) as exc_info: + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "admin", + "san-password": "secret", + "protocol": "invalid", + } + ) + + assert "protocol" in str(exc_info.value).lower() diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_opene.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_opene.py new file mode 100644 index 000000000..898c88fd8 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_opene.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Open-E backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestOpeneBackend(BaseBackendTests): + """Tests for Open-E backend.""" + + @pytest.fixture + def backend(self, opene_backend): + """Provide Open-E backend instance.""" + return opene_backend + + def test_backend_type_is_opene(self, backend): + """Test that backend type is 'opene'.""" + assert backend.backend_type == "opene" + + def test_charm_name_is_opene_charm(self, backend): + """Test that charm name is cinder-volume-opene.""" + assert backend.charm_name == "cinder-volume-opene" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_prophetstor.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_prophetstor.py new file mode 100644 index 000000000..090636cc3 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_prophetstor.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for ProphetStor backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestProphetStorBackend(BaseBackendTests): + """Tests for ProphetStor backend.""" + + @pytest.fixture + def backend(self, prophetstor_backend): + """Provide ProphetStor backend instance.""" + return prophetstor_backend + + def test_backend_type_is_prophetstor(self, backend): + """Test that backend type is 'prophetstor'.""" + assert backend.backend_type == "prophetstor" + + def test_charm_name_is_prophetstor_charm(self, backend): + """Test that charm name is cinder-volume-prophetstor.""" + assert backend.charm_name == "cinder-volume-prophetstor" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_qnap.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_qnap.py new file mode 100644 index 000000000..2703cb76d --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_qnap.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for QNAP backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestQnapBackend(BaseBackendTests): + """Tests for QNAP backend.""" + + @pytest.fixture + def backend(self, qnap_backend): + """Provide QNAP backend instance.""" + return qnap_backend + + def test_backend_type_is_qnap(self, backend): + """Test that backend type is 'qnap'.""" + assert backend.backend_type == "qnap" + + def test_charm_name_is_qnap_charm(self, backend): + """Test that charm name is cinder-volume-qnap.""" + assert backend.charm_name == "cinder-volume-qnap" 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..a55c83239 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_sandstone.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Sandstone backend.""" + +import pytest + +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" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_solidfire.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_solidfire.py new file mode 100644 index 000000000..228945e23 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_solidfire.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for NetApp SolidFire backend.""" + +import pytest +from pydantic import ValidationError + +from sunbeam.storage.models import SecretDictField +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestSolidFireBackend(BaseBackendTests): + """Tests for NetApp SolidFire backend.""" + + @pytest.fixture + def backend(self, solidfire_backend): + """Provide SolidFire backend instance.""" + return solidfire_backend + + def test_backend_type_is_solidfire(self, backend): + """Test that backend type is 'solidfire'.""" + assert backend.backend_type == "solidfire" + + def test_display_name_mentions_solidfire(self, backend): + """Test that display name mentions SolidFire.""" + assert "solidfire" in backend.display_name.lower() + + def test_charm_name_is_solidfire_charm(self, backend): + """Test that charm name is cinder-volume-solidfire.""" + assert backend.charm_name == "cinder-volume-solidfire" + + def test_solidfire_config_has_required_fields(self, backend): + """Test that SolidFire config has all required fields.""" + config_class = backend.config_type() + fields = config_class.model_fields + + required_fields = [ + "san_ip", + "san_login", + "san_password", + ] + for field in required_fields: + assert field in fields, f"Required field {field} not found in config" + + def test_solidfire_protocol_optional_iscsi(self, backend): + """Test that protocol field is optional and accepts iscsi.""" + config_class = backend.config_type() + + config_minimal = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "user", + "san-password": "secret", + } + ) + assert config_minimal.protocol is None + + config_iscsi = config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "user", + "san-password": "secret", + "protocol": "iscsi", + } + ) + assert config_iscsi.protocol == "iscsi" + + def test_solidfire_credentials_are_secrets(self, backend): + """Test that SAN login and password are marked as secrets.""" + config_class = backend.config_type() + + for fname in ("san_login", "san_password"): + finfo = config_class.model_fields.get(fname) + assert finfo is not None + assert any(isinstance(m, SecretDictField) for m in finfo.metadata), ( + f"{fname} should be marked as secret" + ) + + def test_solidfire_provisioning_calc_enum_field_exists(self, backend): + """Test that provisioning calculation field exists.""" + config_class = backend.config_type() + assert "sf_provisioning_calc" in config_class.model_fields + + +class TestSolidFireConfigValidation: + """Test SolidFire config validation behavior.""" + + def test_protocol_rejects_invalid_value(self, solidfire_backend): + """Test that protocol field rejects values other than iscsi.""" + config_class = solidfire_backend.config_type() + + with pytest.raises(ValidationError) as exc_info: + config_class.model_validate( + { + "san-ip": "192.168.1.1", + "san-login": "user", + "san-password": "secret", + "protocol": "fc", + } + ) + + assert "protocol" in str(exc_info.value).lower() diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_stx.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_stx.py new file mode 100644 index 000000000..98b3151d7 --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_stx.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Stx backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestStxBackend(BaseBackendTests): + """Tests for Stx backend.""" + + @pytest.fixture + def backend(self, stx_backend): + """Provide Stx backend instance.""" + return stx_backend + + def test_backend_type_is_stx(self, backend): + """Test that backend type is 'stx'.""" + assert backend.backend_type == "stx" + + def test_charm_name_is_stx_charm(self, backend): + """Test that charm name is cinder-volume-stx.""" + assert backend.charm_name == "cinder-volume-stx" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_synology.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_synology.py new file mode 100644 index 000000000..ff4d2a5ba --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_synology.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Synology backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestSynologyBackend(BaseBackendTests): + """Tests for Synology backend.""" + + @pytest.fixture + def backend(self, synology_backend): + """Provide Synology backend instance.""" + return synology_backend + + def test_backend_type_is_synology(self, backend): + """Test that backend type is 'synology'.""" + assert backend.backend_type == "synology" + + def test_charm_name_is_synology_charm(self, backend): + """Test that charm name is cinder-volume-synology.""" + assert backend.charm_name == "cinder-volume-synology" 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..ee6526f7e --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_toyouacs5000.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Toyou ACS5000 backend.""" + +import pytest + +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" diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_veritasaccess.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_veritasaccess.py new file mode 100644 index 000000000..46f7b4e4b --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_veritasaccess.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Veritas Access backend.""" + +import pytest + +from tests.unit.sunbeam.storage.backends.test_common import BaseBackendTests + + +class TestVeritasAccessBackend(BaseBackendTests): + """Tests for Veritas Access backend.""" + + @pytest.fixture + def backend(self, veritasaccess_backend): + """Provide Veritas Access backend instance.""" + return veritasaccess_backend + + def test_backend_type_is_veritasaccess(self, backend): + """Test that backend type is 'veritasaccess'.""" + assert backend.backend_type == "veritasaccess" + + def test_charm_name_is_veritasaccess_charm(self, backend): + """Test that charm name is cinder-volume-veritasaccess.""" + assert backend.charm_name == "cinder-volume-veritasaccess" 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..c1aa7f51d --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_yadro.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Yadro backend.""" + +import pytest + +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" 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..c06e15a9e --- /dev/null +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_zadara.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2026 - Canonical Ltd +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Zadara backend.""" + +import pytest + +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" From 78433e78c0c56ea0cb082a5c2cd7c5fb4f8b97e8 Mon Sep 17 00:00:00 2001 From: Ahmad Hassan Date: Tue, 26 May 2026 18:26:29 +0500 Subject: [PATCH 2/2] fix: change latest/edge to 2024.1/edge in all backends file Signed-off-by: Ahmad Hassan --- .../storage/backends/datacore/backend.py | 2 +- .../storage/backends/datera/backend.py | 2 +- .../storage/backends/dellpowermax/backend.py | 2 +- .../backends/dellpowervault/backend.py | 2 +- .../storage/backends/dellsc/backend.py | 2 +- .../backends/fujitsueternusdx/backend.py | 2 +- .../sunbeam/storage/backends/hpexp/backend.py | 2 +- .../backends/ibmflashsystemcommon/backend.py | 2 +- .../backends/ibmflashsystemiscsi/backend.py | 2 +- .../storage/backends/ibmgpfs/backend.py | 2 +- .../storage/backends/ibmibmstorage/backend.py | 2 +- .../backends/ibmstorwizesvc/backend.py | 2 +- .../storage/backends/infinidat/backend.py | 2 +- .../storage/backends/inspuras13000/backend.py | 2 +- .../backends/inspurinstorage/backend.py | 2 +- .../storage/backends/kaminario/backend.py | 2 +- .../storage/backends/linstor/backend.py | 2 +- .../storage/backends/macrosan/backend.py | 2 +- .../sunbeam/storage/backends/necv/backend.py | 2 +- .../storage/backends/netapp/backend.py | 2 +- .../storage/backends/nexenta/backend.py | 2 +- .../storage/backends/nimble/backend.py | 2 +- .../sunbeam/storage/backends/opene/backend.py | 2 +- .../storage/backends/prophetstor/backend.py | 2 +- .../storage/backends/purestorage/backend.py | 2 +- .../sunbeam/storage/backends/qnap/backend.py | 2 +- .../storage/backends/sandstone/backend.py | 2 +- .../storage/backends/solidfire/backend.py | 2 +- .../sunbeam/storage/backends/stx/backend.py | 2 +- .../storage/backends/synology/backend.py | 2 +- .../storage/backends/toyouacs5000/backend.py | 2 +- .../storage/backends/veritasaccess/backend.py | 2 +- .../sunbeam/storage/backends/yadro/backend.py | 2 +- .../storage/backends/zadara/backend.py | 2 +- .../unit/sunbeam/storage/backends/conftest.py | 125 +++----- .../sunbeam/storage/backends/test_common.py | 280 ++---------------- 36 files changed, 108 insertions(+), 365 deletions(-) diff --git a/sunbeam-python/sunbeam/storage/backends/datacore/backend.py b/sunbeam-python/sunbeam/storage/backends/datacore/backend.py index 985c97e3c..9ef52690f 100644 --- a/sunbeam-python/sunbeam/storage/backends/datacore/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/datacore/backend.py @@ -104,7 +104,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/datera/backend.py b/sunbeam-python/sunbeam/storage/backends/datera/backend.py index d84af2089..97f07666a 100644 --- a/sunbeam-python/sunbeam/storage/backends/datera/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/datera/backend.py @@ -119,7 +119,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py b/sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py index 9cd79e36f..2fb5e644d 100644 --- a/sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/dellpowermax/backend.py @@ -196,7 +196,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py b/sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py index 5074cc34b..38d177a5b 100644 --- a/sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/dellpowervault/backend.py @@ -75,7 +75,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/dellsc/backend.py b/sunbeam-python/sunbeam/storage/backends/dellsc/backend.py index 4812a5695..d69dceb54 100644 --- a/sunbeam-python/sunbeam/storage/backends/dellsc/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/dellsc/backend.py @@ -162,7 +162,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the charm channel for this backend.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py b/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py index 01ca412ad..81b5199e5 100644 --- a/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/fujitsueternusdx/backend.py @@ -75,7 +75,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/hpexp/backend.py b/sunbeam-python/sunbeam/storage/backends/hpexp/backend.py index 92e082b2d..13baf4508 100644 --- a/sunbeam-python/sunbeam/storage/backends/hpexp/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/hpexp/backend.py @@ -321,7 +321,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py index 8ccdd7efa..f357eab18 100644 --- a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemcommon/backend.py @@ -82,7 +82,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py index 520557736..4c27889ef 100644 --- a/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/ibmflashsystemiscsi/backend.py @@ -62,7 +62,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py index 38ca6a6e3..2cbf6137a 100644 --- a/sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/ibmgpfs/backend.py @@ -133,7 +133,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py index 032f496c5..25b434f2e 100644 --- a/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/ibmibmstorage/backend.py @@ -139,7 +139,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py b/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py index e90cbe130..cd57b325a 100644 --- a/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/ibmstorwizesvc/backend.py @@ -285,7 +285,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/infinidat/backend.py b/sunbeam-python/sunbeam/storage/backends/infinidat/backend.py index 722c74712..63893c1de 100644 --- a/sunbeam-python/sunbeam/storage/backends/infinidat/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/infinidat/backend.py @@ -99,7 +99,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the charm channel for this backend.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py b/sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py index 861ee567c..8a0b86423 100644 --- a/sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/inspuras13000/backend.py @@ -86,7 +86,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py b/sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py index b5e3f9053..e150ef835 100644 --- a/sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/inspurinstorage/backend.py @@ -160,7 +160,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/kaminario/backend.py b/sunbeam-python/sunbeam/storage/backends/kaminario/backend.py index 95f201703..706e60401 100644 --- a/sunbeam-python/sunbeam/storage/backends/kaminario/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/kaminario/backend.py @@ -62,7 +62,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/linstor/backend.py b/sunbeam-python/sunbeam/storage/backends/linstor/backend.py index 245bd3e4d..d32efc03c 100644 --- a/sunbeam-python/sunbeam/storage/backends/linstor/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/linstor/backend.py @@ -87,7 +87,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/macrosan/backend.py b/sunbeam-python/sunbeam/storage/backends/macrosan/backend.py index 72844f7fa..547614f7f 100644 --- a/sunbeam-python/sunbeam/storage/backends/macrosan/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/macrosan/backend.py @@ -174,7 +174,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/necv/backend.py b/sunbeam-python/sunbeam/storage/backends/necv/backend.py index a225820b8..3520cc1f6 100644 --- a/sunbeam-python/sunbeam/storage/backends/necv/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/necv/backend.py @@ -276,7 +276,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/netapp/backend.py b/sunbeam-python/sunbeam/storage/backends/netapp/backend.py index ca3b21f8d..f6c4311b1 100644 --- a/sunbeam-python/sunbeam/storage/backends/netapp/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/netapp/backend.py @@ -395,7 +395,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/nexenta/backend.py b/sunbeam-python/sunbeam/storage/backends/nexenta/backend.py index 403ac3e63..7d680c9e6 100644 --- a/sunbeam-python/sunbeam/storage/backends/nexenta/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/nexenta/backend.py @@ -310,7 +310,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/nimble/backend.py b/sunbeam-python/sunbeam/storage/backends/nimble/backend.py index e9b4176c1..d94fdbde0 100644 --- a/sunbeam-python/sunbeam/storage/backends/nimble/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/nimble/backend.py @@ -91,7 +91,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/opene/backend.py b/sunbeam-python/sunbeam/storage/backends/opene/backend.py index a7dc6c6c3..6464d0b41 100644 --- a/sunbeam-python/sunbeam/storage/backends/opene/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/opene/backend.py @@ -79,7 +79,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py b/sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py index 62494c916..6b863d4ee 100644 --- a/sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/prophetstor/backend.py @@ -68,7 +68,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/purestorage/backend.py b/sunbeam-python/sunbeam/storage/backends/purestorage/backend.py index 1984dda74..bdc34dab6 100644 --- a/sunbeam-python/sunbeam/storage/backends/purestorage/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/purestorage/backend.py @@ -161,7 +161,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the charm channel for this backend.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/qnap/backend.py b/sunbeam-python/sunbeam/storage/backends/qnap/backend.py index c33ac4b09..89799e899 100644 --- a/sunbeam-python/sunbeam/storage/backends/qnap/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/qnap/backend.py @@ -92,7 +92,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py b/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py index 6386c551b..430bf6dc8 100644 --- a/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/sandstone/backend.py @@ -70,7 +70,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/solidfire/backend.py b/sunbeam-python/sunbeam/storage/backends/solidfire/backend.py index f380704f2..8b0941376 100644 --- a/sunbeam-python/sunbeam/storage/backends/solidfire/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/solidfire/backend.py @@ -141,7 +141,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/stx/backend.py b/sunbeam-python/sunbeam/storage/backends/stx/backend.py index 0e3536bc2..73443e48b 100644 --- a/sunbeam-python/sunbeam/storage/backends/stx/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/stx/backend.py @@ -77,7 +77,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/synology/backend.py b/sunbeam-python/sunbeam/storage/backends/synology/backend.py index d091572c7..647389d0f 100644 --- a/sunbeam-python/sunbeam/storage/backends/synology/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/synology/backend.py @@ -103,7 +103,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py index daf5cca63..b3ffcaa2e 100644 --- a/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/toyouacs5000/backend.py @@ -96,7 +96,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py b/sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py index 1e508c0f8..69ad6184e 100644 --- a/sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/veritasaccess/backend.py @@ -66,7 +66,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/yadro/backend.py b/sunbeam-python/sunbeam/storage/backends/yadro/backend.py index 0a4bb39db..e8fc46a67 100644 --- a/sunbeam-python/sunbeam/storage/backends/yadro/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/yadro/backend.py @@ -98,7 +98,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/sunbeam/storage/backends/zadara/backend.py b/sunbeam-python/sunbeam/storage/backends/zadara/backend.py index 1508cf499..ad4b3b95d 100644 --- a/sunbeam-python/sunbeam/storage/backends/zadara/backend.py +++ b/sunbeam-python/sunbeam/storage/backends/zadara/backend.py @@ -102,7 +102,7 @@ def charm_name(self) -> str: @property def charm_channel(self) -> str: """Return the default charm channel.""" - return "latest/edge" + return "2024.1/edge" @property def charm_revision(self) -> str | None: diff --git a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py index 9abe74721..6c24b939f 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/conftest.py @@ -270,86 +270,49 @@ def hpe3par_backend(): return HPEthreeparBackend() -@pytest.fixture( - params=[ - "hitachi", - "purestorage", - "dellsc", - "datacore", - "datera", - "dellpowermax", - "dellpowervault", - "fujitsueternusdx", - "hpexp", - "ibmflashsystemcommon", - "ibmflashsystemiscsi", - "ibmgpfs", - "nimble", - "ibmibmstorage", - "ibmstorwizesvc", - "inspuras13000", - "inspurinstorage", - "kaminario", - "linstor", - "macrosan", - "necv", - "netapp", - "nexenta", - "opene", - "prophetstor", - "qnap", - "sandstone", - "stx", - "synology", - "toyouacs5000", - "veritasaccess", - "yadro", - "zadara", - "dellpowerstore", - "solidfire", - "hpe3par", - "infinidat", - ] -) +# Single source of truth for all backend types and their factories. +BACKENDS = { + "hitachi": HitachiBackend, + "purestorage": PureStorageBackend, + "dellsc": DellSCBackend, + "datacore": DatacoreBackend, + "datera": DateraBackend, + "dellpowermax": DellpowermaxBackend, + "dellpowervault": DellPowerVaultBackend, + "fujitsueternusdx": FujitsueternusdxBackend, + "hpexp": HpexpBackend, + "ibmflashsystemcommon": IbmflashsystemcommonBackend, + "ibmflashsystemiscsi": IbmflashsystemiscsiBackend, + "ibmgpfs": IbmgpfsBackend, + "nimble": NimbleBackend, + "ibmibmstorage": IbmibmstorageBackend, + "ibmstorwizesvc": IbmstorwizesvcBackend, + "inspuras13000": Inspuras13000Backend, + "inspurinstorage": InspurinstorageBackend, + "kaminario": KaminarioBackend, + "linstor": LinstorBackend, + "macrosan": MacrosanBackend, + "necv": NecvBackend, + "netapp": NetAppBackend, + "nexenta": NexentaBackend, + "opene": OpeneBackend, + "prophetstor": ProphetStorBackend, + "qnap": QnapBackend, + "sandstone": SandstoneBackend, + "stx": StxBackend, + "synology": SynologyBackend, + "toyouacs5000": Toyouacs5000Backend, + "veritasaccess": VeritasAccessBackend, + "yadro": YadroBackend, + "zadara": ZadaraBackend, + "dellpowerstore": DellPowerstoreBackend, + "solidfire": SolidFireBackend, + "hpe3par": HPEthreeparBackend, + "infinidat": InfinidatBackend, +} + + +@pytest.fixture(params=list(BACKENDS.keys())) def any_backend(request): """Parametrized fixture that provides each backend type.""" - backends = { - "hitachi": HitachiBackend(), - "purestorage": PureStorageBackend(), - "dellsc": DellSCBackend(), - "datacore": DatacoreBackend(), - "datera": DateraBackend(), - "dellpowermax": DellpowermaxBackend(), - "dellpowervault": DellPowerVaultBackend(), - "fujitsueternusdx": FujitsueternusdxBackend(), - "hpexp": HpexpBackend(), - "ibmflashsystemcommon": IbmflashsystemcommonBackend(), - "ibmflashsystemiscsi": IbmflashsystemiscsiBackend(), - "ibmgpfs": IbmgpfsBackend(), - "nimble": NimbleBackend(), - "ibmibmstorage": IbmibmstorageBackend(), - "ibmstorwizesvc": IbmstorwizesvcBackend(), - "inspuras13000": Inspuras13000Backend(), - "inspurinstorage": InspurinstorageBackend(), - "kaminario": KaminarioBackend(), - "linstor": LinstorBackend(), - "macrosan": MacrosanBackend(), - "necv": NecvBackend(), - "netapp": NetAppBackend(), - "nexenta": NexentaBackend(), - "opene": OpeneBackend(), - "prophetstor": ProphetStorBackend(), - "qnap": QnapBackend(), - "sandstone": SandstoneBackend(), - "stx": StxBackend(), - "synology": SynologyBackend(), - "toyouacs5000": Toyouacs5000Backend(), - "veritasaccess": VeritasAccessBackend(), - "yadro": YadroBackend(), - "zadara": ZadaraBackend(), - "dellpowerstore": DellPowerstoreBackend(), - "solidfire": SolidFireBackend(), - "hpe3par": HPEthreeparBackend(), - "infinidat": InfinidatBackend(), - } - return backends[request.param] + 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 6693f3504..98485cd0f 100644 --- a/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py +++ b/sunbeam-python/tests/unit/sunbeam/storage/backends/test_common.py @@ -156,170 +156,22 @@ def backend(self, any_backend): # Backend uniqueness tests -def test_all_backends_have_unique_types( - hitachi_backend, - purestorage_backend, - dellsc_backend, - datacore_backend, - datera_backend, - dellpowermax_backend, - dellpowervault_backend, - fujitsueternusdx_backend, - hpexp_backend, - ibmflashsystemcommon_backend, - ibmflashsystemiscsi_backend, - ibmgpfs_backend, - nimble_backend, - ibmibmstorage_backend, - ibmstorwizesvc_backend, - inspuras13000_backend, - inspurinstorage_backend, - kaminario_backend, - linstor_backend, - macrosan_backend, - necv_backend, - netapp_backend, - nexenta_backend, - opene_backend, - prophetstor_backend, - qnap_backend, - sandstone_backend, - stx_backend, - synology_backend, - toyouacs5000_backend, - veritasaccess_backend, - yadro_backend, - zadara_backend, - dellpowerstore_backend, - solidfire_backend, - hpe3par_backend, - infinidat_backend, -): +def test_all_backends_have_unique_types(): """Test that all backends have unique type identifiers.""" - backends = [ - hitachi_backend, - purestorage_backend, - dellsc_backend, - datacore_backend, - datera_backend, - dellpowermax_backend, - dellpowervault_backend, - fujitsueternusdx_backend, - hpexp_backend, - ibmflashsystemcommon_backend, - ibmflashsystemiscsi_backend, - ibmgpfs_backend, - nimble_backend, - ibmibmstorage_backend, - ibmstorwizesvc_backend, - inspuras13000_backend, - inspurinstorage_backend, - kaminario_backend, - linstor_backend, - macrosan_backend, - necv_backend, - netapp_backend, - nexenta_backend, - opene_backend, - prophetstor_backend, - qnap_backend, - sandstone_backend, - stx_backend, - synology_backend, - toyouacs5000_backend, - veritasaccess_backend, - yadro_backend, - zadara_backend, - dellpowerstore_backend, - solidfire_backend, - hpe3par_backend, - infinidat_backend, - ] + from tests.unit.sunbeam.storage.backends.conftest import BACKENDS + + backends = [cls() for cls in BACKENDS.values()] types = [b.backend_type for b in backends] # Check no duplicates assert len(types) == len(set(types)), f"Duplicate backend types found: {types}" -def test_all_backends_have_unique_charm_names( - hitachi_backend, - purestorage_backend, - dellsc_backend, - datacore_backend, - datera_backend, - dellpowermax_backend, - dellpowervault_backend, - fujitsueternusdx_backend, - hpexp_backend, - ibmflashsystemcommon_backend, - ibmflashsystemiscsi_backend, - ibmgpfs_backend, - nimble_backend, - ibmibmstorage_backend, - ibmstorwizesvc_backend, - inspuras13000_backend, - inspurinstorage_backend, - kaminario_backend, - linstor_backend, - macrosan_backend, - necv_backend, - netapp_backend, - nexenta_backend, - opene_backend, - prophetstor_backend, - qnap_backend, - sandstone_backend, - stx_backend, - synology_backend, - toyouacs5000_backend, - veritasaccess_backend, - yadro_backend, - zadara_backend, - dellpowerstore_backend, - solidfire_backend, - hpe3par_backend, - infinidat_backend, -): +def test_all_backends_have_unique_charm_names(): """Test that all backends have unique charm names.""" - backends = [ - hitachi_backend, - purestorage_backend, - dellsc_backend, - datacore_backend, - datera_backend, - dellpowermax_backend, - dellpowervault_backend, - fujitsueternusdx_backend, - hpexp_backend, - ibmflashsystemcommon_backend, - ibmflashsystemiscsi_backend, - ibmgpfs_backend, - nimble_backend, - ibmibmstorage_backend, - ibmstorwizesvc_backend, - inspuras13000_backend, - inspurinstorage_backend, - kaminario_backend, - linstor_backend, - macrosan_backend, - necv_backend, - netapp_backend, - nexenta_backend, - opene_backend, - prophetstor_backend, - qnap_backend, - sandstone_backend, - stx_backend, - synology_backend, - toyouacs5000_backend, - veritasaccess_backend, - yadro_backend, - zadara_backend, - dellpowerstore_backend, - solidfire_backend, - hpe3par_backend, - infinidat_backend, - ] + from tests.unit.sunbeam.storage.backends.conftest import BACKENDS + + backends = [cls() for cls in BACKENDS.values()] charm_names = [b.charm_name for b in backends] # Check no duplicates @@ -328,97 +180,25 @@ def test_all_backends_have_unique_charm_names( ) -@pytest.mark.parametrize( - "backend_type,expected_type", - [ - ("hitachi", "hitachi"), - ("purestorage", "purestorage"), - ("dellsc", "dellsc"), - ("datacore", "datacore"), - ("datera", "datera"), - ("dellpowermax", "dellpowermax"), - ("dellpowervault", "dellpowervault"), - ("fujitsueternusdx", "fujitsueternusdx"), - ("hpexp", "hpexp"), - ("ibmflashsystemcommon", "ibmflashsystemcommon"), - ("ibmflashsystemiscsi", "ibmflashsystemiscsi"), - ("ibmgpfs", "ibmgpfs"), - ("nimble", "nimble"), - ("ibmibmstorage", "ibmibmstorage"), - ("ibmstorwizesvc", "ibmstorwizesvc"), - ("inspuras13000", "inspuras13000"), - ("inspurinstorage", "inspurinstorage"), - ("kaminario", "kaminario"), - ("linstor", "linstor"), - ("macrosan", "macrosan"), - ("necv", "necv"), - ("netapp", "netapp"), - ("nexenta", "nexenta"), - ("opene", "opene"), - ("prophetstor", "prophetstor"), - ("qnap", "qnap"), - ("sandstone", "sandstone"), - ("stx", "stx"), - ("synology", "synology"), - ("toyouacs5000", "toyouacs5000"), - ("veritasaccess", "veritasaccess"), - ("yadro", "yadro"), - ("zadara", "zadara"), - ("dellpowerstore", "dellpowerstore"), - ("solidfire", "solidfire"), - ("hpe3par", "hpe3par"), - ("infinidat", "infinidat"), - ], -) -def test_backend_types_match_expected(any_backend, backend_type, expected_type): - """Test that backend types match expected values.""" - if any_backend.backend_type == backend_type: - assert any_backend.backend_type == expected_type - - -@pytest.mark.parametrize( - "backend_type,expected_charm", - [ - ("hitachi", "cinder-volume-hitachi"), - ("purestorage", "cinder-volume-purestorage"), - ("dellsc", "cinder-volume-dellsc"), - ("datacore", "cinder-volume-datacore"), - ("datera", "cinder-volume-datera"), - ("dellpowermax", "cinder-volume-dellpowermax"), - ("dellpowervault", "cinder-volume-dellpowervault"), - ("fujitsueternusdx", "cinder-volume-fujitsueternusdx"), - ("hpexp", "cinder-volume-hpexp"), - ("ibmflashsystemcommon", "cinder-volume-ibmflashsystemcommon"), - ("ibmflashsystemiscsi", "cinder-volume-ibmflashsystemiscsi"), - ("ibmgpfs", "cinder-volume-ibmgpfs"), - ("nimble", "cinder-volume-nimble"), - ("ibmibmstorage", "cinder-volume-ibmibmstorage"), - ("ibmstorwizesvc", "cinder-volume-ibmstorwizesvc"), - ("inspuras13000", "cinder-volume-inspuras13000"), - ("inspurinstorage", "cinder-volume-inspurinstorage"), - ("kaminario", "cinder-volume-kaminario"), - ("linstor", "cinder-volume-linstor"), - ("macrosan", "cinder-volume-macrosan"), - ("necv", "cinder-volume-necv"), - ("netapp", "cinder-volume-netapp"), - ("nexenta", "cinder-volume-nexenta"), - ("opene", "cinder-volume-opene"), - ("prophetstor", "cinder-volume-prophetstor"), - ("qnap", "cinder-volume-qnap"), - ("sandstone", "cinder-volume-sandstone"), - ("stx", "cinder-volume-stx"), - ("synology", "cinder-volume-synology"), - ("toyouacs5000", "cinder-volume-toyouacs5000"), - ("veritasaccess", "cinder-volume-veritasaccess"), - ("yadro", "cinder-volume-yadro"), - ("zadara", "cinder-volume-zadara"), - ("dellpowerstore", "cinder-volume-dellpowerstore"), - ("solidfire", "cinder-volume-solidfire"), - ("hpe3par", "cinder-volume-hpe3par"), - ("infinidat", "cinder-volume-infinidat"), - ], -) -def test_backend_charm_names_match_expected(any_backend, backend_type, expected_charm): - """Test that backend charm names match expected values.""" - if any_backend.backend_type == backend_type: - assert any_backend.charm_name == expected_charm +def test_backend_types_match_expected(): + """Test that each backend's type matches its registry key.""" + from tests.unit.sunbeam.storage.backends.conftest import BACKENDS + + for key, cls in BACKENDS.items(): + backend = cls() + assert backend.backend_type == key, ( + f"Backend registered as '{key}' has type '{backend.backend_type}'" + ) + + +def test_backend_charm_names_match_expected(): + """Test that each backend's charm name follows convention.""" + from tests.unit.sunbeam.storage.backends.conftest import BACKENDS + + for key, cls in BACKENDS.items(): + backend = cls() + expected_charm = f"cinder-volume-{key}" + assert backend.charm_name == expected_charm, ( + f"Backend '{key}' has charm_name '{backend.charm_name}', " + f"expected '{expected_charm}'" + )