Add service dynamic log change/query

This patch adds 2 new APIs for microversion 3.32, one to dynamically
change the log level of cinder services, and the other that allows
querying their current log levels.

DocImpact
APIImpact
Implements: blueprint dynamic-log-levels
Change-Id: Ia5ef81135044733f1dd3970a116f97457b0371de
This commit is contained in:
Gorka Eguileor 2017-03-13 18:35:40 +01:00
parent a570b061b8
commit a60a09ce5f
20 changed files with 547 additions and 5 deletions

View File

@ -24,11 +24,15 @@ import webob.exc
from cinder.api import common
from cinder.api import extensions
from cinder.api.openstack import wsgi
from cinder.backup import rpcapi as backup_rpcapi
from cinder.common import constants
from cinder import exception
from cinder.i18n import _
from cinder import objects
from cinder.scheduler import rpcapi as scheduler_rpcapi
from cinder import utils
from cinder import volume
from cinder.volume import rpcapi as volume_rpcapi
CONF = cfg.CONF
@ -38,10 +42,18 @@ authorize = extensions.extension_authorizer('volume', 'services')
class ServiceController(wsgi.Controller):
LOG_BINARIES = (constants.SCHEDULER_BINARY, constants.VOLUME_BINARY,
constants.BACKUP_BINARY, constants.API_BINARY)
def __init__(self, ext_mgr=None):
self.ext_mgr = ext_mgr
super(ServiceController, self).__init__()
self.volume_api = volume.API()
self.rpc_apis = {
constants.SCHEDULER_BINARY: scheduler_rpcapi.SchedulerAPI(),
constants.VOLUME_BINARY: volume_rpcapi.VolumeAPI(),
constants.BACKUP_BINARY: backup_rpcapi.BackupAPI(),
}
def index(self, req):
"""Return a list of all running services.
@ -138,6 +150,72 @@ class ServiceController(wsgi.Controller):
cluster_name, body.get('backend_id'))
return webob.Response(status_int=http_client.ACCEPTED)
def _log_params_binaries_services(self, context, body):
"""Get binaries and services referred by given log set/get request."""
query_filters = {'is_up': True}
binary = body.get('binary')
if binary in ('*', None, ''):
binaries = self.LOG_BINARIES
elif binary == constants.API_BINARY:
return [binary], []
elif binary in self.LOG_BINARIES:
binaries = [binary]
query_filters['binary'] = binary
else:
raise exception.InvalidInput(reason=_('%s is not a valid binary.')
% binary)
server = body.get('server')
if server:
query_filters['host_or_cluster'] = server
services = objects.ServiceList.get_all(context, filters=query_filters)
return binaries, services
def _set_log(self, context, body):
"""Set log levels of services dynamically."""
prefix = body.get('prefix')
level = body.get('level')
# Validate log level
utils.get_log_method(level)
binaries, services = self._log_params_binaries_services(context, body)
log_req = objects.LogLevel(context, prefix=prefix, level=level)
if constants.API_BINARY in binaries:
utils.set_log_levels(prefix, level)
for service in services:
self.rpc_apis[service.binary].set_log_levels(context,
service, log_req)
return webob.Response(status_int=202)
def _get_log(self, context, body):
"""Get current log levels for services."""
prefix = body.get('prefix')
binaries, services = self._log_params_binaries_services(context, body)
result = []
log_req = objects.LogLevel(context, prefix=prefix)
if constants.API_BINARY in binaries:
levels = utils.get_log_levels(prefix)
result.append({'host': CONF.host,
'binary': constants.API_BINARY,
'levels': levels})
for service in services:
levels = self.rpc_apis[service.binary].get_log_levels(context,
service,
log_req)
result.append({'host': service.host,
'binary': service.binary,
'levels': {l.prefix: l.level for l in levels}})
return {'log_levels': result}
def update(self, req, id, body):
"""Enable/Disable scheduling for a service.
@ -149,6 +227,8 @@ class ServiceController(wsgi.Controller):
context = req.environ['cinder.context']
authorize(context, action='update')
support_dynamic_log = req.api_version_request.matches('3.32')
ext_loaded = self.ext_mgr.is_loaded('os-extended-services')
ret_val = {}
if id == "enable":
@ -168,6 +248,10 @@ class ServiceController(wsgi.Controller):
return self._failover(context, req, body, False)
elif req.api_version_request.matches('3.26') and id == 'failover':
return self._failover(context, req, body, True)
elif support_dynamic_log and id == 'set-log':
return self._set_log(context, body)
elif support_dynamic_log and id == 'get-log':
return self._get_log(context, body)
else:
raise exception.InvalidInput(reason=_("Unknown action"))

