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,89 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import sys
|
|
3
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
4
|
+
|
|
5
|
+
from midpoint_cli.client import MidpointObjectType, MidpointObjectTypes, MidpointServerError
|
|
6
|
+
from midpoint_cli.client.objects import MidpointTypeNotFound
|
|
7
|
+
from midpoint_cli.prompt.base import PromptBase
|
|
8
|
+
from midpoint_cli.prompt.complete import OBJECT_OF_TYPE, OBJECT_TYPE, completes
|
|
9
|
+
from midpoint_cli.prompt.console import print_info
|
|
10
|
+
|
|
11
|
+
delete_parser = ArgumentParser(
|
|
12
|
+
formatter_class=RawTextHelpFormatter,
|
|
13
|
+
prog='delete',
|
|
14
|
+
description='Delete a server object by OID or name.',
|
|
15
|
+
epilog='A name must designate a single object: when several objects have it, their OIDs are listed\n'
|
|
16
|
+
'and nothing is deleted. Deleting by name asks for confirmation, or needs --yes when not run\n'
|
|
17
|
+
'from a terminal.',
|
|
18
|
+
)
|
|
19
|
+
completes(delete_parser.add_argument('objectclass', help='Type of the object to delete (Java Type).'), OBJECT_TYPE)
|
|
20
|
+
completes(delete_parser.add_argument('oid', metavar='OID_OR_NAME', help='Object ID, or object name.'), OBJECT_OF_TYPE)
|
|
21
|
+
delete_parser.add_argument(
|
|
22
|
+
'-y', '--yes', action='store_true', help='Delete an object given by name without asking for confirmation.'
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DeleteClientPrompt(PromptBase):
|
|
27
|
+
parser_delete = parser_del = parser_rm = delete_parser
|
|
28
|
+
|
|
29
|
+
def do_rm(self, inp: str) -> None:
|
|
30
|
+
"""Alias for delete."""
|
|
31
|
+
self.do_delete(inp)
|
|
32
|
+
|
|
33
|
+
def do_del(self, inp: str) -> None:
|
|
34
|
+
"""Alias for delete."""
|
|
35
|
+
self.do_delete(inp)
|
|
36
|
+
|
|
37
|
+
def do_delete(self, inp: str) -> None:
|
|
38
|
+
"""Delete a server object by type and OID or name."""
|
|
39
|
+
try:
|
|
40
|
+
delete_args = shlex.split(inp)
|
|
41
|
+
ns = delete_parser.parse_args(delete_args)
|
|
42
|
+
|
|
43
|
+
# Resolve the type string to a MidpointObjectType
|
|
44
|
+
midpoint_type = MidpointObjectTypes.find_by_name(ns.objectclass)
|
|
45
|
+
|
|
46
|
+
if midpoint_type.endpoint is None:
|
|
47
|
+
self._fail(f'{ns.objectclass} cannot be deleted through the REST API')
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
oid = self.client.resolve_oid(midpoint_type, ns.oid)
|
|
51
|
+
|
|
52
|
+
# A name is a weaker reference than an OID: make sure the resolved object is the intended one.
|
|
53
|
+
if oid != ns.oid and not ns.yes and not self._confirm_delete(midpoint_type, ns.oid, oid):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
self.client.delete(midpoint_type, oid)
|
|
57
|
+
|
|
58
|
+
except (MidpointTypeNotFound, MidpointServerError, AttributeError) as e:
|
|
59
|
+
self._fail(str(e))
|
|
60
|
+
|
|
61
|
+
def _confirm_delete(self, midpoint_type: MidpointObjectType, name: str, oid: str) -> bool:
|
|
62
|
+
"""Ask before deleting an object designated by name; refuse when nobody can answer."""
|
|
63
|
+
if not (hasattr(sys.stdin, 'isatty') and sys.stdin.isatty()):
|
|
64
|
+
self._fail(
|
|
65
|
+
f'{name!r} is a name, not an OID: add --yes to delete {midpoint_type.tagname} {oid} '
|
|
66
|
+
f'without confirmation'
|
|
67
|
+
)
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
# The question is a message, so it goes to stderr: input() would write it on stdout.
|
|
71
|
+
sys.stderr.write(f"Delete {midpoint_type.tagname} '{name}' ({oid})? [y/N] ")
|
|
72
|
+
sys.stderr.flush()
|
|
73
|
+
try:
|
|
74
|
+
answer = input()
|
|
75
|
+
except EOFError:
|
|
76
|
+
answer = ''
|
|
77
|
+
if answer.strip().lower() in ('y', 'yes'):
|
|
78
|
+
return True
|
|
79
|
+
print_info('Deletion cancelled')
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def help_delete(self) -> None:
|
|
83
|
+
delete_parser.print_help()
|
|
84
|
+
|
|
85
|
+
def help_rm(self) -> None:
|
|
86
|
+
delete_parser.print_help()
|
|
87
|
+
|
|
88
|
+
def help_del(self) -> None:
|
|
89
|
+
delete_parser.print_help()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
3
|
+
from xml.etree import ElementTree
|
|
4
|
+
|
|
5
|
+
from midpoint_cli.client import MidpointObjectTypes, MidpointServerError
|
|
6
|
+
from midpoint_cli.client.constants import ResultStatus
|
|
7
|
+
from midpoint_cli.client.objects import MidpointTypeNotFound, namespaces
|
|
8
|
+
from midpoint_cli.client.patch import write_private_file
|
|
9
|
+
from midpoint_cli.client.safe_xml import fromstring as safe_fromstring
|
|
10
|
+
from midpoint_cli.prompt.base import PromptBase
|
|
11
|
+
from midpoint_cli.prompt.complete import FILE, OBJECT_OF_TYPE, OBJECT_TYPE, completes
|
|
12
|
+
from midpoint_cli.prompt.console import print_xml
|
|
13
|
+
|
|
14
|
+
get_parser = ArgumentParser(
|
|
15
|
+
formatter_class=RawTextHelpFormatter,
|
|
16
|
+
prog='get',
|
|
17
|
+
description='Get a server object by OID or name.',
|
|
18
|
+
epilog='A name must designate a single object: when several objects have it, their OIDs are listed\n'
|
|
19
|
+
'and nothing is fetched.',
|
|
20
|
+
)
|
|
21
|
+
completes(get_parser.add_argument('objectclass', help='Type of the object to fetch (Java Type).'), OBJECT_TYPE)
|
|
22
|
+
completes(get_parser.add_argument('oid', metavar='OID_OR_NAME', help='Object ID, or object name.'), OBJECT_OF_TYPE)
|
|
23
|
+
completes(get_parser.add_argument('file', help='Save the XML data to this file.', nargs='?'), FILE)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GetClientPrompt(PromptBase):
|
|
27
|
+
parser_get = get_parser
|
|
28
|
+
|
|
29
|
+
def do_get(self, inp: str) -> None:
|
|
30
|
+
"""Get an XML definition from the server by type and OID or name."""
|
|
31
|
+
try:
|
|
32
|
+
get_args = shlex.split(inp)
|
|
33
|
+
ns = get_parser.parse_args(get_args)
|
|
34
|
+
|
|
35
|
+
midpoint_type = MidpointObjectTypes.find_by_name(ns.objectclass)
|
|
36
|
+
|
|
37
|
+
if midpoint_type.endpoint is None:
|
|
38
|
+
self._fail(f'{ns.objectclass} cannot be fetched through the REST API')
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
oid = self.client.resolve_oid(midpoint_type, ns.oid)
|
|
42
|
+
xml_text = self.client.get_xml(midpoint_type, oid)
|
|
43
|
+
xml_root = safe_fromstring(xml_text)
|
|
44
|
+
|
|
45
|
+
status_node = xml_root.find('c:status', namespaces)
|
|
46
|
+
|
|
47
|
+
if status_node is not None and status_node.text == ResultStatus.FATAL_ERROR:
|
|
48
|
+
message_node = xml_root.find('c:message', namespaces)
|
|
49
|
+
self._fail((message_node.text if message_node is not None else None) or 'Unknown error')
|
|
50
|
+
elif ns.file is None:
|
|
51
|
+
print_xml(xml_text)
|
|
52
|
+
else:
|
|
53
|
+
# The file is created with mode 0600 and never through a symbolic link:
|
|
54
|
+
# object XML can hold PII and protected-string or hash material.
|
|
55
|
+
try:
|
|
56
|
+
write_private_file(ns.file, xml_text)
|
|
57
|
+
except OSError as e:
|
|
58
|
+
self._fail(f'Cannot write {ns.file}: {e.strerror or e}')
|
|
59
|
+
|
|
60
|
+
except (MidpointTypeNotFound, MidpointServerError, AttributeError) as e:
|
|
61
|
+
self._fail(str(e))
|
|
62
|
+
except ElementTree.ParseError as e:
|
|
63
|
+
self._fail(f'Server returned a malformed XML document: {e}')
|
|
64
|
+
|
|
65
|
+
def help_get(self) -> None:
|
|
66
|
+
get_parser.print_help()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from midpoint_cli.prompt.base import PromptBase
|
|
2
|
+
from midpoint_cli.prompt.console import (
|
|
3
|
+
create_table,
|
|
4
|
+
get_console,
|
|
5
|
+
is_machine_output,
|
|
6
|
+
print_info,
|
|
7
|
+
print_rows,
|
|
8
|
+
print_warning,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
INFO_COLUMNS = ('Property', 'Value')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InfoClientPrompt(PromptBase):
|
|
15
|
+
def do_info(self, inp: str) -> None:
|
|
16
|
+
"""Display server information (version, repository, subscription, nodes, account)."""
|
|
17
|
+
# Printed before calling the server, so a failing call still shows which server,
|
|
18
|
+
# account and timeouts were used, and where each setting came from.
|
|
19
|
+
if self.configuration is not None:
|
|
20
|
+
print_info(self.configuration.describe())
|
|
21
|
+
try:
|
|
22
|
+
server_info = self.client.get_server_information()
|
|
23
|
+
except Exception as e:
|
|
24
|
+
self._fail(f'Could not retrieve server information: {e}')
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
for problem in server_info.unavailable:
|
|
28
|
+
print_warning(f'Not shown, could not read {problem}')
|
|
29
|
+
|
|
30
|
+
if is_machine_output():
|
|
31
|
+
rows = [dict(zip(INFO_COLUMNS, item, strict=True)) for item in server_info.properties.items()]
|
|
32
|
+
print_rows(rows, title='Server Information', noun='properties', columns=INFO_COLUMNS)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if not server_info.properties:
|
|
36
|
+
print_info('No server information available')
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
table = create_table(title='Server Information')
|
|
40
|
+
table.add_column('Property', style='bright_cyan')
|
|
41
|
+
table.add_column('Value', style='white')
|
|
42
|
+
for key, value in server_info.properties.items():
|
|
43
|
+
table.add_row(key, value)
|
|
44
|
+
get_console().print(table)
|
|
45
|
+
|
|
46
|
+
def help_info(self) -> None:
|
|
47
|
+
print(
|
|
48
|
+
'Display server information: Midpoint version and build, repository, subscription,\n'
|
|
49
|
+
'deployment name, cluster nodes and the account in use. Sources the account may not\n'
|
|
50
|
+
'read are left out with a warning. The first line names the server URL, user and\n'
|
|
51
|
+
'timeouts in use and where each setting came from. Honours -o: json, csv and tsv\n'
|
|
52
|
+
'print Property/Value rows.'
|
|
53
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
3
|
+
|
|
4
|
+
from midpoint_cli.client import MidpointServerError
|
|
5
|
+
from midpoint_cli.client.logging_config import LOGGING_LEVELS
|
|
6
|
+
from midpoint_cli.prompt.base import PromptBase
|
|
7
|
+
from midpoint_cli.prompt.console import is_machine_output, print_info, print_rows, print_success
|
|
8
|
+
|
|
9
|
+
log_parser = ArgumentParser(
|
|
10
|
+
formatter_class=RawTextHelpFormatter,
|
|
11
|
+
prog='log',
|
|
12
|
+
description='Show and set the logger levels of the server (system configuration, logging section).',
|
|
13
|
+
epilog=f"""
|
|
14
|
+
Available commands:
|
|
15
|
+
ls List the class loggers and their levels.
|
|
16
|
+
set Set the level of a logger: log set LOGGER LEVEL. The logger is added
|
|
17
|
+
when it is not configured yet.
|
|
18
|
+
|
|
19
|
+
Short form of set: log LOGGER LEVEL, e.g. log com.evolveum.midpoint.model DEBUG
|
|
20
|
+
(use "log set" for a logger named ls or set).
|
|
21
|
+
LEVEL is one of {', '.join(LOGGING_LEVELS)} (any case).
|
|
22
|
+
The change is saved in the system configuration and Midpoint applies it at once.
|
|
23
|
+
""",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
log_ls_parser = ArgumentParser(
|
|
27
|
+
formatter_class=RawTextHelpFormatter,
|
|
28
|
+
prog='log ls',
|
|
29
|
+
description='List the class loggers of the system configuration with their level and appenders.\n'
|
|
30
|
+
'The root logger level is printed after the table.',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
log_set_parser = ArgumentParser(
|
|
34
|
+
formatter_class=RawTextHelpFormatter,
|
|
35
|
+
prog='log set',
|
|
36
|
+
description='Set the level of a class logger, adding the logger when it is not configured yet.\n'
|
|
37
|
+
'Only the logging section of the system configuration is modified.',
|
|
38
|
+
)
|
|
39
|
+
log_set_parser.add_argument('logger', help='Logger name: a package or class, e.g. com.evolveum.midpoint.model.')
|
|
40
|
+
log_set_parser.add_argument(
|
|
41
|
+
'level', type=str.upper, choices=LOGGING_LEVELS, help='Logging level, case-insensitive: %(choices)s.'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Each subcommand with its parser; None for those taking no arguments.
|
|
45
|
+
log_subparsers: dict[str, ArgumentParser | None] = {'ls': log_ls_parser, 'set': log_set_parser}
|
|
46
|
+
|
|
47
|
+
LOG_COLUMNS = ('Logger', 'Level', 'Appenders')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LogClientPrompt(PromptBase):
|
|
51
|
+
subparsers_log = log_subparsers
|
|
52
|
+
|
|
53
|
+
def do_log(self, inp: str) -> None:
|
|
54
|
+
"""Show and set server logger levels (subcommands: ls, set; or: log LOGGER LEVEL)."""
|
|
55
|
+
try:
|
|
56
|
+
log_args = shlex.split(inp)
|
|
57
|
+
if self._subcommand_help(log_args, log_subparsers):
|
|
58
|
+
return
|
|
59
|
+
if not log_args or log_args[0] in ('-h', '--help'):
|
|
60
|
+
log_parser.print_help()
|
|
61
|
+
self.error_code = 0 if log_args else 1
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if log_args[0] == 'ls':
|
|
65
|
+
log_ls_parser.parse_args(log_args[1:])
|
|
66
|
+
self._list_loggers()
|
|
67
|
+
elif log_args[0] == 'set':
|
|
68
|
+
ns = log_set_parser.parse_args(log_args[1:])
|
|
69
|
+
self._set_logger(ns.logger, ns.level)
|
|
70
|
+
else:
|
|
71
|
+
# Short form: log LOGGER LEVEL
|
|
72
|
+
ns = log_set_parser.parse_args(log_args)
|
|
73
|
+
self._set_logger(ns.logger, ns.level)
|
|
74
|
+
except (MidpointServerError, ValueError) as e:
|
|
75
|
+
self._fail(str(e))
|
|
76
|
+
|
|
77
|
+
def _list_loggers(self) -> None:
|
|
78
|
+
configuration = self.client.get_logging_configuration()
|
|
79
|
+
rows = [
|
|
80
|
+
dict(zip(LOG_COLUMNS, (logger.package, logger.level or '', ', '.join(logger.appenders)), strict=True))
|
|
81
|
+
for logger in configuration.class_loggers
|
|
82
|
+
]
|
|
83
|
+
print_rows(rows, title='Class Loggers', noun='class loggers', columns=LOG_COLUMNS)
|
|
84
|
+
# Machine formats write data only, so the root level is a table-mode note.
|
|
85
|
+
if not is_machine_output():
|
|
86
|
+
print_info(f'Root logger level: {configuration.root_level or "(not set)"}')
|
|
87
|
+
|
|
88
|
+
def _set_logger(self, logger: str, level: str) -> None:
|
|
89
|
+
outcome = self.client.set_class_logger(logger, level)
|
|
90
|
+
if outcome == 'unchanged':
|
|
91
|
+
print_info(f'Logger {logger} is already at level {level}')
|
|
92
|
+
elif outcome == 'added':
|
|
93
|
+
print_success(f'Logger {logger} added at level {level}')
|
|
94
|
+
else:
|
|
95
|
+
print_success(f'Logger {logger} set to level {level}')
|
|
96
|
+
|
|
97
|
+
def help_log(self) -> None:
|
|
98
|
+
log_parser.print_help()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
3
|
+
|
|
4
|
+
# Org command wrapper parser
|
|
5
|
+
from midpoint_cli.client import MidpointClient, MidpointObjectList, MidpointObjectTypes, MidpointServerError
|
|
6
|
+
from midpoint_cli.prompt.base import PromptBase, add_listing_arguments, print_object_detail
|
|
7
|
+
from midpoint_cli.prompt.complete import ORG, completes
|
|
8
|
+
from midpoint_cli.prompt.console import print_rows
|
|
9
|
+
|
|
10
|
+
org_parser = ArgumentParser(
|
|
11
|
+
formatter_class=RawTextHelpFormatter,
|
|
12
|
+
prog='org',
|
|
13
|
+
description='Manage server organizations.',
|
|
14
|
+
epilog="""
|
|
15
|
+
Available commands:
|
|
16
|
+
ls List all organizations.
|
|
17
|
+
search Search for an organization.
|
|
18
|
+
get Get organization details by OID or name.
|
|
19
|
+
""",
|
|
20
|
+
)
|
|
21
|
+
org_parser.add_argument('command', help='Organization command to execute.')
|
|
22
|
+
org_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
|
|
23
|
+
|
|
24
|
+
org_ls_parser = add_listing_arguments(
|
|
25
|
+
ArgumentParser(
|
|
26
|
+
formatter_class=RawTextHelpFormatter,
|
|
27
|
+
prog='org ls',
|
|
28
|
+
description='List organizations.',
|
|
29
|
+
),
|
|
30
|
+
'organizations',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
org_search = ArgumentParser(
|
|
34
|
+
formatter_class=RawTextHelpFormatter,
|
|
35
|
+
prog='org search',
|
|
36
|
+
description='Search for organizations by substring of their name or display name, ignoring case.\n'
|
|
37
|
+
'Terms are combined with OR. Prefix a term with + to make it mandatory (AND).',
|
|
38
|
+
)
|
|
39
|
+
org_search.add_argument(
|
|
40
|
+
'searchquery',
|
|
41
|
+
help='String fragments to search for. Use +term to require a match (AND); plain terms are optional (OR).',
|
|
42
|
+
nargs='+',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
org_get_parser = ArgumentParser(
|
|
46
|
+
formatter_class=RawTextHelpFormatter,
|
|
47
|
+
prog='org get',
|
|
48
|
+
description='Get organization details by OID or name.',
|
|
49
|
+
)
|
|
50
|
+
completes(org_get_parser.add_argument('oid', help='An organization OID or name.'), ORG)
|
|
51
|
+
|
|
52
|
+
# Each subcommand with its parser; None for those taking no arguments.
|
|
53
|
+
org_subparsers: dict[str, ArgumentParser | None] = {'ls': org_ls_parser, 'search': org_search, 'get': org_get_parser}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _enrich_parent_names(orgs: MidpointObjectList, client: MidpointClient) -> None:
|
|
57
|
+
"""Show each parent as 'OID / name', fetching the parents missing from orgs in one search."""
|
|
58
|
+
known = {org['OID']: org for org in orgs}
|
|
59
|
+
missing = sorted({org['Parent'] for org in orgs if org.get('Parent') and org['Parent'] not in known})
|
|
60
|
+
if missing:
|
|
61
|
+
with contextlib.suppress(MidpointServerError): # Parent names are cosmetic: show the bare OIDs.
|
|
62
|
+
known.update({org['OID']: org for org in client.get_orgs_by_oid(missing)})
|
|
63
|
+
lookup = {oid: org.get('DisplayName') or org.get('Name', '') for oid, org in known.items()}
|
|
64
|
+
for org in orgs:
|
|
65
|
+
parent_oid = org.get('Parent')
|
|
66
|
+
if parent_oid:
|
|
67
|
+
parent_name = lookup.get(parent_oid, '')
|
|
68
|
+
org['Parent'] = f'{parent_oid} / {parent_name}' if parent_name else parent_oid
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class OrgClientPrompt(PromptBase):
|
|
72
|
+
subparsers_org = org_subparsers
|
|
73
|
+
|
|
74
|
+
def do_org(self, inp: str) -> None:
|
|
75
|
+
"""Manage organizations (subcommands: ls, search, get)."""
|
|
76
|
+
self._dispatch_subcommand(
|
|
77
|
+
inp,
|
|
78
|
+
org_parser,
|
|
79
|
+
org_subparsers,
|
|
80
|
+
{'ls': self._org_ls, 'search': self._org_search, 'get': self._org_get},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _org_ls(self, args: list[str]) -> None:
|
|
84
|
+
ls_ns = org_ls_parser.parse_args(args)
|
|
85
|
+
orgs = self.client.get_orgs(limit=ls_ns.limit, name_filter=ls_ns.filter)
|
|
86
|
+
_enrich_parent_names(orgs, self.client)
|
|
87
|
+
self._print_orgs_table(orgs)
|
|
88
|
+
|
|
89
|
+
def _org_search(self, args: list[str]) -> None:
|
|
90
|
+
search_ns = org_search.parse_args(args)
|
|
91
|
+
orgs = self.client.search_orgs(search_ns.searchquery)
|
|
92
|
+
_enrich_parent_names(orgs, self.client)
|
|
93
|
+
self._print_orgs_table(orgs)
|
|
94
|
+
|
|
95
|
+
def _org_get(self, args: list[str]) -> None:
|
|
96
|
+
get_ns = org_get_parser.parse_args(args)
|
|
97
|
+
oid = self.client.resolve_oid(MidpointObjectTypes.ORG.value, get_ns.oid)
|
|
98
|
+
org = self.client.get_org(oid)
|
|
99
|
+
parent_oid = org.get('Parent')
|
|
100
|
+
if parent_oid:
|
|
101
|
+
try:
|
|
102
|
+
parent = self.client.get_org(parent_oid)
|
|
103
|
+
parent_name = parent.get('DisplayName') or parent.get('Name', '')
|
|
104
|
+
org['Parent'] = f'{parent_oid} / {parent_name}' if parent_name else parent_oid
|
|
105
|
+
except MidpointServerError:
|
|
106
|
+
pass
|
|
107
|
+
print_object_detail(org, 'Organization Details')
|
|
108
|
+
|
|
109
|
+
def help_org(self) -> None:
|
|
110
|
+
org_parser.print_help()
|
|
111
|
+
|
|
112
|
+
def do_orgs(self, inp: str) -> None:
|
|
113
|
+
"""Shortcut for 'org ls' — list all organizations."""
|
|
114
|
+
self._list_shortcut(inp, 'orgs', 'org', self.do_org)
|
|
115
|
+
|
|
116
|
+
def help_orgs(self) -> None:
|
|
117
|
+
print('List all server organizations. This is a shortcut for "org ls"')
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def _print_orgs_table(orgs: MidpointObjectList) -> None:
|
|
121
|
+
"""Print organizations in the selected output format."""
|
|
122
|
+
print_rows(orgs, title='Organizations', noun='organizations')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""`output` command: show or change the output format for the rest of the session."""
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
5
|
+
|
|
6
|
+
from midpoint_cli.prompt.base import PromptBase
|
|
7
|
+
from midpoint_cli.prompt.console import OUTPUT_FORMATS, get_output_format, print_info, set_output_format
|
|
8
|
+
|
|
9
|
+
output_parser = ArgumentParser(
|
|
10
|
+
formatter_class=RawTextHelpFormatter,
|
|
11
|
+
prog='output',
|
|
12
|
+
description='Show or change the output format of list and detail commands for the rest of the session.',
|
|
13
|
+
epilog='Without a format, the current one is shown. The format starts as the -o/--output option\n'
|
|
14
|
+
'selects (by default table on a terminal, tsv when the output is redirected or piped).',
|
|
15
|
+
)
|
|
16
|
+
output_parser.add_argument('format', nargs='?', choices=OUTPUT_FORMATS, help='The new output format: %(choices)s.')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OutputClientPrompt(PromptBase):
|
|
20
|
+
parser_output = output_parser
|
|
21
|
+
|
|
22
|
+
def do_output(self, inp: str) -> None:
|
|
23
|
+
"""Show or change the output format of this session (table, json, csv, tsv)."""
|
|
24
|
+
ns = output_parser.parse_args(shlex.split(inp))
|
|
25
|
+
if ns.format is None:
|
|
26
|
+
print_info(f'Output format: {get_output_format()}')
|
|
27
|
+
else:
|
|
28
|
+
set_output_format(ns.format)
|
|
29
|
+
print_info(f'Output format set to {ns.format}')
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def help_output() -> None:
|
|
33
|
+
output_parser.print_help()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
3
|
+
from xml.etree import ElementTree
|
|
4
|
+
|
|
5
|
+
from midpoint_cli.client import MidpointServerError, MidpointUnsupportedOperation
|
|
6
|
+
from midpoint_cli.client.objects import MidpointTypeNotFound
|
|
7
|
+
from midpoint_cli.client.patch import PatchError
|
|
8
|
+
from midpoint_cli.prompt.base import PromptBase
|
|
9
|
+
from midpoint_cli.prompt.complete import FILE, completes
|
|
10
|
+
from midpoint_cli.prompt.console import print_success
|
|
11
|
+
|
|
12
|
+
put_parser = ArgumentParser(
|
|
13
|
+
formatter_class=RawTextHelpFormatter,
|
|
14
|
+
prog='put',
|
|
15
|
+
description='Create or update a server object.',
|
|
16
|
+
epilog='A root element with an oid attribute creates or replaces the object at that OID.\n'
|
|
17
|
+
'Without one, a new object is created and the server assigns its OID.',
|
|
18
|
+
)
|
|
19
|
+
completes(
|
|
20
|
+
put_parser.add_argument(
|
|
21
|
+
'-p', '--patch', help='Patch the object according to the following patch file.', action='store'
|
|
22
|
+
),
|
|
23
|
+
FILE,
|
|
24
|
+
)
|
|
25
|
+
put_parser.add_argument(
|
|
26
|
+
'-w', '--patchwrite', help='Write the result of the patching phase in a file <patchfile>.xml', action='store_true'
|
|
27
|
+
)
|
|
28
|
+
completes(put_parser.add_argument('file', nargs='+', help='Load XML data from this file (one or more).'), FILE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PutClientPrompt(PromptBase):
|
|
32
|
+
parser_put = put_parser
|
|
33
|
+
|
|
34
|
+
def do_put(self, inp: str) -> None:
|
|
35
|
+
"""Create or update a server object from an XML file."""
|
|
36
|
+
# Parsed outside the upload loop so an argparse SystemExit is not mistaken
|
|
37
|
+
# for a failed file; onecmd turns it into the exit code.
|
|
38
|
+
put_args = shlex.split(inp)
|
|
39
|
+
ns = put_parser.parse_args(put_args)
|
|
40
|
+
|
|
41
|
+
for f in ns.file:
|
|
42
|
+
try:
|
|
43
|
+
result = self.client.put_xml(f, ns.patch, ns.patchwrite)
|
|
44
|
+
if not result.created:
|
|
45
|
+
print_success(f'Object uploaded: {result.object_type.object_type} {result.oid}')
|
|
46
|
+
elif result.oid is not None:
|
|
47
|
+
print_success(f'Object created: {result.object_type.object_type} {result.oid}')
|
|
48
|
+
else:
|
|
49
|
+
print_success(
|
|
50
|
+
f'Object accepted: {result.object_type.object_type} '
|
|
51
|
+
'(the server reported no OID; creation may be pending approval)'
|
|
52
|
+
)
|
|
53
|
+
except (MidpointServerError, MidpointUnsupportedOperation, MidpointTypeNotFound) as e:
|
|
54
|
+
self._fail_put(f, str(e))
|
|
55
|
+
return
|
|
56
|
+
except ElementTree.ParseError as e:
|
|
57
|
+
self._fail_put(f, f'not a well-formed XML document ({e})')
|
|
58
|
+
return
|
|
59
|
+
except PatchError as e:
|
|
60
|
+
self._fail_put(f, str(e))
|
|
61
|
+
return
|
|
62
|
+
except OSError as e:
|
|
63
|
+
self._fail_put(f, str(e.strerror or e))
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
def _fail_put(self, filename: str, reason: str) -> None:
|
|
67
|
+
"""Stop the upload run, reporting which file failed and why."""
|
|
68
|
+
self._fail(f'{filename}: {reason}')
|
|
69
|
+
|
|
70
|
+
def help_put(self) -> None:
|
|
71
|
+
put_parser.print_help()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
2
|
+
|
|
3
|
+
from midpoint_cli.client import MidpointObjectTypes
|
|
4
|
+
from midpoint_cli.prompt.base import PromptBase, add_listing_arguments, add_new_task_arguments
|
|
5
|
+
from midpoint_cli.prompt.complete import REPORT, completes
|
|
6
|
+
from midpoint_cli.prompt.console import print_rows
|
|
7
|
+
|
|
8
|
+
report_parser = ArgumentParser(
|
|
9
|
+
formatter_class=RawTextHelpFormatter,
|
|
10
|
+
prog='report',
|
|
11
|
+
description='List and run reports.',
|
|
12
|
+
epilog="""
|
|
13
|
+
Available commands:
|
|
14
|
+
ls List reports.
|
|
15
|
+
run Run a report (creates and runs a report export task).
|
|
16
|
+
""",
|
|
17
|
+
)
|
|
18
|
+
report_parser.add_argument('command', help='Report command to execute.')
|
|
19
|
+
report_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
|
|
20
|
+
|
|
21
|
+
report_ls_parser = add_listing_arguments(
|
|
22
|
+
ArgumentParser(
|
|
23
|
+
formatter_class=RawTextHelpFormatter,
|
|
24
|
+
prog='report ls',
|
|
25
|
+
description='List reports.',
|
|
26
|
+
),
|
|
27
|
+
'reports',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
report_run_parser = add_new_task_arguments(
|
|
31
|
+
ArgumentParser(
|
|
32
|
+
formatter_class=RawTextHelpFormatter,
|
|
33
|
+
prog='report run',
|
|
34
|
+
description='Run a report: create a report export task and run it.',
|
|
35
|
+
epilog='The server stores the output as a report data object, which the Midpoint GUI lists\n'
|
|
36
|
+
'under Reports > Created reports.',
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
completes(report_run_parser.add_argument('report', help='Report OID or name.'), REPORT)
|
|
40
|
+
|
|
41
|
+
# Each subcommand with its parser; None for those taking no arguments.
|
|
42
|
+
report_subparsers: dict[str, ArgumentParser | None] = {'ls': report_ls_parser, 'run': report_run_parser}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ReportClientPrompt(PromptBase):
|
|
46
|
+
subparsers_report = report_subparsers
|
|
47
|
+
|
|
48
|
+
def do_report(self, inp: str) -> None:
|
|
49
|
+
"""List and run reports (subcommands: ls, run)."""
|
|
50
|
+
self._dispatch_subcommand(
|
|
51
|
+
inp, report_parser, report_subparsers, {'ls': self._report_ls, 'run': self._report_run}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _report_ls(self, args: list[str]) -> None:
|
|
55
|
+
ls_ns = report_ls_parser.parse_args(args)
|
|
56
|
+
reports = self.client.get_reports(limit=ls_ns.limit, name_filter=ls_ns.filter)
|
|
57
|
+
print_rows(reports, title='Reports', noun='reports')
|
|
58
|
+
|
|
59
|
+
def _report_run(self, args: list[str]) -> None:
|
|
60
|
+
run_ns = report_run_parser.parse_args(args)
|
|
61
|
+
report = self._require_object(MidpointObjectTypes.REPORT.value, run_ns.report, 'Report')
|
|
62
|
+
if report is None:
|
|
63
|
+
return
|
|
64
|
+
task_oid, task_name = self.client.start_report_task(report)
|
|
65
|
+
self._follow_new_task(task_oid, task_name, run_ns.no_wait, show_changes=not run_ns.no_changes)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def help_report() -> None:
|
|
69
|
+
report_parser.print_help()
|