]>
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 ..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
):
40 cls
._ceph
_cmd
(['dashboard', 'ac-user-show', username
])
41 cls
._ceph
_cmd
(['dashboard', 'ac-user-delete', username
])
42 except CommandFailedError
as ex
:
43 if ex
.exitstatus
!= 2:
46 cls
._ceph
_cmd
(['dashboard', 'ac-user-create', username
, password
])
48 set_roles_args
= ['dashboard', 'ac-user-set-roles', username
]
49 for idx
, role
in enumerate(roles
):
50 if isinstance(role
, str):
51 set_roles_args
.append(role
)
53 assert isinstance(role
, dict)
54 rolename
= 'test_role_{}'.format(idx
)
56 cls
._ceph
_cmd
(['dashboard', 'ac-role-show', rolename
])
57 cls
._ceph
_cmd
(['dashboard', 'ac-role-delete', rolename
])
58 except CommandFailedError
as ex
:
59 if ex
.exitstatus
!= 2:
61 cls
._ceph
_cmd
(['dashboard', 'ac-role-create', rolename
])
62 for mod
, perms
in role
.items():
63 args
= ['dashboard', 'ac-role-add-scope-perms', rolename
, mod
]
66 set_roles_args
.append(rolename
)
67 cls
._ceph
_cmd
(set_roles_args
)
70 def login(cls
, username
, password
):
73 cls
._post
('/api/auth', {'username': username
, 'password': password
})
74 cls
._token
= cls
.jsonBody()['token']
80 cls
._post
('/api/auth/logout')
85 def delete_user(cls
, username
, roles
=None):
88 cls
._ceph
_cmd
(['dashboard', 'ac-user-delete', username
])
89 for idx
, role
in enumerate(roles
):
90 if isinstance(role
, dict):
91 cls
._ceph
_cmd
(['dashboard', 'ac-role-delete', 'test_role_{}'.format(idx
)])
94 def RunAs(cls
, username
, password
, roles
):
96 def execute(self
, *args
, **kwargs
):
97 self
.create_user(username
, password
, roles
)
98 self
.login(username
, password
)
99 res
= func(self
, *args
, **kwargs
)
101 self
.delete_user(username
, roles
)
107 def set_jwt_token(cls
, token
):
112 super(DashboardTestCase
, cls
).setUpClass()
113 cls
._assign
_ports
("dashboard", "ssl_server_port")
114 cls
._load
_module
("dashboard")
115 cls
._base
_uri
= cls
._get
_uri
("dashboard").rstrip('/')
118 cls
.mds_cluster
.clear_firewall()
120 # To avoid any issues with e.g. unlink bugs, we destroy and recreate
121 # the filesystem rather than just doing a rm -rf of files
122 cls
.mds_cluster
.mds_stop()
123 cls
.mds_cluster
.mds_fail()
124 cls
.mds_cluster
.delete_all_filesystems()
125 cls
.fs
= None # is now invalid!
127 cls
.fs
= cls
.mds_cluster
.newfs(create
=True)
130 # In case some test messed with auth caps, reset them
131 # pylint: disable=not-an-iterable
132 client_mount_ids
= [m
.client_id
for m
in cls
.mounts
]
133 for client_id
in client_mount_ids
:
134 cls
.mds_cluster
.mon_manager
.raw_cluster_cmd_result(
135 'auth', 'caps', "client.{0}".format(client_id
),
138 'osd', 'allow rw pool={0}'.format(cls
.fs
.get_data_pool_name()))
140 # wait for mds restart to complete...
141 cls
.fs
.wait_for_daemons()
144 cls
._session
= requests
.Session()
147 cls
.create_user('admin', 'admin', cls
.AUTH_ROLES
)
148 if cls
.AUTO_AUTHENTICATE
:
149 cls
.login('admin', 'admin')
152 if not self
._loggedin
and self
.AUTO_AUTHENTICATE
:
153 self
.login('admin', 'admin')
154 self
.wait_for_health_clear(20)
157 def tearDownClass(cls
):
158 super(DashboardTestCase
, cls
).tearDownClass()
160 # pylint: disable=inconsistent-return-statements
162 def _request(cls
, url
, method
, data
=None, params
=None):
163 url
= "{}{}".format(cls
._base
_uri
, url
)
164 log
.info("Request %s to %s", method
, url
)
167 headers
['Authorization'] = "Bearer {}".format(cls
._token
)
170 cls
._resp
= cls
._session
.get(url
, params
=params
, verify
=False,
172 elif method
== 'POST':
173 cls
._resp
= cls
._session
.post(url
, json
=data
, params
=params
,
174 verify
=False, headers
=headers
)
175 elif method
== 'DELETE':
176 cls
._resp
= cls
._session
.delete(url
, json
=data
, params
=params
,
177 verify
=False, headers
=headers
)
178 elif method
== 'PUT':
179 cls
._resp
= cls
._session
.put(url
, json
=data
, params
=params
,
180 verify
=False, headers
=headers
)
185 # Output response for easier debugging.
186 log
.error("Request response: %s", cls
._resp
.text
)
187 content_type
= cls
._resp
.headers
['content-type']
188 if content_type
== 'application/json' and cls
._resp
.text
and cls
._resp
.text
!= "":
189 return cls
._resp
.json()
190 return cls
._resp
.text
191 except ValueError as ex
:
192 log
.exception("Failed to decode response: %s", cls
._resp
.text
)
196 def _get(cls
, url
, params
=None):
197 return cls
._request
(url
, 'GET', params
=params
)
200 def _view_cache_get(cls
, url
, retries
=5):
202 while retry
and retries
> 0:
205 if isinstance(res
, dict):
208 assert 'value' in view
209 if not view
['value']:
213 raise Exception("{} view cache exceeded number of retries={}"
214 .format(url
, retries
))
218 def _post(cls
, url
, data
=None, params
=None):
219 cls
._request
(url
, 'POST', data
, params
)
222 def _delete(cls
, url
, data
=None, params
=None):
223 cls
._request
(url
, 'DELETE', data
, params
)
226 def _put(cls
, url
, data
=None, params
=None):
227 cls
._request
(url
, 'PUT', data
, params
)
230 def _assertEq(cls
, v1
, v2
):
232 raise Exception("assertion failed: {} != {}".format(v1
, v2
))
235 def _assertIn(cls
, v1
, v2
):
237 raise Exception("assertion failed: {} not in {}".format(v1
, v2
))
240 def _assertIsInst(cls
, v1
, v2
):
241 if not isinstance(v1
, v2
):
242 raise Exception("assertion failed: {} not instance of {}".format(v1
, v2
))
244 # pylint: disable=too-many-arguments
246 def _task_request(cls
, method
, url
, data
, timeout
):
247 res
= cls
._request
(url
, method
, data
)
248 cls
._assertIn
(cls
._resp
.status_code
, [200, 201, 202, 204, 400, 403])
250 if cls
._resp
.status_code
== 403:
253 if cls
._resp
.status_code
!= 202:
254 log
.info("task finished immediately")
257 cls
._assertIn
('name', res
)
258 cls
._assertIn
('metadata', res
)
259 task_name
= res
['name']
260 task_metadata
= res
['metadata']
262 retries
= int(timeout
)
264 while retries
> 0 and not res_task
:
266 log
.info("task (%s, %s) is still executing", task_name
,
269 _res
= cls
._get
('/api/task?name={}'.format(task_name
))
270 cls
._assertEq
(cls
._resp
.status_code
, 200)
271 executing_tasks
= [task
for task
in _res
['executing_tasks'] if
272 task
['metadata'] == task_metadata
]
273 finished_tasks
= [task
for task
in _res
['finished_tasks'] if
274 task
['metadata'] == task_metadata
]
275 if not executing_tasks
and finished_tasks
:
276 res_task
= finished_tasks
[0]
279 raise Exception("Waiting for task ({}, {}) to finish timed out. {}"
280 .format(task_name
, task_metadata
, _res
))
282 log
.info("task (%s, %s) finished", task_name
, task_metadata
)
283 if res_task
['success']:
285 cls
._resp
.status_code
= 201
286 elif method
== 'PUT':
287 cls
._resp
.status_code
= 200
288 elif method
== 'DELETE':
289 cls
._resp
.status_code
= 204
290 return res_task
['ret_value']
292 if 'status' in res_task
['exception']:
293 cls
._resp
.status_code
= res_task
['exception']['status']
295 cls
._resp
.status_code
= 500
296 return res_task
['exception']
299 def _task_post(cls
, url
, data
=None, timeout
=60):
300 return cls
._task
_request
('POST', url
, data
, timeout
)
303 def _task_delete(cls
, url
, timeout
=60):
304 return cls
._task
_request
('DELETE', url
, None, timeout
)
307 def _task_put(cls
, url
, data
=None, timeout
=60):
308 return cls
._task
_request
('PUT', url
, data
, timeout
)
312 return cls
._resp
.cookies
316 return cls
._resp
.json()
319 def reset_session(cls
):
320 cls
._session
= requests
.Session()
322 def assertSubset(self
, data
, biggerData
):
323 for key
, value
in data
.items():
324 self
.assertEqual(biggerData
[key
], value
)
326 def assertJsonBody(self
, data
):
327 body
= self
._resp
.json()
328 self
.assertEqual(body
, data
)
330 def assertJsonSubset(self
, data
):
331 self
.assertSubset(data
, self
._resp
.json())
333 def assertSchema(self
, data
, schema
):
335 return _validate_json(data
, schema
)
336 except _ValError
as e
:
337 self
.assertEqual(data
, str(e
))
339 def assertSchemaBody(self
, schema
):
340 self
.assertSchema(self
.jsonBody(), schema
)
342 def assertBody(self
, body
):
343 self
.assertEqual(self
._resp
.text
, body
)
345 def assertStatus(self
, status
):
346 if isinstance(status
, list):
347 self
.assertIn(self
._resp
.status_code
, status
)
349 self
.assertEqual(self
._resp
.status_code
, status
)
351 def assertHeaders(self
, headers
):
352 for name
, value
in headers
.items():
353 self
.assertIn(name
, self
._resp
.headers
)
354 self
.assertEqual(self
._resp
.headers
[name
], value
)
356 def assertError(self
, code
=None, component
=None, detail
=None):
357 body
= self
._resp
.json()
359 self
.assertEqual(body
['code'], code
)
361 self
.assertEqual(body
['component'], component
)
363 self
.assertEqual(body
['detail'], detail
)
366 def _ceph_cmd(cls
, cmd
):
367 res
= cls
.mgr_cluster
.mon_manager
.raw_cluster_cmd(*cmd
)
368 log
.info("command result: %s", res
)
371 def set_config_key(self
, key
, value
):
372 self
._ceph
_cmd
(['config-key', 'set', key
, value
])
374 def get_config_key(self
, key
):
375 return self
._ceph
_cmd
(['config-key', 'get', key
])
378 def _rbd_cmd(cls
, cmd
):
383 cls
.mgr_cluster
.admin_remote
.run(args
=args
)
386 def _radosgw_admin_cmd(cls
, cmd
):
391 cls
.mgr_cluster
.admin_remote
.run(args
=args
)
394 def _rados_cmd(cls
, cmd
):
397 cls
.mgr_cluster
.admin_remote
.run(args
=args
)
401 out
= cls
.ceph_cluster
.mon_manager
.raw_cluster_cmd('mon_status')
403 return [mon
['name'] for mon
in j
['monmap']['mons']]
406 def find_object_in_list(cls
, key
, value
, iterable
):
408 Get the first occurrence of an object within a list with
409 the specified key/value.
410 :param key: The name of the key.
411 :param value: The value to search for.
412 :param iterable: The list to process.
413 :return: Returns the found object or None.
416 if key
in obj
and obj
[key
] == value
:
421 class JLeaf(namedtuple('JLeaf', ['typ', 'none'])):
422 def __new__(cls
, typ
, none
=False):
424 typ
= six
.string_types
425 return super(JLeaf
, cls
).__new
__(cls
, typ
, none
)
428 JList
= namedtuple('JList', ['elem_typ'])
430 JTuple
= namedtuple('JList', ['elem_typs'])
433 class JObj(namedtuple('JObj', ['sub_elems', 'allow_unknown', 'none', 'unknown_schema'])):
434 def __new__(cls
, sub_elems
, allow_unknown
=False, none
=False, unknown_schema
=None):
436 :type sub_elems: dict[str, JAny | JLeaf | JList | JObj | type]
437 :type allow_unknown: bool
439 :type unknown_schema: int, str, JAny | JLeaf | JList | JObj
442 return super(JObj
, cls
).__new
__(cls
, sub_elems
, allow_unknown
, none
, unknown_schema
)
445 JAny
= namedtuple('JAny', ['none'])
448 class _ValError(Exception):
449 def __init__(self
, msg
, path
):
450 path_str
= ''.join('[{}]'.format(repr(p
)) for p
in path
)
451 super(_ValError
, self
).__init
__('In `input{}`: {}'.format(path_str
, msg
))
454 # pylint: disable=dangerous-default-value,inconsistent-return-statements
455 def _validate_json(val
, schema
, path
=[]):
457 >>> d = {'a': 1, 'b': 'x', 'c': range(10)}
458 ... ds = JObj({'a': int, 'b': str, 'c': JList(int)})
459 ... _validate_json(d, ds)
462 if isinstance(schema
, JAny
):
463 if not schema
.none
and val
is None:
464 raise _ValError('val is None', path
)
466 if isinstance(schema
, JLeaf
):
467 if schema
.none
and val
is None:
469 if not isinstance(val
, schema
.typ
):
470 raise _ValError('val not of type {}'.format(schema
.typ
), path
)
472 if isinstance(schema
, JList
):
473 if not isinstance(val
, list):
474 raise _ValError('val="{}" is not a list'.format(val
), path
)
475 return all(_validate_json(e
, schema
.elem_typ
, path
+ [i
]) for i
, e
in enumerate(val
))
476 if isinstance(schema
, JTuple
):
477 return all(_validate_json(val
[i
], typ
, path
+ [i
])
478 for i
, typ
in enumerate(schema
.elem_typs
))
479 if isinstance(schema
, JObj
):
480 if val
is None and schema
.none
:
483 raise _ValError('val is None', path
)
484 if not hasattr(val
, 'keys'):
485 raise _ValError('val="{}" is not a dict'.format(val
), path
)
486 missing_keys
= set(schema
.sub_elems
.keys()).difference(set(val
.keys()))
488 raise _ValError('missing keys: {}'.format(missing_keys
), path
)
489 unknown_keys
= set(val
.keys()).difference(set(schema
.sub_elems
.keys()))
490 if not schema
.allow_unknown
and unknown_keys
:
491 raise _ValError('unknown keys: {}'.format(unknown_keys
), path
)
493 _validate_json(val
[key
], sub_schema
, path
+ [key
])
494 for key
, sub_schema
in schema
.sub_elems
.items()
496 if unknown_keys
and schema
.allow_unknown
and schema
.unknown_schema
:
498 _validate_json(val
[key
], schema
.unknown_schema
, path
+ [key
])
499 for key
in unknown_keys
502 if schema
in [str, int, float, bool, six
.string_types
]:
503 return _validate_json(val
, JLeaf(schema
), path
)
505 assert False, str(path
)