View File

@ -82,7 +82,7 @@ REST_API_VERSION_HISTORY = """
* 3.29 - Add filter, sorter and pagination support in group snapshot.
* 3.30 - Support sort snapshots with "name".
* 3.31 - Add support for configure resource query filters.
* 3.32 - Add set-log and get-log service actions.
"""
# The minimum and maximum versions of the API supported
@ -90,7 +90,7 @@ REST_API_VERSION_HISTORY = """
# minimum version of the API supported.
# Explicitly using /v1 or /v2 enpoints will still work
_MIN_API_VERSION = "3.0"
_MAX_API_VERSION = "3.31"
_MAX_API_VERSION = "3.32"
_LEGACY_API_VERSION1 = "1.0"
_LEGACY_API_VERSION2 = "2.0"

View File

@ -300,3 +300,7 @@ user documentation.
3.31
----
Add support for configure resource query filters.
3.32
----
Added ``set-log`` and ``get-log`` service actions.

View File

@ -45,9 +45,10 @@ class BackupAPI(rpc.RPCAPI):
set to 1.3.
2.0 - Remove 1.x compatibility
2.1 - Adds set_log_levels and get_log_levels
"""
RPC_API_VERSION = '2.0'
RPC_API_VERSION = '2.1'
RPC_DEFAULT_VERSION = '2.0'
TOPIC = constants.BACKUP_TOPIC
BINARY = 'cinder-backup'
@ -100,3 +101,13 @@ class BackupAPI(rpc.RPCAPI):
"on host %(host)s.", {'host': host})
cctxt = self._get_cctxt(server=host)
return cctxt.call(ctxt, 'check_support_to_force_delete')
@rpc.assert_min_rpc_version('2.1')
def set_log_levels(self, context, service, log_request):
cctxt = self._get_cctxt(server=service.host, version='2.1')
cctxt.cast(context, 'set_log_levels', log_request=log_request)
@rpc.assert_min_rpc_version('2.1')
def get_log_levels(self, context, service, log_request):
cctxt = self._get_cctxt(server=service.host, version='2.1')
return cctxt.call(context, 'get_log_levels', log_request=log_request)

View File

@ -18,6 +18,7 @@
DB_MAX_INT = 0x7FFFFFFF
# The cinder services binaries and topics' names
API_BINARY = "cinder-api"
SCHEDULER_BINARY = "cinder-scheduler"
VOLUME_BINARY = "cinder-volume"
BACKUP_BINARY = "cinder-backup"

View File

@ -65,6 +65,7 @@ from cinder import exception
from cinder import objects
from cinder import rpc
from cinder.scheduler import rpcapi as scheduler_rpcapi
from cinder import utils
from eventlet import greenpool
@ -144,6 +145,15 @@ class Manager(base.Base, PeriodicTasks):
rpc.LAST_OBJ_VERSIONS = {}
rpc.LAST_RPC_VERSIONS = {}
def set_log_levels(self, context, log_request):
utils.set_log_levels(log_request.prefix, log_request.level)
def get_log_levels(self, context, log_request):
levels = utils.get_log_levels(log_request.prefix)
log_levels = [objects.LogLevel(context, prefix=prefix, level=level)
for prefix, level in levels.items()]
return objects.LogLevelList(context, objects=log_levels)
class ThreadPoolManager(Manager):
def __init__(self, *args, **kwargs):

View File

