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