# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from collections.abc import Callable, Generator, Iterable, Sequence
import queue
from typing import Any, ClassVar, Literal, cast, overload
import warnings

from openstack._utils import renamed_param
from openstack.block_storage.v3 import attachment as _attachment
from openstack.block_storage.v3 import availability_zone
from openstack.block_storage.v3 import backup as _backup
from openstack.block_storage.v3 import block_storage_summary as _summary
from openstack.block_storage.v3 import capabilities as _capabilities
from openstack.block_storage.v3 import default_type as _default_type
from openstack.block_storage.v3 import extension as _extension
from openstack.block_storage.v3 import group as _group
from openstack.block_storage.v3 import group_snapshot as _group_snapshot
from openstack.block_storage.v3 import group_type as _group_type
from openstack.block_storage.v3 import limits as _limits
from openstack.block_storage.v3 import qos_spec as _qos_spec
from openstack.block_storage.v3 import quota_class_set as _quota_class_set
from openstack.block_storage.v3 import quota_set as _quota_set
from openstack.block_storage.v3 import resource_filter as _resource_filter
from openstack.block_storage.v3 import service as _service
from openstack.block_storage.v3 import snapshot as _snapshot
from openstack.block_storage.v3 import stats as _stats
from openstack.block_storage.v3 import transfer as _transfer
from openstack.block_storage.v3 import type as _type
from openstack.block_storage.v3 import volume as _volume
from openstack import exceptions
from openstack.identity.v3 import project as _project
from openstack.image.v2 import image as _image
from openstack import proxy
from openstack import resource
from openstack import utils
from openstack import warnings as os_warnings


