1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-arguments
9 from typing
import Any
, Dict
, List
, Optional
10 from unittest
import mock
11 from unittest
.mock
import Mock
14 from cherrypy
._cptools
import HandlerWrapperTool
15 from cherrypy
.test
import helper
16 from mgr_module
import HandleCommandResult
17 from orchestrator
import HostSpec
, InventoryHost
18 from pyfakefs
import fake_filesystem
21 from ..controllers
import generate_controller_routes
, json_error_page
22 from ..controllers
._version
import APIVersion
23 from ..module
import Module
24 from ..plugins
import PLUGIN_MANAGER
, debug
, feature_toggles
# noqa
25 from ..services
.auth
import AuthManagerTool
26 from ..services
.exception
import dashboard_exception_handler
27 from ..tools
import RequestLoggingTool
29 PLUGIN_MANAGER
.hook
.init()
30 PLUGIN_MANAGER
.hook
.register_commands()
33 logger
= logging
.getLogger('tests')
36 class ModuleTestClass(Module
):
37 """Dashboard module subclass for testing the module methods."""
39 def __init__(self
) -> None:
42 def _unconfigure_logging(self
) -> None:
46 class CmdException(Exception):
47 def __init__(self
, retcode
, message
):
48 super(CmdException
, self
).__init
__(message
)
49 self
.retcode
= retcode
52 class KVStoreMockMixin(object):
56 def mock_set_module_option(cls
, attr
, val
):
57 cls
.CONFIG_KEY_DICT
[attr
] = val
60 def mock_get_module_option(cls
, attr
, default
=None):
61 return cls
.CONFIG_KEY_DICT
.get(attr
, default
)
64 def mock_kv_store(cls
):
65 cls
.CONFIG_KEY_DICT
.clear()
66 mgr
.set_module_option
.side_effect
= cls
.mock_set_module_option
67 mgr
.get_module_option
.side_effect
= cls
.mock_get_module_option
69 mgr
.set_store
.side_effect
= cls
.mock_set_module_option
70 mgr
.get_store
.side_effect
= cls
.mock_get_module_option
73 def get_key(cls
, key
):
74 return cls
.CONFIG_KEY_DICT
.get(key
, None)
77 # pylint: disable=protected-access
78 class CLICommandTestMixin(KVStoreMockMixin
):
79 _dashboard_module
= ModuleTestClass()
82 def exec_cmd(cls
, cmd
, **kwargs
):
83 inbuf
= kwargs
['inbuf'] if 'inbuf' in kwargs
else None
84 cmd_dict
= {'prefix': 'dashboard {}'.format(cmd
)}
85 cmd_dict
.update(kwargs
)
87 result
= HandleCommandResult(*cls
._dashboard
_module
._handle
_command
(inbuf
, cmd_dict
))
90 raise CmdException(result
.retval
, result
.stderr
)
92 return json
.loads(result
.stdout
)
97 class FakeFsMixin(object):
98 fs
= fake_filesystem
.FakeFilesystem()
99 f_open
= fake_filesystem
.FakeFileOpen(fs
)
100 f_os
= fake_filesystem
.FakeOsModule(fs
)
101 builtins_open
= 'builtins.open'
104 class ControllerTestCase(helper
.CPWebCase
):
105 _endpoints_cache
= {}
108 def setup_controllers(cls
, ctrl_classes
, base_url
='', cp_config
: Dict
[str, Any
] = None):
109 if not isinstance(ctrl_classes
, list):
110 ctrl_classes
= [ctrl_classes
]
111 mapper
= cherrypy
.dispatch
.RoutesDispatcher()
113 for ctrl
in ctrl_classes
:
115 'tools.dashboard_exception_handler.on': True,
116 'tools.authenticate.on': False
119 ctrl
._cp
_config
.update(cp_config
)
122 # We need to cache the controller endpoints because
123 # BaseController#endpoints method is not idempontent
124 # and a controller might be needed by more than one
126 if ctrl
not in cls
._endpoints
_cache
:
127 ctrl_endpoints
= ctrl
.endpoints()
128 cls
._endpoints
_cache
[ctrl
] = ctrl_endpoints
130 ctrl_endpoints
= cls
._endpoints
_cache
[ctrl
]
131 for endpoint
in ctrl_endpoints
:
133 endpoint_list
.append(endpoint
)
134 endpoint_list
= sorted(endpoint_list
, key
=lambda e
: e
.url
)
135 for endpoint
in endpoint_list
:
136 generate_controller_routes(endpoint
, mapper
, base_url
)
139 cherrypy
.tree
.mount(None, config
={
140 base_url
: {'request.dispatch': mapper
}})
142 _request_logging
= False
147 cherrypy
.tools
.authenticate
= AuthManagerTool()
148 cherrypy
.tools
.dashboard_exception_handler
= HandlerWrapperTool(dashboard_exception_handler
,
150 cherrypy
.config
.update({
151 'error_page.default': json_error_page
,
152 'tools.json_in.on': True,
153 'tools.json_in.force': False
155 PLUGIN_MANAGER
.hook
.configure_cherrypy(config
=cherrypy
.config
)
157 if cls
._request
_logging
:
158 cherrypy
.tools
.request_logging
= RequestLoggingTool()
159 cherrypy
.config
.update({'tools.request_logging.on': True})
162 def tearDownClass(cls
):
163 if cls
._request
_logging
:
164 cherrypy
.config
.update({'tools.request_logging.on': False})
166 def _request(self
, url
, method
, data
=None, headers
=None, version
=APIVersion
.DEFAULT
):
170 h
= [('Accept', version
.to_mime_type()),
171 ('Content-Length', '0')]
176 if version
is not None:
177 h
= [('Accept', version
.to_mime_type()),
178 ('Content-Type', 'application/json'),
179 ('Content-Length', str(len(b
)))]
182 h
= [('Content-Type', 'application/json'),
183 ('Content-Length', str(len(b
)))]
187 self
.getPage(url
, method
=method
, body
=b
, headers
=h
)
189 def _get(self
, url
, headers
=None, version
=APIVersion
.DEFAULT
):
190 self
._request
(url
, 'GET', headers
=headers
, version
=version
)
192 def _post(self
, url
, data
=None, version
=APIVersion
.DEFAULT
):
193 self
._request
(url
, 'POST', data
, version
=version
)
195 def _delete(self
, url
, data
=None, version
=APIVersion
.DEFAULT
):
196 self
._request
(url
, 'DELETE', data
, version
=version
)
198 def _put(self
, url
, data
=None, version
=APIVersion
.DEFAULT
):
199 self
._request
(url
, 'PUT', data
, version
=version
)
201 def _task_request(self
, method
, url
, data
, timeout
, version
=APIVersion
.DEFAULT
):
202 self
._request
(url
, method
, data
, version
=version
)
203 if self
.status
!= '202 Accepted':
204 logger
.info("task finished immediately")
207 res
= self
.json_body()
208 self
.assertIsInstance(res
, dict)
209 self
.assertIn('name', res
)
210 self
.assertIn('metadata', res
)
212 task_name
= res
['name']
213 task_metadata
= res
['metadata']
215 thread
= Waiter(task_name
, task_metadata
, self
, version
)
217 status
= thread
.ev
.wait(timeout
)
222 raise Exception("Waiting for task ({}, {}) to finish timed out"
223 .format(task_name
, task_metadata
))
224 logger
.info("task (%s, %s) finished", task_name
, task_metadata
)
225 if thread
.res_task
['success']:
226 self
.body
= json
.dumps(thread
.res_task
['ret_value'])
227 self
._set
_success
_status
(method
)
229 if 'status' in thread
.res_task
['exception']:
230 self
.status
= thread
.res_task
['exception']['status']
233 self
.body
= json
.dumps(thread
.res_task
['exception'])
235 def _set_success_status(self
, method
):
237 self
.status
= '201 Created'
238 elif method
== 'PUT':
239 self
.status
= '200 OK'
240 elif method
== 'DELETE':
241 self
.status
= '204 No Content'
243 def _task_post(self
, url
, data
=None, timeout
=60, version
=APIVersion
.DEFAULT
):
244 self
._task
_request
('POST', url
, data
, timeout
, version
=version
)
246 def _task_delete(self
, url
, timeout
=60, version
=APIVersion
.DEFAULT
):
247 self
._task
_request
('DELETE', url
, None, timeout
, version
=version
)
249 def _task_put(self
, url
, data
=None, timeout
=60, version
=APIVersion
.DEFAULT
):
250 self
._task
_request
('PUT', url
, data
, timeout
, version
=version
)
253 body_str
= self
.body
.decode('utf-8') if isinstance(self
.body
, bytes
) else self
.body
254 return json
.loads(body_str
)
256 def assertJsonBody(self
, data
, msg
=None): # noqa: N802
257 """Fail if value != self.body."""
258 json_body
= self
.json_body()
259 if data
!= json_body
:
261 msg
= 'expected body:\n%r\n\nactual body:\n%r' % (
263 self
._handlewebError
(msg
)
265 def assertInJsonBody(self
, data
, msg
=None): # noqa: N802
266 json_body
= self
.json_body()
267 if data
not in json_body
:
269 msg
= 'expected %r to be in %r' % (data
, json_body
)
270 self
._handlewebError
(msg
)
274 """Test class for returning predefined values"""
277 def get_mgr_no_services(cls
):
278 mgr
.get
= Mock(return_value
={})
284 def get_daemons(cls
):
285 mgr
.get
= Mock(return_value
={'services': {'rgw': {'daemons': {
287 'addr': '192.168.178.3:49774/1534999298',
289 'frontend_config#0': 'beast port=8000',
291 'realm_name': 'realm1',
292 'zonegroup_name': 'zonegroup1',
297 'addr': '[2001:db8:85a3::8a2e:370:7334]:49774/1534999298',
299 'frontend_config#0': 'civetweb port=8002',
301 'realm_name': 'realm2',
302 'zonegroup_name': 'zonegroup2',
309 def get_settings(cls
):
311 'RGW_API_ACCESS_KEY': 'fake-access-key',
312 'RGW_API_SECRET_KEY': 'fake-secret-key',
314 mgr
.get_module_option
= Mock(side_effect
=settings
.get
)
317 # pylint: disable=protected-access
318 class Waiter(threading
.Thread
):
319 def __init__(self
, task_name
, task_metadata
, tc
, version
):
320 super(Waiter
, self
).__init
__()
321 self
.task_name
= task_name
322 self
.task_metadata
= task_metadata
323 self
.ev
= threading
.Event()
327 self
.version
= version
331 while running
and not self
.abort
:
332 logger
.info("task (%s, %s) is still executing", self
.task_name
,
335 self
.tc
._get
('/api/task?name={}'.format(self
.task_name
), version
=self
.version
)
336 res
= self
.tc
.json_body()
337 for task
in res
['finished_tasks']:
338 if task
['metadata'] == self
.task_metadata
:
345 @contextlib.contextmanager
346 def patch_orch(available
: bool, missing_features
: Optional
[List
[str]] = None,
347 hosts
: Optional
[List
[HostSpec
]] = None,
348 inventory
: Optional
[List
[dict]] = None):
349 with mock
.patch('dashboard.controllers.orchestrator.OrchClient.instance') as instance
:
350 fake_client
= mock
.Mock()
351 fake_client
.available
.return_value
= available
352 fake_client
.get_missing_features
.return_value
= missing_features
354 if hosts
is not None:
355 fake_client
.hosts
.list.return_value
= hosts
357 if inventory
is not None:
358 def _list_inventory(hosts
=None, refresh
=False): # pylint: disable=unused-argument
360 for inv_host
in inventory
:
361 if hosts
is None or inv_host
['name'] in hosts
:
362 inv_hosts
.append(InventoryHost
.from_json(inv_host
))
364 fake_client
.inventory
.list.side_effect
= _list_inventory
366 instance
.return_value
= fake_client