diff --git a/cinder/api/contrib/services.py b/cinder/api/contrib/services.py index 6b959f20ea2..5ce7ac2f961 100644 --- a/cinder/api/contrib/services.py +++ b/cinder/api/contrib/services.py @@ -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")) diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index ee33efd8222..4de2fbf3e75 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -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" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 2d2dc1a0a11..088526070ac 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -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. diff --git a/cinder/backup/rpcapi.py b/cinder/backup/rpcapi.py index 7126acbd365..0988fc7cdbc 100644 --- a/cinder/backup/rpcapi.py +++ b/cinder/backup/rpcapi.py @@ -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) diff --git a/cinder/common/constants.py b/cinder/common/constants.py index f7af0c916bf..d93960d2dce 100644 --- a/cinder/common/constants.py +++ b/cinder/common/constants.py @@ -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" diff --git a/cinder/manager.py b/cinder/manager.py index fb7b98c37d2..f9582979bee 100644 --- a/cinder/manager.py +++ b/cinder/manager.py @@ -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): diff --git a/cinder/objects/__init__.py b/cinder/objects/__init__.py index af38e187da4..0414627217e 100644 --- a/cinder/objects/__init__.py +++ b/cinder/objects/__init__.py @@ -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') diff --git a/cinder/objects/base.py b/cinder/objects/base.py index ce1cd4de485..9afab74c2c6 100644 --- a/cinder/objects/base.py +++ b/cinder/objects/base.py @@ -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): diff --git a/cinder/objects/dynamic_log.py b/cinder/objects/dynamic_log.py new file mode 100644 index 00000000000..b72e82789de --- /dev/null +++ b/cinder/objects/dynamic_log.py @@ -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'), + } diff --git a/cinder/scheduler/rpcapi.py b/cinder/scheduler/rpcapi.py index 6510d3edd71..00c531ef08b 100644 --- a/cinder/scheduler/rpcapi.py +++ b/cinder/scheduler/rpcapi.py @@ -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) diff --git a/cinder/tests/unit/api/contrib/test_services.py b/cinder/tests/unit/api/contrib/test_services.py index ed8384c763d..f977a370292 100644 --- a/cinder/tests/unit/api/contrib/test_services.py +++ b/cinder/tests/unit/api/contrib/test_services.py @@ -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) diff --git a/cinder/tests/unit/backup/test_rpcapi.py b/cinder/tests/unit/backup/test_rpcapi.py index 66b40774739..5d6c6f88f8f 100644 --- a/cinder/tests/unit/backup/test_rpcapi.py +++ b/cinder/tests/unit/backup/test_rpcapi.py @@ -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') diff --git a/cinder/tests/unit/objects/test_objects.py b/cinder/tests/unit/objects/test_objects.py index d311427d8dd..8bb590eda6e 100644 --- a/cinder/tests/unit/objects/test_objects.py +++ b/cinder/tests/unit/objects/test_objects.py @@ -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', diff --git a/cinder/tests/unit/scheduler/test_rpcapi.py b/cinder/tests/unit/scheduler/test_rpcapi.py index 6d9e2be1dde..d3863dd9195 100644 --- a/cinder/tests/unit/scheduler/test_rpcapi.py +++ b/cinder/tests/unit/scheduler/test_rpcapi.py @@ -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') diff --git a/cinder/tests/unit/test_manager.py b/cinder/tests/unit/test_manager.py new file mode 100644 index 00000000000..f51e3cc34ea --- /dev/null +++ b/cinder/tests/unit/test_manager.py @@ -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)) diff --git a/cinder/tests/unit/test_utils.py b/cinder/tests/unit/test_utils.py index 6e72a712602..f27da0f7ef4 100644 --- a/cinder/tests/unit/test_utils.py +++ b/cinder/tests/unit/test_utils.py @@ -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]) diff --git a/cinder/tests/unit/volume/test_rpcapi.py b/cinder/tests/unit/volume/test_rpcapi.py index 736f80f3508..cc9efe05d4d 100644 --- a/cinder/tests/unit/volume/test_rpcapi.py +++ b/cinder/tests/unit/volume/test_rpcapi.py @@ -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') diff --git a/cinder/utils.py b/cinder/utils.py index 879ee990caa..42c36022546 100644 --- a/cinder/utils.py +++ b/cinder/utils.py @@ -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)} diff --git a/cinder/volume/rpcapi.py b/cinder/volume/rpcapi.py index 25de3438c7a..c83008f3167 100644 --- a/cinder/volume/rpcapi.py +++ b/cinder/volume/rpcapi.py @@ -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) diff --git a/releasenotes/notes/service_dynamic_log_change-55147d288be903f1.yaml b/releasenotes/notes/service_dynamic_log_change-55147d288be903f1.yaml new file mode 100644 index 00000000000..b9f0138ac91 --- /dev/null +++ b/releasenotes/notes/service_dynamic_log_change-55147d288be903f1.yaml @@ -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.