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:
parent
a570b061b8
commit
a60a09ce5f
@ -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"))
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -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):
|
||||
|
51
cinder/objects/dynamic_log.py
Normal file
51
cinder/objects/dynamic_log.py
Normal 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'),
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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',
|
||||
|
@ -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')
|
||||
|
57
cinder/tests/unit/test_manager.py
Normal file
57
cinder/tests/unit/test_manager.py
Normal 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))
|
@ -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])
|
||||
|
@ -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')
|
||||
|
@ -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)}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user