Merge "block storage: Add support for services"

This commit is contained in:
Zuul 2023-08-28 11:08:19 +00:00 committed by Gerrit Code Review
commit 99530c8b1d
9 changed files with 611 additions and 1 deletions

View File

@ -91,6 +91,14 @@ Group Type Operations
update_group_type_group_specs_property,
delete_group_type_group_specs_property
Service Operations
^^^^^^^^^^^^^^^^^^
.. autoclass:: openstack.block_storage.v3._proxy.Proxy
:noindex:
:members: find_service, services, enable_service, disable_service,
thaw_service, freeze_service, failover_service
Type Operations
^^^^^^^^^^^^^^^
@ -140,4 +148,3 @@ BlockStorageSummary Operations
.. autoclass:: openstack.block_storage.v3._proxy.Proxy
:noindex:
:members: summary

View File

@ -15,4 +15,5 @@ Block Storage Resources
v3/snapshot
v3/type
v3/volume
v3/service
v3/block_storage_summary

View File

@ -0,0 +1,12 @@
openstack.block_storage.v3.service
==================================
.. automodule:: openstack.block_storage.v3.service
The Service Class
-----------------
The ``Service`` class inherits from :class:`~openstack.resource.Resource`.
.. autoclass:: openstack.block_storage.v3.service.Service
:members:

View File

