]> git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/mgr/dashboard/helper.py
64cbba9e3f623a69986a276651a91c1c81b574f2
[ceph.git] / ceph / qa / tasks / mgr / dashboard / helper.py
1 # -*- coding: utf-8 -*-
2 # pylint: disable=W0212,too-many-return-statements
3 from __future__ import absolute_import
4
5 import json
6 import logging
7 from collections import namedtuple
8 import time
9
10 import requests
11 import six
12 from teuthology.exceptions import CommandFailedError
13
14 from tasks.mgr.mgr_test_case import MgrTestCase
15
16
17 log = logging.getLogger(__name__)
18
19
20 class DashboardTestCase(MgrTestCase):
21 # Display full error diffs
22 maxDiff = None
23
24 # Increased x3 (20 -> 60)
25 TIMEOUT_HEALTH_CLEAR = 60
26
27 MGRS_REQUIRED = 2
28 MDSS_REQUIRED = 1
29 REQUIRE_FILESYSTEM = True
30 CLIENTS_REQUIRED = 1
31 CEPHFS = False
32
33 _session = None # type: requests.sessions.Session
34 _token = None
35 _resp = None # type: requests.models.Response
36 _loggedin = False
37 _base_uri = None
38
39 AUTO_AUTHENTICATE = True
40
41 AUTH_ROLES = ['administrator']
42
43 @classmethod
44 def create_user(cls, username, password, roles=None,
45 force_password=True, cmd_args=None):
46 """
47 :param username: The name of the user.
48 :type username: str
49 :param password: The password.
50 :type password: str
51 :param roles: A list of roles.
52 :type roles: list
53 :param force_password: Force the use of the specified password. This
54 will bypass the password complexity check. Defaults to 'True'.
55 :type force_password: bool
56 :param cmd_args: Additional command line arguments for the
57 'ac-user-create' command.
58 :type cmd_args: None | list[str]
59 """
60 try:
61 cls._ceph_cmd(['dashboard', 'ac-user-show', username])
62 cls._ceph_cmd(['dashboard', 'ac-user-delete', username])
63 except CommandFailedError as ex:
64 if ex.exitstatus != 2:
65 raise ex
66
67 user_create_args = [
68 'dashboard', 'ac-user-create', username, password
69 ]
70 if force_password:
71 user_create_args.append('--force-password')
72 if cmd_args:
73 user_create_args.extend(cmd_args)
74 cls._ceph_cmd(user_create_args)
75
76 if roles:
77 set_roles_args = ['dashboard', 'ac-user-set-roles', username]
78 for idx, role in enumerate(roles):
79 if isinstance(role, str):
80 set_roles_args.append(role)
81 else:
82 assert isinstance(role, dict)
83 rolename = 'test_role_{}'.format(idx)
84 try:
85 cls._ceph_cmd(['dashboard', 'ac-role-show', rolename])
86 cls._ceph_cmd(['dashboard', 'ac-role-delete', rolename])
87 except CommandFailedError as ex:
88 if ex.exitstatus != 2:
89 raise ex
90 cls._ceph_cmd(['dashboard', 'ac-role-create', rolename])
91 for mod, perms in role.items():
92 args = ['dashboard', 'ac-role-add-scope-perms', rolename, mod]
93 args.extend(perms)
94 cls._ceph_cmd(args)
95 set_roles_args.append(rolename)
96 cls._ceph_cmd(set_roles_args)
97
98 @classmethod
99 def login(cls, username, password, set_cookies=False):
100 if cls._loggedin:
101 cls.logout()
102 cls._post('/api/auth', {'username': username,
103 'password': password}, set_cookies=set_cookies)
104 cls._assertEq(cls._resp.status_code, 201)
105 cls._token = cls.jsonBody()['token']
106 cls._loggedin = True
107
108 @classmethod
109 def logout(cls, set_cookies=False):
110 if cls._loggedin:
111 cls._post('/api/auth/logout', set_cookies=set_cookies)
112 cls._assertEq(cls._resp.status_code, 200)
113 cls._token = None
114 cls._loggedin = False
115
116 @classmethod
117 def delete_user(cls, username, roles=None):
118 if roles is None:
119 roles = []
120 cls._ceph_cmd(['dashboard', 'ac-user-delete', username])
121 for idx, role in enumerate(roles):
122 if isinstance(role, dict):
123 cls._ceph_cmd(['dashboard', 'ac-role-delete', 'test_role_{}'.format(idx)])
124
125 @classmethod
126 def RunAs(cls, username, password, roles=None, force_password=True,
127 cmd_args=None, login=True):
128 def wrapper(func):
129 def execute(self, *args, **kwargs):
130 self.create_user(username, password, roles,
131 force_password, cmd_args)
132 if login:
133 self.login(username, password)
134 res = func(self, *args, **kwargs)
135 if login:
136 self.logout()
137 self.delete_user(username, roles)
138 return res
139
140 return execute
141
142 return wrapper
143
144 @classmethod
145 def set_jwt_token(cls, token):
146 cls._token = token
147
148 @classmethod
149 def setUpClass(cls):
150 super(DashboardTestCase, cls).setUpClass()
151 cls._assign_ports("dashboard", "ssl_server_port")
152 cls._load_module("dashboard")
153 cls._base_uri = cls._get_uri("dashboard").rstrip('/')
154
155 if cls.CEPHFS:
156 cls.mds_cluster.clear_firewall()
157
158 # To avoid any issues with e.g. unlink bugs, we destroy and recreate
159 # the filesystem rather than just doing a rm -rf of files
160 cls.mds_cluster.mds_stop()
161 cls.mds_cluster.mds_fail()
162 cls.mds_cluster.delete_all_filesystems()
163 cls.fs = None # is now invalid!
164
165 cls.fs = cls.mds_cluster.newfs(create=True)
166 cls.fs.mds_restart()
167
168 # In case some test messed with auth caps, reset them
169 # pylint: disable=not-an-iterable
170 client_mount_ids = [m.client_id for m in cls.mounts]
171 for client_id in client_mount_ids:
172 cls.mds_cluster.mon_manager.raw_cluster_cmd_result(
173 'auth', 'caps', "client.{0}".format(client_id),
174 'mds', 'allow',
175 'mon', 'allow r',
176 'osd', 'allow rw pool={0}'.format(cls.fs.get_data_pool_name()))
177
178 # wait for mds restart to complete...
179 cls.fs.wait_for_daemons()
180
181 cls._token = None
182 cls._session = requests.Session()
183 cls._resp = None
184
185 cls.create_user('admin', 'admin', cls.AUTH_ROLES)
186 if cls.AUTO_AUTHENTICATE:
187 cls.login('admin', 'admin')
188
189 def setUp(self):
190 super(DashboardTestCase, self).setUp()
191 if not self._loggedin and self.AUTO_AUTHENTICATE:
192 self.login('admin', 'admin')
193 self.wait_for_health_clear(self.TIMEOUT_HEALTH_CLEAR)
194
195 @classmethod
196 def tearDownClass(cls):
197 super(DashboardTestCase, cls).tearDownClass()
198
199 # pylint: disable=inconsistent-return-statements, too-many-branches
200 @classmethod
201 def _request(cls, url, method, data=None, params=None, set_cookies=False):
202 url = "{}{}".format(cls._base_uri, url)
203 log.info("Request %s to %s", method, url)
204 headers = {}
205 cookies = {}
206 if cls._token:
207 if set_cookies:
208 cookies['token'] = cls._token
209 else:
210 headers['Authorization'] = "Bearer {}".format(cls._token)
211
212 if set_cookies:
213 if method == 'GET':
214 cls._resp = cls._session.get(url, params=params, verify=False,
215 headers=headers, cookies=cookies)
216 elif method == 'POST':
217 cls._resp = cls._session.post(url, json=data, params=params,
218 verify=False, headers=headers, cookies=cookies)
219 elif method == 'DELETE':
220 cls._resp = cls._session.delete(url, json=data, params=params,
221 verify=False, headers=headers, cookies=cookies)
222 elif method == 'PUT':
223 cls._resp = cls._session.put(url, json=data, params=params,
224 verify=False, headers=headers, cookies=cookies)
225 else:
226 assert False
227 else:
228 if method == 'GET':
229 cls._resp = cls._session.get(url, params=params, verify=False,
230 headers=headers)
231 elif method == 'POST':
232 cls._resp = cls._session.post(url, json=data, params=params,
233 verify=False, headers=headers)
234 elif method == 'DELETE':
235 cls._resp = cls._session.delete(url, json=data, params=params,
236 verify=False, headers=headers)
237 elif method == 'PUT':
238 cls._resp = cls._session.put(url, json=data, params=params,
239 verify=False, headers=headers)
240 else:
241 assert False
242 try:
243 if not cls._resp.ok:
244 # Output response for easier debugging.
245 log.error("Request response: %s", cls._resp.text)
246 content_type = cls._resp.headers['content-type']
247 if content_type == 'application/json' and cls._resp.text and cls._resp.text != "":
248 return cls._resp.json()
249 return cls._resp.text
250 except ValueError as ex:
251 log.exception("Failed to decode response: %s", cls._resp.text)
252 raise ex
253
254 @classmethod
255 def _get(cls, url, params=None, set_cookies=False):
256 return cls._request(url, 'GET', params=params, set_cookies=set_cookies)
257
258 @classmethod
259 def _view_cache_get(cls, url, retries=5):
260 retry = True
261 while retry and retries > 0:
262 retry = False
263 res = cls._get(url)
264 if isinstance(res, dict):
265 res = [res]
266 for view in res:
267 assert 'value' in view
268 if not view['value']:
269 retry = True
270 retries -= 1
271 if retries == 0:
272 raise Exception("{} view cache exceeded number of retries={}"
273 .format(url, retries))
274 return res
275
276 @classmethod
277 def _post(cls, url, data=None, params=None, set_cookies=False):
278 cls._request(url, 'POST', data, params, set_cookies=set_cookies)
279
280 @classmethod
281 def _delete(cls, url, data=None, params=None, set_cookies=False):
282 cls._request(url, 'DELETE', data, params, set_cookies=set_cookies)
283
284 @classmethod
285 def _put(cls, url, data=None, params=None, set_cookies=False):
286 cls._request(url, 'PUT', data, params, set_cookies=set_cookies)
287
288 @classmethod
289 def _assertEq(cls, v1, v2):
290 if not v1 == v2:
291 raise Exception("assertion failed: {} != {}".format(v1, v2))
292
293 @classmethod
294 def _assertIn(cls, v1, v2):
295 if v1 not in v2:
296 raise Exception("assertion failed: {} not in {}".format(v1, v2))
297
298 @classmethod
299 def _assertIsInst(cls, v1, v2):
300 if not isinstance(v1, v2):
301 raise Exception("assertion failed: {} not instance of {}".format(v1, v2))
302
303 # pylint: disable=too-many-arguments
304 @classmethod
305 def _task_request(cls, method, url, data, timeout, set_cookies=False):
306 res = cls._request(url, method, data, set_cookies=set_cookies)
307 cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 400, 403, 404])
308
309 if cls._resp.status_code == 403:
310 return None
311
312 if cls._resp.status_code != 202:
313 log.info("task finished immediately")
314 return res
315
316 cls._assertIn('name', res)
317 cls._assertIn('metadata', res)
318 task_name = res['name']
319 task_metadata = res['metadata']
320
321 retries = int(timeout)
322 res_task = None
323 while retries > 0 and not res_task:
324 retries -= 1
325 log.info("task (%s, %s) is still executing", task_name,
326 task_metadata)
327 time.sleep(1)
328 _res = cls._get('/api/task?name={}'.format(task_name))
329 cls._assertEq(cls._resp.status_code, 200)
330 executing_tasks = [task for task in _res['executing_tasks'] if
331 task['metadata'] == task_metadata]
332 finished_tasks = [task for task in _res['finished_tasks'] if
333 task['metadata'] == task_metadata]
334 if not executing_tasks and finished_tasks:
335 res_task = finished_tasks[0]
336
337 if retries <= 0:
338 raise Exception("Waiting for task ({}, {}) to finish timed out. {}"
339 .format(task_name, task_metadata, _res))
340
341 log.info("task (%s, %s) finished", task_name, task_metadata)
342 if res_task['success']:
343 if method == 'POST':
344 cls._resp.status_code = 201
345 elif method == 'PUT':
346 cls._resp.status_code = 200
347 elif method == 'DELETE':
348 cls._resp.status_code = 204
349 return res_task['ret_value']
350 else:
351 if 'status' in res_task['exception']:
352 cls._resp.status_code = res_task['exception']['status']
353 else:
354 cls._resp.status_code = 500
355 return res_task['exception']
356
357 @classmethod
358 def _task_post(cls, url, data=None, timeout=60, set_cookies=False):
359 return cls._task_request('POST', url, data, timeout, set_cookies=set_cookies)
360
361 @classmethod
362 def _task_delete(cls, url, timeout=60, set_cookies=False):
363 return cls._task_request('DELETE', url, None, timeout, set_cookies=set_cookies)
364
365 @classmethod
366 def _task_put(cls, url, data=None, timeout=60, set_cookies=False):
367 return cls._task_request('PUT', url, data, timeout, set_cookies=set_cookies)
368
369 @classmethod
370 def cookies(cls):
371 return cls._resp.cookies
372
373 @classmethod
374 def jsonBody(cls):
375 return cls._resp.json()
376
377 @classmethod
378 def reset_session(cls):
379 cls._session = requests.Session()
380
381 def assertSubset(self, data, biggerData):
382 for key, value in data.items():
383 self.assertEqual(biggerData[key], value)
384
385 def assertJsonBody(self, data):
386 body = self._resp.json()
387 self.assertEqual(body, data)
388
389 def assertJsonSubset(self, data):
390 self.assertSubset(data, self._resp.json())
391
392 def assertSchema(self, data, schema):
393 try:
394 return _validate_json(data, schema)
395 except _ValError as e:
396 self.assertEqual(data, str(e))
397
398 def assertSchemaBody(self, schema):
399 self.assertSchema(self.jsonBody(), schema)
400
401 def assertBody(self, body):
402 self.assertEqual(self._resp.text, body)
403
404 def assertStatus(self, status):
405 if isinstance(status, list):
406 self.assertIn(self._resp.status_code, status)
407 else:
408 self.assertEqual(self._resp.status_code, status)
409
410 def assertHeaders(self, headers):
411 for name, value in headers.items():
412 self.assertIn(name, self._resp.headers)
413 self.assertEqual(self._resp.headers[name], value)
414
415 def assertError(self, code=None, component=None, detail=None):
416 body = self._resp.json()
417 if code:
418 self.assertEqual(body['code'], code)
419 if component:
420 self.assertEqual(body['component'], component)
421 if detail:
422 self.assertEqual(body['detail'], detail)
423
424 @classmethod
425 def _ceph_cmd(cls, cmd):
426 res = cls.mgr_cluster.mon_manager.raw_cluster_cmd(*cmd)
427 log.info("command result: %s", res)
428 return res
429
430 @classmethod
431 def _ceph_cmd_result(cls, cmd):
432 exitstatus = cls.mgr_cluster.mon_manager.raw_cluster_cmd_result(*cmd)
433 log.info("command exit status: %d", exitstatus)
434 return exitstatus
435
436 def set_config_key(self, key, value):
437 self._ceph_cmd(['config-key', 'set', key, value])
438
439 def get_config_key(self, key):
440 return self._ceph_cmd(['config-key', 'get', key])
441
442 @classmethod
443 def _cmd(cls, args):
444 return cls.mgr_cluster.admin_remote.run(args=args)
445
446 @classmethod
447 def _rbd_cmd(cls, cmd):
448 args = ['rbd']
449 args.extend(cmd)
450 cls._cmd(args)
451
452 @classmethod
453 def _radosgw_admin_cmd(cls, cmd):
454 args = ['radosgw-admin']
455 args.extend(cmd)
456 cls._cmd(args)
457
458 @classmethod
459 def _rados_cmd(cls, cmd):
460 args = ['rados']
461 args.extend(cmd)
462 cls._cmd(args)
463
464 @classmethod
465 def mons(cls):
466 out = cls.ceph_cluster.mon_manager.raw_cluster_cmd('quorum_status')
467 j = json.loads(out)
468 return [mon['name'] for mon in j['monmap']['mons']]
469
470 @classmethod
471 def find_object_in_list(cls, key, value, iterable):
472 """
473 Get the first occurrence of an object within a list with
474 the specified key/value.
475 :param key: The name of the key.
476 :param value: The value to search for.
477 :param iterable: The list to process.
478 :return: Returns the found object or None.
479 """
480 for obj in iterable:
481 if key in obj and obj[key] == value:
482 return obj
483 return None
484
485
486 class JLeaf(namedtuple('JLeaf', ['typ', 'none'])):
487 def __new__(cls, typ, none=False):
488 if typ == str:
489 typ = six.string_types
490 return super(JLeaf, cls).__new__(cls, typ, none)
491
492
493 JList = namedtuple('JList', ['elem_typ'])
494
495 JTuple = namedtuple('JList', ['elem_typs'])
496
497 JUnion = namedtuple('JUnion', ['elem_typs'])
498
499 class JObj(namedtuple('JObj', ['sub_elems', 'allow_unknown', 'none', 'unknown_schema'])):
500 def __new__(cls, sub_elems, allow_unknown=False, none=False, unknown_schema=None):
501 """
502 :type sub_elems: dict[str, JAny | JLeaf | JList | JObj | type]
503 :type allow_unknown: bool
504 :type none: bool
505 :type unknown_schema: int, str, JAny | JLeaf | JList | JObj
506 :return:
507 """
508 return super(JObj, cls).__new__(cls, sub_elems, allow_unknown, none, unknown_schema)
509
510
511 JAny = namedtuple('JAny', ['none'])
512
513
514 class _ValError(Exception):
515 def __init__(self, msg, path):
516 path_str = ''.join('[{}]'.format(repr(p)) for p in path)
517 super(_ValError, self).__init__('In `input{}`: {}'.format(path_str, msg))
518
519
520 # pylint: disable=dangerous-default-value,inconsistent-return-statements
521 def _validate_json(val, schema, path=[]):
522 """
523 >>> d = {'a': 1, 'b': 'x', 'c': range(10)}
524 ... ds = JObj({'a': int, 'b': str, 'c': JList(int)})
525 ... _validate_json(d, ds)
526 True
527 >>> _validate_json({'num': 1}, JObj({'num': JUnion([int,float])}))
528 True
529 >>> _validate_json({'num': 'a'}, JObj({'num': JUnion([int,float])}))
530 False
531 """
532 if isinstance(schema, JAny):
533 if not schema.none and val is None:
534 raise _ValError('val is None', path)
535 return True
536 if isinstance(schema, JLeaf):
537 if schema.none and val is None:
538 return True
539 if not isinstance(val, schema.typ):
540 raise _ValError('val not of type {}'.format(schema.typ), path)
541 return True
542 if isinstance(schema, JList):
543 if not isinstance(val, list):
544 raise _ValError('val="{}" is not a list'.format(val), path)
545 return all(_validate_json(e, schema.elem_typ, path + [i]) for i, e in enumerate(val))
546 if isinstance(schema, JTuple):
547 return all(_validate_json(val[i], typ, path + [i])
548 for i, typ in enumerate(schema.elem_typs))
549 if isinstance(schema, JUnion):
550 for typ in schema.elem_typs:
551 try:
552 if _validate_json(val, typ, path):
553 return True
554 except _ValError:
555 pass
556 return False
557 if isinstance(schema, JObj):
558 if val is None and schema.none:
559 return True
560 elif val is None:
561 raise _ValError('val is None', path)
562 if not hasattr(val, 'keys'):
563 raise _ValError('val="{}" is not a dict'.format(val), path)
564 missing_keys = set(schema.sub_elems.keys()).difference(set(val.keys()))
565 if missing_keys:
566 raise _ValError('missing keys: {}'.format(missing_keys), path)
567 unknown_keys = set(val.keys()).difference(set(schema.sub_elems.keys()))
568 if not schema.allow_unknown and unknown_keys:
569 raise _ValError('unknown keys: {}'.format(unknown_keys), path)
570 result = all(
571 _validate_json(val[key], sub_schema, path + [key])
572 for key, sub_schema in schema.sub_elems.items()
573 )
574 if unknown_keys and schema.allow_unknown and schema.unknown_schema:
575 result += all(
576 _validate_json(val[key], schema.unknown_schema, path + [key])
577 for key in unknown_keys
578 )
579 return result
580 if schema in [str, int, float, bool, six.string_types]:
581 return _validate_json(val, JLeaf(schema), path)
582
583 assert False, str(path)