]>
git.proxmox.com Git - ceph.git/blob - 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
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
):
23 REQUIRE_FILESYSTEM
= True
27 _session
= None # type: requests.sessions.Session
29 _resp
= None # type: requests.models.Response
33 AUTO_AUTHENTICATE
= True
35 AUTH_ROLES
= ['administrator']
38 def create_user(cls
, username
, password
, roles
=None,
39 force_password
=True, cmd_args
=None):
41 :param username: The name of the user.
43 :param password: The password.
45 :param roles: A list of roles.
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]
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:
62 'dashboard', 'ac-user-create', username
, password
65 user_create_args
.append('--force-password')
67 user_create_args
.extend(cmd_args
)
68 cls
._ceph
_cmd
(user_create_args
)
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
)
76 assert isinstance(role
, dict)
77 rolename
= 'test_role_{}'.format(idx
)
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:
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
]
89 set_roles_args
.append(rolename
)
90 cls
._ceph
_cmd
(set_roles_args
)
93 def login(cls
, username
, password
):
96 cls
._post
('/api/auth', {'username': username
, 'password': password
})
97 cls
._assertEq
(cls
._resp
.status_code
, 201)
98 cls
._token
= cls
.jsonBody()['token']
104 cls
._post
('/api/auth/logout')
105 cls
._assertEq
(cls
._resp
.status_code
, 200)
107 cls
._loggedin
= False
110 def delete_user(cls
, username
, roles
=None):
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
)])
119 def RunAs(cls
, username
, password
, roles
=None, force_password
=True,
120 cmd_args
=None, login
=True):
122 def execute(self
, *args
, **kwargs
):
123 self
.create_user(username
, password
, roles
,
124 force_password
, cmd_args
)
126 self
.login(username
, password
)
127 res
= func(self
, *args
, **kwargs
)
130 self
.delete_user(username
, roles
)
138 def set_jwt_token(cls
, token
):
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('/')
149 cls
.mds_cluster
.clear_firewall()
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!
158 cls
.fs
= cls
.mds_cluster
.newfs(create
=True)
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
),
169 'osd', 'allow rw pool={0}'.format(cls
.fs
.get_data_pool_name()))
171 # wait for mds restart to complete...
172 cls
.fs
.wait_for_daemons()
175 cls
._session
= requests
.Session()
178 cls
.create_user('admin', 'admin', cls
.AUTH_ROLES
)
179 if cls
.AUTO_AUTHENTICATE
:
180 cls
.login('admin', 'admin')
183 super(DashboardTestCase
, self
).setUp()
184 if not self
._loggedin
and self
.AUTO_AUTHENTICATE
:
185 self
.login('admin', 'admin')
186 self
.wait_for_health_clear(20)
189 def tearDownClass(cls
):
190 super(DashboardTestCase
, cls
).tearDownClass()
192 # pylint: disable=inconsistent-return-statements
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
)
199 headers
['Authorization'] = "Bearer {}".format(cls
._token
)
202 cls
._resp
= cls
._session
.get(url
, params
=params
, verify
=False,
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
)
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
)
228 def _get(cls
, url
, params
=None):
229 return cls
._request
(url
, 'GET', params
=params
)
232 def _view_cache_get(cls
, url
, retries
=5):
234 while retry
and retries
> 0:
237 if isinstance(res
, dict):
240 assert 'value' in view
241 if not view
['value']:
245 raise Exception("{} view cache exceeded number of retries={}"
246 .format(url
, retries
))
250 def _post(cls
, url
, data
=None, params
=None):
251 cls
._request
(url
, 'POST', data
, params
)
254 def _delete(cls
, url
, data
=None, params
=None):
255 cls
._request
(url
, 'DELETE', data
, params
)
258 def _put(cls
, url
, data
=None, params
=None):
259 cls
._request
(url
, 'PUT', data
, params
)
262 def _assertEq(cls
, v1
, v2
):
264 raise Exception("assertion failed: {} != {}".format(v1
, v2
))
267 def _assertIn(cls
, v1
, v2
):
269 raise Exception("assertion failed: {} not in {}".format(v1
, v2
))
272 def _assertIsInst(cls
, v1
, v2
):
273 if not isinstance(v1
, v2
):
274 raise Exception("assertion failed: {} not instance of {}".format(v1
, v2
))
276 # pylint: disable=too-many-arguments
278 def _task_request(cls
, method
, url
, data
, timeout
):
279 res
= cls
._request
(url
, method
, data
)
280 cls
._assertIn
(cls
._resp
.status_code
, [200, 201, 202, 204, 400, 403, 404])
282 if cls
._resp
.status_code
== 403:
285 if cls
._resp
.status_code
!= 202:
286 log
.info("task finished immediately")
289 cls
._assertIn
('name', res
)
290 cls
._assertIn
('metadata', res
)
291 task_name
= res
['name']
292 task_metadata
= res
['metadata']
294 retries
= int(timeout
)
296 while retries
> 0 and not res_task
:
298 log
.info("task (%s, %s) is still executing", task_name
,
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]
311 raise Exception("Waiting for task ({}, {}) to finish timed out. {}"
312 .format(task_name
, task_metadata
, _res
))
314 log
.info("task (%s, %s) finished", task_name
, task_metadata
)
315 if res_task
['success']:
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']
324 if 'status' in res_task
['exception']:
325 cls
._resp
.status_code
= res_task
['exception']['status']
327 cls
._resp
.status_code
= 500
328 return res_task
['exception']
331 def _task_post(cls
, url
, data
=None, timeout
=60):
332 return cls
._task
_request
('POST', url
, data
, timeout
)
335 def _task_delete(cls
, url
, timeout
=60):
336 return cls
._task
_request
('DELETE', url
, None, timeout
)
339 def _task_put(cls
, url
, data
=None, timeout
=60):
340 return cls
._task
_request
('PUT', url
, data
, timeout
)
344 return cls
._resp
.cookies
348 return cls
._resp
.json()
351 def reset_session(cls
):
352 cls
._session
= requests
.Session()
354 def assertSubset(self
, data
, biggerData
):
355 for key
, value
in data
.items():
356 self
.assertEqual(biggerData
[key
], value
)
358 def assertJsonBody(self
, data
):
359 body
= self
._resp
.json()
360 self
.assertEqual(body
, data
)
362 def assertJsonSubset(self
, data
):
363 self
.assertSubset(data
, self
._resp
.json())
365 def assertSchema(self
, data
, schema
):
367 return _validate_json(data
, schema
)
368 except _ValError
as e
:
369 self
.assertEqual(data
, str(e
))
371 def assertSchemaBody(self
, schema
):
372 self
.assertSchema(self
.jsonBody(), schema
)
374 def assertBody(self
, body
):
375 self
.assertEqual(self
._resp
.text
, body
)
377 def assertStatus(self
, status
):
378 if isinstance(status
, list):
379 self
.assertIn(self
._resp
.status_code
, status
)
381 self
.assertEqual(self
._resp
.status_code
, status
)
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
)
388 def assertError(self
, code
=None, component
=None, detail
=None):
389 body
= self
._resp
.json()
391 self
.assertEqual(body
['code'], code
)
393 self
.assertEqual(body
['component'], component
)
395 self
.assertEqual(body
['detail'], detail
)
398 def _ceph_cmd(cls
, cmd
):
399 res
= cls
.mgr_cluster
.mon_manager
.raw_cluster_cmd(*cmd
)
400 log
.info("command result: %s", res
)
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
)
409 def set_config_key(self
, key
, value
):
410 self
._ceph
_cmd
(['config-key', 'set', key
, value
])
412 def get_config_key(self
, key
):
413 return self
._ceph
_cmd
(['config-key', 'get', key
])
417 return cls
.mgr_cluster
.admin_remote
.run(args
=args
)
420 def _rbd_cmd(cls
, cmd
):
426 def _radosgw_admin_cmd(cls
, cmd
):
427 args
= ['radosgw-admin']
432 def _rados_cmd(cls
, cmd
):
439 out
= cls
.ceph_cluster
.mon_manager
.raw_cluster_cmd('quorum_status')
441 return [mon
['name'] for mon
in j
['monmap']['mons']]
444 def find_object_in_list(cls
, key
, value
, iterable
):
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.
454 if key
in obj
and obj
[key
] == value
:
459 class JLeaf(namedtuple('JLeaf', ['typ', 'none'])):
460 def __new__(cls
, typ
, none
=False):
462 typ
= six
.string_types
463 return super(JLeaf
, cls
).__new
__(cls
, typ
, none
)
466 JList
= namedtuple('JList', ['elem_typ'])
468 JTuple
= namedtuple('JList', ['elem_typs'])
471 class JObj(namedtuple('JObj', ['sub_elems', 'allow_unknown', 'none', 'unknown_schema'])):
472 def __new__(cls
, sub_elems
, allow_unknown
=False, none
=False, unknown_schema
=None):
474 :type sub_elems: dict[str, JAny | JLeaf | JList | JObj | type]
475 :type allow_unknown: bool
477 :type unknown_schema: int, str, JAny | JLeaf | JList | JObj
480 return super(JObj
, cls
).__new
__(cls
, sub_elems
, allow_unknown
, none
, unknown_schema
)
483 JAny
= namedtuple('JAny', ['none'])
486 class _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
))
492 # pylint: disable=dangerous-default-value,inconsistent-return-statements
493 def _validate_json(val
, schema
, path
=[]):
495 >>> d = {'a': 1, 'b': 'x', 'c': range(10)}
496 ... ds = JObj({'a': int, 'b': str, 'c': JList(int)})
497 ... _validate_json(d, ds)
500 if isinstance(schema
, JAny
):
501 if not schema
.none
and val
is None:
502 raise _ValError('val is None', path
)
504 if isinstance(schema
, JLeaf
):
505 if schema
.none
and val
is None:
507 if not isinstance(val
, schema
.typ
):
508 raise _ValError('val not of type {}'.format(schema
.typ
), path
)
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
:
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()))
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
)
531 _validate_json(val
[key
], sub_schema
, path
+ [key
])
532 for key
, sub_schema
in schema
.sub_elems
.items()
534 if unknown_keys
and schema
.allow_unknown
and schema
.unknown_schema
:
536 _validate_json(val
[key
], schema
.unknown_schema
, path
+ [key
])
537 for key
in unknown_keys
540 if schema
in [str, int, float, bool, six
.string_types
]:
541 return _validate_json(val
, JLeaf(schema
), path
)
543 assert False, str(path
)