class Proxy(proxy.Proxy):
    api_version: ClassVar[Literal['3']] = '3'

    _resource_registry = {
        "availability_zone": availability_zone.AvailabilityZone,
        "attachment": _attachment.Attachment,
        "backup": _backup.Backup,
        "capabilities": _capabilities.Capabilities,
        "extension": _extension.Extension,
        "group": _group.Group,
        "group_snapshot": _group_snapshot.GroupSnapshot,
        "group_type": _group_type.GroupType,
        "limits": _limits.Limits,
        "quota_set": _quota_set.QuotaSet,
        "resource_filter": _resource_filter.ResourceFilter,
        "snapshot": _snapshot.Snapshot,
        "stats_pools": _stats.Pools,
        "summary": _summary.BlockStorageSummary,
        "transfer": _transfer.Transfer,
        "type": _type.Type,
        "volume": _volume.Volume,
    }

    # ====== IMAGES ======

    # TODO(stephenfin): Deprecate the unused wait, timeout parameters
    def create_image(
        self,
        name: str,
        volume: str,
        allow_duplicates: bool,
        container_format: str | None,
        disk_format: str | None,
        wait: bool,
        timeout: int,
    ) -> _image.Image:
        if not disk_format:
            disk_format = self._connection.config.config['image_format']
        if not container_format:
            # https://docs.openstack.org/image-guide/image-formats.html
            container_format = 'bare'

        data = self._get_resource(_volume.Volume, volume).upload_to_image(
            self,
            name,
            force=allow_duplicates,
            disk_format=disk_format,
            container_format=container_format,
        )
        # we know we're realistically always working with image v2 nowadays
        return cast(
            _image.Image,
            self._connection.image._existing_image(id=data['image_id']),
        )

    # ====== SNAPSHOTS ======
    def get_snapshot(
        self, snapshot: str | _snapshot.Snapshot
    ) -> _snapshot.Snapshot:
        """Get a single snapshot

        :param snapshot: The value can be the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot`
            instance.

        :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        return self._get(_snapshot.Snapshot, snapshot)

    @overload
    def find_snapshot(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
        *,
        details: bool = True,
        all_projects: bool = False,
    ) -> _snapshot.Snapshot: ...

    @overload
    def find_snapshot(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
        all_projects: bool = False,
    ) -> _snapshot.Snapshot | None: ...

    def find_snapshot(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
        all_projects: bool = False,
    ) -> _snapshot.Snapshot | None:
        """Find a single snapshot

        :param snapshot: The name or ID a snapshot
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the snapshot does not exist. When set to ``True``, None will
            be returned when attempting to find a nonexistent resource.
        :param details: When set to ``False`` :class:
            `~openstack.block_storage.v3.snapshot.Snapshot` objects will be
            returned. The default, ``True``, will cause more attributes to be
            returned.
        :param all_projects: When set to ``True``, search for snapshot by
            name across all projects. Note that this will likely result in
            a higher chance of duplicates. Admin-only by default.

        :returns: One :class:`~openstack.block_storage.v3.snapshot.Snapshot`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        query = {}
        if all_projects:
            query['all_projects'] = True
        list_base_path = '/snapshots/detail' if details else None
        return self._find(
            _snapshot.Snapshot,
            name_or_id,
            ignore_missing=ignore_missing,
            list_base_path=list_base_path,
            **query,
        )

    def snapshots(
        self,
        *,
        details: bool = True,
        all_projects: bool = False,
        **query: Any,
    ) -> Generator[_snapshot.Snapshot, None, None]:
        """Retrieve a generator of snapshots

        :param details: When set to ``False`` :class:
            `~openstack.block_storage.v3.snapshot.Snapshot`
            objects will be returned. The default, ``True``, will cause
            more attributes to be returned.
        :param all_projects: When set to ``True``, list snapshots from all
            projects. Admin-only by default.
        :param query: Optional query parameters to be sent to limit
            the snapshots being returned.  Available parameters include:

            * name: Name of the snapshot as a string.
            * project_id: Filter the snapshots by project.
            * volume_id: volume id of a snapshot.
            * status: Value of the status of the snapshot so that you can
              filter on "available" for example.

        :returns: A generator of snapshot objects.
        """
        if all_projects:
            query['all_projects'] = True
        base_path = '/snapshots/detail' if details else None
        return self._list(_snapshot.Snapshot, base_path=base_path, **query)

    def create_snapshot(self, **attrs: Any) -> _snapshot.Snapshot:
        """Create a new snapshot from attributes

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.snapshot.Snapshot`,
            comprised of the properties on the Snapshot class.

        :returns: The results of snapshot creation
        """
        return self._create(_snapshot.Snapshot, **attrs)

    def update_snapshot(
        self,
        snapshot: str | _snapshot.Snapshot,
        **attrs: Any,
    ) -> _snapshot.Snapshot:
        """Update a snapshot

        :param snapshot: Either the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance.
        :param attrs: The attributes to update on the snapshot.

        :returns: The updated snapshot
        """
        return self._update(_snapshot.Snapshot, snapshot, **attrs)

    def delete_snapshot(
        self,
        snapshot: str | _snapshot.Snapshot,
        ignore_missing: bool = True,
        force: bool = False,
    ) -> None:
        """Delete a snapshot

        :param snapshot: The value can be either the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be
            raised when the snapshot does not exist.
            When set to ``True``, no exception will be set when
            attempting to delete a nonexistent snapshot.
        :param force: Whether to try forcing snapshot deletion.

        :returns: ``None``
        """
        if not force:
            self._delete(
                _snapshot.Snapshot, snapshot, ignore_missing=ignore_missing
            )
        else:
            snapshot = self._get_resource(_snapshot.Snapshot, snapshot)
            snapshot.force_delete(self)

    def fetch_snapshot_metadata(
        self, snapshot: str | _snapshot.Snapshot
    ) -> _snapshot.Snapshot:
        """Return a dictionary of metadata for a snapshot

        :param snapshot: Either the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot`.

        :returns: A
            :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the
            snapshot's metadata. All keys and values are Unicode text.
        """
        snapshot = self._get_resource(_snapshot.Snapshot, snapshot)
        return snapshot.fetch_metadata(self)

    # TODO(stephenfin): Remove in 5.0
    def get_snapshot_metadata(
        self, snapshot: str | _snapshot.Snapshot
    ) -> _snapshot.Snapshot:
        """Return a dictionary of metadata for a snapshot

        .. deprecated:: 4.14.0
            Use :meth:`fetch_snapshot_metadata` instead.
        """
        warnings.warn(
            "The 'get_snapshot_metadata' method is deprecated; use "
            "'fetch_snapshot_metadata' instead.",
            os_warnings.RemovedInSDK50Warning,
        )
        return self.fetch_snapshot_metadata(snapshot)

    def set_snapshot_metadata(
        self, snapshot: str | _snapshot.Snapshot, **metadata: str
    ) -> _snapshot.Snapshot:
        """Update metadata for a snapshot

        :param snapshot: Either the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot`.
        :param metadata: Key/value pairs to be updated in the snapshot's
            metadata. No other metadata is modified by this call. All keys
            and values are stored as Unicode.

        :returns: A
            :class:`~openstack.block_storage.v3.snapshot.Snapshot` with the
            snapshot's metadata. All keys and values are Unicode text.
        """
        snapshot = self._get_resource(_snapshot.Snapshot, snapshot)
        return snapshot.set_metadata(self, metadata=metadata)

    def delete_snapshot_metadata(
        self,
        snapshot: str | _snapshot.Snapshot,
        keys: Iterable[str] | None = None,
    ) -> None:
        """Delete metadata for a snapshot

        :param snapshot: Either the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot`.
        :param keys: The keys to delete. If omitted, all metadata is removed.

        :returns: ``None``
        """
        snapshot = self._get_resource(_snapshot.Snapshot, snapshot)
        if keys is not None:
            for key in keys:
                snapshot.delete_metadata_item(self, key)
        else:
            snapshot.delete_metadata(self)

    # ====== SNAPSHOT ACTIONS ======
    def reset_snapshot_status(
        self, snapshot: str | _snapshot.Snapshot, status: str
    ) -> None:
        """Reset status of the snapshot

        :param snapshot: The value can be either the ID of a backup or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance.
        :param status: New snapshot status

        :returns: None
        """
        snapshot = self._get_resource(_snapshot.Snapshot, snapshot)
        snapshot.reset_status(self, status)

    def reset_snapshot(
        self, snapshot: str | _snapshot.Snapshot, status: str
    ) -> None:
        warnings.warn(
            "reset_snapshot is a deprecated alias for reset_snapshot_status "
            "and will be removed in a future release.",
            os_warnings.RemovedInSDK60Warning,
        )
        return self.reset_snapshot_status(snapshot, status)

    def set_snapshot_status(
        self,
        snapshot: str | _snapshot.Snapshot,
        status: str,
        progress: str | None = None,
    ) -> None:
        """Update fields related to the status of a snapshot.

        :param snapshot: The value can be either the ID of a backup or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance.
        :param status: New snapshot status
        :param progress: A percentage value for snapshot build progress.

        :returns: None
        """
        snapshot = self._get_resource(_snapshot.Snapshot, snapshot)
        snapshot.set_status(self, status, progress)

    def manage_snapshot(self, **attrs: Any) -> _snapshot.Snapshot:
        """Creates a snapshot by using existing storage rather than
        allocating new storage.

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.snapshot.Snapshot`,
            comprised of the properties on the Snapshot class.

        :returns: The results of snapshot creation
        """
        return _snapshot.Snapshot.manage(self, **attrs)

    def unmanage_snapshot(self, snapshot: str | _snapshot.Snapshot) -> None:
        """Unmanage a snapshot from block storage provisioning.

        :param snapshot: Either the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot`.

        :returns: None
        """
        snapshot_obj = self._get_resource(_snapshot.Snapshot, snapshot)
        snapshot_obj.unmanage(self)

    # ====== TYPES ======
    def get_type(self, type: str | _type.Type) -> _type.Type:
        """Get a single type

        :param type: The value can be the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.

        :returns: One :class:`~openstack.block_storage.v3.type.Type`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        return self._get(_type.Type, type)

    @overload
    def find_type(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
    ) -> _type.Type: ...

    @overload
    def find_type(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
    ) -> _type.Type | None: ...

    def find_type(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
    ) -> _type.Type | None:
        """Find a single volume type

        :param snapshot: The name or ID a volume type
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the volume type does not exist.

        :returns: One :class:`~openstack.block_storage.v3.type.Type`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        return self._find(
            _type.Type,
            name_or_id,
            ignore_missing=ignore_missing,
        )

    def types(self, **query: Any) -> Generator[_type.Type, None, None]:
        """Retrieve a generator of volume types

        :returns: A generator of volume type objects.
        """
        return self._list(_type.Type, **query)

    def create_type(self, **attrs: Any) -> _type.Type:
        """Create a new type from attributes

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.type.Type`,
            comprised of the properties on the Type class.

        :returns: The results of type creation
        """
        return self._create(_type.Type, **attrs)

    def delete_type(
        self, type: str | _type.Type, ignore_missing: bool = True
    ) -> None:
        """Delete a type

        :param type: The value can be either the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be
            raised when the type does not exist.
            When set to ``True``, no exception will be set when
            attempting to delete a nonexistent type.

        :returns: ``None``
        """
        self._delete(_type.Type, type, ignore_missing=ignore_missing)

    def update_type(
        self,
        type: str | _type.Type,
        **attrs: Any,
    ) -> _type.Type:
        """Update a type

        :param type: The value can be either the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
        :param attrs: The attributes to update on the type

        :returns: The updated type
        """
        return self._update(_type.Type, type, **attrs)

    def update_type_extra_specs(
        self,
        type: str | _type.Type,
        **attrs: Any,
    ) -> _type.Type:
        """Update the extra_specs for a type

        :param type: The value can be either the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
        :param attrs: The extra spec attributes to update on the type

        :returns: A dict containing updated extra_specs
        """
        res = self._get_resource(_type.Type, type)
        extra_specs = res.set_extra_specs(self, **attrs)
        result = _type.Type.existing(id=res.id, extra_specs=extra_specs)
        return result

    def delete_type_extra_specs(
        self,
        type: str | _type.Type,
        keys: Iterable[str],
    ) -> None:
        """Delete the extra_specs for a type

        Note: This method will do a HTTP DELETE request for every key in keys.

        :param type: The value can be either the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
        :param keys: The keys to delete.

        :returns: ``None``
        """
        res = self._get_resource(_type.Type, type)
        return res.delete_extra_specs(self, keys)

    def get_type_access(self, type: str | _type.Type) -> list[dict[str, Any]]:
        """Lists project IDs that have access to private volume type.

        :param type: The value can be either the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.

        :returns: List of dictionaries describing projects that have access to
            the specified type
        """
        res = self._get_resource(_type.Type, type)
        return res.get_private_access(self)

    @renamed_param('project_id', 'project')
    def add_type_access(
        self, type: str | _type.Type, project: str | _project.Project
    ) -> None:
        """Adds private volume type access to a project.

        :param type: The value can be either the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
        :param project: An ID or
            :class:`~openstack.identity.v3.identity.Project` instance of the
            project to add access for.

        :returns: ``None``
        """
        project_id = resource.Resource._get_id(project)
        res = self._get_resource(_type.Type, type)
        return res.add_private_access(self, project_id)

    @renamed_param('project_id', 'project')
    def remove_type_access(
        self, type: str | _type.Type, project: str | _project.Project
    ) -> None:
        """Remove private volume type access from a project.

        :param type: The value can be either the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
        :param project: An ID or
            :class:`~openstack.identity.v3.identity.Project` instance of the
            project to remove access for.

        :returns: ``None``
        """
        project_id = resource.Resource._get_id(project)
        res = self._get_resource(_type.Type, type)
        return res.remove_private_access(self, project_id)

    @renamed_param('volume_type_id', 'volume_type')
    def get_type_encryption(
        self, volume_type: str | _type.Type
    ) -> _type.TypeEncryption:
        """Get the encryption details of a volume type

        :param volume_type: The value can be the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type`
            instance.

        :returns: One :class:`~openstack.block_storage.v3.type.TypeEncryption`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        volume_type_id = resource.Resource._get_id(volume_type)
        return self._get(
            _type.TypeEncryption,
            volume_type_id=volume_type_id,
            requires_id=False,
        )

    def create_type_encryption(
        self, volume_type: str | _type.Type, **attrs: Any
    ) -> _type.TypeEncryption:
        """Create new type encryption from attributes

        :param volume_type: The value can be the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type`
            instance.

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.type.TypeEncryption`,
            comprised of the properties on the TypeEncryption class.

        :returns: The results of type encryption creation
        """
        volume_type_id = resource.Resource._get_id(volume_type)
        return self._create(
            _type.TypeEncryption, volume_type_id=volume_type_id, **attrs
        )

    def delete_type_encryption(
        self,
        encryption: str | _type.TypeEncryption | None = None,
        volume_type: str | _type.Type | None = None,
        ignore_missing: bool = True,
    ) -> None:
        """Delete type encryption attributes

        :param encryption: The value can be None or a
            :class:`~openstack.block_storage.v3.type.TypeEncryption`
            instance. If encryption is None then volume_type must be
            specified.
        :param volume_type: The value can be the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance. Required
            if encryption is None.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the type does not exist. When set to ``True``, no exception
            will be set when attempting to delete a nonexistent type.

        :returns: ``None``
        """
        if volume_type:
            volume_type_id = resource.Resource._get_id(volume_type)
            encryption = self._get(
                _type.TypeEncryption,
                volume_type_id=volume_type_id,
                requires_id=False,
            )

        self._delete(
            _type.TypeEncryption, encryption, ignore_missing=ignore_missing
        )

    def update_type_encryption(
        self,
        encryption: str | _type.TypeEncryption | None = None,
        volume_type: str | _type.Type | None = None,
        **attrs: Any,
    ) -> _type.TypeEncryption:
        """Update a type

        :param encryption: The value can be None or a
            :class:`~openstack.block_storage.v3.type.TypeEncryption`
            instance. If this is ``None`` then ``volume_type`` must be
            specified.
        :param volume_type: The value can be the ID of a type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
            Required if ``encryption`` is None.
        :param attrs: The attributes to update on the type encryption.

        :returns: The updated type encryption
        """

        if volume_type:
            volume_type_id = resource.Resource._get_id(volume_type)
            encryption = self._get(
                _type.TypeEncryption,
                volume_type_id=volume_type_id,
                requires_id=False,
            )

        return self._update(_type.TypeEncryption, encryption, **attrs)

    # ====== DEFAULT TYPES ======

    def default_types(
        self,
    ) -> Generator[_default_type.DefaultType, None, None]:
        """Lists default types.

        :returns: List of default types associated to projects.
        """
        # This is required since previously default types did not accept
        # URL with project ID
        if not utils.supports_microversion(self, '3.67'):
            raise exceptions.SDKException(
                'List default types require at least microversion 3.67'
            )

        return self._list(_default_type.DefaultType)

    def show_default_type(
        self, project: str | _project.Project
    ) -> _default_type.DefaultType:
        """Show default type for a project.

        :param project: The value can be either the ID of a project or a
            :class:`~openstack.identity.v3.project.Project` instance.

        :returns: Default type associated to the project.
        """
        # This is required since previously default types did not accept
        # URL with project ID
        if not utils.supports_microversion(self, '3.67'):
            raise exceptions.SDKException(
                'Show default type require at least microversion 3.67'
            )

        project_id = resource.Resource._get_id(project)
        return self._get(_default_type.DefaultType, project_id)

    def set_default_type(
        self, project: str | _project.Project, type: str | _type.Type
    ) -> _default_type.DefaultType:
        """Set default type for a project.

        :param project: The value can be either the ID of a project or a
             :class:`~openstack.identity.v3.project.Project` instance.
        :param type: The value can be either the ID of a type or a
             :class:`~openstack.block_storage.v3.type.Type` instance.

        :returns: Dictionary of project ID and it's associated default type.
        """
        # This is required since previously default types did not accept
        # URL with project ID
        if not utils.supports_microversion(self, '3.67'):
            raise exceptions.SDKException(
                'Set default type require at least microversion 3.67'
            )

        type_id = resource.Resource._get_id(type)
        project_id = resource.Resource._get_id(project)
        return self._create(
            _default_type.DefaultType,
            id=project_id,
            volume_type_id=type_id,
        )

    def unset_default_type(self, project: str | _project.Project) -> None:
        """Unset default type for a project.

        :param project: The value can be either the ID of a project or a
            :class:`~openstack.identity.v3.project.Project` instance.

        :returns: ``None``
        """
        # This is required since previously default types did not accept
        # URL with project ID
        if not utils.supports_microversion(self, '3.67'):
            raise exceptions.SDKException(
                'Unset default type require at least microversion 3.67'
            )

        project_id = resource.Resource._get_id(project)
        self._delete(_default_type.DefaultType, project_id)

    # ====== VOLUMES ======
    def get_volume(self, volume: str | _volume.Volume) -> _volume.Volume:
        """Get a single volume

        :param volume: The value can be the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.

        :returns: One :class:`~openstack.block_storage.v3.volume.Volume`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        return self._get(_volume.Volume, volume)

    @overload
    def find_volume(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
        *,
        details: bool = True,
        all_projects: bool = False,
    ) -> _volume.Volume: ...

    @overload
    def find_volume(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
        all_projects: bool = False,
    ) -> _volume.Volume | None: ...

    def find_volume(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
        all_projects: bool = False,
    ) -> _volume.Volume | None:
        """Find a single volume

        :param snapshot: The name or ID a volume
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the volume does not exist.
        :param details: When set to ``False`` no extended attributes
            will be returned. The default, ``True``, will cause objects with
            additional attributes to be returned.
        :param all_projects: When set to ``True``, search for volume by
            name across all projects. Note that this will likely result in
            a higher chance of duplicates. Admin-only by default.

        :returns: One :class:`~openstack.block_storage.v3.volume.Volume`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        query = {}
        if all_projects:
            query['all_projects'] = True
        list_base_path = '/volumes/detail' if details else None
        return self._find(
            _volume.Volume,
            name_or_id,
            ignore_missing=ignore_missing,
            list_base_path=list_base_path,
            **query,
        )

    def volumes(
        self,
        *,
        details: bool = True,
        all_projects: bool = False,
        **query: Any,
    ) -> Generator[_volume.Volume, None, None]:
        """Retrieve a generator of volumes

        :param details: When set to ``False`` no extended attributes
            will be returned. The default, ``True``, will cause objects with
            additional attributes to be returned.
        :param all_projects: When set to ``True``, list volumes from all
            projects. Admin-only by default.
        :param query: Optional query parameters to be sent to limit
            the volumes being returned.  Available parameters include:

            * name: Name of the volume as a string.
            * status: Value of the status of the volume so that you can filter
              on "available" for example.

        :returns: A generator of volume objects.
        """
        if all_projects:
            query['all_projects'] = True
        base_path = '/volumes/detail' if details else None
        return self._list(_volume.Volume, base_path=base_path, **query)

    def create_volume(self, **attrs: Any) -> _volume.Volume:
        """Create a new volume from attributes

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.volume.Volume`,
            comprised of the properties on the Volume class.

        :returns: The results of volume creation
        """
        return self._create(_volume.Volume, **attrs)

    def delete_volume(
        self,
        volume: str | _volume.Volume,
        ignore_missing: bool = True,
        *,
        force: bool = False,
        cascade: bool = False,
    ) -> None:
        """Delete a volume

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be
            raised when the volume does not exist.
            When set to ``True``, no exception will be set when
            attempting to delete a nonexistent volume.
        :param force: Whether to try forcing volume deletion.
        :param cascade: Whether to remove any snapshots along with the
            volume.

        :returns: ``None``
        """
        volume = self._get_resource(_volume.Volume, volume)

        params = {'cascade': cascade}
        if utils.supports_microversion(self, '3.23'):
            params['force'] = force

        try:
            if force and not utils.supports_microversion(self, '3.23'):
                volume.force_delete(self)
            else:
                volume.delete(self, params=params)
        except exceptions.NotFoundException:
            if ignore_missing:
                return None
            raise

    def update_volume(
        self,
        volume: str | _volume.Volume,
        **attrs: Any,
    ) -> _volume.Volume:
        """Update a volume

        :param volume: Either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param attrs: The attributes to update on the volume.

        :returns: The updated volume
        """
        return self._update(_volume.Volume, volume, **attrs)

    def fetch_volume_metadata(
        self, volume: str | _volume.Volume
    ) -> _volume.Volume:
        """Return a dictionary of metadata for a volume

        :param volume: Either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume`.

        :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the
            volume's metadata. All keys and values are Unicode text.
        """
        volume = self._get_resource(_volume.Volume, volume)
        return volume.fetch_metadata(self)

    # TODO(stephenfin): Remove in 5.0
    def get_volume_metadata(
        self, volume: str | _volume.Volume
    ) -> _volume.Volume:
        """Return a dictionary of metadata for a volume

        .. deprecated:: 4.14.0
            Use :meth:`fetch_volume_metadata` instead.
        """
        warnings.warn(
            "The 'get_volume_metadata' method is deprecated; use "
            "'fetch_volume_metadata' instead.",
            os_warnings.RemovedInSDK50Warning,
        )
        return self.fetch_volume_metadata(volume)

    def set_volume_metadata(
        self, volume: str | _volume.Volume, **metadata: str
    ) -> _volume.Volume:
        """Update metadata for a volume

        :param volume: Either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume`.
        :param metadata: Key/value pairs to be updated in the volume's
            metadata. No other metadata is modified by this call. All keys
            and values are stored as Unicode.

        :returns: A :class:`~openstack.block_storage.v3.volume.Volume` with the
            volume's metadata. All keys and values are Unicode text.
        """
        volume = self._get_resource(_volume.Volume, volume)
        return volume.set_metadata(self, metadata=metadata)

    def delete_volume_metadata(
        self,
        volume: str | _volume.Volume,
        keys: Iterable[str] | None = None,
    ) -> None:
        """Delete metadata for a volume

        :param volume: Either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume`.
        :param keys: The keys to delete. If omitted, all metadata is removed.

        :returns: ``None``
        """
        volume = self._get_resource(_volume.Volume, volume)
        if keys is not None:
            for key in keys:
                volume.delete_metadata_item(self, key)
        else:
            volume.delete_metadata(self)

    def summary(
        self, all_projects: bool, **kwargs: Any
    ) -> _summary.BlockStorageSummary:
        """Get Volumes Summary

        This method returns the volumes summary in the deployment.

        :param all_projects: Whether to return the summary of all projects
            or not.

        :returns: One :class:
            `~openstack.block_storage.v3.block_storage_summary.Summary`
            instance.
        """
        res = self._get(_summary.BlockStorageSummary, requires_id=False)
        return res.fetch(
            self,
            requires_id=False,
            resource_response_key='volume-summary',
            all_tenants=all_projects,
            **kwargs,
        )

    # ====== VOLUME ACTIONS ======
    def extend_volume(self, volume: str | _volume.Volume, size: int) -> None:
        """Extend a volume

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param size: New volume size

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.extend(self, size)

    def complete_volume_extend(
        self, volume: str | _volume.Volume, error: bool = False
    ) -> None:
        """Complete a volume extend operation.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param error: Used to indicate if an error has occured that
            requires Cinder to roll back the extend operation.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.complete_extend(self, error)

    def set_volume_readonly(
        self, volume: str | _volume.Volume, readonly: bool = True
    ) -> None:
        """Set a volume's read-only flag.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param readonly: Whether the volume should be a read-only volume
            or not.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.set_readonly(self, readonly)

    def retype_volume(
        self,
        volume: str | _volume.Volume,
        new_type: str | _type.Type,
        migration_policy: str = "never",
    ) -> None:
        """Retype the volume.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param new_type: The new volume type that volume is changed with.
            The value can be either the ID of the volume type or a
            :class:`~openstack.block_storage.v3.type.Type` instance.
        :param migration_policy: Specify if the volume should be migrated
            when it is re-typed. Possible values are on-demand or never.
            Default: never.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        type_id = resource.Resource._get_id(new_type)
        volume.retype(self, type_id, migration_policy)

    def set_volume_bootable_status(
        self, volume: str | _volume.Volume, bootable: bool
    ) -> None:
        """Set bootable status of the volume.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param bootable: Specifies whether the volume should be bootable
            or not.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.set_bootable_status(self, bootable)

    def set_volume_image_metadata(
        self, volume: str | _volume.Volume, **metadata: str
    ) -> None:
        """Update image metadata for a volume

        :param volume: Either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume`.
        :param metadata: Key/value pairs to be updated in the volume's
            image metadata. No other metadata is modified by this call.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        return volume.set_image_metadata(self, metadata=metadata)

    def delete_volume_image_metadata(
        self,
        volume: str | _volume.Volume,
        keys: Iterable[str] | None = None,
    ) -> None:
        """Delete metadata for a volume

        :param volume: Either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume`.
        :param keys: The keys to delete. If omitted, all metadata is removed.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        if keys is not None:
            for key in keys:
                volume.delete_image_metadata_item(self, key)
        else:
            volume.delete_image_metadata(self)

    def reset_volume_status(
        self,
        volume: str | _volume.Volume,
        status: str | None = None,
        attach_status: str | None = None,
        migration_status: str | None = None,
    ) -> None:
        """Reset volume statuses.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param status: The new volume status.
        :param attach_status: The new volume attach status.
        :param migration_status: The new volume migration status (admin
            only).

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.reset_status(self, status, attach_status, migration_status)

    def revert_volume_to_snapshot(
        self,
        volume: str | _volume.Volume,
        snapshot: str | _snapshot.Snapshot,
    ) -> None:
        """Revert a volume to its latest snapshot.

        This method only support reverting a detached volume, and the
        volume status must be available.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param snapshot:  The value can be either the ID of a snapshot or a
            :class:`~openstack.block_storage.v3.snapshot.Snapshot` instance.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        snapshot = self._get_resource(_snapshot.Snapshot, snapshot)
        volume.revert_to_snapshot(self, snapshot.id)

    def attach_volume(
        self,
        volume: str | _volume.Volume,
        mountpoint: str,
        instance: str | None = None,
        host_name: str | None = None,
    ) -> None:
        """Attaches a volume to a server.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param mountpoint: The attaching mount point.
        :param instance: The UUID of the attaching instance.
        :param host_name: The name of the attaching host.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.attach(self, mountpoint, instance, host_name)

    def detach_volume(
        self,
        volume: str | _volume.Volume,
        attachment: str,
        force: bool = False,
        connector: dict[str, Any] | None = None,
    ) -> None:
        """Detaches a volume from a server.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param attachment: The ID of the attachment.
        :param force: Whether to force volume detach (Rolls back an
            unsuccessful detach operation after you disconnect the volume.)
        :param connector: The connector object.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.detach(self, attachment, force, connector)

    def manage_volume(self, **attrs: Any) -> _volume.Volume:
        """Creates a volume by using existing storage rather than
            allocating new storage.

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.volume.Volume`, comprised of
            the properties on the Volume class.
        :returns: The results of volume creation
        """
        return _volume.Volume.manage(self, **attrs)

    def unmanage_volume(self, volume: str | _volume.Volume) -> None:
        """Removes a volume from Block Storage management without removing the
            back-end storage object that is associated with it.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.unmanage(self)

    def migrate_volume(
        self,
        volume: str | _volume.Volume,
        host: str | None = None,
        force_host_copy: bool = False,
        lock_volume: bool = False,
        cluster: str | None = None,
    ) -> None:
        """Migrates a volume to the specified host.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param host: The target host for the volume migration. Host
            format is host@backend.
        :param force_host_copy: If false (the default), rely on the volume
            backend driver to perform the migration, which might be optimized.
            If true, or the volume driver fails to migrate the volume itself,
            a generic host-based migration is performed.
        :param lock_volume: If true, migrating an available volume will
            change its status to maintenance preventing other operations from
            being performed on the volume such as attach, detach, retype, etc.
        :param cluster: The target cluster for the volume migration.
            Cluster format is cluster@backend. Starting with microversion
            3.16, either cluster or host must be specified. If host is
            specified and is part of a cluster, the cluster is used as the
            target for the migration.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.migrate(self, host, force_host_copy, lock_volume, cluster)

    def complete_volume_migration(
        self,
        volume: str | _volume.Volume,
        new_volume: str,
        error: bool = False,
    ) -> None:
        """Complete the migration of a volume.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param new_volume: The UUID of the new volume.
        :param error: Used to indicate if an error has occured elsewhere
            that requires clean up.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.complete_migration(self, new_volume, error)

    def upload_volume_to_image(
        self,
        volume: str | _volume.Volume,
        image_name: str,
        force: bool = False,
        disk_format: str | None = None,
        container_format: str | None = None,
        visibility: str | None = None,
        protected: bool | None = None,
    ) -> dict[str, Any]:
        """Uploads the specified volume to image service.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param image_name: The name for the new image.
        :param force: Enables or disables upload of a volume that is
            attached to an instance.
        :param disk_format: Disk format for the new image.
        :param container_format: Container format for the new image.
        :param visibility: The visibility property of the new image.
        :param protected: Whether the new image is protected.

        :returns: dictionary describing the image.
        """
        volume = self._get_resource(_volume.Volume, volume)
        return volume.upload_to_image(
            self,
            image_name,
            force=force,
            disk_format=disk_format,
            container_format=container_format,
            visibility=visibility,
            protected=protected,
        )

    def reserve_volume(self, volume: str | _volume.Volume) -> None:
        """Mark volume as reserved.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.

        :returns: None"""
        volume = self._get_resource(_volume.Volume, volume)
        volume.reserve(self)

    def unreserve_volume(self, volume: str | _volume.Volume) -> None:
        """Unmark volume as reserved.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.

        :returns: None"""
        volume = self._get_resource(_volume.Volume, volume)
        volume.unreserve(self)

    def begin_volume_detaching(self, volume: str | _volume.Volume) -> None:
        """Update volume status to 'detaching'.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.

        :returns: None"""
        volume = self._get_resource(_volume.Volume, volume)
        volume.begin_detaching(self)

    def abort_volume_detaching(self, volume: str | _volume.Volume) -> None:
        """Update volume status to 'in-use'.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.

        :returns: None"""
        volume = self._get_resource(_volume.Volume, volume)
        volume.abort_detaching(self)

    def init_volume_attachment(
        self,
        volume: str | _volume.Volume,
        connector: dict[str, Any],
    ) -> dict[str, Any]:
        """Initialize volume attachment.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param connector: The connector object.

        :returns: Dictionary containing the modified connector object"""
        volume = self._get_resource(_volume.Volume, volume)
        return volume.init_attachment(self, connector)

    def terminate_volume_attachment(
        self,
        volume: str | _volume.Volume,
        connector: dict[str, Any],
    ) -> None:
        """Update volume status to 'in-use'.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param connector: The connector object.

        :returns: None
        """
        volume = self._get_resource(_volume.Volume, volume)
        volume.terminate_attachment(self, connector)

    # ====== ATTACHMENTS ======

    def create_attachment(
        self, volume: str | _volume.Volume, **attrs: Any
    ) -> _attachment.Attachment:
        """Create a new attachment

        This is an internal API and should only be called by services
        consuming volume attachments like nova, glance, ironic etc.

        :param volume: The value can be either the ID of a volume or a
            :class:`~openstack.block_storage.v3.volume.Volume` instance.
        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.attachment.Attachment`
            comprised of the properties on the Attachment class like
            connector, instance_id, mode etc.
        :returns: The results of attachment creation
        """
        volume_id = resource.Resource._get_id(volume)
        return self._create(
            _attachment.Attachment, volume_id=volume_id, **attrs
        )

    def get_attachment(
        self, attachment: str | _attachment.Attachment
    ) -> _attachment.Attachment:
        """Get a single volume

        This is an internal API and should only be called by services
        consuming volume attachments like nova, glance, ironic etc.

        :param attachment: The value can be the ID of an attachment or a
            :class:`~attachment.Attachment` instance.

        :returns: One :class:`~attachment.Attachment`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        return self._get(_attachment.Attachment, attachment)

    def attachments(
        self,
        **query: Any,
    ) -> Generator[_attachment.Attachment, None, None]:
        """Returns a generator of attachments.

        This is an internal API and should only be called by services
        consuming volume attachments like nova, glance, ironic etc.

        :param query: Optional query parameters to be sent to limit
            the resources being returned.

        :returns: A generator of attachment objects.
        """
        return self._list(_attachment.Attachment, **query)

    def delete_attachment(
        self,
        attachment: str | _attachment.Attachment,
        ignore_missing: bool = True,
    ) -> None:
        """Delete an attachment

        This is an internal API and should only be called by services
        consuming volume attachments like nova, glance, ironic etc.

        :param attachment: The value can be either the ID of an attachment or a
            :class:`~openstack.block_storage.v3.attachment.Attachment`
            instance.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be
            raised when the attachment does not exist.
            When set to ``True``, no exception will be set when
            attempting to delete a nonexistent attachment.

        :returns: ``None``
        """
        self._delete(
            _attachment.Attachment,
            attachment,
            ignore_missing=ignore_missing,
        )

    def update_attachment(
        self,
        attachment: str | _attachment.Attachment,
        **attrs: Any,
    ) -> _attachment.Attachment:
        """Update an attachment

        This is an internal API and should only be called by services
        consuming volume attachments like nova, glance, ironic etc.

        :param attachment: The value can be the ID of an attachment or a
            :class:`~openstack.block_storage.v3.attachment.Attachment`
            instance.
        :param attrs: Keyword arguments which will be used to update
            a :class:`~openstack.block_storage.v3.attachment.Attachment`
            comprised of the properties on the Attachment class

        :returns: The updated attachment
        """
        return self._update(_attachment.Attachment, attachment, **attrs)

    def complete_attachment(
        self, attachment: str | _attachment.Attachment
    ) -> None:
        """Complete an attachment

        This is an internal API and should only be called by services
        consuming volume attachments like nova, glance, ironic etc.

        :param attachment: The value can be the ID of an attachment or a
            :class:`~openstack.block_storage.v3.attachment.Attachment`
            instance.

        :returns: ``None``
        """
        attachment_obj = self._get_resource(_attachment.Attachment, attachment)
        return attachment_obj.complete(self)

    # ====== BACKEND POOLS ======
    def backend_pools(
        self,
        **query: Any,
    ) -> Generator[_stats.Pools, None, None]:
        """Returns a generator of cinder Back-end storage pools

        :param query: Optional query parameters to be sent to limit
            the resources being returned.

        :returns A generator of cinder Back-end storage pools objects
        """
        return self._list(_stats.Pools, **query)

    # ====== BACKUPS ======
    def backups(
        self,
        *,
        details: bool = True,
        **query: Any,
    ) -> Generator[_backup.Backup, None, None]:
        """Retrieve a generator of backups

        :param details: When set to ``False``
            no additional details will be returned. The default, ``True``,
            will cause objects with additional attributes to be returned.
        :param query: Optional query parameters to be sent to limit the
            resources being returned:

            * offset: pagination marker
            * limit: pagination limit
            * sort_key: Sorts by an attribute. A valid value is
              name, status, container_format, disk_format, size, id,
              created_at, or updated_at. Default is created_at.
              The API uses the natural sorting direction of the
              sort_key attribute value.
            * sort_dir: Sorts by one or more sets of attribute and sort
              direction combinations. If you omit the sort direction
              in a set, default is desc.
            * project_id: Project ID to query backups for.

        :returns: A generator of backup objects.
        """
        base_path = '/backups/detail' if details else None
        return self._list(_backup.Backup, base_path=base_path, **query)

    def get_backup(self, backup: str | _backup.Backup) -> _backup.Backup:
        """Get a backup

        :param backup: The value can be the ID of a backup
            or a :class:`~openstack.block_storage.v3.backup.Backup`
            instance.

        :returns: Backup instance
        """
        return self._get(_backup.Backup, backup)

    @overload
    def find_backup(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
        *,
        details: bool = True,
    ) -> _backup.Backup: ...

    @overload
    def find_backup(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
    ) -> _backup.Backup | None: ...

    def find_backup(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
    ) -> _backup.Backup | None:
        """Find a single backup

        :param snapshot: The name or ID a backup
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the backup does not exist.
        :param details: When set to ``False`` no additional details will
            be returned. The default, ``True``, will cause objects with
            additional attributes to be returned.

        :returns: One :class:`~openstack.block_storage.v3.backup.Backup`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        list_base_path = '/backups/detail' if details else None
        return self._find(
            _backup.Backup,
            name_or_id,
            ignore_missing=ignore_missing,
            list_base_path=list_base_path,
        )

    def create_backup(self, **attrs: Any) -> _backup.Backup:
        """Create a new Backup from attributes with native API

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.backup.Backup`
            comprised of the properties on the Backup class.

        :returns: The results of Backup creation
        """
        return self._create(_backup.Backup, **attrs)

    def delete_backup(
        self,
        backup: str | _backup.Backup,
        ignore_missing: bool = True,
        force: bool = False,
    ) -> None:
        """Delete a CloudBackup

        :param backup: The value can be the ID of a backup or a
            :class:`~openstack.block_storage.v3.backup.Backup` instance
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the zone does not exist. When set to ``True``, no exception
            will be set when attempting to delete a nonexistent zone.
        :param force: Whether to try forcing backup deletion

        :returns: ``None``
        """
        if not force:
            self._delete(_backup.Backup, backup, ignore_missing=ignore_missing)
        else:
            backup = self._get_resource(_backup.Backup, backup)
            backup.force_delete(self)

    def update_backup(
        self,
        backup: str | _backup.Backup,
        **attrs: Any,
    ) -> _backup.Backup:
        """Update a backup

        :param backup: Either the ID of a backup or a
            :class:`~openstack.block_storage.v3.backup.Backup`.
        :param attrs: The attributes to update on the volume.

        :returns: The updated backup
        """
        return self._update(_backup.Backup, backup, **attrs)

    def fetch_backup_metadata(
        self, backup: str | _backup.Backup
    ) -> _backup.Backup:
        """Return a dictionary of metadata for a backup

        :param backup: Either the ID of a backup or a
            :class:`~openstack.block_storage.v3.backup.Backup`.

        :returns: A :class:`~openstack.block_storage.v3.backup.Backup` with the
            backup's metadata.
        """
        backup = self._get_resource(_backup.Backup, backup)
        return backup.fetch_metadata(self)

    # TODO(stephenfin): Remove in 5.0
    def get_backup_metadata(
        self, backup: str | _backup.Backup
    ) -> _backup.Backup:
        """Return a dictionary of metadata for a backup

        .. deprecated:: 4.14.0
            Use :meth:`fetch_backup_metadata` instead.
        """
        warnings.warn(
            "The 'get_backup_metadata' method is deprecated; use "
            "'fetch_backup_metadata' instead.",
            os_warnings.RemovedInSDK50Warning,
        )
        return self.fetch_backup_metadata(backup)

    def export_record(self, backup: str | _backup.Backup) -> Any:
        """Get a backup meatadata to export

        :param backup: The value can be the ID of a backup
            or a :class:`~openstack.block_storage.v2.backup.Backup`
            instance.

        :returns: The backup export record fields
        """
        backup = self._get_resource(_backup.Backup, backup)
        return backup.export(self)

    def set_backup_metadata(
        self, backup: str | _backup.Backup, **metadata: str
    ) -> _backup.Backup:
        """Update metadata for a backup

        :param backup: Either the ID of a backup or a
            :class:`~openstack.block_storage.v3.backup.Backup`.
        :param metadata: Key/value pairs to be updated in the backup's
            metadata. No other metadata is modified by this call.

        :returns: A :class:`~openstack.block_storage.v3.backup.Backup` with the
            backup's metadata.
        """
        backup = self._get_resource(_backup.Backup, backup)
        return backup.set_metadata(self, metadata=metadata)

    def delete_backup_metadata(
        self,
        backup: str | _backup.Backup,
        keys: Iterable[str] | None = None,
    ) -> None:
        """Delete metadata for a backup

        :param backup: Either the ID of a backup or a
            :class:`~openstack.block_storage.v3.backup.Backup`.
        :param keys: The keys to delete. If omitted, all metadata is removed.

        :returns: ``None``
        """
        backup = self._get_resource(_backup.Backup, backup)
        if keys is not None:
            for key in keys:
                backup.delete_metadata_item(self, key)
        else:
            backup.delete_metadata(self)

    # ====== BACKUP ACTIONS ======

    @renamed_param('volume_id', 'volume')
    def restore_backup(
        self,
        backup: str | _backup.Backup,
        volume: str | _volume.Volume | None = None,
        name: str | None = None,
    ) -> _backup.Backup:
        """Restore a Backup to volume

        :param backup: The value can be the ID of a backup or a
            :class:`~openstack.block_storage.v3.backup.Backup` instance
        :param volume: An ID or
            :class:`~openstack.volume.v3.volume.Volume` instance of the
            volume to restore the backup to.
        :param name: The name for new volume creation to restore.

        :returns: Updated backup instance
        """
        volume_id = resource.Resource._get_id(volume) if volume else None
        return self._get_resource(_backup.Backup, backup).restore(
            self, volume_id=volume_id, name=name
        )

    def reset_backup_status(
        self, backup: str | _backup.Backup, status: str
    ) -> None:
        """Reset status of the backup

        :param backup: The value can be either the ID of a backup or a
            :class:`~openstack.block_storage.v3.backup.Backup` instance.
        :param status: New backup status

        :returns: None
        """
        backup = self._get_resource(_backup.Backup, backup)
        backup.reset_status(self, status)

    def reset_backup(self, backup: str | _backup.Backup, status: str) -> None:
        warnings.warn(
            "reset_backup is a deprecated alias for reset_backup_status "
            "and will be removed in a future release.",
            os_warnings.RemovedInSDK60Warning,
        )
        return self.reset_backup_status(backup, status)

    # ====== LIMITS ======
    def get_limits(
        self, project: str | _project.Project | None = None
    ) -> _limits.Limits:
        """Retrieves limits

        :param project: A project to get limits for. The value can be either
            the ID of a project or an
            :class:`~openstack.identity.v3.project.Project` instance.
        :returns: A Limits object, including both
            :class:`~openstack.block_storage.v3.limits.AbsoluteLimit` and
            :class:`~openstack.block_storage.v3.limits.RateLimit`
        """
        project_id = resource.Resource._get_id(project) if project else None

        # we don't use Proxy._get since that doesn't allow passing arbitrary
        # query string parameters
        res = self._get_resource(_limits.Limits, None)
        return res.fetch(
            self,
            requires_id=False,
            project_id=project_id,
        )

    # ====== CAPABILITIES ======
    def get_capabilities(
        self, host: str | _capabilities.Capabilities
    ) -> _capabilities.Capabilities:
        """Get a backend's capabilites

        :param host: Specified backend to obtain volume stats and properties.

        :returns: One :class:
            `~openstack.block_storage.v3.capabilites.Capabilities` instance.
        :raises: :class:`~openstack.exceptions.NotFoundException` when no
            resource can be found.
        """
        return self._get(_capabilities.Capabilities, host)

    # ====== GROUPS ======

    # TODO(stephenfin): Remove **attrs in 5.0
    @renamed_param('group_id', 'group')
    def get_group(
        self, group: str | _group.Group, **attrs: Any
    ) -> _group.Group:
        """Get a group

        :param group: The value can be the ID of a group or a
            :class:`~openstack.block_storage.v3.group.Group` instance.
        :param attrs: **DEPRECATED** Optional query parameters to be sent to
            limit the resources being returned.

        :returns: A Group instance.
        """
        if attrs:
            warnings.warn(
                'Passing kwargs to get_group is deprecated and will be '
                'removed in a future release',
                os_warnings.RemovedInSDK50Warning,
            )
        return self._get(_group.Group, group, **attrs)

    @overload
    def find_group(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
        *,
        details: bool = True,
    ) -> _group.Group: ...

    @overload
    def find_group(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
    ) -> _group.Group | None: ...

    def find_group(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
    ) -> _group.Group | None:
        """Find a single group

        :param name_or_id: The name or ID of a group.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the group snapshot does not exist.
        :param details: When set to ``False``, no additional details will
            be returned. The default, ``True``, will cause additional details
            to be returned.

        :returns: One :class:`~openstack.block_storage.v3.group.Group`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        list_base_path = '/groups/detail' if details else None
        return self._find(
            _group.Group,
            name_or_id,
            ignore_missing=ignore_missing,
            list_base_path=list_base_path,
        )

    def groups(
        self,
        *,
        details: bool = True,
        **query: Any,
    ) -> Generator[_group.Group, None, None]:
        """Retrieve a generator of groups

        :param details: When set to ``False``, no additional details will
            be returned. The default, ``True``, will cause additional details
            to be returned.
        :param query: Optional query parameters to be sent to limit the
            resources being returned:

            * all_tenants: Shows details for all project.
            * sort: Comma-separated list of sort keys and optional sort
              directions.
            * limit: Returns a number of items up to the limit value.
            * offset: Used in conjunction with limit to return a slice of
              items. Specifies where to start in the list.
            * marker: The ID of the last-seen item.
            * list_volume: Show volume ids in this group.
            * detailed: If True, will list groups with details.
            * search_opts: Search options.

        :returns: A generator of group objects.
        """
        base_path = '/groups/detail' if details else '/groups'
        return self._list(_group.Group, base_path=base_path, **query)

    def create_group(self, **attrs: Any) -> _group.Group:
        """Create a new group from attributes

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.group.Group` comprised of
            the properties on the Group class.

        :returns: The results of group creation.
        """
        return self._create(_group.Group, **attrs)

    def create_group_from_source(self, **attrs: Any) -> _group.Group:
        """Creates a new group from source

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.group.Group` comprised of
            the properties on the Group class.

        :returns: The results of group creation.
        """
        return _group.Group.create_from_source(self, **attrs)

    def delete_group(
        self,
        group: str | _group.Group,
        delete_volumes: bool = False,
        ignore_missing: bool = True,
    ) -> None:
        """Delete a group

        :param group: The :class:`~openstack.block_storage.v3.group.Group` to
            delete.
        :param delete_volumes: When set to ``True``, volumes in group
            will be deleted.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the group does not exist. When set to ``True``, no exception
            will be set when attempting to delete a nonexistent group.

        :returns: ``None``.
        """
        res = self._get_resource(_group.Group, group)
        try:
            res.delete(self, delete_volumes=delete_volumes)
        except exceptions.NotFoundException:
            if not ignore_missing:
                raise

    def update_group(
        self,
        group: str | _group.Group,
        **attrs: Any,
    ) -> _group.Group:
        """Update a group

        :param group: The value can be the ID of a group or a
            :class:`~openstack.block_storage.v3.group.Group` instance.
        :param attrs: The attributes to update on the group.

        :returns: The updated group
        """
        return self._update(_group.Group, group, **attrs)

    def reset_group_status(
        self, group: str | _group.Group, status: str
    ) -> None:
        """Reset group status

        :param group: The :class:`~openstack.block_storage.v3.group.Group`
            to set the state.
        :param status: The status for a group.

        :returns: ``None``
        """
        res = self._get_resource(_group.Group, group)
        return res.reset_status(self, status)

    def reset_group_state(
        self, group: str | _group.Group, status: str
    ) -> None:
        warnings.warn(
            "reset_group_state is a deprecated alias for reset_group_status "
            "and will be removed in a future release.",
            os_warnings.RemovedInSDK60Warning,
        )
        return self.reset_group_status(group, status)

    def enable_group_replication(self, group: str | _group.Group) -> None:
        """Enable replication for a group

        :param group: The :class:`~openstack.block_storage.v3.group.Group`
            to enable replication for.

        :returns: ``None``
        """
        res = self._get_resource(_group.Group, group)
        return res.enable_replication(self)

    def disable_group_replication(self, group: str | _group.Group) -> None:
        """Disable replication for a group

        :param group: The :class:`~openstack.block_storage.v3.group.Group`
            to disable replication for.

        :returns: ``None``
        """
        res = self._get_resource(_group.Group, group)
        return res.disable_replication(self)

    def failover_group_replication(
        self,
        group: str | _group.Group,
        *,
        allowed_attached_volume: bool = False,
        secondary_backend_id: str | None = None,
    ) -> None:
        """Failover replication for a group

        :param group: The :class:`~openstack.block_storage.v3.group.Group`
            to failover replication for.
        :param allowed_attached_volume: Whether to allow attached volumes in
            the group.
        :param secondary_backend_id: The secondary backend ID.

        :returns: ``None``
        """
        res = self._get_resource(_group.Group, group)
        return res.failover_replication(
            self,
            allowed_attached_volume=allowed_attached_volume,
            secondary_backend_id=secondary_backend_id,
        )

    # ====== AVAILABILITY ZONES ======
    def availability_zones(
        self,
    ) -> Generator[availability_zone.AvailabilityZone, None, None]:
        """Return a generator of availability zones

        :returns: A generator of availability zone
            :class:`~openstack.block_storage.v3.availability_zone.AvailabilityZone`
        """

        return self._list(availability_zone.AvailabilityZone)

    # ====== GROUP SNAPSHOT ======

    @renamed_param('group_snapshot_id', 'group_snapshot')
    def get_group_snapshot(
        self, group_snapshot: str | _group_snapshot.GroupSnapshot
    ) -> _group_snapshot.GroupSnapshot:
        """Get a group snapshot

        :param group_snapshot: The value can be the ID of a group snapshot or a
            :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot`
            instance.

        :returns: A GroupSnapshot instance.
        """
        return self._get(_group_snapshot.GroupSnapshot, group_snapshot)

    @overload
    def find_group_snapshot(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
        *,
        details: bool = True,
    ) -> _group_snapshot.GroupSnapshot: ...

    @overload
    def find_group_snapshot(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
    ) -> _group_snapshot.GroupSnapshot | None: ...

    def find_group_snapshot(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        *,
        details: bool = True,
    ) -> _group_snapshot.GroupSnapshot | None:
        """Find a single group snapshot

        :param name_or_id: The name or ID of a group snapshot.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the group snapshot does not exist.
        :param details: When set to ``False``, no additional details will
            be returned. The default, ``True``, will cause additional details
            to be returned.

        :returns: One :class:`~openstack.block_storage.v3.group_snapshot`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        list_base_path = '/group_snapshots/detail' if details else None
        return self._find(
            _group_snapshot.GroupSnapshot,
            name_or_id,
            ignore_missing=ignore_missing,
            list_base_path=list_base_path,
        )

    def group_snapshots(
        self,
        *,
        details: bool = True,
        **query: Any,
    ) -> Generator[_group_snapshot.GroupSnapshot, None, None]:
        """Retrieve a generator of group snapshots

        :param details: When ``True``, returns
            :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot`
            objects with additional attributes filled.
        :param query: Optional query parameters to be sent to limit
            the group snapshots being returned.
        :returns: A generator of group snapshtos.
        """
        base_path = '/group_snapshots/detail' if details else None
        return self._list(
            _group_snapshot.GroupSnapshot,
            base_path=base_path,
            **query,
        )

    def create_group_snapshot(
        self, **attrs: Any
    ) -> _group_snapshot.GroupSnapshot:
        """Create a group snapshot

        :param attrs: Keyword arguments which will be used to create a
            :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot`
            comprised of the properties on the GroupSnapshot class.

        :returns: The results of group snapshot creation.
        """
        return self._create(_group_snapshot.GroupSnapshot, **attrs)

    def reset_group_snapshot_status(
        self,
        group_snapshot: str | _group_snapshot.GroupSnapshot,
        status: str,
    ) -> None:
        """Reset group snapshot status

        :param group_snapshot: The
            :class:`~openstack.block_storage.v3.group_snapshot.GroupSnapshot`
            to set the state.
        :param state: The status of the group snapshot to be set.

        :returns: None
        """
        res = self._get_resource(_group_snapshot.GroupSnapshot, group_snapshot)
        res.reset_state(self, status)

    def reset_group_snapshot_state(
        self,
        group_snapshot: str | _group_snapshot.GroupSnapshot,
        state: str,
    ) -> None:
        warnings.warn(
            "reset_group_snapshot_state is a deprecated alias for "
            "reset_group_snapshot_status and will be removed in a future "
            "release.",
            os_warnings.RemovedInSDK60Warning,
        )
        return self.reset_group_snapshot_status(group_snapshot, state)

    def delete_group_snapshot(
        self,
        group_snapshot: str | _group_snapshot.GroupSnapshot,
        ignore_missing: bool = True,
    ) -> None:
        """Delete a group snapshot

        :param group_snapshot: The :class:`~openstack.block_storage.v3.
            group_snapshot.GroupSnapshot` to delete.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the group snapshot does not exist. When set to ``True``, no
            exception will be set when attempting to delete a nonexistent
            group snapshot.

        :returns: None
        """
        self._delete(
            _group_snapshot.GroupSnapshot,
            group_snapshot,
            ignore_missing=ignore_missing,
        )

    # ====== GROUP TYPE ======
    def get_group_type(
        self, group_type: str | _group_type.GroupType
    ) -> _group_type.GroupType:
        """Get a specific group type

        :param group_type: The value can be the ID of a group type
            or a :class:`~openstack.block_storage.v3.group_type.GroupType`
            instance.

        :returns: One :class:
            `~openstack.block_storage.v3.group_type.GroupType` instance.
        :raises: :class:`~openstack.exceptions.NotFoundException` when no
            resource can be found.
        """
        return self._get(_group_type.GroupType, group_type)

    @overload
    def find_group_type(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
    ) -> _group_type.GroupType: ...

    @overload
    def find_group_type(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
    ) -> _group_type.GroupType | None: ...

    def find_group_type(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
    ) -> _group_type.GroupType | None:
        """Find a single group type

        :param name_or_id: The name or ID of a group type.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the group type does not exist.

        :returns: One
            :class:`~openstack.block_storage.v3.group_type.GroupType`
        :raises: :class:`~openstack.exceptions.NotFoundException` when no
            resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        return self._find(
            _group_type.GroupType,
            name_or_id,
            ignore_missing=ignore_missing,
        )

    def group_types(
        self,
        **query: Any,
    ) -> Generator[_group_type.GroupType, None, None]:
        """Retrive a generator of group types

        :param query: Optional query parameters to be sent to limit the
            resources being returned:

            * sort: Comma-separated list of sort keys and optional sort
              directions in the form of <key> [:<direction>]. A valid
              direction is asc (ascending) or desc (descending).
            * limit: Requests a page size of items. Returns a number of items
              up to a limit value. Use the limit parameter to make an
              initial limited request and use the ID of the last-seen item
              from the response as the marker parameter value in a
              subsequent limited request.
            * offset: Used in conjunction with limit to return a slice of
              items. Is where to start in the list.
            * marker: The ID of the last-seen item.

        :returns: A generator of group type objects.
        """
        return self._list(_group_type.GroupType, **query)

    def create_group_type(self, **attrs: Any) -> _group_type.GroupType:
        """Create a group type

        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.group_type.GroupType`
            comprised of the properties on the GroupType class.

        :returns: The results of group type creation.
        """
        return self._create(_group_type.GroupType, **attrs)

    def delete_group_type(
        self,
        group_type: str | _group_type.GroupType,
        ignore_missing: bool = True,
    ) -> None:
        """Delete a group type

        :param group_type: The value can be the ID of a group type
            or a :class:`~openstack.block_storage.v3.group_type.GroupType`
            instance.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the zone does not exist. When set to ``True``, no exception
            will be set when attempting to delete a nonexistent zone.

        :returns: None
        """
        self._delete(
            _group_type.GroupType, group_type, ignore_missing=ignore_missing
        )

    def update_group_type(
        self,
        group_type: str | _group_type.GroupType,
        **attrs: Any,
    ) -> _group_type.GroupType:
        """Update a group_type

        :param group_type: The value can be the ID of a group type or a
            :class:`~openstack.block_storage.v3.group_type.GroupType`
            instance.
        :param attrs: The attributes to update on the group type.

        :returns: The updated group type.
        """
        return self._update(_group_type.GroupType, group_type, **attrs)

    def fetch_group_type_group_specs(
        self, group_type: str | _group_type.GroupType
    ) -> _group_type.GroupType:
        """Lists group specs of a group type.

        :param group_type: Either the ID of a group type or a
            :class:`~openstack.block_storage.v3.group_type.GroupType` instance.

        :returns: One :class:`~openstack.block_storage.v3.group_type.GroupType`
        """
        group_type = self._get_resource(_group_type.GroupType, group_type)
        return group_type.fetch_group_specs(self)

    def create_group_type_group_specs(
        self,
        group_type: str | _group_type.GroupType,
        group_specs: dict[str, Any],
    ) -> _group_type.GroupType:
        """Create group specs for a group type.

        :param group_type: Either the ID of a group type or a
            :class:`~openstack.block_storage.v3.group_type.GroupType` instance.
        :param group_specs: dict of extra specs

        :returns: One :class:`~openstack.block_storage.v3.group_type.GroupType`
        """
        group_type = self._get_resource(_group_type.GroupType, group_type)
        return group_type.create_group_specs(self, specs=group_specs)

    def get_group_type_group_specs_property(
        self, group_type: str | _group_type.GroupType, prop: str
    ) -> str | None:
        """Retrieve a group spec property for a group type.

        :param group_type: Either the ID of a group type or a
            :class:`~openstack.block_storage.v3.group_type.GroupType` instance.
        :param prop: Property name.

        :returns: String value of the requested property.
        """
        group_type = self._get_resource(_group_type.GroupType, group_type)
        return group_type.get_group_specs_property(self, prop)

    def update_group_type_group_specs_property(
        self,
        group_type: str | _group_type.GroupType,
        prop: str,
        val: str,
    ) -> str:
        """Update a group spec property for a group type.

        :param group_type: Either the ID of a group type or a
            :class:`~openstack.block_storage.v3.group_type.GroupType` instance.
        :param prop: Property name.
        :param val: Property value.

        :returns: String value of the requested property.
        """
        group_type = self._get_resource(_group_type.GroupType, group_type)
        return group_type.update_group_specs_property(self, prop, val)

    def delete_group_type_group_specs_property(
        self,
        group_type: str | _group_type.GroupType,
        prop: str,
        ignore_missing: bool = True,
    ) -> None:
        """Delete a group spec property from a group type.

        :param group_type: Either the ID of a group type or a
            :class:`~openstack.block_storage.v3.group_type.GroupType` instance.
        :param prop: Property name.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the group type or group spec does not exist. When set to
            ``True``, no exception will be set when attempting to delete a
            nonexistent group type group spec.

        :returns: None
        """
        group_type = self._get_resource(_group_type.GroupType, group_type)
        try:
            group_type.delete_group_specs_property(self, prop)
        except exceptions.NotFoundException:
            if not ignore_missing:
                raise

    # ====== QoS ======

    def create_qos_spec(self, **attrs: Any) -> _qos_spec.QoSSpec:
        """Create a new QoS Spec from attributes

        :param attrs: Keyword arguments which will be used to create a
            :class:`~openstack.block_storage.v3.qos_spec.QoSSpec`, comprised
            of the properties on the QoS class.

        :returns: The results of a qos spec creation
        :rtype: :class:`~openstack.block_storage.v3.qos_spec.QoSSpec`
        """
        return self._create(_qos_spec.QoSSpec, **attrs)

    def delete_qos_spec(
        self, qos_spec: str | _qos_spec.QoSSpec, ignore_missing: bool = True
    ) -> None:
        """Delete a QoS Spec

        :param qos_spec: The value can be either the ID of a qos spec or a
            :class:`~openstack.block_storage.v3.qos_spec.QoSSpec` instance.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.ResourceNotFound` will be raised when
            the type does not exist. When set to ``True``, no exception will be
            set when attempting to delete a nonexistent type.

        :returns: ``None``
        """
        self._delete(
            _qos_spec.QoSSpec, qos_spec, ignore_missing=ignore_missing
        )

    def update_qos_spec(
        self,
        qos_spec: str | _qos_spec.QoSSpec,
        **attrs: Any,
    ) -> _qos_spec.QoSSpec:
        """Update a qos spec

        :param qos_spec: The value can be either the ID of a qos spec or a
            :class:`~openstack.block_storage.v3.qos_spec.QoSSpec` instance.
        :param dict attrs: The attributes to update on the qos spec

        :returns: The updated qos spec
        :rtype: :class:`~openstack.block_storage.v3.qos_spec.QoSSpec`
        """
        return self._update(_qos_spec.QoSSpec, qos_spec, **attrs)

    def get_qos_spec(self, qos_spec: _qos_spec.QoSSpec) -> _qos_spec.QoSSpec:
        """Get a single QoS Spec

        :param qos_spec: The value can be either the ID of a qos spec or a
            :class:`~openstack.block_storage.v3.qos_spec.QoSSpec` instance.

        :returns: One :class:`~openstack.block_storage.v3.qos_spec.QoSSpec`
        :raises: :class:`~openstack.exceptions.ResourceNotFound` when no
            resource can be found.
        """
        return self._get(_qos_spec.QoSSpec, qos_spec)

    # ====== QUOTA CLASS SETS ======

    def get_quota_class_set(
        self,
        quota_class_set: str | _quota_class_set.QuotaClassSet = 'default',
    ) -> _quota_class_set.QuotaClassSet:
        """Get a single quota class set

        Only one quota class is permitted, ``default``.

        :param quota_class_set: The value can be the ID of a quota class set
            (only ``default`` is supported) or a
            :class:`~openstack.block_storage.v3.quota_class_set.QuotaClassSet`
            instance.

        :returns: One
            :class:`~openstack.block_storage.v3.quota_class_set.QuotaClassSet`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        return self._get(_quota_class_set.QuotaClassSet, quota_class_set)

    def update_quota_class_set(
        self,
        quota_class_set: str | _quota_class_set.QuotaClassSet,
        **attrs: Any,
    ) -> _quota_class_set.QuotaClassSet:
        """Update a QuotaClassSet.

        Only one quota class is permitted, ``default``.

        :param quota_class_set: Either the ID of a quota class set (only
            ``default`` is supported) or a
        :param attrs: The attributes to update on the QuotaClassSet represented
            by ``quota_class_set``.

        :returns: The updated QuotaSet
        """
        return self._update(
            _quota_class_set.QuotaClassSet, quota_class_set, **attrs
        )

    # ====== QUOTA SETS ======

    def get_quota_set(
        self,
        project: str | _project.Project,
        usage: bool = False,
        **query: Any,
    ) -> _quota_set.QuotaSet:
        """Show QuotaSet information for the project

        :param project: ID or instance of
            :class:`~openstack.identity.project.Project` of the project for
            which the quota should be retrieved
        :param usage: When set to ``True`` quota usage and reservations
            would be filled.
        :param query: Additional query parameters to use.

        :returns: One :class:`~openstack.block_storage.v3.quota_set.QuotaSet`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        project_id = resource.Resource._get_id(project)
        res = self._get_resource(
            _quota_set.QuotaSet, None, project_id=project_id
        )
        return res.fetch(self, usage=usage, **query)

    def get_quota_set_defaults(
        self, project: str | _project.Project
    ) -> _quota_set.QuotaSet:
        """Show QuotaSet defaults for the project

        :param project: ID or instance of
            :class:`~openstack.identity.project.Project` of the project for
            which the quota should be retrieved

        :returns: One :class:`~openstack.block_storage.v3.quota_set.QuotaSet`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        project_id = resource.Resource._get_id(project)
        res = self._get_resource(
            _quota_set.QuotaSet, None, project_id=project_id
        )
        return res.fetch(
            self, base_path=(f'/os-quota-sets/{project_id}/defaults')
        )

    def revert_quota_set(
        self, project: str | _project.Project, **query: Any
    ) -> None:
        """Reset Quota for the project/user.

        :param project: ID or instance of
            :class:`~openstack.identity.project.Project` of the project for
            which the quota should be resetted.
        :param query: Additional parameters to be used.

        :returns: ``None``
        """
        project_id = resource.Resource._get_id(project)
        res = self._get_resource(
            _quota_set.QuotaSet, None, project_id=project_id
        )

        res.delete(self, **query)

    # TODO(stephenfin): Drop the QuotaSet fallback in 5.0
    def update_quota_set(
        self,
        project: str | _project.Project | _quota_set.QuotaSet,
        **attrs: Any,
    ) -> _quota_set.QuotaSet:
        """Update a QuotaSet.

        :param project: ID or instance of
            :class:`~openstack.identity.project.Project` of the project for
            which the quota should be reset.
        :param attrs: The attributes to update on the QuotaSet represented
            by ``quota_set``.

        :returns: The updated QuotaSet
        """
        if 'project_id' in attrs or isinstance(project, _quota_set.QuotaSet):
            warnings.warn(
                "The signature of 'update_quota_set' has changed and it "
                "now expects a Project as the first argument, in line "
                "with the other quota set methods.",
                os_warnings.RemovedInSDK50Warning,
            )
            # cinder doesn't support any query parameters so we simply pop
            # these
            if 'query' in attrs:
                warnings.warn(
                    "The query argument is no longer supported and should "
                    "be removed.",
                    os_warnings.RemovedInSDK50Warning,
                )
                attrs.pop('query')

            res = self._get_resource(
                _quota_set.QuotaSet,
                cast('str | _quota_set.QuotaSet | None', project),
                **attrs,
            )
            return res.commit(self)
        else:
            project = self._get_resource(_project.Project, project)
            attrs['project_id'] = project.id
            return self._update(_quota_set.QuotaSet, None, **attrs)

    # ====== SERVICES ======
    @overload
    def find_service(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
        **query: Any,
    ) -> _service.Service: ...

    @overload
    def find_service(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        **query: Any,
    ) -> _service.Service | None: ...

    def find_service(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
        **query: Any,
    ) -> _service.Service | None:
        """Find a single service

        :param name_or_id: The name or ID of a service
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the resource does not exist. When set to ``True``, None will
            be returned when attempting to find a nonexistent resource.
        :param query: Additional attributes like 'host'

        :returns: One: class:`~openstack.block_storage.v3.service.Service` or
            None
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        return self._find(
            _service.Service,
            name_or_id,
            ignore_missing=ignore_missing,
            **query,
        )

    def services(
        self,
        **query: Any,
    ) -> Generator[_service.Service, None, None]:
        """Return a generator of service

        :param query: Optional query parameters to be sent to limit
            the resources being returned.
        :returns: A generator of Service objects
        """
        return self._list(_service.Service, **query)

    def enable_service(
        self,
        service: str | _service.Service,
    ) -> _service.Service:
        """Enable a service

        :param service: Either the ID of a service or a
            :class:`~openstack.block_storage.v3.service.Service` instance.

        :returns: Updated service instance
        """
        service_obj = self._get_resource(_service.Service, service)
        return service_obj.enable(self)

    def disable_service(
        self,
        service: str | _service.Service,
        *,
        reason: str | None = None,
    ) -> _service.Service:
        """Disable a service

        :param service: Either the ID of a service or a
            :class:`~openstack.block_storage.v3.service.Service` instance
        :param reason: The reason to disable a service

        :returns: Updated service instance
        """
        service_obj = self._get_resource(_service.Service, service)
        return service_obj.disable(self, reason=reason)

    def thaw_service(
        self,
        service: str | _service.Service,
    ) -> _service.Service:
        """Thaw a service

        :param service: Either the ID of a service or a
            :class:`~openstack.block_storage.v3.service.Service` instance

        :returns: Updated service instance
        """
        service_obj = self._get_resource(_service.Service, service)
        return service_obj.thaw(self)

    def freeze_service(
        self,
        service: str | _service.Service,
    ) -> _service.Service:
        """Freeze a service

        :param service: Either the ID of a service or a
            :class:`~openstack.block_storage.v3.service.Service` instance

        :returns: Updated service instance
        """
        service_obj = self._get_resource(_service.Service, service)
        return service_obj.freeze(self)

    def set_service_log_levels(
        self,
        *,
        level: _service.Level,
        binary: _service.Binary | None = None,
        server: str | None = None,
        prefix: str | None = None,
    ) -> None:
        """Set log level for services.

        :param level: The log level to set, case insensitive, accepted values
            are ``INFO``, ``WARNING``, ``ERROR`` and ``DEBUG``.
        :param binary: The binary name of the service.
        :param server: The name of the host.
        :param prefix: The prefix for the log path we are querying, for example
            ``cinder.`` or ``sqlalchemy.engine.`` When not present or the empty
            string is passed all log levels will be retrieved.
        :returns: None.
        """
        return _service.Service.set_log_levels(
            self, level=level, binary=binary, server=server, prefix=prefix
        )

    def get_service_log_levels(
        self,
        *,
        binary: _service.Binary | None = None,
        server: str | None = None,
        prefix: str | None = None,
    ) -> Generator[_service.LogLevel, None, None]:
        """Get log level for services.

        :param binary: The binary name of the service.
        :param server: The name of the host.
        :param prefix: The prefix for the log path we are querying, for example
            ``cinder.`` or ``sqlalchemy.engine.`` When not present or the empty
            string is passed all log levels will be retrieved.
        :returns: A generator of
            :class:`~openstack.block_storage.v3.log_level.LogLevel` objects.
        """
        return _service.Service.get_log_levels(
            self, binary=binary, server=server, prefix=prefix
        )

    def failover_service(
        self,
        service: str | _service.Service,
        *,
        cluster: str | None = None,
        backend_id: str | None = None,
    ) -> _service.Service:
        """Failover a service

        Only applies to replicating cinder-volume services.

        :param service: Either the ID of a service or a
            :class:`~openstack.block_storage.v3.service.Service` instance

        :returns: Updated service instance
        """
        service_obj = self._get_resource(_service.Service, service)
        return service_obj.failover(
            self, cluster=cluster, backend_id=backend_id
        )

    # ====== RESOURCE FILTERS ======
    def resource_filters(
        self,
        **query: Any,
    ) -> Generator[_resource_filter.ResourceFilter, None, None]:
        """Retrieve a generator of resource filters

        :returns: A generator of resource filters.
        """
        return self._list(_resource_filter.ResourceFilter, **query)

    # ====== EXTENSIONS ======
    def extensions(self) -> Generator[_extension.Extension, None, None]:
        """Return a generator of extensions

        :returns: A generator of extension
        """
        return self._list(_extension.Extension)

    # ===== TRANFERS =====

    def create_transfer(self, **attrs: Any) -> _transfer.Transfer:
        """Create a new Transfer record

        :param volume_id: The value is ID of the volume.
        :param name: The value is name of the transfer
        :param attrs: Keyword arguments which will be used to create
            a :class:`~openstack.block_storage.v3.transfer.Transfer`
            comprised of the properties on the Transfer class.
        :returns: The results of Transfer creation
        """
        return self._create(_transfer.Transfer, **attrs)

    def delete_transfer(
        self, transfer: str | _transfer.Transfer, ignore_missing: bool = True
    ) -> None:
        """Delete a volume transfer

        :param transfer: The value can be either the ID of a transfer or a
            :class:`~openstack.block_storage.v3.transfer.Transfer`` instance.
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be
            raised when the transfer does not exist.
            When set to ``True``, no exception will be set when
            attempting to delete a nonexistent transfer.

        :returns: ``None``
        """
        self._delete(
            _transfer.Transfer,
            transfer,
            ignore_missing=ignore_missing,
        )

    @overload
    def find_transfer(
        self,
        name_or_id: str,
        ignore_missing: Literal[False],
    ) -> _transfer.Transfer: ...

    @overload
    def find_transfer(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
    ) -> _transfer.Transfer | None: ...

    def find_transfer(
        self,
        name_or_id: str,
        ignore_missing: bool = True,
    ) -> _transfer.Transfer | None:
        """Find a single transfer

        :param name_or_id: The name or ID a transfer
        :param ignore_missing: When set to ``False``
            :class:`~openstack.exceptions.NotFoundException` will be raised
            when the volume transfer does not exist.

        :returns: One :class:`~openstack.block_storage.v3.transfer.Transfer`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        :raises: :class:`~openstack.exceptions.DuplicateResource` when multiple
            resources are found.
        """
        return self._find(
            _transfer.Transfer,
            name_or_id,
            ignore_missing=ignore_missing,
        )

    def get_transfer(
        self, transfer: str | _transfer.Transfer
    ) -> _transfer.Transfer:
        """Get a single transfer

        :param transfer: The value can be the ID of a transfer or a
            :class:`~openstack.block_storage.v3.transfer.Transfer`
            instance.

        :returns: One :class:`~openstack.block_storage.v3.transfer.Transfer`
        :raises: :class:`~openstack.exceptions.NotFoundException`
            when no resource can be found.
        """
        return self._get(_transfer.Transfer, transfer)

    def transfers(
        self,
        *,
        details: bool = True,
        all_projects: bool = False,
        **query: Any,
    ) -> Generator[_transfer.Transfer, None, None]:
        """Retrieve a generator of transfers

        :param details: When set to ``False`` no extended attributes
            will be returned. The default, ``True``, will cause objects with
            additional attributes to be returned.
        :param all_projects: When set to ``True``, list transfers from
            all projects. Admin-only by default.
        :param query: Optional query parameters to be sent to limit
            the transfers being returned.

        :returns: A generator of transfer objects.
        """
        if all_projects:
            query['all_projects'] = True
        base_path = '/volume-transfers'
        if not utils.supports_microversion(self, '3.55'):
            base_path = '/os-volume-transfer'
        if details:
            base_path = utils.urljoin(base_path, 'detail')
        return self._list(_transfer.Transfer, base_path=base_path, **query)

    @renamed_param('transfer_id', 'transfer')
    def accept_transfer(
        self,
        transfer: str | _transfer.Transfer,
        auth_key: str,
    ) -> _transfer.Transfer:
        """Accept a Transfer

        :param transfer: The value can be the ID of a transfer or a
            :class:`~openstack.block_storage.v3.transfer.Transfer`
            instance.
        :param auth_key: The key to authenticate volume transfer.

        :returns: The results of Transfer creation
        """
        transfer = self._get_resource(_transfer.Transfer, transfer)
        return transfer.accept(self, auth_key=auth_key)

    # ====== UTILS ======
    def wait_for_status(
        self,
        res: resource.ResourceT,
        status: str = 'available',
        failures: list[str] | None = None,
        interval: int | float | None = 2,
        wait: int | None = None,
        attribute: str = 'status',
        callback: Callable[[int], None] | None = None,
    ) -> resource.ResourceT:
        """Wait for the resource to be in a particular status.

        :param session: The session to use for making this request.
        :param resource: The resource to wait on to reach the status. The
            resource must have a status attribute specified via ``attribute``.
        :param status: Desired status of the resource.
        :param failures: Statuses that would indicate the transition
            failed such as 'ERROR'. Defaults to ['ERROR'].
        :param interval: Number of seconds to wait between checks.
        :param wait: Maximum number of seconds to wait for transition.
            Set to ``None`` to wait forever.
        :param attribute: Name of the resource attribute that contains the
            status.
        :param callback: A callback function. This will be called with a single
            value, progress. This is API specific but is generally a percentage
            value from 0-100.

        :returns: The updated resource.
        :raises: :class:`~openstack.exceptions.ResourceTimeout` if the
            transition to status failed to occur in ``wait`` seconds.
        :raises: :class:`~openstack.exceptions.ResourceFailure` if the resource
            transitioned to one of the states in ``failures``.
        :raises: :class:`~AttributeError` if the resource does not have a
            ``status`` attribute
        """
        if failures is None:
            failures = ['error']

        return resource.wait_for_status(
            self, res, status, failures, interval, wait, attribute, callback
        )

    def wait_for_delete(
        self,
        res: resource.ResourceT,
        interval: int | float | None = 2,
        wait: int | None = 120,
        callback: Callable[[int], None] | None = None,
    ) -> resource.ResourceT:
        """Wait for a resource to be deleted.

        :param res: The resource to wait on to be deleted.
        :param interval: Number of seconds to wait before to consecutive
            checks.
        :param wait: Maximum number of seconds to wait before the change.
        :param callback: A callback function. This will be called with a single
            value, progress, which is a percentage value from 0-100.

        :returns: The resource is returned on success.
        :raises: :class:`~openstack.exceptions.ResourceTimeout` if transition
            to delete failed to occur in the specified seconds.
        """
        return resource.wait_for_delete(self, res, interval, wait, callback)

    def _get_cleanup_dependencies(
        self,
    ) -> dict[str, proxy.CleanupDependency] | None:
        return {'block_storage': {'before': [], 'after': []}}

    def _service_cleanup(
        self,
        dry_run: bool = True,
        client_status_queue: queue.Queue[resource.Resource] | None = None,
        identified_resources: dict[str, resource.Resource] | None = None,
        filters: dict[str, Any] | None = None,
        resource_evaluation_fn: Callable[
            [
                resource.Resource,
                dict[str, Any] | None,
                dict[str, resource.Resource] | None,
            ],
            bool,
        ]
        | None = None,
        skip_resources: Sequence[str] | None = None,
    ) -> None:
        # It is not possible to delete backup if there are dependent backups.
        # In order to be able to do cleanup those is required to have multiple
        # iterations (first clean up backups with has no dependent backups, and
        # in next iterations there should be no backups with dependencies
        # remaining. Logically we can have also failures, therefore it is
        # required to limit amount of iterations we do (currently pick 10).  In
        # dry_run all those iterations are doing not what we want, therefore
        # only iterate in a real cleanup mode.
        if not self.should_skip_resource_cleanup("backup", skip_resources):
            if dry_run:
                # Just iterate and evaluate backups in dry_run mode
                for obj in self.backups(details=False):
                    need_delete = self._service_cleanup_del_res(
                        self.delete_backup,
                        obj,
                        dry_run=dry_run,
                        client_status_queue=client_status_queue,
                        identified_resources=identified_resources,
                        filters=filters,
                        resource_evaluation_fn=resource_evaluation_fn,
                    )
            else:
                # Set initial iterations conditions
                need_backup_iteration = True
                max_iterations = 10
                while need_backup_iteration and max_iterations > 0:
                    # Reset iteration controls
                    need_backup_iteration = False
                    max_iterations -= 1
                    backups = []
                    # To increase success chance sort backups by age, dependent
                    # backups are logically younger.
                    for obj in self.backups(
                        details=True, sort_key='created_at', sort_dir='desc'
                    ):
                        if not obj.has_dependent_backups:
                            # If no dependent backups - go with it
                            need_delete = self._service_cleanup_del_res(
                                self.delete_backup,
                                obj,
                                dry_run=dry_run,
                                client_status_queue=client_status_queue,
                                identified_resources=identified_resources,
                                filters=filters,
                                resource_evaluation_fn=resource_evaluation_fn,
                            )
                            if not dry_run and need_delete:
                                backups.append(obj)
                        else:
                            # Otherwise we need another iteration
                            need_backup_iteration = True

                    # Before proceeding need to wait for backups to be deleted
                    for obj in backups:
                        try:
                            self.wait_for_delete(obj)
                        except exceptions.SDKException:
                            # Well, did our best, still try further
                            pass

        if not self.should_skip_resource_cleanup("snapshot", skip_resources):
            snapshots = []
            for snap_obj in self.snapshots(details=False):
                need_delete = self._service_cleanup_del_res(
                    self.delete_snapshot,
                    snap_obj,
                    dry_run=dry_run,
                    client_status_queue=client_status_queue,
                    identified_resources=identified_resources,
                    filters=filters,
                    resource_evaluation_fn=resource_evaluation_fn,
                )
                if not dry_run and need_delete:
                    snapshots.append(snap_obj)

            # Before deleting volumes need to wait for snapshots to be deleted
            for snap_obj in snapshots:
                try:
                    self.wait_for_delete(snap_obj)
                except exceptions.SDKException:
                    # Well, did our best, still try further
                    pass

        if not self.should_skip_resource_cleanup("volume", skip_resources):
            for vol_obj in self.volumes(details=True):
                self._service_cleanup_del_res(
                    self.delete_volume,
                    vol_obj,
                    dry_run=dry_run,
                    client_status_queue=client_status_queue,
                    identified_resources=identified_resources,
                    filters=filters,
                    resource_evaluation_fn=resource_evaluation_fn,
                )
