]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/tests/__init__.py
import ceph 16.2.7
[ceph.git] / ceph / src / pybind / mgr / dashboard / tests / __init__.py
1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-arguments
3 from __future__ import absolute_import
4
5 import json
6 import logging
7 import threading
8 import time
9 from typing import Any, Dict
10 from unittest.mock import Mock
11
12 import cherrypy
13 from cherrypy._cptools import HandlerWrapperTool
14 from cherrypy.test import helper
15 from mgr_module import HandleCommandResult
16 from pyfakefs import fake_filesystem
17
18 from .. import mgr
19 from ..controllers import generate_controller_routes, json_error_page
20 from ..controllers._version import APIVersion
21 from ..module import Module
22 from ..plugins import PLUGIN_MANAGER, debug, feature_toggles # noqa
23 from ..services.auth import AuthManagerTool
24 from ..services.exception import dashboard_exception_handler
25 from ..tools import RequestLoggingTool
26
27 PLUGIN_MANAGER.hook.init()
28 PLUGIN_MANAGER.hook.register_commands()
29
30
31 logger = logging.getLogger('tests')
32
33
34 class ModuleTestClass(Module):
35 """Dashboard module subclass for testing the module methods."""
36
37 def __init__(self) -> None:
38 pass
39
40 def _unconfigure_logging(self) -> None:
41 pass
42
43
44 class CmdException(Exception):
45 def __init__(self, retcode, message):
46 super(CmdException, self).__init__(message)
47 self.retcode = retcode
48
49
50 class KVStoreMockMixin(object):
51 CONFIG_KEY_DICT = {}
52
53 @classmethod
54 def mock_set_module_option(cls, attr, val):
55 cls.CONFIG_KEY_DICT[attr] = val
56
57 @classmethod
58 def mock_get_module_option(cls, attr, default=None):
59 return cls.CONFIG_KEY_DICT.get(attr, default)
60
61 @classmethod
62 def mock_kv_store(cls):
63 cls.CONFIG_KEY_DICT.clear()
64 mgr.set_module_option.side_effect = cls.mock_set_module_option
65 mgr.get_module_option.side_effect = cls.mock_get_module_option
66 # kludge below
67 mgr.set_store.side_effect = cls.mock_set_module_option
68 mgr.get_store.side_effect = cls.mock_get_module_option
69
70 @classmethod
71 def get_key(cls, key):
72 return cls.CONFIG_KEY_DICT.get(key, None)
73
74
75 # pylint: disable=protected-access
76 class CLICommandTestMixin(KVStoreMockMixin):
77 _dashboard_module = ModuleTestClass()
78
79 @classmethod
80 def exec_cmd(cls, cmd, **kwargs):
81 inbuf = kwargs['inbuf'] if 'inbuf' in kwargs else None
82 cmd_dict = {'prefix': 'dashboard {}'.format(cmd)}
83 cmd_dict.update(kwargs)
84
85 result = HandleCommandResult(*cls._dashboard_module._handle_command(inbuf, cmd_dict))
86
87 if result.retval < 0:
88 raise CmdException(result.retval, result.stderr)
89 try:
90 return json.loads(result.stdout)
91 except ValueError:
92 return result.stdout
93
94
95 class FakeFsMixin(object):
96 fs = fake_filesystem.FakeFilesystem()
97 f_open = fake_filesystem.FakeFileOpen(fs)
98 f_os = fake_filesystem.FakeOsModule(fs)
99 builtins_open = 'builtins.open'
100
101
102 class ControllerTestCase(helper.CPWebCase):
103 _endpoints_cache = {}
104
105 @classmethod
106 def setup_controllers(cls, ctrl_classes, base_url='', cp_config: Dict[str, Any] = None):
107 if not isinstance(ctrl_classes, list):
108 ctrl_classes = [ctrl_classes]
109 mapper = cherrypy.dispatch.RoutesDispatcher()
110 endpoint_list = []
111 for ctrl in ctrl_classes:
112 ctrl._cp_config = {
113 'tools.dashboard_exception_handler.on': True,
114 'tools.authenticate.on': False
115 }
116 if cp_config:
117 ctrl._cp_config.update(cp_config)
118 inst = ctrl()
119
120 # We need to cache the controller endpoints because
121 # BaseController#endpoints method is not idempontent
122 # and a controller might be needed by more than one
123 # unit test.
124 if ctrl not in cls._endpoints_cache:
125 ctrl_endpoints = ctrl.endpoints()
126 cls._endpoints_cache[ctrl] = ctrl_endpoints
127
128 ctrl_endpoints = cls._endpoints_cache[ctrl]
129 for endpoint in ctrl_endpoints:
130 endpoint.inst = inst
131 endpoint_list.append(endpoint)
132 endpoint_list = sorted(endpoint_list, key=lambda e: e.url)
133 for endpoint in endpoint_list:
134 generate_controller_routes(endpoint, mapper, base_url)
135 if base_url == '':
136 base_url = '/'
137 cherrypy.tree.mount(None, config={
138 base_url: {'request.dispatch': mapper}})
139
140 _request_logging = False
141
142 @classmethod
143 def setUpClass(cls):
144 super().setUpClass()
145 cherrypy.tools.authenticate = AuthManagerTool()
146 cherrypy.tools.dashboard_exception_handler = HandlerWrapperTool(dashboard_exception_handler,
147 priority=31)
148 cherrypy.config.update({
149 'error_page.default': json_error_page,
150 'tools.json_in.on': True,
151 'tools.json_in.force': False
152 })
153 PLUGIN_MANAGER.hook.configure_cherrypy(config=cherrypy.config)
154
155 if cls._request_logging:
156 cherrypy.tools.request_logging = RequestLoggingTool()
157 cherrypy.config.update({'tools.request_logging.on': True})
158
159 @classmethod
160 def tearDownClass(cls):
161 if cls._request_logging:
162 cherrypy.config.update({'tools.request_logging.on': False})
163
164 def _request(self, url, method, data=None, headers=None, version=APIVersion.DEFAULT):
165 if not data:
166 b = None
167 if version:
168 h = [('Accept', version.to_mime_type()),
169 ('Content-Length', '0')]
170 else:
171 h = None
172 else:
173 b = json.dumps(data)
174 if version is not None:
175 h = [('Accept', version.to_mime_type()),
176 ('Content-Type', 'application/json'),
177 ('Content-Length', str(len(b)))]
178
179 else:
180 h = [('Content-Type', 'application/json'),
181 ('Content-Length', str(len(b)))]
182
183 if headers:
184 h = headers
185 self.getPage(url, method=method, body=b, headers=h)
186
187 def _get(self, url, headers=None, version=APIVersion.DEFAULT):
188 self._request(url, 'GET', headers=headers, version=version)
189
190 def _post(self, url, data=None, version=APIVersion.DEFAULT):
191 self._request(url, 'POST', data, version=version)
192
193 def _delete(self, url, data=None, version=APIVersion.DEFAULT):
194 self._request(url, 'DELETE', data, version=version)
195
196 def _put(self, url, data=None, version=APIVersion.DEFAULT):
197 self._request(url, 'PUT', data, version=version)
198
199 def _task_request(self, method, url, data, timeout, version=APIVersion.DEFAULT):
200 self._request(url, method, data, version=version)
201 if self.status != '202 Accepted':
202 logger.info("task finished immediately")
203 return
204
205 res = self.json_body()
206 self.assertIsInstance(res, dict)
207 self.assertIn('name', res)
208 self.assertIn('metadata', res)
209
210 task_name = res['name']
211 task_metadata = res['metadata']
212
213 # pylint: disable=protected-access
214 class Waiter(threading.Thread):
215 def __init__(self, task_name, task_metadata, tc):
216 super(Waiter, self).__init__()
217 self.task_name = task_name
218 self.task_metadata = task_metadata
219 self.ev = threading.Event()
220 self.abort = False
221 self.res_task = None
222 self.tc = tc
223
224 def run(self):
225 running = True
226 while running and not self.abort:
227 logger.info("task (%s, %s) is still executing", self.task_name,
228 self.task_metadata)
229 time.sleep(1)
230 self.tc._get('/api/task?name={}'.format(self.task_name), version=version)
231 res = self.tc.json_body()
232 for task in res['finished_tasks']:
233 if task['metadata'] == self.task_metadata:
234 # task finished
235 running = False
236 self.res_task = task
237 self.ev.set()
238
239 thread = Waiter(task_name, task_metadata, self)
240 thread.start()
241 status = thread.ev.wait(timeout)
242 if not status:
243 # timeout expired
244 thread.abort = True
245 thread.join()
246 raise Exception("Waiting for task ({}, {}) to finish timed out"
247 .format(task_name, task_metadata))
248 logger.info("task (%s, %s) finished", task_name, task_metadata)
249 if thread.res_task['success']:
250 self.body = json.dumps(thread.res_task['ret_value'])
251 if method == 'POST':
252 self.status = '201 Created'
253 elif method == 'PUT':
254 self.status = '200 OK'
255 elif method == 'DELETE':
256 self.status = '204 No Content'
257 return
258
259 if 'status' in thread.res_task['exception']:
260 self.status = thread.res_task['exception']['status']
261 else:
262 self.status = 500
263 self.body = json.dumps(thread.res_task['exception'])
264
265 def _task_post(self, url, data=None, timeout=60, version=APIVersion.DEFAULT):
266 self._task_request('POST', url, data, timeout, version=version)
267
268 def _task_delete(self, url, timeout=60, version=APIVersion.DEFAULT):
269 self._task_request('DELETE', url, None, timeout, version=version)
270
271 def _task_put(self, url, data=None, timeout=60, version=APIVersion.DEFAULT):
272 self._task_request('PUT', url, data, timeout, version=version)
273
274 def json_body(self):
275 body_str = self.body.decode('utf-8') if isinstance(self.body, bytes) else self.body
276 return json.loads(body_str)
277
278 def assertJsonBody(self, data, msg=None): # noqa: N802
279 """Fail if value != self.body."""
280 json_body = self.json_body()
281 if data != json_body:
282 if msg is None:
283 msg = 'expected body:\n%r\n\nactual body:\n%r' % (
284 data, json_body)
285 self._handlewebError(msg)
286
287 def assertInJsonBody(self, data, msg=None): # noqa: N802
288 json_body = self.json_body()
289 if data not in json_body:
290 if msg is None:
291 msg = 'expected %r to be in %r' % (data, json_body)
292 self._handlewebError(msg)
293
294
295 class Stub:
296 """Test class for returning predefined values"""
297
298 @classmethod
299 def get_mgr_no_services(cls):
300 mgr.get = Mock(return_value={})
301
302
303 class RgwStub(Stub):
304
305 @classmethod
306 def get_daemons(cls):
307 mgr.get = Mock(return_value={'services': {'rgw': {'daemons': {
308 '5297': {
309 'addr': '192.168.178.3:49774/1534999298',
310 'metadata': {
311 'frontend_config#0': 'beast port=8000',
312 'id': 'daemon1',
313 'realm_name': 'realm1',
314 'zonegroup_name': 'zonegroup1',
315 'zone_name': 'zone1'
316 }
317 },
318 '5398': {
319 'addr': '[2001:db8:85a3::8a2e:370:7334]:49774/1534999298',
320 'metadata': {
321 'frontend_config#0': 'civetweb port=8002',
322 'id': 'daemon2',
323 'realm_name': 'realm2',
324 'zonegroup_name': 'zonegroup2',
325 'zone_name': 'zone2'
326 }
327 }
328 }}}})
329
330 @classmethod
331 def get_settings(cls):
332 settings = {
333 'RGW_API_ACCESS_KEY': 'fake-access-key',
334 'RGW_API_SECRET_KEY': 'fake-secret-key',
335 }
336 mgr.get_module_option = Mock(side_effect=settings.get)