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 common
|
||||||
from cinder.api import extensions
|
from cinder.api import extensions
|
||||||
from cinder.api.openstack import wsgi
|
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 import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder import objects
|
from cinder import objects
|
||||||
|
from cinder.scheduler import rpcapi as scheduler_rpcapi
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
from cinder import volume
|
from cinder import volume
|
||||||
|
from cinder.volume import rpcapi as volume_rpcapi
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -38,10 +42,18 @@ authorize = extensions.extension_authorizer('volume', 'services')
|
|||||||
|
|
||||||
|
|
||||||
class ServiceController(wsgi.Controller):
|
class ServiceController(wsgi.Controller):
|
||||||
|
LOG_BINARIES = (constants.SCHEDULER_BINARY, constants.VOLUME_BINARY,
|
||||||
|
constants.BACKUP_BINARY, constants.API_BINARY)
|
||||||
|
|
||||||
def __init__(self, ext_mgr=None):
|
def __init__(self, ext_mgr=None):
|
||||||
self.ext_mgr = ext_mgr
|
self.ext_mgr = ext_mgr
|
||||||
super(ServiceController, self).__init__()
|
super(ServiceController, self).__init__()
|
||||||
self.volume_api = volume.API()
|
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):
|
def index(self, req):
|
||||||
"""Return a list of all running services.
|
"""Return a list of all running services.
|
||||||
@ -138,6 +150,72 @@ class ServiceController(wsgi.Controller):
|
|||||||
cluster_name, body.get('backend_id'))
|
cluster_name, body.get('backend_id'))
|
||||||
return webob.Response(status_int=http_client.ACCEPTED)
|
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):
|
def update(self, req, id, body):
|
||||||
"""Enable/Disable scheduling for a service.
|
"""Enable/Disable scheduling for a service.
|
||||||
|
|
||||||
@ -149,6 +227,8 @@ class ServiceController(wsgi.Controller):
|
|||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
authorize(context, action='update')
|
authorize(context, action='update')
|
||||||
|
|
||||||
|
support_dynamic_log = req.api_version_request.matches('3.32')
|
||||||
|
|
||||||
ext_loaded = self.ext_mgr.is_loaded('os-extended-services')
|
ext_loaded = self.ext_mgr.is_loaded('os-extended-services')
|
||||||
ret_val = {}
|
ret_val = {}
|
||||||
if id == "enable":
|
if id == "enable":
|
||||||
@ -168,6 +248,10 @@ class ServiceController(wsgi.Controller):
|
|||||||
return self._failover(context, req, body, False)
|
return self._failover(context, req, body, False)
|
||||||
elif req.api_version_request.matches('3.26') and id == 'failover':
|
elif req.api_version_request.matches('3.26') and id == 'failover':
|
||||||
return self._failover(context, req, body, True)
|
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:
|
else:
|
||||||
raise exception.InvalidInput(reason=_("Unknown action"))
|
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.29 - Add filter, sorter and pagination support in group snapshot.
|
||||||
* 3.30 - Support sort snapshots with "name".
|
* 3.30 - Support sort snapshots with "name".
|
||||||
* 3.31 - Add support for configure resource query filters.
|
* 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
|
# The minimum and maximum versions of the API supported
|
||||||
@ -90,7 +90,7 @@ REST_API_VERSION_HISTORY = """
|
|||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
# Explicitly using /v1 or /v2 enpoints will still work
|
# Explicitly using /v1 or /v2 enpoints will still work
|
||||||
_MIN_API_VERSION = "3.0"
|
_MIN_API_VERSION = "3.0"
|
||||||
_MAX_API_VERSION = "3.31"
|
_MAX_API_VERSION = "3.32"
|
||||||
_LEGACY_API_VERSION1 = "1.0"
|
_LEGACY_API_VERSION1 = "1.0"
|
||||||
_LEGACY_API_VERSION2 = "2.0"
|
_LEGACY_API_VERSION2 = "2.0"
|
||||||
|
|
||||||
|
@ -300,3 +300,7 @@ user documentation.
|
|||||||
3.31
|
3.31
|
||||||
----
|
----
|
||||||
Add support for configure resource query filters.
|
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.
|
set to 1.3.
|
||||||
|
|
||||||
2.0 - Remove 1.x compatibility
|
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'
|
RPC_DEFAULT_VERSION = '2.0'
|
||||||
TOPIC = constants.BACKUP_TOPIC
|
TOPIC = constants.BACKUP_TOPIC
|
||||||
BINARY = 'cinder-backup'
|
BINARY = 'cinder-backup'
|
||||||
@ -100,3 +101,13 @@ class BackupAPI(rpc.RPCAPI):
|
|||||||
"on host %(host)s.", {'host': host})
|
"on host %(host)s.", {'host': host})
|
||||||
cctxt = self._get_cctxt(server=host)
|
cctxt = self._get_cctxt(server=host)
|
||||||
return cctxt.call(ctxt, 'check_support_to_force_delete')
|
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
|
DB_MAX_INT = 0x7FFFFFFF
|
||||||
|
|
||||||
# The cinder services binaries and topics' names
|
# The cinder services binaries and topics' names
|
||||||
|
API_BINARY = "cinder-api"
|
||||||
SCHEDULER_BINARY = "cinder-scheduler"
|
SCHEDULER_BINARY = "cinder-scheduler"
|
||||||
VOLUME_BINARY = "cinder-volume"
|
VOLUME_BINARY = "cinder-volume"
|
||||||
BACKUP_BINARY = "cinder-backup"
|
BACKUP_BINARY = "cinder-backup"
|
||||||
|
@ -65,6 +65,7 @@ from cinder import exception
|
|||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder import rpc
|
from cinder import rpc
|
||||||
from cinder.scheduler import rpcapi as scheduler_rpcapi
|
from cinder.scheduler import rpcapi as scheduler_rpcapi
|
||||||
|
from cinder import utils
|
||||||
|
|
||||||
from eventlet import greenpool
|
from eventlet import greenpool
|
||||||
|
|
||||||
@ -144,6 +145,15 @@ class Manager(base.Base, PeriodicTasks):
|
|||||||
rpc.LAST_OBJ_VERSIONS = {}
|
rpc.LAST_OBJ_VERSIONS = {}
|
||||||
rpc.LAST_RPC_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):
|
class ThreadPoolManager(Manager):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -41,3 +41,4 @@ def register_all():
|
|||||||
__import__('cinder.objects.group')
|
__import__('cinder.objects.group')
|
||||||
__import__('cinder.objects.group_snapshot')
|
__import__('cinder.objects.group_snapshot')
|
||||||
__import__('cinder.objects.manageableresources')
|
__import__('cinder.objects.manageableresources')
|
||||||
|
__import__('cinder.objects.dynamic_log')
|
||||||
|
@ -131,6 +131,7 @@ OBJ_VERSIONS.add('1.21', {'ManageableSnapshot': '1.0',
|
|||||||
'ManageableSnapshotList': '1.0'})
|
'ManageableSnapshotList': '1.0'})
|
||||||
OBJ_VERSIONS.add('1.22', {'Snapshot': '1.4'})
|
OBJ_VERSIONS.add('1.22', {'Snapshot': '1.4'})
|
||||||
OBJ_VERSIONS.add('1.23', {'VolumeAttachment': '1.2'})
|
OBJ_VERSIONS.add('1.23', {'VolumeAttachment': '1.2'})
|
||||||
|
OBJ_VERSIONS.add('1.24', {'LogLevel': '1.0', 'LogLevelList': '1.0'})
|
||||||
|
|
||||||
|
|
||||||
class CinderObjectRegistry(base.VersionedObjectRegistry):
|
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.4 - Adds work_cleanup and do_cleanup methods.
|
||||||
3.5 - Make notify_service_capabilities support A/A
|
3.5 - Make notify_service_capabilities support A/A
|
||||||
3.6 - Removed create_consistencygroup method
|
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'
|
RPC_DEFAULT_VERSION = '3.0'
|
||||||
TOPIC = constants.SCHEDULER_TOPIC
|
TOPIC = constants.SCHEDULER_TOPIC
|
||||||
BINARY = 'cinder-scheduler'
|
BINARY = 'cinder-scheduler'
|
||||||
@ -208,3 +209,13 @@ class SchedulerAPI(rpc.RPCAPI):
|
|||||||
"""Perform this scheduler's resource cleanup as per cleanup_request."""
|
"""Perform this scheduler's resource cleanup as per cleanup_request."""
|
||||||
cctxt = self.client.prepare(version='3.4')
|
cctxt = self.client.prepare(version='3.4')
|
||||||
cctxt.cast(ctxt, 'do_cleanup', cleanup_request=cleanup_request)
|
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
|
import ddt
|
||||||
from iso8601 import iso8601
|
from iso8601 import iso8601
|
||||||
import mock
|
import mock
|
||||||
|
from oslo_config import cfg
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
import webob.exc
|
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.api.openstack import api_version_request as api_version
|
||||||
from cinder import context
|
from cinder import context
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
|
from cinder import objects
|
||||||
from cinder import test
|
from cinder import test
|
||||||
from cinder.tests.unit.api import fakes
|
from cinder.tests.unit.api import fakes
|
||||||
from cinder.tests.unit import fake_constants as fake
|
from cinder.tests.unit import fake_constants as fake
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
fake_services_list = [
|
fake_services_list = [
|
||||||
{'binary': 'cinder-scheduler',
|
{'binary': 'cinder-scheduler',
|
||||||
'host': 'host1',
|
'host': 'host1',
|
||||||
@ -858,3 +863,160 @@ class ServicesTest(test.TestCase):
|
|||||||
req = fakes.HTTPRequest.blank(url)
|
req = fakes.HTTPRequest.blank(url)
|
||||||
self.assertRaises(exception.InvalidInput,
|
self.assertRaises(exception.InvalidInput,
|
||||||
self.controller.update, req, method, {})
|
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
|
Unit Tests for cinder.backup.rpcapi
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
from cinder.backup import rpcapi as backup_rpcapi
|
from cinder.backup import rpcapi as backup_rpcapi
|
||||||
|
from cinder import objects
|
||||||
from cinder import test
|
from cinder import test
|
||||||
from cinder.tests.unit.backup import fake_backup
|
from cinder.tests.unit.backup import fake_backup
|
||||||
from cinder.tests.unit import fake_constants as fake
|
from cinder.tests.unit import fake_constants as fake
|
||||||
@ -79,3 +82,23 @@ class BackupRPCAPITestCase(test.RPCAPITestCase):
|
|||||||
server='fake_volume_host',
|
server='fake_volume_host',
|
||||||
host='fake_volume_host',
|
host='fake_volume_host',
|
||||||
retval=True)
|
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',
|
'CGSnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||||
'ConsistencyGroup': '1.4-7bf01a79b82516639fc03cd3ab6d9c01',
|
'ConsistencyGroup': '1.4-7bf01a79b82516639fc03cd3ab6d9c01',
|
||||||
'ConsistencyGroupList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
'ConsistencyGroupList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||||
|
'LogLevel': '1.0-7a8200b6b5063b33ec7b569dc6be66d2',
|
||||||
|
'LogLevelList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||||
'ManageableSnapshot': '1.0-5be933366eb17d12db0115c597158d0d',
|
'ManageableSnapshot': '1.0-5be933366eb17d12db0115c597158d0d',
|
||||||
'ManageableSnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
'ManageableSnapshotList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||||
'ManageableVolume': '1.0-5fd0152237ec9dfb7b5c7095b8b09ffa',
|
'ManageableVolume': '1.0-5fd0152237ec9dfb7b5c7095b8b09ffa',
|
||||||
|
@ -223,3 +223,23 @@ class SchedulerRPCAPITestCase(test.RPCAPITestCase):
|
|||||||
self.context,
|
self.context,
|
||||||
cleanup_request)
|
cleanup_request)
|
||||||
can_send_mock.assert_called_once_with('3.4')
|
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')
|
group='oslo_messaging_notifications')
|
||||||
result = self._decorated_method()
|
result = self._decorated_method()
|
||||||
self.assertEqual(utils.DO_NOTHING, result)
|
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,
|
expected_kwargs_diff=expected_kwargs_diff,
|
||||||
version=version)
|
version=version)
|
||||||
can_send_version.assert_has_calls([mock.call('3.10')])
|
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 f(*args, **kwargs)
|
||||||
return DO_NOTHING
|
return DO_NOTHING
|
||||||
return wrapped
|
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,
|
3.11 - Removes create_consistencygroup, delete_consistencygroup,
|
||||||
create_cgsnapshot, delete_cgsnapshot, update_consistencygroup,
|
create_cgsnapshot, delete_cgsnapshot, update_consistencygroup,
|
||||||
and create_consistencygroup_from_src.
|
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'
|
RPC_DEFAULT_VERSION = '3.0'
|
||||||
TOPIC = constants.VOLUME_TOPIC
|
TOPIC = constants.VOLUME_TOPIC
|
||||||
BINARY = 'cinder-volume'
|
BINARY = 'cinder-volume'
|
||||||
@ -426,3 +427,13 @@ class VolumeAPI(rpc.RPCAPI):
|
|||||||
# cinder.manager.CleanableManager unless in the future we overwrite it
|
# cinder.manager.CleanableManager unless in the future we overwrite it
|
||||||
# in cinder.volume.manager
|
# in cinder.volume.manager
|
||||||
cctxt.cast(ctxt, 'do_cleanup', cleanup_request=cleanup_request)
|
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