]>
git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/mgr/dashboard/helper.py
64cbba9e3f623a69986a276651a91c1c81b574f2
1 # -*- coding: utf-8 -*-
2 # pylint: disable=W0212,too-many-return-statements
3 from __future__
import absolute_import
7 from collections
import namedtuple
12 from teuthology
.exceptions
import CommandFailedError
14 from tasks
.mgr
.mgr_test_case
import MgrTestCase
17 log
= logging
.getLogger(__name__
)
20 class DashboardTestCase(MgrTestCase
):
21 # Display full error diffs
24 # Increased x3 (20 -> 60)
25 TIMEOUT_HEALTH_CLEAR
= 60
29 REQUIRE_FILESYSTEM
= True
33 _session
= None # type: requests.sessions.Session
35 _resp
= None # type: requests.models.Response
39 AUTO_AUTHENTICATE
= True
41 AUTH_ROLES
= ['administrator']
44 def create_user(cls
, username
, password
, roles
=None,
45 force_password
=True, cmd_args
=None):
47 :param username: The name of the user.
49 :param password: The password.
51 :param roles: A list of roles.
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]
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:
68 'dashboard', 'ac-user-create', username
, password
71 user_create_args
.append('--force-password')
73 user_create_args
.extend(cmd_args
)
74 cls
._ceph
_cmd
(user_create_args
)
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
)
82 assert isinstance(role
, dict)
83 rolename
= 'test_role_{}'.format(idx
)
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:
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
]
95 set_roles_args
.append(rolename
)
96 cls
._ceph
_cmd
(set_roles_args
)
99 def login(cls
, username
, password
, set_cookies
=False):
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']
109 def logout(cls
, set_cookies
=False):
111 cls
._post
('/api/auth/logout', set_cookies
=set_cookies
)
112 cls
._assertEq
(cls
._resp
.status_code
, 200)
114 cls
._loggedin
= False
117 def delete_user(cls
, username
, roles
=None):
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
)])
126 def RunAs(cls
, username
, password
, roles
=None, force_password
=True,
127 cmd_args
=None, login
=True):
129 def execute(self
, *args
, **kwargs
):
130 self
.create_user(username
, password
, roles
,
131 force_password
, cmd_args
)
133 self
.login(username
, password
)
134 res
= func(self
, *args
, **kwargs
)
137 self
.delete_user(username
, roles
)
145 def set_jwt_token(cls
, token
):
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('/')
156 cls
.mds_cluster
.clear_firewall()
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!
165 cls
.fs
= cls
.mds_cluster
.newfs(create
=True)
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
),
176 'osd', 'allow rw pool={0}'.format(cls
.fs
.get_data_pool_name()))
178 # wait for mds restart to complete...
179 cls
.fs
.wait_for_daemons()
182 cls
._session
= requests
.Session()
185 cls
.create_user('admin', 'admin', cls
.AUTH_ROLES
)
186 if cls
.AUTO_AUTHENTICATE
:
187 cls
.login('admin', 'admin')
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
)
196 def tearDownClass(cls
):
197 super(DashboardTestCase
, cls
).tearDownClass()
199 # pylint: disable=inconsistent-return-statements, too-many-branches
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
)
208 cookies
['token'] = cls
._token
210 headers
['Authorization'] = "Bearer {}".format(cls
._token
)
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
)
229 cls
._resp
= cls
._session
.get(url
, params
=params
, verify
=False,
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
)
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
)
255 def _get(cls
, url
, params
=None, set_cookies
=False):
256 return cls
._request
(url
, 'GET', params
=params
, set_cookies
=set_cookies
)
259 def _view_cache_get(cls
, url
, retries
=5):
261 while retry
and retries
> 0:
264 if isinstance(res
, dict):
267 assert 'value' in view
268 if not view
['value']:
272 raise Exception("{} view cache exceeded number of retries={}"
273 .format(url
, retries
))
277 def _post(cls
, url
, data
=None, params
=None, set_cookies
=False):
278 cls
._request
(url
, 'POST', data
, params
, set_cookies
=set_cookies
)
281 def _delete(cls
, url
, data
=None, params
=None, set_cookies
=False):
282 cls
._request
(url
, 'DELETE', data
, params
, set_cookies
=set_cookies
)
285 def _put(cls
, url
, data
=None, params
=None, set_cookies
=False):
286 cls
._request
(url
, 'PUT', data
, params
, set_cookies
=set_cookies
)
289 def _assertEq(cls
, v1
, v2
):
291 raise Exception("assertion failed: {} != {}".format(v1
, v2
))
294 def _assertIn(cls
, v1
, v2
):
296 raise Exception("assertion failed: {} not in {}".format(v1
, v2
))
299 def _assertIsInst(cls
, v1
, v2
):
300 if not isinstance(v1
, v2
):
301 raise Exception("assertion failed: {} not instance of {}".format(v1
, v2
))
303 # pylint: disable=too-many-arguments
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])
309 if cls
._resp
.status_code
== 403:
312 if cls
._resp
.status_code
!= 202:
313 log
.info("task finished immediately")
316 cls
._assertIn
('name', res
)
317 cls
._assertIn
('metadata', res
)
318 task_name
= res
['name']
319 task_metadata
= res
['metadata']
321 retries
= int(timeout
)
323 while retries
> 0 and not res_task
:
325 log
.info("task (%s, %s) is still executing", task_name
,
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]
338 raise Exception("Waiting for task ({}, {}) to finish timed out. {}"
339 .format(task_name
, task_metadata
, _res
))
341 log
.info("task (%s, %s) finished", task_name
, task_metadata
)
342 if res_task
['success']:
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']
351 if 'status' in res_task
['exception']:
352 cls
._resp
.status_code
= res_task
['exception']['status']
354 cls
._resp
.status_code
= 500
355 return res_task
['exception']
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
)
362 def _task_delete(cls
, url
, timeout
=60, set_cookies
=False):
363 return cls
._task
_request
('DELETE', url
, None, timeout
, set_cookies
=set_cookies
)
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
)
371 return cls
._resp
.cookies
375 return cls
._resp
.json()
378 def reset_session(cls
):
379 cls
._session
= requests
.Session()
381 def assertSubset(self
, data
, biggerData
):
382 for key
, value
in data
.items():
383 self
.assertEqual(biggerData
[key
], value
)
385 def assertJsonBody(self
, data
):
386 body
= self
._resp
.json()
387 self
.assertEqual(body
, data
)
389 def assertJsonSubset(self
, data
):
390 self
.assertSubset(data
, self
._resp
.json())
392 def assertSchema(self
, data
, schema
):
394 return _validate_json(data
, schema
)
395 except _ValError
as e
:
396 self
.assertEqual(data
, str(e
))
398 def assertSchemaBody(self
, schema
):
399 self
.assertSchema(self
.jsonBody(), schema
)
401 def assertBody(self
, body
):
402 self
.assertEqual(self
._resp
.text
, body
)
404 def assertStatus(self
, status
):
405 if isinstance(status
, list):
406 self
.assertIn(self
._resp
.status_code
, status
)
408 self
.assertEqual(self
._resp
.status_code
, status
)
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
)
415 def assertError(self
, code
=None, component
=None, detail
=None):
416 body
= self
._resp
.json()
418 self
.assertEqual(body
['code'], code
)
420 self
.assertEqual(body
['component'], component
)
422 self
.assertEqual(body
['detail'], detail
)
425 def _ceph_cmd(cls
, cmd
):
426 res
= cls
.mgr_cluster
.mon_manager
.raw_cluster_cmd(*cmd
)
427 log
.info("command result: %s", res
)
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
)
436 def set_config_key(self
, key
, value
):
437 self
._ceph
_cmd
(['config-key', 'set', key
, value
])
439 def get_config_key(self
, key
):
440 return self
._ceph
_cmd
(['config-key', 'get', key
])
444 return cls
.mgr_cluster
.admin_remote
.run(args
=args
)
447 def _rbd_cmd(cls
, cmd
):
453 def _radosgw_admin_cmd(cls
, cmd
):
454 args
= ['radosgw-admin']
459 def _rados_cmd(cls
, cmd
):
466 out
= cls
.ceph_cluster
.mon_manager
.raw_cluster_cmd('quorum_status')
468 return [mon
['name'] for mon
in j
['monmap']['mons']]
471 def find_object_in_list(cls
, key
, value
, iterable
):
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.
481 if key
in obj
and obj
[key
] == value
:
486 class JLeaf(namedtuple('JLeaf', ['typ', 'none'])):
487 def __new__(cls
, typ
, none
=False):
489 typ
= six
.string_types
490 return super(JLeaf
, cls
).__new
__(cls
, typ
, none
)
493 JList
= namedtuple('JList', ['elem_typ'])
495 JTuple
= namedtuple('JList', ['elem_typs'])
497 JUnion
= namedtuple('JUnion', ['elem_typs'])
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):
502 :type sub_elems: dict[str, JAny | JLeaf | JList | JObj | type]
503 :type allow_unknown: bool
505 :type unknown_schema: int, str, JAny | JLeaf | JList | JObj
508 return super(JObj
, cls
).__new
__(cls
, sub_elems
, allow_unknown
, none
, unknown_schema
)
511 JAny
= namedtuple('JAny', ['none'])
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
))
520 # pylint: disable=dangerous-default-value,inconsistent-return-statements
521 def _validate_json(val
, schema
, path
=[]):
523 >>> d = {'a': 1, 'b': 'x', 'c': range(10)}
524 ... ds = JObj({'a': int, 'b': str, 'c': JList(int)})
525 ... _validate_json(d, ds)
527 >>> _validate_json({'num': 1}, JObj({'num': JUnion([int,float])}))
529 >>> _validate_json({'num': 'a'}, JObj({'num': JUnion([int,float])}))
532 if isinstance(schema
, JAny
):
533 if not schema
.none
and val
is None:
534 raise _ValError('val is None', path
)
536 if isinstance(schema
, JLeaf
):
537 if schema
.none
and val
is None:
539 if not isinstance(val
, schema
.typ
):
540 raise _ValError('val not of type {}'.format(schema
.typ
), path
)
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
:
552 if _validate_json(val
, typ
, path
):
557 if isinstance(schema
, JObj
):
558 if val
is None and schema
.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()))
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
)
571 _validate_json(val
[key
], sub_schema
, path
+ [key
])
572 for key
, sub_schema
in schema
.sub_elems
.items()
574 if unknown_keys
and schema
.allow_unknown
and schema
.unknown_schema
:
576 _validate_json(val
[key
], schema
.unknown_schema
, path
+ [key
])
577 for key
in unknown_keys
580 if schema
in [str, int, float, bool, six
.string_types
]:
581 return _validate_json(val
, JLeaf(schema
), path
)
583 assert False, str(path
)