midpoint-cli 1.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- midpoint_cli/__init__.py +234 -0
- midpoint_cli/client/__init__.py +199 -0
- midpoint_cli/client/audit.py +178 -0
- midpoint_cli/client/audit_query.py +247 -0
- midpoint_cli/client/configuration.py +15 -0
- midpoint_cli/client/constants.py +37 -0
- midpoint_cli/client/errors.py +74 -0
- midpoint_cli/client/logging_config.py +122 -0
- midpoint_cli/client/midpoint_client.py +866 -0
- midpoint_cli/client/object_query.py +230 -0
- midpoint_cli/client/objects.py +814 -0
- midpoint_cli/client/observer.py +29 -0
- midpoint_cli/client/patch.py +113 -0
- midpoint_cli/client/progress.py +80 -0
- midpoint_cli/client/rest.py +490 -0
- midpoint_cli/client/safe_xml.py +24 -0
- midpoint_cli/client/server_info.py +130 -0
- midpoint_cli/client/server_version.py +74 -0
- midpoint_cli/client/session.py +142 -0
- midpoint_cli/client/task_definitions.py +110 -0
- midpoint_cli/prompt/__init__.py +196 -0
- midpoint_cli/prompt/audit.py +253 -0
- midpoint_cli/prompt/base.py +218 -0
- midpoint_cli/prompt/complete.py +291 -0
- midpoint_cli/prompt/configuration.py +258 -0
- midpoint_cli/prompt/connector.py +45 -0
- midpoint_cli/prompt/console.py +818 -0
- midpoint_cli/prompt/delete.py +89 -0
- midpoint_cli/prompt/get.py +66 -0
- midpoint_cli/prompt/info.py +53 -0
- midpoint_cli/prompt/log.py +98 -0
- midpoint_cli/prompt/org.py +122 -0
- midpoint_cli/prompt/output.py +33 -0
- midpoint_cli/prompt/put.py +71 -0
- midpoint_cli/prompt/report.py +69 -0
- midpoint_cli/prompt/resource.py +217 -0
- midpoint_cli/prompt/role.py +96 -0
- midpoint_cli/prompt/script.py +154 -0
- midpoint_cli/prompt/search.py +51 -0
- midpoint_cli/prompt/shadow.py +87 -0
- midpoint_cli/prompt/task.py +296 -0
- midpoint_cli/prompt/user.py +406 -0
- midpoint_cli-1.6.0.dist-info/METADATA +1334 -0
- midpoint_cli-1.6.0.dist-info/RECORD +47 -0
- midpoint_cli-1.6.0.dist-info/WHEEL +4 -0
- midpoint_cli-1.6.0.dist-info/entry_points.txt +3 -0
- midpoint_cli-1.6.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
2
|
+
from typing import Any, cast
|
|
3
|
+
|
|
4
|
+
# Task command wrapper parser
|
|
5
|
+
from midpoint_cli.client import (
|
|
6
|
+
MidpointObjectList,
|
|
7
|
+
MidpointObjectTypes,
|
|
8
|
+
MidpointTask,
|
|
9
|
+
NopProgressMonitor,
|
|
10
|
+
TaskExecutionFailure,
|
|
11
|
+
)
|
|
12
|
+
from midpoint_cli.client.constants import ResultStatus, TaskExecutionState
|
|
13
|
+
from midpoint_cli.client.object_query import TASK_STATUS_FILTERS
|
|
14
|
+
from midpoint_cli.client.safe_xml import fromstring as safe_fromstring
|
|
15
|
+
from midpoint_cli.prompt.base import PromptBase, add_listing_arguments
|
|
16
|
+
from midpoint_cli.prompt.complete import TASK, completes
|
|
17
|
+
from midpoint_cli.prompt.console import (
|
|
18
|
+
OUTCOME_STYLE,
|
|
19
|
+
create_progress_monitor,
|
|
20
|
+
get_console,
|
|
21
|
+
is_machine_output,
|
|
22
|
+
print_details,
|
|
23
|
+
print_error,
|
|
24
|
+
print_info,
|
|
25
|
+
print_rows,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
task_parser = ArgumentParser(
|
|
29
|
+
formatter_class=RawTextHelpFormatter,
|
|
30
|
+
prog='task',
|
|
31
|
+
description='Manage server tasks.',
|
|
32
|
+
epilog="""
|
|
33
|
+
Available commands:
|
|
34
|
+
ls List all server tasks.
|
|
35
|
+
get Show the details of task(s).
|
|
36
|
+
run Run task(s) sequentially.
|
|
37
|
+
suspend Suspend task(s).
|
|
38
|
+
resume Resume task(s).
|
|
39
|
+
wait Wait for task completion.
|
|
40
|
+
""",
|
|
41
|
+
)
|
|
42
|
+
task_parser.add_argument('command', help='Task command to execute.')
|
|
43
|
+
task_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
|
|
44
|
+
|
|
45
|
+
# Task LS parser
|
|
46
|
+
|
|
47
|
+
task_ls_parser = add_listing_arguments(
|
|
48
|
+
ArgumentParser(
|
|
49
|
+
formatter_class=RawTextHelpFormatter,
|
|
50
|
+
prog='task ls',
|
|
51
|
+
description='List server tasks.',
|
|
52
|
+
),
|
|
53
|
+
'tasks',
|
|
54
|
+
)
|
|
55
|
+
task_ls_parser.add_argument(
|
|
56
|
+
'--status',
|
|
57
|
+
action='append',
|
|
58
|
+
choices=list(TASK_STATUS_FILTERS),
|
|
59
|
+
metavar='STATUS',
|
|
60
|
+
help='Only list tasks in this status (filtered on the server); repeat to list several.\n'
|
|
61
|
+
'Execution states: running, runnable, waiting, suspended, closed.\n'
|
|
62
|
+
'Last run results: in_progress, success, warning, failed (fatal or partial error).',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Task GET parser
|
|
66
|
+
|
|
67
|
+
task_get_parser = ArgumentParser(
|
|
68
|
+
formatter_class=RawTextHelpFormatter,
|
|
69
|
+
prog='task get',
|
|
70
|
+
description='Show the details of tasks.',
|
|
71
|
+
)
|
|
72
|
+
completes(task_get_parser.add_argument('task', help='Task to show. Can be an OID or a task name.', nargs='+'), TASK)
|
|
73
|
+
|
|
74
|
+
# Task RUN parser
|
|
75
|
+
|
|
76
|
+
task_run_parser = ArgumentParser(
|
|
77
|
+
formatter_class=RawTextHelpFormatter,
|
|
78
|
+
prog='task run',
|
|
79
|
+
description='Run tasks synchronously.',
|
|
80
|
+
)
|
|
81
|
+
completes(task_run_parser.add_argument('task', help='Task to be run. Can be an OID or a task name.', nargs='+'), TASK)
|
|
82
|
+
task_run_parser.add_argument(
|
|
83
|
+
'--no-changes',
|
|
84
|
+
action='store_true',
|
|
85
|
+
help="Don't stream audit events as the task runs (silences the inline change log).",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Task WAIT parser
|
|
89
|
+
|
|
90
|
+
task_wait_parser = ArgumentParser(
|
|
91
|
+
formatter_class=RawTextHelpFormatter,
|
|
92
|
+
prog='task wait',
|
|
93
|
+
description='Wait for tasks to complete. Without a task, waits for every task in progress.',
|
|
94
|
+
)
|
|
95
|
+
completes(
|
|
96
|
+
task_wait_parser.add_argument('task', help='Task to wait for. Can be an OID or a task name.', nargs='*'), TASK
|
|
97
|
+
)
|
|
98
|
+
task_wait_parser.add_argument(
|
|
99
|
+
'--no-changes',
|
|
100
|
+
action='store_true',
|
|
101
|
+
help="Don't stream audit events while waiting (silences the inline change log).",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Task SUSPEND and RESUME parsers
|
|
105
|
+
|
|
106
|
+
task_suspend_parser = ArgumentParser(
|
|
107
|
+
formatter_class=RawTextHelpFormatter,
|
|
108
|
+
prog='task suspend',
|
|
109
|
+
description='Suspend tasks.',
|
|
110
|
+
)
|
|
111
|
+
completes(
|
|
112
|
+
task_suspend_parser.add_argument('task', help='Task to suspend. Can be an OID or a task name.', nargs='+'), TASK
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
task_resume_parser = ArgumentParser(
|
|
116
|
+
formatter_class=RawTextHelpFormatter,
|
|
117
|
+
prog='task resume',
|
|
118
|
+
description='Resume suspended tasks.',
|
|
119
|
+
)
|
|
120
|
+
completes(
|
|
121
|
+
task_resume_parser.add_argument('task', help='Task to resume. Can be an OID or a task name.', nargs='+'), TASK
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Each subcommand with its parser; None for those taking no arguments.
|
|
125
|
+
task_subparsers: dict[str, ArgumentParser | None] = {
|
|
126
|
+
'ls': task_ls_parser,
|
|
127
|
+
'get': task_get_parser,
|
|
128
|
+
'run': task_run_parser,
|
|
129
|
+
'wait': task_wait_parser,
|
|
130
|
+
'suspend': task_suspend_parser,
|
|
131
|
+
'resume': task_resume_parser,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _task_cell_style(column: str, value: str) -> str | None:
|
|
136
|
+
"""Color the result status of a task row by outcome; an empty status keeps the column style."""
|
|
137
|
+
if column == 'Result Status' and value:
|
|
138
|
+
return OUTCOME_STYLE.get(value.replace('-', '_').lower())
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def task_details(task: MidpointTask) -> dict[str, Any]:
|
|
143
|
+
"""A task as one detail object: the properties of `task get`, then its Transitions and Actions.
|
|
144
|
+
|
|
145
|
+
The nested records use the column names of the Transitions and Actions tables.
|
|
146
|
+
"""
|
|
147
|
+
details: dict[str, Any] = dict(task)
|
|
148
|
+
details['Transitions'] = [
|
|
149
|
+
{'From State': from_state, 'To State': to_state, 'Count': count}
|
|
150
|
+
for from_state, to_state, count in task.get_transitions()
|
|
151
|
+
]
|
|
152
|
+
details['Actions'] = [
|
|
153
|
+
{'Type': object_type, 'Action': operation, 'Success': success, 'Failure': failure}
|
|
154
|
+
for object_type, operation, success, failure in task.get_actions()
|
|
155
|
+
]
|
|
156
|
+
return details
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TaskClientPrompt(PromptBase):
|
|
160
|
+
subparsers_task = task_subparsers
|
|
161
|
+
|
|
162
|
+
def do_task(self, inp: str) -> None:
|
|
163
|
+
"""Manage server tasks (subcommands: ls, run, suspend, resume, wait, get)."""
|
|
164
|
+
# Strict: an unknown subcommand followed by arguments is a usage error, so a typo
|
|
165
|
+
# in a subcommand is not mistaken for another command.
|
|
166
|
+
self._dispatch_subcommand(
|
|
167
|
+
inp,
|
|
168
|
+
task_parser,
|
|
169
|
+
task_subparsers,
|
|
170
|
+
{
|
|
171
|
+
'ls': self._task_ls,
|
|
172
|
+
'get': self._task_get,
|
|
173
|
+
'wait': self._task_wait,
|
|
174
|
+
'run': lambda args: self._task_action('run', task_run_parser, args),
|
|
175
|
+
'resume': lambda args: self._task_action('resume', task_resume_parser, args),
|
|
176
|
+
'suspend': lambda args: self._task_action('suspend', task_suspend_parser, args),
|
|
177
|
+
},
|
|
178
|
+
strict=True,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _task_ls(self, args: list[str]) -> None:
|
|
182
|
+
ls_ns = task_ls_parser.parse_args(args)
|
|
183
|
+
if ls_ns.status:
|
|
184
|
+
tasks = self.client.get_tasks(limit=ls_ns.limit, name_filter=ls_ns.filter, statuses=ls_ns.status)
|
|
185
|
+
else:
|
|
186
|
+
tasks = self.client.get_tasks(limit=ls_ns.limit, name_filter=ls_ns.filter)
|
|
187
|
+
self._print_tasks_table(tasks)
|
|
188
|
+
|
|
189
|
+
def _task_get(self, args: list[str]) -> None:
|
|
190
|
+
get_ns = task_get_parser.parse_args(args)
|
|
191
|
+
details = []
|
|
192
|
+
|
|
193
|
+
for task_id in get_ns.task:
|
|
194
|
+
task_obj = self._find_reference(MidpointObjectTypes.TASK.value, task_id, 'Task')
|
|
195
|
+
if task_obj is None:
|
|
196
|
+
continue
|
|
197
|
+
if is_machine_output():
|
|
198
|
+
details.append(task_details(cast(MidpointTask, task_obj)))
|
|
199
|
+
else:
|
|
200
|
+
get_console().print(cast(MidpointTask, task_obj).get_full_description())
|
|
201
|
+
|
|
202
|
+
if is_machine_output():
|
|
203
|
+
# Shaped by the arguments, not by how many were found: one task, one object.
|
|
204
|
+
print_details(details, as_list=len(get_ns.task) > 1)
|
|
205
|
+
|
|
206
|
+
def _task_wait(self, args: list[str]) -> None:
|
|
207
|
+
wait_ns = task_wait_parser.parse_args(args)
|
|
208
|
+
tasks_to_wait: list[str] = wait_ns.task or [
|
|
209
|
+
oid
|
|
210
|
+
for task in self.client.get_tasks()
|
|
211
|
+
if task['Result Status'] == ResultStatus.IN_PROGRESS and (oid := task.get_oid()) is not None
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
for task_id in tasks_to_wait:
|
|
215
|
+
task_obj = self._find_reference(MidpointObjectTypes.TASK.value, task_id, 'Task')
|
|
216
|
+
if task_obj is None:
|
|
217
|
+
continue
|
|
218
|
+
task_oid = task_obj.get_oid()
|
|
219
|
+
if task_oid is None:
|
|
220
|
+
raise AssertionError('Task OID cannot be None')
|
|
221
|
+
print_info(f'Waiting for task {task_oid} / {task_obj.get_name()}')
|
|
222
|
+
try:
|
|
223
|
+
self.client.task_wait(task_oid, create_progress_monitor(), show_changes=not wait_ns.no_changes)
|
|
224
|
+
except TaskExecutionFailure as e:
|
|
225
|
+
# Reported now, so a later failure does not hide this one.
|
|
226
|
+
self._fail()
|
|
227
|
+
self._print_failed_task(task_oid, e)
|
|
228
|
+
print_error(e.message)
|
|
229
|
+
|
|
230
|
+
def _task_action(self, action: str, parser: ArgumentParser, args: list[str]) -> None:
|
|
231
|
+
"""task run/resume/suspend: apply the action to each task in turn, stopping at the first failure."""
|
|
232
|
+
action_ns = parser.parse_args(args)
|
|
233
|
+
|
|
234
|
+
for task_id in action_ns.task:
|
|
235
|
+
task_obj = self._find_reference(MidpointObjectTypes.TASK.value, task_id, 'Task')
|
|
236
|
+
if task_obj is None:
|
|
237
|
+
continue
|
|
238
|
+
task_oid = task_obj.get_oid()
|
|
239
|
+
if task_oid is None:
|
|
240
|
+
raise AssertionError('Task OID cannot be None')
|
|
241
|
+
if action == 'run' and task_obj['Execution Status'] == TaskExecutionState.SUSPENDED:
|
|
242
|
+
print_info('Task currently suspended, activating it...')
|
|
243
|
+
self.client.task_action(task_oid, 'resume', NopProgressMonitor())
|
|
244
|
+
print_info('Now running task...')
|
|
245
|
+
|
|
246
|
+
print_info(f'Task {task_obj.get_name()} - {action}')
|
|
247
|
+
# Only a run is followed to completion.
|
|
248
|
+
monitor = create_progress_monitor() if action == 'run' else NopProgressMonitor()
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
task_result = self.client.task_action(
|
|
252
|
+
task_oid,
|
|
253
|
+
action,
|
|
254
|
+
monitor,
|
|
255
|
+
show_changes=not getattr(action_ns, 'no_changes', False),
|
|
256
|
+
)
|
|
257
|
+
# Always display the task result if available
|
|
258
|
+
if task_result is not None:
|
|
259
|
+
get_console().print(task_result.get_full_description())
|
|
260
|
+
except TaskExecutionFailure as e:
|
|
261
|
+
self._print_failed_task(task_oid, e)
|
|
262
|
+
self._fail(e.message)
|
|
263
|
+
break
|
|
264
|
+
|
|
265
|
+
def _print_failed_task(self, task_oid: str, failure: TaskExecutionFailure) -> None:
|
|
266
|
+
"""Show the details and statistics of a failed task, before its error is reported.
|
|
267
|
+
|
|
268
|
+
The error and action counts are what tells why a task failed, so they are shown
|
|
269
|
+
exactly as for a successful run. The task comes with the failure when the client
|
|
270
|
+
read it while waiting; otherwise it is fetched again.
|
|
271
|
+
"""
|
|
272
|
+
failed_task = failure.task
|
|
273
|
+
if failed_task is None:
|
|
274
|
+
try:
|
|
275
|
+
failed_task_xml = self.client.api_client.get_element(MidpointObjectTypes.TASK.value, task_oid)
|
|
276
|
+
failed_task = MidpointTask(safe_fromstring(failed_task_xml), self.client.get_server_version())
|
|
277
|
+
except Exception:
|
|
278
|
+
return # Without the task details, the error is still reported
|
|
279
|
+
get_console().print(failed_task.get_full_description())
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
def help_task() -> None:
|
|
283
|
+
task_parser.print_help()
|
|
284
|
+
|
|
285
|
+
def do_tasks(self, inp: str) -> None:
|
|
286
|
+
"""Shortcut for 'task ls' — list all server tasks."""
|
|
287
|
+
self._list_shortcut(inp, 'tasks', 'task', self.do_task)
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def help_tasks() -> None:
|
|
291
|
+
print('List all server tasks. This is a shortcut for "task ls"')
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def _print_tasks_table(tasks: MidpointObjectList) -> None:
|
|
295
|
+
"""Print tasks in the selected output format, coloring the result status."""
|
|
296
|
+
print_rows(tasks, title='Server Tasks', noun='tasks', cell_style=_task_cell_style)
|