@ -41,3 +41,4 @@ def register_all():
__import__('cinder.objects.group')
__import__('cinder.objects.group_snapshot')
__import__('cinder.objects.manageableresources')
__import__('cinder.objects.dynamic_log')

View File

@ -131,6 +131,7 @@ OBJ_VERSIONS.add('1.21', {'ManageableSnapshot': '1.0',
'ManageableSnapshotList': '1.0'})
OBJ_VERSIONS.add('1.22', {'Snapshot': '1.4'})
OBJ_VERSIONS.add('1.23', {'VolumeAttachment': '1.2'})
OBJ_VERSIONS.add('1.24', {'LogLevel': '1.0', 'LogLevelList': '1.0'})
class CinderObjectRegistry(base.VersionedObjectRegistry):

View File

@ -0,0 +1,51 @@
# Copyright (c) 2017 Red Hat, Inc.
# All Rights Reserved.
#
# 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 oslo_versionedobjects import fields
from cinder.objects import base
@base.CinderObjectRegistry.register
class LogLevel(base.CinderObject):
"""Versioned Object to send log change requests."""
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'prefix': fields.StringField(nullable=True),
'level': fields.StringField(nullable=True),
}
def __init__(self, context=None, **kwargs):
super(LogLevel, self).__init__(**kwargs)
# Set non initialized fields with default or None values
for field_name in self.fields:
if not self.obj_attr_is_set(field_name):
field = self.fields[field_name]
if field.default != fields.UnspecifiedDefault:
setattr(self, field_name, field.default)
elif field.nullable:
setattr(self, field_name, None)
@base.CinderObjectRegistry.register
class LogLevelList(base.ObjectListBase, base.CinderObject):
VERSION = '1.0'
fields = {
'objects': fields.ListOfObjectsField('LogLevel'),
}

View File

@ -67,9 +67,10 @@ class SchedulerAPI(rpc.RPCAPI):
3.4 - Adds work_cleanup and do_cleanup methods.
3.5 - Make notify_service_capabilities support A/A
3.6 - Removed create_consistencygroup method
3.7 - Adds set_log_levels and get_log_levels
"""
RPC_API_VERSION = '3.6'
RPC_API_VERSION = '3.7'
RPC_DEFAULT_VERSION = '3.0'
TOPIC = constants.SCHEDULER_TOPIC
BINARY = 'cinder-scheduler'
@ -208,3 +209,13 @@ class SchedulerAPI(rpc.RPCAPI):
"""Perform this scheduler's resource cleanup as per cleanup_request."""
cctxt = self.client.prepare(version='3.4')
cctxt.cast(ctxt, 'do_cleanup', cleanup_request=cleanup_request)
@rpc.assert_min_rpc_version('3.7')
def set_log_levels(self, context, service, log_request):
cctxt = self._get_cctxt(server=service.host, version='3.7')
cctxt.cast(context, 'set_log_levels', log_request=log_request)
@rpc.assert_min_rpc_version('3.7')
def get_log_levels(self, context, service, log_request):
cctxt = self._get_cctxt(server=service.host, version='3.7')
return cctxt.call(context, 'get_log_levels', log_request=log_request)

View File

