volume: Add Service to volume v2 API
Another one needed for OSC purposes. Change-Id: I5d69d3f71df1a489f860bab3f6244e5661ac4ca0 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
parent
c4f19bb721
commit
6994223691
@ -82,6 +82,14 @@ QuotaSet Operations
|
||||
:members: get_quota_set, get_quota_set_defaults,
|
||||
revert_quota_set, update_quota_set
|
||||
|
||||
Service Operations
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. autoclass:: openstack.block_storage.v2._proxy.Proxy
|
||||
:noindex:
|
||||
:members: find_service, services, enable_service, disable_service,
|
||||
thaw_service, freeze_service, failover_service
|
||||
|
||||
Helpers
|
||||
^^^^^^^
|
||||
|
||||
|
12
doc/source/user/resources/block_storage/v2/service.rst
Normal file
12
doc/source/user/resources/block_storage/v2/service.rst
Normal file
@ -0,0 +1,12 @@
|
||||
openstack.block_storage.v2.service
|
||||
==================================
|
||||
|
||||
.. automodule:: openstack.block_storage.v2.service
|
||||
|
||||
The Service Class
|
||||
-----------------
|
||||
|
||||
The ``Service`` class inherits from :class:`~openstack.resource.Resource`.
|
||||
|
||||
.. autoclass:: openstack.block_storage.v2.service.Service
|
||||
:members:
|
@ -19,6 +19,7 @@ from openstack.block_storage.v2 import extension as _extension
|
||||
from openstack.block_storage.v2 import limits as _limits
|
||||
from openstack.block_storage.v2 import quota_class_set as _quota_class_set
|
||||
from openstack.block_storage.v2 import quota_set as _quota_set
|
||||
from openstack.block_storage.v2 import service as _service
|
||||
from openstack.block_storage.v2 import snapshot as _snapshot
|
||||
from openstack.block_storage.v2 import stats as _stats
|
||||
from openstack.block_storage.v2 import type as _type
|
||||
@ -886,7 +887,7 @@ class Proxy(proxy.Proxy):
|
||||
by ``quota_set``.
|
||||
|
||||
:returns: The updated QuotaSet
|
||||
:rtype: :class:`~openstack.block_storage.v3.quota_set.QuotaSet`
|
||||
:rtype: :class:`~openstack.block_storage.v2.quota_set.QuotaSet`
|
||||
"""
|
||||
if 'project_id' in attrs or isinstance(project, _quota_set.QuotaSet):
|
||||
warnings.warn(
|
||||
@ -912,6 +913,157 @@ class Proxy(proxy.Proxy):
|
||||
attrs['project_id'] = project.id
|
||||
return self._update(_quota_set.QuotaSet, None, **attrs)
|
||||
|
||||
# ========== Services ==========
|
||||
@ty.overload
|
||||
def find_service(
|
||||
self,
|
||||
name_or_id: str,
|
||||
ignore_missing: ty.Literal[True] = True,
|
||||
**query: ty.Any,
|
||||
) -> ty.Optional[_service.Service]: ...
|
||||
|
||||
@ty.overload
|
||||
def find_service(
|
||||
self,
|
||||
name_or_id: str,
|
||||
ignore_missing: ty.Literal[False],
|
||||
**query: ty.Any,
|
||||
) -> _service.Service: ...
|
||||
|
||||
# excuse the duplication here: it's mypy's fault
|
||||
# https://github.com/python/mypy/issues/14764
|
||||
@ty.overload
|
||||
def find_service(
|
||||
self,
|
||||
name_or_id: str,
|
||||
ignore_missing: bool,
|
||||
**query: ty.Any,
|
||||
) -> ty.Optional[_service.Service]: ...
|
||||
|
||||
def find_service(
|
||||
self,
|
||||
name_or_id: str,
|
||||
ignore_missing: bool = True,
|
||||
**query: ty.Any,
|
||||
) -> 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.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 dict query: Additional attributes like 'host'
|
||||
|
||||
:returns: One: class:`~openstack.block_storage.v2.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: 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.v2.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.v2.service.Service` instance.
|
||||
|
||||
:returns: Updated service instance
|
||||
:rtype: class: `~openstack.block_storage.v2.service.Service`
|
||||
"""
|
||||
service_obj = self._get_resource(_service.Service, service)
|
||||
return service_obj.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.v2.service.Service` instance
|
||||
:param str reason: The reason to disable a service
|
||||
|
||||
:returns: Updated service instance
|
||||
:rtype: class: `~openstack.block_storage.v2.service.Service`
|
||||
"""
|
||||
service_obj = self._get_resource(_service.Service, service)
|
||||
return service_obj.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.v2.service.Service` instance
|
||||
|
||||
:returns: Updated service instance
|
||||
:rtype: class: `~openstack.block_storage.v2.service.Service`
|
||||
"""
|
||||
service_obj = self._get_resource(_service.Service, service)
|
||||
return service_obj.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.v2.service.Service` instance
|
||||
|
||||
:returns: Updated service instance
|
||||
:rtype: class: `~openstack.block_storage.v2.service.Service`
|
||||
"""
|
||||
service_obj = self._get_resource(_service.Service, service)
|
||||
return service_obj.freeze(self)
|
||||
|
||||
def failover_service(
|
||||
self,
|
||||
service: ty.Union[str, _service.Service],
|
||||
*,
|
||||
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.v2.service.Service` instance
|
||||
|
||||
:returns: Updated service instance
|
||||
:rtype: class: `~openstack.block_storage.v2.service.Service`
|
||||
"""
|
||||
service_obj = self._get_resource(_service.Service, service)
|
||||
return service_obj.failover(self, backend_id=backend_id)
|
||||
|
||||
# ========== Volume metadata ==========
|
||||
|
||||
def get_volume_metadata(self, volume):
|
||||
|
143
openstack/block_storage/v2/service.py
Normal file
143
openstack/block_storage/v2/service.py
Normal file
@ -0,0 +1,143 @@
|
||||
# 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')
|
||||
#: Binary name of service
|
||||
binary = resource.Body('binary')
|
||||
#: 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')
|
||||
|
||||
@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.NotFoundException(
|
||||
f"No {cls.__name__} found for {name_or_id}"
|
||||
)
|
||||
|
||||
def commit(self, session, prepend_key=False, *args, **kwargs):
|
||||
# we need to set prepend_key to false
|
||||
return super().commit(
|
||||
session,
|
||||
prepend_key,
|
||||
*args,
|
||||
**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
|
||||
|
||||
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,
|
||||
*,
|
||||
backend_id=None,
|
||||
):
|
||||
"""Failover a service
|
||||
|
||||
Only applies to replicating cinder-volume services.
|
||||
"""
|
||||
body = {'host': self.host}
|
||||
if backend_id:
|
||||
body['backend_id'] = backend_id
|
||||
|
||||
return self._action(session, 'failover_host', body)
|
@ -69,7 +69,7 @@ class Service(resource.Resource):
|
||||
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)
|
||||
#: The state of storage backend (cinder-volume services only) (since 3.49)
|
||||
backend_state = resource.Body('backend_state')
|
||||
#: Binary name of service
|
||||
binary = resource.Body('binary')
|
||||
|
@ -19,6 +19,7 @@ from openstack.block_storage.v2 import capabilities
|
||||
from openstack.block_storage.v2 import limits
|
||||
from openstack.block_storage.v2 import quota_class_set
|
||||
from openstack.block_storage.v2 import quota_set
|
||||
from openstack.block_storage.v2 import service
|
||||
from openstack.block_storage.v2 import snapshot
|
||||
from openstack.block_storage.v2 import stats
|
||||
from openstack.block_storage.v2 import type
|
||||
@ -614,3 +615,50 @@ class TestQuotaSet(TestVolumeProxy):
|
||||
"The signature of 'update_quota_set' has changed ",
|
||||
str(w[-1]),
|
||||
)
|
||||
|
||||
|
||||
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.v2.service.Service.enable',
|
||||
self.proxy.enable_service,
|
||||
method_args=["value"],
|
||||
expected_args=[self.proxy],
|
||||
)
|
||||
|
||||
def test_disable_service(self):
|
||||
self._verify(
|
||||
'openstack.block_storage.v2.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.v2.service.Service.thaw',
|
||||
self.proxy.thaw_service,
|
||||
method_args=["value"],
|
||||
expected_args=[self.proxy],
|
||||
)
|
||||
|
||||
def test_freeze_service(self):
|
||||
self._verify(
|
||||
'openstack.block_storage.v2.service.Service.freeze',
|
||||
self.proxy.freeze_service,
|
||||
method_args=["value"],
|
||||
expected_args=[self.proxy],
|
||||
)
|
||||
|
||||
def test_failover_service(self):
|
||||
self._verify(
|
||||
'openstack.block_storage.v2.service.Service.failover',
|
||||
self.proxy.failover_service,
|
||||
method_args=["value"],
|
||||
expected_args=[self.proxy],
|
||||
expected_kwargs={"backend_id": None},
|
||||
)
|
||||
|
165
openstack/tests/unit/block_storage/v2/test_service.py
Normal file
165
openstack/tests/unit/block_storage/v2/test_service.py
Normal file
@ -0,0 +1,165 @@
|
||||
# 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.v2 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 = None # nothing uses this
|
||||
self.resp.json = mock.Mock(return_value={'service': {}})
|
||||
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,
|
||||
)
|
||||
|
||||
def test_failover(self):
|
||||
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,
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user