midpoint-cli 1.6.0.dev3__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.dev3.dist-info/METADATA +1334 -0
- midpoint_cli-1.6.0.dev3.dist-info/RECORD +47 -0
- midpoint_cli-1.6.0.dev3.dist-info/WHEEL +4 -0
- midpoint_cli-1.6.0.dev3.dist-info/entry_points.txt +3 -0
- midpoint_cli-1.6.0.dev3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
2
|
+
|
|
3
|
+
from midpoint_cli.client import SHADOW_SITUATIONS, MidpointObjectList, MidpointObjectTypes, MidpointServerError
|
|
4
|
+
from midpoint_cli.client.constants import ResultStatus
|
|
5
|
+
from midpoint_cli.client.task_definitions import SHADOW_KINDS
|
|
6
|
+
from midpoint_cli.prompt.base import PromptBase, add_listing_arguments, add_new_task_arguments
|
|
7
|
+
from midpoint_cli.prompt.complete import RESOURCE, completes
|
|
8
|
+
from midpoint_cli.prompt.console import print_error, print_info, print_rows, print_success, print_warning
|
|
9
|
+
|
|
10
|
+
resource_parser = ArgumentParser(
|
|
11
|
+
formatter_class=RawTextHelpFormatter,
|
|
12
|
+
prog='resource',
|
|
13
|
+
description='Manage resources.',
|
|
14
|
+
epilog="""
|
|
15
|
+
Available commands:
|
|
16
|
+
ls List all server resources. Exits with status 1 when any resource
|
|
17
|
+
has a known availability status other than 'up'. With --metrics,
|
|
18
|
+
also count the shadows of each resource by synchronization situation.
|
|
19
|
+
test Test a resource.
|
|
20
|
+
import Import objects from a resource (creates and runs an import task).
|
|
21
|
+
reconcile Reconcile a resource (creates and runs a reconciliation task).
|
|
22
|
+
""",
|
|
23
|
+
)
|
|
24
|
+
resource_parser.add_argument('command', help='Resource command to execute.')
|
|
25
|
+
resource_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
|
|
26
|
+
|
|
27
|
+
resource_ls_parser = add_listing_arguments(
|
|
28
|
+
ArgumentParser(
|
|
29
|
+
formatter_class=RawTextHelpFormatter,
|
|
30
|
+
prog='resource ls',
|
|
31
|
+
description='List server resources. Exits with status 1 when any listed resource\n'
|
|
32
|
+
"has a known availability status other than 'up'.",
|
|
33
|
+
),
|
|
34
|
+
'resources',
|
|
35
|
+
)
|
|
36
|
+
resource_ls_parser.add_argument(
|
|
37
|
+
'--metrics',
|
|
38
|
+
action='store_true',
|
|
39
|
+
help='Also count the shadows of each resource by synchronization situation.\n'
|
|
40
|
+
'Midpoint 4.10+ counts on the server (6 small searches per resource); earlier\n'
|
|
41
|
+
'versions download every shadow of the resource, which is slow on large ones.',
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Shadow count columns of resource ls --metrics: (count key, column header).
|
|
45
|
+
METRIC_COLUMNS = (
|
|
46
|
+
*((situation, situation.capitalize()) for situation in SHADOW_SITUATIONS),
|
|
47
|
+
('none', 'No Situation'),
|
|
48
|
+
('total', 'Shadows'),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# resource RUN parser
|
|
52
|
+
|
|
53
|
+
resource_test_parser = ArgumentParser(
|
|
54
|
+
formatter_class=RawTextHelpFormatter,
|
|
55
|
+
prog='resource test',
|
|
56
|
+
description='Test a resource status.',
|
|
57
|
+
)
|
|
58
|
+
completes(resource_test_parser.add_argument('resource', help='Resource to be tested.', nargs='+'), RESOURCE)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resource_task_parser(operation: str, description: str) -> ArgumentParser:
|
|
62
|
+
parser = ArgumentParser(
|
|
63
|
+
formatter_class=RawTextHelpFormatter,
|
|
64
|
+
prog=f'resource {operation}',
|
|
65
|
+
description=description,
|
|
66
|
+
epilog='Objects are selected by kind and intent, by object class, or both. Without --kind and\n'
|
|
67
|
+
'--object-class, the accounts of the resource are processed. The task is created like the\n'
|
|
68
|
+
'Midpoint GUI creates it and stays on the server, so `task run` can run it again.',
|
|
69
|
+
)
|
|
70
|
+
completes(parser.add_argument('resource', help='Resource OID or name.'), RESOURCE)
|
|
71
|
+
parser.add_argument('--kind', choices=SHADOW_KINDS, help='Kind of the resource objects (default: account).')
|
|
72
|
+
parser.add_argument('--intent', help='Intent of the resource objects (default: the default intent of the kind).')
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
'--object-class',
|
|
75
|
+
metavar='NAME',
|
|
76
|
+
help='Object class of the resource objects, e.g. AccountObjectClass or inetOrgPerson.',
|
|
77
|
+
)
|
|
78
|
+
return add_new_task_arguments(parser)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
resource_import_parser = _resource_task_parser(
|
|
82
|
+
'import', 'Import objects from a resource: create an import task and run it.'
|
|
83
|
+
)
|
|
84
|
+
resource_reconcile_parser = _resource_task_parser(
|
|
85
|
+
'reconcile', 'Reconcile a resource: create a reconciliation task and run it.'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Each subcommand with its parser; None for those taking no arguments.
|
|
89
|
+
resource_subparsers: dict[str, ArgumentParser | None] = {
|
|
90
|
+
'ls': resource_ls_parser,
|
|
91
|
+
'test': resource_test_parser,
|
|
92
|
+
'import': resource_import_parser,
|
|
93
|
+
'reconcile': resource_reconcile_parser,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _status_style(column: str, value: str) -> str | None:
|
|
98
|
+
if 'status' in column.lower():
|
|
99
|
+
return {'up': 'green', 'down': 'red'}.get(value.lower())
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ResourceClientPrompt(PromptBase):
|
|
104
|
+
subparsers_resource = resource_subparsers
|
|
105
|
+
|
|
106
|
+
def do_resource(self, inp: str) -> None:
|
|
107
|
+
"""Manage server resources (subcommands: ls, test, import, reconcile)."""
|
|
108
|
+
self._dispatch_subcommand(
|
|
109
|
+
inp,
|
|
110
|
+
resource_parser,
|
|
111
|
+
resource_subparsers,
|
|
112
|
+
{
|
|
113
|
+
'ls': self._resource_ls,
|
|
114
|
+
'test': self._resource_test,
|
|
115
|
+
'import': lambda args: self._start_resource_task('import', args),
|
|
116
|
+
'reconcile': lambda args: self._start_resource_task('reconcile', args),
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _resource_ls(self, args: list[str]) -> None:
|
|
121
|
+
ls_ns = resource_ls_parser.parse_args(args)
|
|
122
|
+
resources = self.client.get_resources(limit=ls_ns.limit, name_filter=ls_ns.filter)
|
|
123
|
+
connectors = self.client.get_connectors()
|
|
124
|
+
|
|
125
|
+
connectors_by_oid = {c['OID']: c for c in connectors}
|
|
126
|
+
|
|
127
|
+
for resource in resources:
|
|
128
|
+
connector = connectors_by_oid.get(resource['connectorRef'])
|
|
129
|
+
|
|
130
|
+
if connector is not None:
|
|
131
|
+
connector_type = connector['Connector type']
|
|
132
|
+
connector_version = connector['Version']
|
|
133
|
+
elif resource['connectorRef']:
|
|
134
|
+
# connectorRef points at a connector the server did not return:
|
|
135
|
+
# a dangling reference, or one hidden by authorizations.
|
|
136
|
+
connector_type = '(unknown)'
|
|
137
|
+
connector_version = ''
|
|
138
|
+
else:
|
|
139
|
+
# No connectorRef at all, e.g. an abstract resource template.
|
|
140
|
+
connector_type = ''
|
|
141
|
+
connector_version = ''
|
|
142
|
+
|
|
143
|
+
# Applied on every path: _print_resources_table takes its columns
|
|
144
|
+
# from resources[0] alone, so a row with different keys would be
|
|
145
|
+
# rendered under the wrong headers.
|
|
146
|
+
del resource['connectorRef']
|
|
147
|
+
resource['Connector Type'] = connector_type
|
|
148
|
+
resource['Version'] = connector_version
|
|
149
|
+
|
|
150
|
+
if ls_ns.metrics:
|
|
151
|
+
self._add_shadow_metrics(resources)
|
|
152
|
+
|
|
153
|
+
self._print_resources_table(resources)
|
|
154
|
+
|
|
155
|
+
s = {r['Availability Status'] for r in resources if r['Availability Status']}
|
|
156
|
+
self.error_code = 1 if s - {'up'} else 0
|
|
157
|
+
|
|
158
|
+
def _resource_test(self, args: list[str]) -> None:
|
|
159
|
+
run_ns = resource_test_parser.parse_args(args)
|
|
160
|
+
|
|
161
|
+
for resource_id in run_ns.resource:
|
|
162
|
+
print_info(f'Testing resource {resource_id}...')
|
|
163
|
+
resource_obj = self._find_reference(MidpointObjectTypes.RESOURCE.value, resource_id, 'Resource')
|
|
164
|
+
|
|
165
|
+
if resource_obj is not None:
|
|
166
|
+
resource_oid = resource_obj.get_oid()
|
|
167
|
+
if resource_oid is None:
|
|
168
|
+
raise AssertionError('Resource OID cannot be None')
|
|
169
|
+
status = self.client.test_resource(resource_oid)
|
|
170
|
+
if status == ResultStatus.SUCCESS:
|
|
171
|
+
print_success(f'Test status: {status}')
|
|
172
|
+
else:
|
|
173
|
+
self._fail()
|
|
174
|
+
print_error(f'Test status: {status}')
|
|
175
|
+
|
|
176
|
+
def _start_resource_task(self, operation: str, args: list[str]) -> None:
|
|
177
|
+
"""resource import/reconcile: create the task for the resource, then follow it."""
|
|
178
|
+
parser = resource_import_parser if operation == 'import' else resource_reconcile_parser
|
|
179
|
+
task_ns = parser.parse_args(args)
|
|
180
|
+
kind = task_ns.kind or (None if task_ns.object_class else 'account')
|
|
181
|
+
resource_obj = self._require_object(MidpointObjectTypes.RESOURCE.value, task_ns.resource, 'Resource')
|
|
182
|
+
if resource_obj is None:
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
task_oid, task_name = self.client.start_resource_task(
|
|
186
|
+
operation, resource_obj, kind=kind, intent=task_ns.intent, object_class=task_ns.object_class
|
|
187
|
+
)
|
|
188
|
+
except ValueError as e:
|
|
189
|
+
self._fail(str(e))
|
|
190
|
+
return
|
|
191
|
+
self._follow_new_task(task_oid, task_name, task_ns.no_wait, show_changes=not task_ns.no_changes)
|
|
192
|
+
|
|
193
|
+
def help_resource(self) -> None:
|
|
194
|
+
resource_parser.print_help()
|
|
195
|
+
|
|
196
|
+
def do_resources(self, inp: str) -> None:
|
|
197
|
+
"""Shortcut for 'resource ls' — list all server resources."""
|
|
198
|
+
self._list_shortcut(inp, 'resources', 'resource', self.do_resource)
|
|
199
|
+
|
|
200
|
+
def help_resources(self) -> None:
|
|
201
|
+
print('List all server resources. This is a shortcut for "resource ls"')
|
|
202
|
+
|
|
203
|
+
def _add_shadow_metrics(self, resources: MidpointObjectList) -> None:
|
|
204
|
+
"""Add the shadow count columns to each resource; one whose shadows cannot be counted gets empty cells."""
|
|
205
|
+
for resource in resources:
|
|
206
|
+
try:
|
|
207
|
+
counts: dict[str, int] | None = self.client.get_resource_shadow_counts(resource['OID'])
|
|
208
|
+
except MidpointServerError as e:
|
|
209
|
+
print_warning(f'Cannot count the shadows of resource {resource["Name"]}: {e}')
|
|
210
|
+
counts = None
|
|
211
|
+
for key, header in METRIC_COLUMNS:
|
|
212
|
+
resource[header] = str(counts[key]) if counts is not None else ''
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _print_resources_table(resources: MidpointObjectList) -> None:
|
|
216
|
+
"""Print resources in the selected output format, coloring the availability status."""
|
|
217
|
+
print_rows(resources, title='Server Resources', noun='resources', cell_style=_status_style)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
2
|
+
|
|
3
|
+
from midpoint_cli.client import MidpointObjectList, MidpointObjectTypes
|
|
4
|
+
from midpoint_cli.prompt.base import PromptBase, add_listing_arguments, print_object_detail
|
|
5
|
+
from midpoint_cli.prompt.complete import ROLE, completes
|
|
6
|
+
from midpoint_cli.prompt.console import print_rows
|
|
7
|
+
|
|
8
|
+
role_parser = ArgumentParser(
|
|
9
|
+
formatter_class=RawTextHelpFormatter,
|
|
10
|
+
prog='role',
|
|
11
|
+
description='Manage server roles.',
|
|
12
|
+
epilog="""
|
|
13
|
+
Available commands:
|
|
14
|
+
ls List all roles.
|
|
15
|
+
search Search for a role.
|
|
16
|
+
get Get role details by OID or name.
|
|
17
|
+
""",
|
|
18
|
+
)
|
|
19
|
+
role_parser.add_argument('command', help='Role command to execute.')
|
|
20
|
+
role_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
|
|
21
|
+
|
|
22
|
+
role_ls_parser = add_listing_arguments(
|
|
23
|
+
ArgumentParser(
|
|
24
|
+
formatter_class=RawTextHelpFormatter,
|
|
25
|
+
prog='role ls',
|
|
26
|
+
description='List roles.',
|
|
27
|
+
),
|
|
28
|
+
'roles',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
role_search = ArgumentParser(
|
|
32
|
+
formatter_class=RawTextHelpFormatter,
|
|
33
|
+
prog='role search',
|
|
34
|
+
description='Search for roles by substring of their name or display name, ignoring case.\n'
|
|
35
|
+
'Terms are combined with OR. Prefix a term with + to make it mandatory (AND).',
|
|
36
|
+
)
|
|
37
|
+
role_search.add_argument(
|
|
38
|
+
'searchquery',
|
|
39
|
+
help='String fragments to search for. Use +term to require a match (AND); plain terms are optional (OR).',
|
|
40
|
+
nargs='+',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
role_get_parser = ArgumentParser(
|
|
44
|
+
formatter_class=RawTextHelpFormatter,
|
|
45
|
+
prog='role get',
|
|
46
|
+
description='Get role details by OID or name.',
|
|
47
|
+
)
|
|
48
|
+
completes(role_get_parser.add_argument('oid', help='A role OID or name.'), ROLE)
|
|
49
|
+
|
|
50
|
+
# Each subcommand with its parser; None for those taking no arguments.
|
|
51
|
+
role_subparsers: dict[str, ArgumentParser | None] = {
|
|
52
|
+
'ls': role_ls_parser,
|
|
53
|
+
'search': role_search,
|
|
54
|
+
'get': role_get_parser,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RoleClientPrompt(PromptBase):
|
|
59
|
+
subparsers_role = role_subparsers
|
|
60
|
+
|
|
61
|
+
def do_role(self, inp: str) -> None:
|
|
62
|
+
"""Manage roles (subcommands: ls, search, get)."""
|
|
63
|
+
self._dispatch_subcommand(
|
|
64
|
+
inp,
|
|
65
|
+
role_parser,
|
|
66
|
+
role_subparsers,
|
|
67
|
+
{'ls': self._role_ls, 'search': self._role_search, 'get': self._role_get},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _role_ls(self, args: list[str]) -> None:
|
|
71
|
+
ls_ns = role_ls_parser.parse_args(args)
|
|
72
|
+
self._print_roles_table(self.client.get_roles(limit=ls_ns.limit, name_filter=ls_ns.filter))
|
|
73
|
+
|
|
74
|
+
def _role_search(self, args: list[str]) -> None:
|
|
75
|
+
search_ns = role_search.parse_args(args)
|
|
76
|
+
self._print_roles_table(self.client.search_roles(search_ns.searchquery))
|
|
77
|
+
|
|
78
|
+
def _role_get(self, args: list[str]) -> None:
|
|
79
|
+
get_ns = role_get_parser.parse_args(args)
|
|
80
|
+
oid = self.client.resolve_oid(MidpointObjectTypes.ROLE.value, get_ns.oid)
|
|
81
|
+
print_object_detail(self.client.get_role(oid), 'Role Details')
|
|
82
|
+
|
|
83
|
+
def help_role(self) -> None:
|
|
84
|
+
role_parser.print_help()
|
|
85
|
+
|
|
86
|
+
def do_roles(self, inp: str) -> None:
|
|
87
|
+
"""Shortcut for 'role ls' — list all roles."""
|
|
88
|
+
self._list_shortcut(inp, 'roles', 'role', self.do_role)
|
|
89
|
+
|
|
90
|
+
def help_roles(self) -> None:
|
|
91
|
+
print('List all server roles. This is a shortcut for "role ls"')
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _print_roles_table(roles: MidpointObjectList) -> None:
|
|
95
|
+
"""Print roles in the selected output format."""
|
|
96
|
+
print_rows(roles, title='Roles', noun='roles')
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
4
|
+
import unittest
|
|
5
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
6
|
+
|
|
7
|
+
# Run command wrapper parser
|
|
8
|
+
from midpoint_cli.prompt.base import PromptBase
|
|
9
|
+
from midpoint_cli.prompt.complete import FILE, completes
|
|
10
|
+
|
|
11
|
+
STDIN_SCRIPT_NAME = '<stdin>'
|
|
12
|
+
|
|
13
|
+
script_parser = ArgumentParser(
|
|
14
|
+
formatter_class=RawTextHelpFormatter,
|
|
15
|
+
prog='script',
|
|
16
|
+
description='Run client Python scripts.',
|
|
17
|
+
epilog="""
|
|
18
|
+
Available commands:
|
|
19
|
+
run Run a local client script.
|
|
20
|
+
""",
|
|
21
|
+
)
|
|
22
|
+
script_parser.add_argument('command', help='Script command to execute.')
|
|
23
|
+
script_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
|
|
24
|
+
|
|
25
|
+
# Script RUN parser
|
|
26
|
+
|
|
27
|
+
script_run_parser = ArgumentParser(
|
|
28
|
+
formatter_class=RawTextHelpFormatter,
|
|
29
|
+
prog='script run',
|
|
30
|
+
description="""Run script. In the script execution environment, the following variables are defined:
|
|
31
|
+
- tc A TestCase instance that can be used to write assertions
|
|
32
|
+
- client The MidpointClient instance object
|
|
33
|
+
|
|
34
|
+
A failed assertion is reported with its location and exits 1. Any other
|
|
35
|
+
exception is reported as a traceback covering the script's own frames. A script
|
|
36
|
+
may also call sys.exit() to choose its own exit code.
|
|
37
|
+
|
|
38
|
+
Script files are executed as-is with the authenticated client in scope, so only
|
|
39
|
+
run scripts you trust.
|
|
40
|
+
""",
|
|
41
|
+
)
|
|
42
|
+
completes(
|
|
43
|
+
script_run_parser.add_argument(
|
|
44
|
+
'script', help='A file name containing the script, or "-" to read script from stdin.'
|
|
45
|
+
),
|
|
46
|
+
FILE,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
script_subparsers: dict[str, ArgumentParser | None] = {'run': script_run_parser}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ScriptClientPrompt(PromptBase):
|
|
53
|
+
subparsers_script = script_subparsers
|
|
54
|
+
|
|
55
|
+
def do_script(self, inp: str) -> None:
|
|
56
|
+
"""Execute a local Python client script (subcommand: run)."""
|
|
57
|
+
# An argparse SystemExit here propagates to onecmd; the script's own
|
|
58
|
+
# sys.exit() is handled separately in _run_script.
|
|
59
|
+
script_args = shlex.split(inp)
|
|
60
|
+
if self._subcommand_help(script_args, script_subparsers):
|
|
61
|
+
return
|
|
62
|
+
ns = script_parser.parse_args(script_args)
|
|
63
|
+
|
|
64
|
+
if ns.command != 'run':
|
|
65
|
+
self._fail(f'Unknown command {ns.command}')
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
run_ns = script_run_parser.parse_args(script_args[1:])
|
|
69
|
+
|
|
70
|
+
if run_ns.script == '-':
|
|
71
|
+
name = STDIN_SCRIPT_NAME
|
|
72
|
+
code = sys.stdin.read()
|
|
73
|
+
else:
|
|
74
|
+
name = run_ns.script
|
|
75
|
+
try:
|
|
76
|
+
with open(name) as f:
|
|
77
|
+
code = f.read()
|
|
78
|
+
except OSError as e:
|
|
79
|
+
self._fail(f'Cannot read {name}: {e.strerror or e}')
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
self._run_script(code, name)
|
|
83
|
+
|
|
84
|
+
def _run_script(self, code: str, name: str) -> None:
|
|
85
|
+
"""Run a client script, reporting its failures instead of our own stack.
|
|
86
|
+
|
|
87
|
+
The script is compiled under its real filename so its frames carry a
|
|
88
|
+
usable location: exec() on a raw string labels them ``<string>``, which
|
|
89
|
+
linecache then resolves against whatever else occupies that line — for a
|
|
90
|
+
console-script entry point, the launcher's own source — so the traceback
|
|
91
|
+
used to quote a line the user never wrote.
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
compiled = compile(code, name, 'exec')
|
|
95
|
+
except SyntaxError as e:
|
|
96
|
+
self.error_code = 1
|
|
97
|
+
location = f'{name}:{e.lineno}' if e.lineno else name
|
|
98
|
+
self.error_message = f'{location}: invalid Python syntax ({e.msg})'
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# One namespace, not separate globals/locals: with two dicts the body
|
|
103
|
+
# behaves like a class body, so a top-level def lands in locals while
|
|
104
|
+
# calls from inside other functions resolve against globals and miss it.
|
|
105
|
+
# Any script whose helpers called each other raised NameError.
|
|
106
|
+
# S102: executing the user's own script file is the purpose of this command.
|
|
107
|
+
exec(compiled, {'client': self.client, 'tc': unittest.TestCase()}) # noqa: S102
|
|
108
|
+
except AssertionError as e:
|
|
109
|
+
# The documented purpose of `tc`: an expected failure, so report it
|
|
110
|
+
# the way a test runner would rather than as a crash.
|
|
111
|
+
self.error_code = 1
|
|
112
|
+
detail = str(e) or 'no message'
|
|
113
|
+
self.error_message = f'Assertion failed at {self._deepest_script_location(e, name)}: {detail}'
|
|
114
|
+
except SystemExit as e:
|
|
115
|
+
self._apply_script_exit(e.code)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
# Arbitrary user code: a traceback is the useful answer, but only over
|
|
118
|
+
# the script's own frames. error_message is left unset so the single
|
|
119
|
+
# traceback below is the whole report.
|
|
120
|
+
self.error_code = 1
|
|
121
|
+
sys.stderr.write(self._script_traceback(e, name))
|
|
122
|
+
|
|
123
|
+
def _apply_script_exit(self, code: object) -> None:
|
|
124
|
+
"""Honour a sys.exit() from the script; it used to be swallowed as argparse's."""
|
|
125
|
+
if code is None:
|
|
126
|
+
self.error_code = 0
|
|
127
|
+
elif isinstance(code, bool):
|
|
128
|
+
self.error_code = int(code)
|
|
129
|
+
elif isinstance(code, int):
|
|
130
|
+
self.error_code = code
|
|
131
|
+
else:
|
|
132
|
+
self._fail(str(code))
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _deepest_script_location(exc: BaseException, name: str) -> str:
|
|
136
|
+
"""Locate the failure in the script itself, past any library frames."""
|
|
137
|
+
lineno = None
|
|
138
|
+
tb = exc.__traceback__
|
|
139
|
+
while tb is not None:
|
|
140
|
+
if tb.tb_frame.f_code.co_filename == name:
|
|
141
|
+
lineno = tb.tb_lineno
|
|
142
|
+
tb = tb.tb_next
|
|
143
|
+
return f'{name}:{lineno}' if lineno is not None else name
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _script_traceback(exc: BaseException, name: str) -> str:
|
|
147
|
+
"""Format the traceback from the script's first frame, hiding our own."""
|
|
148
|
+
tb = exc.__traceback__
|
|
149
|
+
while tb is not None and tb.tb_frame.f_code.co_filename != name:
|
|
150
|
+
tb = tb.tb_next
|
|
151
|
+
return ''.join(traceback.format_exception(type(exc), exc, tb or exc.__traceback__))
|
|
152
|
+
|
|
153
|
+
def help_script(self) -> None:
|
|
154
|
+
script_parser.print_help()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
3
|
+
|
|
4
|
+
from midpoint_cli.client import MidpointObjectTypes, MidpointServerError
|
|
5
|
+
from midpoint_cli.client.objects import MidpointTypeNotFound
|
|
6
|
+
from midpoint_cli.prompt.base import PromptBase, _positive_int
|
|
7
|
+
from midpoint_cli.prompt.complete import OBJECT_TYPE, completes
|
|
8
|
+
from midpoint_cli.prompt.console import print_rows
|
|
9
|
+
|
|
10
|
+
search_parser = ArgumentParser(
|
|
11
|
+
formatter_class=RawTextHelpFormatter,
|
|
12
|
+
prog='search',
|
|
13
|
+
description='Search server objects of any type with a Midpoint Query Language filter.',
|
|
14
|
+
epilog="""The filter is evaluated by the server, e.g.:
|
|
15
|
+
search user --filter 'activation/administrativeStatus = "disabled"'
|
|
16
|
+
search role --filter 'name startsWith "App"' --limit 10
|
|
17
|
+
search shadow --raw --filter 'resourceRef matches (oid = "<resource OID>") and kind = "account"'
|
|
18
|
+
Without --filter, every object of the type is listed. Results are sorted by name.""",
|
|
19
|
+
)
|
|
20
|
+
completes(search_parser.add_argument('objectclass', help='Type of the objects to search (Java Type).'), OBJECT_TYPE)
|
|
21
|
+
search_parser.add_argument('--filter', metavar='QUERY', help='Midpoint Query Language (Axiom) filter.')
|
|
22
|
+
search_parser.add_argument(
|
|
23
|
+
'--limit', type=_positive_int, metavar='N', help='List at most N objects (paged on the server).'
|
|
24
|
+
)
|
|
25
|
+
search_parser.add_argument(
|
|
26
|
+
'--raw',
|
|
27
|
+
action='store_true',
|
|
28
|
+
help='Search the repository only, without contacting resources (needed to search shadows\n'
|
|
29
|
+
'by resource without an object class).',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SearchClientPrompt(PromptBase):
|
|
34
|
+
parser_search = search_parser
|
|
35
|
+
|
|
36
|
+
def do_search(self, inp: str) -> None:
|
|
37
|
+
"""Search server objects of any type with a Midpoint Query Language filter."""
|
|
38
|
+
try:
|
|
39
|
+
ns = search_parser.parse_args(shlex.split(inp))
|
|
40
|
+
object_type = MidpointObjectTypes.find_by_name(ns.objectclass)
|
|
41
|
+
if object_type.endpoint is None:
|
|
42
|
+
self._fail(f'{ns.objectclass} cannot be searched through the REST API')
|
|
43
|
+
return
|
|
44
|
+
objects = self.client.search_text(object_type, ns.filter, limit=ns.limit, raw=ns.raw)
|
|
45
|
+
print_rows(objects, title=f'{object_type.object_type} objects', noun='objects')
|
|
46
|
+
except (MidpointTypeNotFound, MidpointServerError) as e:
|
|
47
|
+
self._fail(str(e))
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def help_search() -> None:
|
|
51
|
+
search_parser.print_help()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from argparse import ArgumentParser, RawTextHelpFormatter
|
|
2
|
+
|
|
3
|
+
from midpoint_cli.client import MidpointObjectList, MidpointObjectTypes
|
|
4
|
+
from midpoint_cli.client.task_definitions import SHADOW_KINDS
|
|
5
|
+
from midpoint_cli.prompt.base import PromptBase, add_listing_arguments
|
|
6
|
+
from midpoint_cli.prompt.complete import RESOURCE, completes
|
|
7
|
+
from midpoint_cli.prompt.console import print_rows
|
|
8
|
+
|
|
9
|
+
shadow_parser = ArgumentParser(
|
|
10
|
+
formatter_class=RawTextHelpFormatter,
|
|
11
|
+
prog='shadow',
|
|
12
|
+
description='Inspect shadows, the copies of resource objects Midpoint keeps in its repository.',
|
|
13
|
+
epilog="""
|
|
14
|
+
Available commands:
|
|
15
|
+
ls List shadows, optionally of one resource, kind and intent.
|
|
16
|
+
""",
|
|
17
|
+
)
|
|
18
|
+
shadow_parser.add_argument('command', help='Shadow command to execute.')
|
|
19
|
+
shadow_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
|
|
20
|
+
|
|
21
|
+
shadow_ls_parser = add_listing_arguments(
|
|
22
|
+
ArgumentParser(
|
|
23
|
+
formatter_class=RawTextHelpFormatter,
|
|
24
|
+
prog='shadow ls',
|
|
25
|
+
description='List the shadows of the repository, sorted by name.\n'
|
|
26
|
+
'The repository is searched without contacting the resources, so the list shows what\n'
|
|
27
|
+
'Midpoint last saw: run `resource import` or `resource reconcile` to refresh it.',
|
|
28
|
+
),
|
|
29
|
+
'shadows',
|
|
30
|
+
)
|
|
31
|
+
completes(
|
|
32
|
+
shadow_ls_parser.add_argument('--resource', help='Only list shadows of this resource (OID or name).'), RESOURCE
|
|
33
|
+
)
|
|
34
|
+
shadow_ls_parser.add_argument('--kind', choices=SHADOW_KINDS, help='Only list shadows of this kind.')
|
|
35
|
+
shadow_ls_parser.add_argument('--intent', help='Only list shadows of this intent.')
|
|
36
|
+
|
|
37
|
+
# Each subcommand with its parser; None for those taking no arguments.
|
|
38
|
+
shadow_subparsers: dict[str, ArgumentParser | None] = {'ls': shadow_ls_parser}
|
|
39
|
+
|
|
40
|
+
SHADOW_COLUMNS = ('OID', 'Name', 'Resource', 'Kind', 'Intent', 'Situation', 'Dead')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ShadowClientPrompt(PromptBase):
|
|
44
|
+
subparsers_shadow = shadow_subparsers
|
|
45
|
+
|
|
46
|
+
def do_shadow(self, inp: str) -> None:
|
|
47
|
+
"""Inspect resource object shadows (subcommand: ls)."""
|
|
48
|
+
self._dispatch_subcommand(inp, shadow_parser, shadow_subparsers, {'ls': self._shadow_ls})
|
|
49
|
+
|
|
50
|
+
def _shadow_ls(self, args: list[str]) -> None:
|
|
51
|
+
ls_ns = shadow_ls_parser.parse_args(args)
|
|
52
|
+
resource_names: dict[str, str] = {}
|
|
53
|
+
resource_oid = None
|
|
54
|
+
if ls_ns.resource is not None:
|
|
55
|
+
resource = self._require_object(MidpointObjectTypes.RESOURCE.value, ls_ns.resource, 'Resource')
|
|
56
|
+
if resource is None:
|
|
57
|
+
return
|
|
58
|
+
resource_oid = resource.get_oid()
|
|
59
|
+
resource_names[str(resource_oid)] = resource.get_name() or str(resource_oid)
|
|
60
|
+
shadows = self.client.get_shadows(
|
|
61
|
+
resource_oid=resource_oid,
|
|
62
|
+
kind=ls_ns.kind,
|
|
63
|
+
intent=ls_ns.intent,
|
|
64
|
+
limit=ls_ns.limit,
|
|
65
|
+
name_filter=ls_ns.filter,
|
|
66
|
+
)
|
|
67
|
+
self._print_shadows(shadows, resource_names)
|
|
68
|
+
|
|
69
|
+
def _print_shadows(self, shadows: MidpointObjectList, resource_names: dict[str, str]) -> None:
|
|
70
|
+
"""Print shadows with the name of their resource, looked up in one search when not known yet."""
|
|
71
|
+
unknown = sorted({str(s['ResourceOID']) for s in shadows if s.get('ResourceOID')} - set(resource_names))
|
|
72
|
+
if unknown:
|
|
73
|
+
for resource in self.client.get_objects_by_oid(MidpointObjectTypes.RESOURCE.value, unknown):
|
|
74
|
+
oid = resource.get_oid()
|
|
75
|
+
if oid is not None:
|
|
76
|
+
resource_names[oid] = resource.get_name() or oid
|
|
77
|
+
rows = []
|
|
78
|
+
for shadow in shadows:
|
|
79
|
+
row = {column: shadow.get(column) for column in SHADOW_COLUMNS}
|
|
80
|
+
resource_oid = shadow.get('ResourceOID')
|
|
81
|
+
row['Resource'] = resource_names.get(str(resource_oid), resource_oid) if resource_oid else ''
|
|
82
|
+
rows.append(row)
|
|
83
|
+
print_rows(rows, title='Shadows', noun='shadows', columns=SHADOW_COLUMNS)
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def help_shadow() -> None:
|
|
87
|
+
shadow_parser.print_help()
|