@ -19,6 +19,7 @@ import datetime
import ddt
from iso8601 import iso8601
import mock
from oslo_config import cfg
from six.moves import http_client
import webob.exc
@ -27,11 +28,15 @@ from cinder.api import extensions
from cinder.api.openstack import api_version_request as api_version
from cinder import context
from cinder import exception
from cinder import objects
from cinder import test
from cinder.tests.unit.api import fakes
from cinder.tests.unit import fake_constants as fake
CONF = cfg.CONF
fake_services_list = [
{'binary': 'cinder-scheduler',
'host': 'host1',
@ -858,3 +863,160 @@ class ServicesTest(test.TestCase):
req = fakes.HTTPRequest.blank(url)
self.assertRaises(exception.InvalidInput,
self.controller.update, req, method, {})
@mock.patch('cinder.api.contrib.services.ServiceController._set_log')
def test_set_log(self, set_log_mock):
set_log_mock.return_value = None
req = FakeRequest(version='3.32')
body = mock.sentinel.body
res = self.controller.update(req, 'set-log', body)
self.assertEqual(set_log_mock.return_value, res)
set_log_mock.assert_called_once_with(mock.ANY, body)
@mock.patch('cinder.api.contrib.services.ServiceController._get_log')
def test_get_log(self, get_log_mock):
get_log_mock.return_value = None
req = FakeRequest(version='3.32')
body = mock.sentinel.body
res = self.controller.update(req, 'get-log', body)
self.assertEqual(get_log_mock.return_value, res)
get_log_mock.assert_called_once_with(mock.ANY, body)
def test__log_params_binaries_services_wrong_binary(self):
body = {'binary': 'wrong-binary'}
self.assertRaises(exception.InvalidInput,
self.controller._log_params_binaries_services,
'get-log', body)
@ddt.data(None, '', '*')
@mock.patch('cinder.objects.ServiceList.get_all')
def test__log_params_binaries_service_all(self, binary, service_list_mock):
body = {'binary': binary, 'server': 'host1'}
binaries, services = self.controller._log_params_binaries_services(
mock.sentinel.context, body)
self.assertEqual(self.controller.LOG_BINARIES, binaries)
self.assertEqual(service_list_mock.return_value, services)
service_list_mock.assert_called_once_with(
mock.sentinel.context, filters={'host_or_cluster': body['server'],
'is_up': True})
@ddt.data('cinder-api', 'cinder-volume', 'cinder-scheduler',
'cinder-backup')
@mock.patch('cinder.objects.ServiceList.get_all')
def test__log_params_binaries_service_one(self, binary, service_list_mock):
body = {'binary': binary, 'server': 'host1'}
binaries, services = self.controller._log_params_binaries_services(
mock.sentinel.context, body)
self.assertEqual([binary], binaries)
if binary == 'cinder-api':
self.assertEqual([], services)
service_list_mock.assert_not_called()
else:
self.assertEqual(service_list_mock.return_value, services)
service_list_mock.assert_called_once_with(
mock.sentinel.context,
filters={'host_or_cluster': body['server'], 'binary': binary,
'is_up': True})
@ddt.data(None, '', 'wronglevel')
def test__set_log_invalid_level(self, level):
body = {'level': level}
self.assertRaises(exception.InvalidInput,
self.controller._set_log, self.context, body)
@mock.patch('cinder.utils.get_log_method')
@mock.patch('cinder.objects.ServiceList.get_all')
@mock.patch('cinder.utils.set_log_levels')
@mock.patch('cinder.scheduler.rpcapi.SchedulerAPI.set_log_levels')
@mock.patch('cinder.volume.rpcapi.VolumeAPI.set_log_levels')
@mock.patch('cinder.backup.rpcapi.BackupAPI.set_log_levels')
def test__set_log(self, backup_rpc_mock, vol_rpc_mock, sch_rpc_mock,
set_log_mock, get_all_mock, get_log_mock):
services = [
objects.Service(self.context, binary='cinder-scheduler'),
objects.Service(self.context, binary='cinder-volume'),
objects.Service(self.context, binary='cinder-backup'),
]
get_all_mock.return_value = services
body = {'binary': '*', 'prefix': 'eventlet.', 'level': 'debug'}
log_level = objects.LogLevel(prefix=body['prefix'],
level=body['level'])
with mock.patch('cinder.objects.LogLevel') as log_level_mock:
log_level_mock.return_value = log_level
res = self.controller._set_log(mock.sentinel.context, body)
log_level_mock.assert_called_once_with(mock.sentinel.context,
prefix=body['prefix'],
level=body['level'])
self.assertEqual(202, res.status_code)
set_log_mock.assert_called_once_with(body['prefix'], body['level'])
sch_rpc_mock.assert_called_once_with(mock.sentinel.context,
services[0], log_level)
vol_rpc_mock.assert_called_once_with(mock.sentinel.context,
services[1], log_level)
backup_rpc_mock.assert_called_once_with(mock.sentinel.context,
services[2], log_level)
get_log_mock.assert_called_once_with(body['level'])
@mock.patch('cinder.objects.ServiceList.get_all')
@mock.patch('cinder.utils.get_log_levels')
@mock.patch('cinder.scheduler.rpcapi.SchedulerAPI.get_log_levels')
@mock.patch('cinder.volume.rpcapi.VolumeAPI.get_log_levels')
@mock.patch('cinder.backup.rpcapi.BackupAPI.get_log_levels')
def test__get_log(self, backup_rpc_mock, vol_rpc_mock, sch_rpc_mock,
get_log_mock, get_all_mock):
get_log_mock.return_value = mock.sentinel.api_levels
backup_rpc_mock.return_value = [
objects.LogLevel(prefix='p1', level='l1'),
objects.LogLevel(prefix='p2', level='l2')
]
vol_rpc_mock.return_value = [
objects.LogLevel(prefix='p3', level='l3'),
objects.LogLevel(prefix='p4', level='l4')
]
sch_rpc_mock.return_value = [
objects.LogLevel(prefix='p5', level='l5'),
objects.LogLevel(prefix='p6', level='l6')
]
services = [
objects.Service(self.context, binary='cinder-scheduler',
host='host'),
objects.Service(self.context, binary='cinder-volume',
host='host@backend#pool'),
objects.Service(self.context, binary='cinder-backup', host='host'),
]
get_all_mock.return_value = services
body = {'binary': '*', 'prefix': 'eventlet.'}
log_level = objects.LogLevel(prefix=body['prefix'])
with mock.patch('cinder.objects.LogLevel') as log_level_mock:
log_level_mock.return_value = log_level
res = self.controller._get_log(mock.sentinel.context, body)
log_level_mock.assert_called_once_with(mock.sentinel.context,
prefix=body['prefix'])
expected = {'log_levels': [
{'binary': 'cinder-api',
'host': CONF.host,
'levels': mock.sentinel.api_levels},
{'binary': 'cinder-scheduler', 'host': 'host',
'levels': {'p5': 'l5', 'p6': 'l6'}},
{'binary': 'cinder-volume',
'host': 'host@backend#pool',
'levels': {'p3': 'l3', 'p4': 'l4'}},
{'binary': 'cinder-backup', 'host': 'host',
'levels': {'p1': 'l1', 'p2': 'l2'}},
]}
self.assertDictEqual(expected, res)
get_log_mock.assert_called_once_with(body['prefix'])
sch_rpc_mock.assert_called_once_with(mock.sentinel.context,
services[0], log_level)
vol_rpc_mock.assert_called_once_with(mock.sentinel.context,
services[1], log_level)
backup_rpc_mock.assert_called_once_with(mock.sentinel.context,
services[2], log_level)

View File

@ -16,7 +16,10 @@
Unit Tests for cinder.backup.rpcapi
"""
import mock
from cinder.backup import rpcapi as backup_rpcapi
from cinder import objects
from cinder import test
from cinder.tests.unit.backup import fake_backup
from cinder.tests.unit import fake_constants as fake
@ -79,3 +82,23 @@ class BackupRPCAPITestCase(test.RPCAPITestCase):
server='fake_volume_host',
host='fake_volume_host',
retval=True)
@mock.patch('oslo_messaging.RPCClient.can_send_version', mock.Mock())
def test_set_log_levels(self):
service = objects.Service(self.context, host='host1')
self._test_rpc_api('set_log_levels',
rpc_method='cast',
server=service.host,
service=service,
log_request='log_request',
version='2.1')
@mock.patch('oslo_messaging.RPCClient.can_send_version', mock.Mock())
def test_get_log_levels(self):
service = objects.Service(self.context, host='host1')
self._test_rpc_api('get_log_levels',
rpc_method='call',
server=service.host,
service=service,
log_request='log_request',
version='2.1')

View File

@ -34,6 +34,8 @@ object_data = {
'CGSnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'ConsistencyGroup': '1.4-7bf01a79b82516639fc03cd3ab6d9c01',
'ConsistencyGroupList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'LogLevel': '1.0-7a8200b6b5063b33ec7b569dc6be66d2',
'LogLevelList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'ManageableSnapshot': '1.0-5be933366eb17d12db0115c597158d0d',
'ManageableSnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
'ManageableVolume': '1.0-5fd0152237ec9dfb7b5c7095b8b09ffa',

View File

@ -223,3 +223,23 @@ class SchedulerRPCAPITestCase(test.RPCAPITestCase):
self.context,
cleanup_request)
can_send_mock.assert_called_once_with('3.4')
@mock.patch('oslo_messaging.RPCClient.can_send_version', mock.Mock())
def test_set_log_levels(self):
service = objects.Service(self.context, host='host1')
self._test_rpc_api('set_log_levels',
rpc_method='cast',
server=service.host,
service=service,
log_request='log_request',
version='3.7')
@mock.patch('oslo_messaging.RPCClient.can_send_version', mock.Mock())
def test_get_log_levels(self):
service = objects.Service(self.context, host='host1')
self._test_rpc_api('get_log_levels',
rpc_method='call',
server=service.host,
service=service,
log_request='log_request',
version='3.7')

View File

@ -0,0 +1,57 @@
# Copyright (c) 2017 Red Hat, Inc.
# All Rights Reserved.
#
# 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.
import mock
import six
from cinder import manager
from cinder import objects
from cinder import test
class FakeManager(manager.CleanableManager):
def __init__(self, service_id=None, keep_after_clean=False):
if service_id:
self.service_id = service_id
self.keep_after_clean = keep_after_clean
def _do_cleanup(self, ctxt, vo_resource):
vo_resource.status += '_cleaned'
vo_resource.save()
return self.keep_after_clean
class TestManager(test.TestCase):
@mock.patch('cinder.utils.set_log_levels')
def test_set_log_levels(self, set_log_mock):
service = manager.Manager()
log_request = objects.LogLevel(prefix='sqlalchemy.', level='debug')
service.set_log_levels(mock.sentinel.context, log_request)
set_log_mock.assert_called_once_with(log_request.prefix,
log_request.level)
@mock.patch('cinder.utils.get_log_levels')
def test_get_log_levels(self, get_log_mock):
get_log_mock.return_value = {'cinder': 'DEBUG', 'cinder.api': 'ERROR'}
service = manager.Manager()
log_request = objects.LogLevel(prefix='sqlalchemy.')
result = service.get_log_levels(mock.sentinel.context, log_request)
get_log_mock.assert_called_once_with(log_request.prefix)
expected = (objects.LogLevel(prefix='cinder', level='DEBUG'),
objects.LogLevel(prefix='cinder.api', level='ERROR'))
self.assertEqual(set(six.text_type(r) for r in result.objects),
set(six.text_type(e) for e in expected))

View File

@ -1455,3 +1455,42 @@ class TestNotificationShortCircuit(test.TestCase):
group='oslo_messaging_notifications')
result = self._decorated_method()
self.assertEqual(utils.DO_NOTHING, result)
@ddt.ddt
class TestLogLevels(test.TestCase):
@ddt.data(None, '', 'wronglevel')
def test_get_log_method_invalid(self, level):
self.assertRaises(exception.InvalidInput,
utils.get_log_method, level)
@ddt.data(('info', utils.logging.INFO), ('warning', utils.logging.WARNING),
('INFO', utils.logging.INFO), ('wArNiNg', utils.logging.WARNING),
('error', utils.logging.ERROR), ('debug', utils.logging.DEBUG))
@ddt.unpack
def test_get_log_method(self, level, logger):
result = utils.get_log_method(level)
self.assertEqual(logger, result)
def test_get_log_levels(self):
levels = utils.get_log_levels('cinder.api')
self.assertTrue(len(levels) > 1)
self.assertSetEqual({'DEBUG'}, set(levels.values()))
@ddt.data(None, '', 'wronglevel')
def test_set_log_levels_invalid(self, level):
self.assertRaises(exception.InvalidInput,
utils.set_log_levels, '', level)
def test_set_log_levels(self):
prefix = 'cinder.utils'
levels = utils.get_log_levels(prefix)
self.assertEqual('DEBUG', levels[prefix])
utils.set_log_levels(prefix, 'warning')
levels = utils.get_log_levels(prefix)
self.assertEqual('WARNING', levels[prefix])
utils.set_log_levels(prefix, 'debug')
levels = utils.get_log_levels(prefix)
self.assertEqual('DEBUG', levels[prefix])

View File

@ -572,3 +572,23 @@ class VolumeRPCAPITestCase(test.RPCAPITestCase):
expected_kwargs_diff=expected_kwargs_diff,
version=version)
can_send_version.assert_has_calls([mock.call('3.10')])
@mock.patch('oslo_messaging.RPCClient.can_send_version', mock.Mock())
def test_set_log_levels(self):
service = objects.Service(self.context, host='host1')
self._test_rpc_api('set_log_levels',
rpc_method='cast',
server=service.host,
service=service,
log_request='log_request',
version='3.12')
@mock.patch('oslo_messaging.RPCClient.can_send_version', mock.Mock())
def test_get_log_levels(self):
service = objects.Service(self.context, host='host1')
self._test_rpc_api('get_log_levels',
rpc_method='call',
server=service.host,
service=service,
log_request='log_request',
version='3.12')

View File

@ -1105,3 +1105,31 @@ def if_notifications_enabled(f):
return f(*args, **kwargs)
return DO_NOTHING
return wrapped
LOG_LEVELS = ('INFO', 'WARNING', 'ERROR', 'DEBUG')
def get_log_method(level_string):
level_string = level_string or ''
upper_level_string = level_string.upper()
if upper_level_string not in LOG_LEVELS:
raise exception.InvalidInput(
reason=_('%s is not a valid log level.') % level_string)
return getattr(logging, upper_level_string)
def set_log_levels(prefix, level_string):
level = get_log_method(level_string)
prefix = prefix or ''
for k, v in logging._loggers.items():
if k and k.startswith(prefix):
v.logger.setLevel(level)
def get_log_levels(prefix):
prefix = prefix or ''
return {k: logging.logging.getLevelName(v.logger.getEffectiveLevel())
for k, v in logging._loggers.items()
if k and k.startswith(prefix)}

View File

@ -127,9 +127,10 @@ class VolumeAPI(rpc.RPCAPI):
3.11 - Removes create_consistencygroup, delete_consistencygroup,
create_cgsnapshot, delete_cgsnapshot, update_consistencygroup,
and create_consistencygroup_from_src.
3.12 - Adds set_log_levels and get_log_levels
"""
RPC_API_VERSION = '3.11'
RPC_API_VERSION = '3.12'
RPC_DEFAULT_VERSION = '3.0'
TOPIC = constants.VOLUME_TOPIC
BINARY = 'cinder-volume'
@ -426,3 +427,13 @@ class VolumeAPI(rpc.RPCAPI):
# cinder.manager.CleanableManager unless in the future we overwrite it
# in cinder.volume.manager
cctxt.cast(ctxt, 'do_cleanup', cleanup_request=cleanup_request)
@rpc.assert_min_rpc_version('3.12')
def set_log_levels(self, context, service, log_request):
cctxt = self._get_cctxt(host=service.host, version='3.12')
cctxt.cast(context, 'set_log_levels', log_request=log_request)
@rpc.assert_min_rpc_version('3.12')
def get_log_levels(self, context, service, log_request):
cctxt = self._get_cctxt(host=service.host, version='3.12')
return cctxt.call(context, 'get_log_levels', log_request=log_request)

View File

@ -0,0 +1,6 @@
---
features:
- |
Added new APIs on microversion 3.32 to support dynamically changing log
levels in Cinder services without restart as well as retrieving current log
levels, which is an easy way to ping via the message broker a service.