@ -10,6 +10,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import typing as ty
from openstack.block_storage import _base_proxy
from openstack.block_storage.v3 import availability_zone
from openstack.block_storage.v3 import backup as _backup
@ -22,6 +24,7 @@ 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 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 type as _type
@ -1581,6 +1584,150 @@ class Proxy(_base_proxy.BaseBlockStorageProxy):
query = {}
return res.commit(self, **query)
# ====== SERVICES ======
@ty.overload
def find_service(
self,
name_or_id: str,
ignore_missing: ty.Literal[True] = True,
**query,
) -> ty.Optional[_service.Service]:
...
@ty.overload
def find_service(
self,
name_or_id: str,
ignore_missing: ty.Literal[False] = True,
**query,
) -> _service.Service:
...
def find_service(
self,
name_or_id: str,
ignore_missing: bool = True,
**query,
) -> ty.Optional[_service.Service]:
"""Find a single service
:param name_or_id: The name or ID of a service
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` 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 dict query: Additional attributes like 'host'
:returns: One: class:`~openstack.block_storage.v3.service.Service` or None
:raises: :class:`~openstack.exceptions.ResourceNotFound`
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: ty.Any,
) -> ty.Generator[_service.Service, None, None]:
"""Return a generator of service
:param kwargs query: Optional query parameters to be sent to limit
the resources being returned.
:returns: A generator of Service objects
:rtype: class: `~openstack.block_storage.v3.service.Service`
"""
return self._list(_service.Service, **query)
def enable_service(
self,
service: ty.Union[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
:rtype: class: `~openstack.block_storage.v3.service.Service`
"""
service = self._get_resource(_service.Service, service)
return service.enable(self)
def disable_service(
self,
service: ty.Union[str, _service.Service],
*,
reason: ty.Optional[str] = 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 str reason: The reason to disable a service
:returns: Updated service instance
:rtype: class: `~openstack.block_storage.v3.service.Service`
"""
service = self._get_resource(_service.Service, service)
return service.disable(self, reason=reason)
def thaw_service(
self,
service: ty.Union[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
:rtype: class: `~openstack.block_storage.v3.service.Service`
"""
service = self._get_resource(_service.Service, service)
return service.thaw(self)
def freeze_service(
self,
service: ty.Union[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
:rtype: class: `~openstack.block_storage.v3.service.Service`
"""
service = self._get_resource(_service.Service, service)
return service.freeze(self)
def failover_service(
self,
service: ty.Union[str, _service.Service],
*,
cluster: ty.Optional[str] = None,
backend_id: ty.Optional[str] = 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
:rtype: class: `~openstack.block_storage.v3.service.Service`
"""
service = self._get_resource(_service.Service, service)
return service.failover(self, cluster=cluster, backend_id=backend_id)
# ====== RESOURCE FILTERS ======
def resource_filters(self, **query):
"""Retrieve a generator of resource filters

View File

@ -0,0 +1,158 @@
# 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 openstack import exceptions
from openstack import resource
from openstack import utils
class Service(resource.Resource):
resources_key = 'services'
base_path = '/os-services'
# capabilities
allow_list = True
_query_mapping = resource.QueryParameters(
'binary',
'host',
)
# Properties
#: The ID of active storage backend (cinder-volume services only)
active_backend_id = resource.Body('active_backend_id')
#: The availability zone of service
availability_zone = resource.Body('zone')
#: The state of storage backend (cinder-volume services only)
backend_state = resource.Body('backend_state')
#: Binary name of service
binary = resource.Body('binary')
#: The cluster name (since 3.7)
cluster = resource.Body('cluster')
#: Disabled reason of service
disabled_reason = resource.Body('disabled_reason')
#: The name of the host where service runs
host = resource.Body('host')
# Whether the host is frozen or not (cinder-volume services only)
is_frozen = resource.Body('frozen')
#: Service name
name = resource.Body('name', alias='binary')
#: The volume service replication status (cinder-volume services only)
replication_status = resource.Body('replication_status')
#: State of service
state = resource.Body('state')
#: Status of service
status = resource.Body('status')
#: The date and time when the resource was updated
updated_at = resource.Body('updated_at')
# 3.7 introduced the 'cluster' field
_max_microversion = '3.7'
@classmethod
def find(cls, session, name_or_id, ignore_missing=True, **params):
# No direct request possible, thus go directly to list
data = cls.list(session, **params)
result = None
for maybe_result in data:
# Since ID might be both int and str force cast
id_value = str(cls._get_id(maybe_result))
name_value = maybe_result.name
if str(name_or_id) in (id_value, name_value):
if 'host' in params and maybe_result['host'] != params['host']:
continue
# Only allow one resource to be found. If we already
# found a match, raise an exception to show it.
if result is None:
result = maybe_result
else:
msg = "More than one %s exists with the name '%s'."
msg = msg % (cls.__name__, name_or_id)
raise exceptions.DuplicateResource(msg)
if result is not None:
return result
if ignore_missing:
return None
raise exceptions.ResourceNotFound(
"No %s found for %s" % (cls.__name__, name_or_id)
)
def commit(self, session, prepend_key=False, **kwargs):
# we need to set prepend_key to false
return super().commit(
session,
prepend_key=prepend_key,
**kwargs,
)
def _action(self, session, action, body, microversion=None):
if not microversion:
microversion = session.default_microversion
url = utils.urljoin(Service.base_path, action)
response = session.put(url, json=body, microversion=microversion)
self._translate_response(response)
return self
# TODO(stephenfin): Add support for log levels once we have the resource
# modelled (it can be done on a deployment wide basis)
def enable(self, session):
"""Enable service."""
body = {'binary': self.binary, 'host': self.host}
return self._action(session, 'enable', body)
def disable(self, session, *, reason=None):
"""Disable service."""
body = {'binary': self.binary, 'host': self.host}
if not reason:
action = 'disable'
else:
action = 'disable-log-reason'
body['disabled_reason'] = reason
return self._action(session, action, body)
def thaw(self, session):
body = {'host': self.host}
return self._action(session, 'thaw', body)
def freeze(self, session):
body = {'host': self.host}
return self._action(session, 'freeze', body)
def failover(
self,
session,
*,
cluster=None,
backend_id=None,
):
"""Failover a service
Only applies to replicating cinder-volume services.
"""
body = {'host': self.host}
if cluster:
body['cluster'] = cluster
if backend_id:
body['backend_id'] = backend_id
action = 'failover_host'
if utils.supports_microversion(self, '3.26'):
action = 'failover'
return self._action(session, action, body)

View File

@ -0,0 +1,38 @@
# 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 openstack.tests.functional import base
class TestService(base.BaseFunctionalTest):
# listing services is slooowwww
TIMEOUT_SCALING_FACTOR = 2.0
def test_list(self):
sot = list(self.operator_cloud.block_storage.services())
self.assertIsNotNone(sot)
def test_disable_enable(self):
for srv in self.operator_cloud.block_storage.services():
# only nova-block_storage can be updated
if srv.name == 'nova-block_storage':
self.operator_cloud.block_storage.disable_service(srv)
self.operator_cloud.block_storage.enable_service(srv)
break
def test_find(self):
for srv in self.operator_cloud.block_storage.services():
self.operator_cloud.block_storage.find_service(
srv.name,
host=srv.host,
ignore_missing=False,
)

View File

@ -22,6 +22,7 @@ from openstack.block_storage.v3 import group_type
from openstack.block_storage.v3 import limits
from openstack.block_storage.v3 import quota_set
from openstack.block_storage.v3 import resource_filter
from openstack.block_storage.v3 import service
from openstack.block_storage.v3 import snapshot
from openstack.block_storage.v3 import stats
from openstack.block_storage.v3 import type
@ -316,6 +317,53 @@ class TestGroupType(TestVolumeProxy):
)
class TestService(TestVolumeProxy):
def test_services(self):
self.verify_list(self.proxy.services, service.Service)
def test_enable_service(self):
self._verify(
'openstack.block_storage.v3.service.Service.enable',
self.proxy.enable_service,
method_args=["value"],
expected_args=[self.proxy],
)
def test_disable_service(self):
self._verify(
'openstack.block_storage.v3.service.Service.disable',
self.proxy.disable_service,
method_args=["value"],
expected_kwargs={"reason": None},
expected_args=[self.proxy],
)
def test_thaw_service(self):
self._verify(
'openstack.block_storage.v3.service.Service.thaw',
self.proxy.thaw_service,
method_args=["value"],
expected_args=[self.proxy],
)
def test_freeze_service(self):
self._verify(
'openstack.block_storage.v3.service.Service.freeze',
self.proxy.freeze_service,
method_args=["value"],
expected_args=[self.proxy],
)
def test_failover_service(self):
self._verify(
'openstack.block_storage.v3.service.Service.failover',
self.proxy.failover_service,
method_args=["value"],
expected_args=[self.proxy],
expected_kwargs={"backend_id": None, "cluster": None},
)
class TestExtension(TestVolumeProxy):
def test_extensions(self):
self.verify_list(self.proxy.extensions, extension.Extension)

View File

@ -0,0 +1,195 @@
# 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 unittest import mock
from openstack.block_storage.v3 import service
from openstack.tests.unit import base
EXAMPLE = {
"binary": "cinder-scheduler",
"disabled_reason": None,
"host": "devstack",
"state": "up",
"status": "enabled",
"updated_at": "2017-06-29T05:50:35.000000",
"zone": "nova",
}
class TestService(base.TestCase):
def setUp(self):
super().setUp()
self.resp = mock.Mock()
self.resp.body = {'service': {}}
self.resp.json = mock.Mock(return_value=self.resp.body)
self.resp.status_code = 200
self.resp.headers = {}
self.sess = mock.Mock()
self.sess.put = mock.Mock(return_value=self.resp)
self.sess.default_microversion = '3.0'
def test_basic(self):
sot = service.Service()
self.assertIsNone(sot.resource_key)
self.assertEqual('services', sot.resources_key)
self.assertEqual('/os-services', sot.base_path)
self.assertFalse(sot.allow_commit)
self.assertTrue(sot.allow_list)
self.assertFalse(sot.allow_fetch)
self.assertFalse(sot.allow_delete)
self.assertDictEqual(
{
'binary': 'binary',
'host': 'host',
'limit': 'limit',
'marker': 'marker',
},
sot._query_mapping._mapping,
)
def test_make_it(self):
sot = service.Service(**EXAMPLE)
self.assertEqual(EXAMPLE['binary'], sot.binary)
self.assertEqual(EXAMPLE['binary'], sot.name)
self.assertEqual(EXAMPLE['disabled_reason'], sot.disabled_reason)
self.assertEqual(EXAMPLE['host'], sot.host)
self.assertEqual(EXAMPLE['state'], sot.state)
self.assertEqual(EXAMPLE['status'], sot.status)
self.assertEqual(EXAMPLE['zone'], sot.availability_zone)
def test_enable(self):
sot = service.Service(**EXAMPLE)
res = sot.enable(self.sess)
self.assertIsNotNone(res)
url = 'os-services/enable'
body = {
'binary': 'cinder-scheduler',
'host': 'devstack',
}
self.sess.put.assert_called_with(
url,
json=body,
microversion=self.sess.default_microversion,
)
def test_disable(self):
sot = service.Service(**EXAMPLE)
res = sot.disable(self.sess)
self.assertIsNotNone(res)
url = 'os-services/disable'
body = {
'binary': 'cinder-scheduler',
'host': 'devstack',
}
self.sess.put.assert_called_with(
url,
json=body,
microversion=self.sess.default_microversion,
)
def test_disable__with_reason(self):
sot = service.Service(**EXAMPLE)
reason = 'fencing'
res = sot.disable(self.sess, reason=reason)
self.assertIsNotNone(res)
url = 'os-services/disable-log-reason'
body = {
'binary': 'cinder-scheduler',
'host': 'devstack',
'disabled_reason': reason,
}
self.sess.put.assert_called_with(
url,
json=body,
microversion=self.sess.default_microversion,
)
def test_thaw(self):
sot = service.Service(**EXAMPLE)
res = sot.thaw(self.sess)
self.assertIsNotNone(res)
url = 'os-services/thaw'
body = {'host': 'devstack'}
self.sess.put.assert_called_with(
url,
json=body,
microversion=self.sess.default_microversion,
)
def test_freeze(self):
sot = service.Service(**EXAMPLE)
res = sot.freeze(self.sess)
self.assertIsNotNone(res)
url = 'os-services/freeze'
body = {'host': 'devstack'}
self.sess.put.assert_called_with(
url,
json=body,
microversion=self.sess.default_microversion,
)
@mock.patch(
'openstack.utils.supports_microversion',
autospec=True,
return_value=False,
)
def test_failover(self, mock_supports):
sot = service.Service(**EXAMPLE)
res = sot.failover(self.sess)
self.assertIsNotNone(res)
url = 'os-services/failover_host'
body = {'host': 'devstack'}
self.sess.put.assert_called_with(
url,
json=body,
microversion=self.sess.default_microversion,
)
@mock.patch(
'openstack.utils.supports_microversion',
autospec=True,
return_value=True,
)
def test_failover__with_cluster(self, mock_supports):
self.sess.default_microversion = '3.26'
sot = service.Service(**EXAMPLE)
res = sot.failover(self.sess, cluster='foo', backend_id='bar')
self.assertIsNotNone(res)
url = 'os-services/failover'
body = {
'host': 'devstack',
'cluster': 'foo',
'backend_id': 'bar',
}
self.sess.put.assert_called_with(
url,
json=body,
microversion='3.26',
)

View File

@ -0,0 +1,4 @@
---
features:
- |
Added support for block storage services.