mreg-cli 1.0.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.
- mreg_cli/__about__.py +7 -0
- mreg_cli/__init__.py +5 -0
- mreg_cli/__main__.py +8 -0
- mreg_cli/_version.py +16 -0
- mreg_cli/api/__init__.py +7 -0
- mreg_cli/api/abstracts.py +486 -0
- mreg_cli/api/endpoints.py +130 -0
- mreg_cli/api/fields.py +104 -0
- mreg_cli/api/history.py +152 -0
- mreg_cli/api/models.py +3416 -0
- mreg_cli/cli.py +382 -0
- mreg_cli/commands/__init__.py +1 -0
- mreg_cli/commands/base.py +62 -0
- mreg_cli/commands/dhcp.py +136 -0
- mreg_cli/commands/group.py +326 -0
- mreg_cli/commands/help.py +77 -0
- mreg_cli/commands/host.py +54 -0
- mreg_cli/commands/host_submodules/__init__.py +28 -0
- mreg_cli/commands/host_submodules/a_aaaa.py +452 -0
- mreg_cli/commands/host_submodules/bacnet.py +126 -0
- mreg_cli/commands/host_submodules/cname.py +166 -0
- mreg_cli/commands/host_submodules/core.py +507 -0
- mreg_cli/commands/host_submodules/rr.py +973 -0
- mreg_cli/commands/label.py +146 -0
- mreg_cli/commands/logging.py +112 -0
- mreg_cli/commands/network.py +516 -0
- mreg_cli/commands/permission.py +202 -0
- mreg_cli/commands/policy.py +519 -0
- mreg_cli/commands/recording.py +59 -0
- mreg_cli/commands/registry.py +56 -0
- mreg_cli/commands/root.py +58 -0
- mreg_cli/commands/zone.py +288 -0
- mreg_cli/config.py +253 -0
- mreg_cli/errorbuilder.py +193 -0
- mreg_cli/exceptions.py +237 -0
- mreg_cli/help_formatter.py +38 -0
- mreg_cli/main.py +238 -0
- mreg_cli/outputmanager.py +466 -0
- mreg_cli/py.typed +0 -0
- mreg_cli/tags.txt +55 -0
- mreg_cli/tokenfile.py +89 -0
- mreg_cli/types.py +160 -0
- mreg_cli/utilities/__init__.py +5 -0
- mreg_cli/utilities/api.py +595 -0
- mreg_cli/utilities/shared.py +65 -0
- mreg_cli/utilities/validators.py +19 -0
- mreg_cli-1.0.0.dist-info/AUTHORS +12 -0
- mreg_cli-1.0.0.dist-info/LICENSE +674 -0
- mreg_cli-1.0.0.dist-info/METADATA +1079 -0
- mreg_cli-1.0.0.dist-info/RECORD +53 -0
- mreg_cli-1.0.0.dist-info/WHEEL +5 -0
- mreg_cli-1.0.0.dist-info/entry_points.txt +2 -0
- mreg_cli-1.0.0.dist-info/top_level.txt +1 -0
mreg_cli/errorbuilder.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Builders for error messages with underlined suggestions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shlex
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
ExcOrStr = Exception | str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Re-usable function for error builders that don't need a specific implementation
|
|
15
|
+
def default_get_offset(self: "ErrorBuilder") -> tuple[int, int]: # noqa: ARG001
|
|
16
|
+
"""Get the start and end index of the underlined part of the command."""
|
|
17
|
+
return -1, -1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ErrorBuilder(ABC):
|
|
21
|
+
"""Base class for building error messages with an underline and suggestion."""
|
|
22
|
+
|
|
23
|
+
# Subclasses should implement this:
|
|
24
|
+
SUGGESTION = ""
|
|
25
|
+
|
|
26
|
+
# Characters used for formatting the error message
|
|
27
|
+
CHAR_UNDERLINE = "^"
|
|
28
|
+
CHAR_OFFSET = " "
|
|
29
|
+
CHAR_SUGGESTION = "└"
|
|
30
|
+
|
|
31
|
+
def __init__(self, command: str, exc_or_str: ExcOrStr) -> None:
|
|
32
|
+
"""Instantiate the builder with the command that caused the error."""
|
|
33
|
+
self.command = command
|
|
34
|
+
self.exc_or_str = exc_or_str
|
|
35
|
+
|
|
36
|
+
def get_underline(self, start: int, end: int) -> str:
|
|
37
|
+
"""Get the underline part of the message.
|
|
38
|
+
|
|
39
|
+
:param int start: The start index of the underlined part of the command.
|
|
40
|
+
:param int end: The end index of the underlined part of the command.
|
|
41
|
+
|
|
42
|
+
:returns: The underlined part of the command.
|
|
43
|
+
"""
|
|
44
|
+
return self.offset(self.CHAR_UNDERLINE * (end - start), start)
|
|
45
|
+
|
|
46
|
+
def get_suggestion(self, start: int) -> str:
|
|
47
|
+
"""Get the suggestion part of the message.
|
|
48
|
+
|
|
49
|
+
:param int start: The start index of the underlined part of the command.
|
|
50
|
+
"""
|
|
51
|
+
msg = self.SUGGESTION
|
|
52
|
+
if self.CHAR_SUGGESTION:
|
|
53
|
+
msg = f"{self.CHAR_SUGGESTION} {msg}"
|
|
54
|
+
return self.offset(msg, start)
|
|
55
|
+
|
|
56
|
+
def offset(self, s: str, n: int) -> str:
|
|
57
|
+
"""Offset the string by n spaces.
|
|
58
|
+
|
|
59
|
+
:param str s: The string to offset.
|
|
60
|
+
:param int n: The number of spaces to offset by.
|
|
61
|
+
|
|
62
|
+
:returns: The offset string.
|
|
63
|
+
"""
|
|
64
|
+
return " " * n + s
|
|
65
|
+
|
|
66
|
+
def build(self) -> str:
|
|
67
|
+
"""Build the error message with an underline and suggestion.
|
|
68
|
+
|
|
69
|
+
:returns: The error message with an underline and suggestion.
|
|
70
|
+
"""
|
|
71
|
+
start, end = self.get_offset()
|
|
72
|
+
lines = [
|
|
73
|
+
self.command,
|
|
74
|
+
self.get_underline(start, end),
|
|
75
|
+
self.get_suggestion(start),
|
|
76
|
+
]
|
|
77
|
+
# prepend the exception message if it exists
|
|
78
|
+
if self.exc_or_str:
|
|
79
|
+
lines.insert(0, str(self.exc_or_str))
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def get_offset(self) -> tuple[int, int]:
|
|
84
|
+
"""Compute the start and end index of the underlined part of the command."""
|
|
85
|
+
raise NotImplementedError
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def can_build(self) -> bool:
|
|
89
|
+
"""Check if the builder can build an error message for the given command."""
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class FallbackErrorBuilder(ErrorBuilder):
|
|
94
|
+
"""Builder for errors without a specific implementation.
|
|
95
|
+
|
|
96
|
+
Renders the error message as a single line without an underline or suggestion.
|
|
97
|
+
The error message is displayed as-is.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
get_offset = default_get_offset # needed for ABC parent class
|
|
101
|
+
|
|
102
|
+
def build(self) -> str:
|
|
103
|
+
"""Build the error message without an underline or suggestion."""
|
|
104
|
+
return str(self.exc_or_str)
|
|
105
|
+
|
|
106
|
+
def can_build(self) -> bool: # noqa: D102
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FilterErrorBuilder(ErrorBuilder):
|
|
111
|
+
"""Error builder for commands with a filter."""
|
|
112
|
+
|
|
113
|
+
SUGGESTION = "Consider enclosing this part in quotes."
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def find_word_with_char_offset(command: str, char: str) -> tuple[int, int]:
|
|
117
|
+
"""Find the start and end index of a word containing a specific character.
|
|
118
|
+
|
|
119
|
+
Cannot fail; will always return a tuple of two integers.
|
|
120
|
+
|
|
121
|
+
:param str command: The command to search.
|
|
122
|
+
:param str char: The character to search for.
|
|
123
|
+
|
|
124
|
+
:returns: Tuple of two integers representing the start and end index of the word.
|
|
125
|
+
Returns (-1, -1) if the character is not found.
|
|
126
|
+
"""
|
|
127
|
+
command = command.strip()
|
|
128
|
+
if not command:
|
|
129
|
+
return -1, -1
|
|
130
|
+
|
|
131
|
+
# Parse the command into parts
|
|
132
|
+
cmd = shlex.split(command, posix=False) # keep quotes and backslashes
|
|
133
|
+
for part in cmd:
|
|
134
|
+
# Skip parts that contain quotes
|
|
135
|
+
# NOTE: This is a very naive check, and will not work for all cases
|
|
136
|
+
# (e.g. escaped quotes)
|
|
137
|
+
if "'" in part or '"' in part:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if char in part:
|
|
141
|
+
try:
|
|
142
|
+
start, end = command.index(part), command.index(part) + len(part)
|
|
143
|
+
if start > end or start >= len(command) or end >= len(command):
|
|
144
|
+
raise ValueError(f"Invalid start and end values: {start}, {end}")
|
|
145
|
+
except ValueError as e:
|
|
146
|
+
logger.error(
|
|
147
|
+
"Failed to get index of char '%s' in part '%s' of command %s: %s",
|
|
148
|
+
char,
|
|
149
|
+
part,
|
|
150
|
+
command,
|
|
151
|
+
e,
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
return start, end
|
|
155
|
+
return -1, -1
|
|
156
|
+
|
|
157
|
+
def get_offset(self) -> tuple[int, int]: # noqa: D102 (missing docstring [inherit it from parent])
|
|
158
|
+
return self.find_word_with_char_offset(self.command, "|")
|
|
159
|
+
|
|
160
|
+
def can_build(self) -> bool: # noqa: D102 (missing docstring [inherit it from parent])
|
|
161
|
+
return "|" in self.command
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
BUILDERS: list[type[ErrorBuilder]] = [
|
|
165
|
+
FilterErrorBuilder,
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_builder(command: str, exc_or_str: ExcOrStr) -> ErrorBuilder:
|
|
170
|
+
"""Get the appropriate error builder for the given command.
|
|
171
|
+
|
|
172
|
+
:param str command: The command that caused the error.
|
|
173
|
+
:param exc_or_str: The exception or error message.
|
|
174
|
+
|
|
175
|
+
:returns: An error builder instance.
|
|
176
|
+
"""
|
|
177
|
+
for builder in BUILDERS:
|
|
178
|
+
b = builder(command, exc_or_str)
|
|
179
|
+
if b.can_build():
|
|
180
|
+
return b
|
|
181
|
+
return FallbackErrorBuilder(command, exc_or_str)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def build_error_message(command: str, exc_or_str: ExcOrStr) -> str:
|
|
185
|
+
"""Build an error message for the given command and exception.
|
|
186
|
+
|
|
187
|
+
:param str command: The command that caused the error.
|
|
188
|
+
:param exc_or_str: The exception or error message.
|
|
189
|
+
|
|
190
|
+
:returns: Error message based on the command and exception.
|
|
191
|
+
"""
|
|
192
|
+
builder = get_builder(command, exc_or_str)
|
|
193
|
+
return builder.build()
|
mreg_cli/exceptions.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Custom exceptions for mreg_cli.
|
|
2
|
+
|
|
3
|
+
Note that Cli exceptions offer a print_self() method that prints the exception
|
|
4
|
+
with appropriate formatting. This is useful for printing exceptions in the
|
|
5
|
+
context of a CLI command.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from prompt_toolkit import print_formatted_text
|
|
15
|
+
from prompt_toolkit.formatted_text import HTML
|
|
16
|
+
from prompt_toolkit.formatted_text.html import html_escape
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CliExit(Exception):
|
|
20
|
+
"""Exception used to exit the CLI."""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CliException(Exception):
|
|
26
|
+
"""Base exception class for the CLI."""
|
|
27
|
+
|
|
28
|
+
def escape(self) -> str:
|
|
29
|
+
"""Get an HTML-escaped string representation of the exception."""
|
|
30
|
+
return html_escape(str(self))
|
|
31
|
+
|
|
32
|
+
def formatted_exception(self) -> str:
|
|
33
|
+
"""Return a formatted string representation of the exception.
|
|
34
|
+
|
|
35
|
+
:returns: Formatted string for the exception message.
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError("This method should be implemented by subclasses")
|
|
38
|
+
|
|
39
|
+
def print_self(self):
|
|
40
|
+
"""Print the exception with appropriate formatting."""
|
|
41
|
+
print_formatted_text(HTML(self.formatted_exception()), file=sys.stdout)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CliError(CliException):
|
|
45
|
+
"""Exception class for CLI errors.
|
|
46
|
+
|
|
47
|
+
Errors are not recoverable and stem from internal failures that
|
|
48
|
+
the user cannot be expected to resolve.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
52
|
+
"""Initialize the error."""
|
|
53
|
+
super().__init__(*args, **kwargs)
|
|
54
|
+
logging.getLogger(__name__).error(str(self))
|
|
55
|
+
from mreg_cli.outputmanager import OutputManager
|
|
56
|
+
|
|
57
|
+
OutputManager().add_error(str(self))
|
|
58
|
+
|
|
59
|
+
def formatted_exception(self) -> str:
|
|
60
|
+
"""Return a string formatted with HTML red tag for the error message.
|
|
61
|
+
|
|
62
|
+
:returns: Formatted error message.
|
|
63
|
+
"""
|
|
64
|
+
return f"<ansired>ERROR: {self.escape()}</ansired>"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CliWarning(CliException):
|
|
68
|
+
"""Exception class for CLI warnings.
|
|
69
|
+
|
|
70
|
+
Warnings should be recoverable by changing the user input.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
|
74
|
+
"""Initialize the warning."""
|
|
75
|
+
from mreg_cli.outputmanager import OutputManager
|
|
76
|
+
|
|
77
|
+
super().__init__(*args, **kwargs)
|
|
78
|
+
logging.getLogger(__name__).warning(str(self))
|
|
79
|
+
OutputManager().add_warning(str(self))
|
|
80
|
+
|
|
81
|
+
def formatted_exception(self) -> str:
|
|
82
|
+
"""Return a string formatted with HTML italic tag for the warning message.
|
|
83
|
+
|
|
84
|
+
:returns: Formatted warning message.
|
|
85
|
+
"""
|
|
86
|
+
return f"<i>{self.escape()}</i>"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CreateError(CliError):
|
|
90
|
+
"""Error class for failed creation."""
|
|
91
|
+
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PatchError(CliError):
|
|
96
|
+
"""Error class for failed patching."""
|
|
97
|
+
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class DeleteError(CliError):
|
|
102
|
+
"""Error class for failed deletion."""
|
|
103
|
+
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class GetError(CliError):
|
|
108
|
+
"""Error class for failed retrieval."""
|
|
109
|
+
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class InternalError(CliError):
|
|
114
|
+
"""Error class for internal errors."""
|
|
115
|
+
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class APIError(CliError):
|
|
120
|
+
"""Error class for API errors."""
|
|
121
|
+
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class UnexpectedDataError(APIError):
|
|
126
|
+
"""Error class for unexpected API data."""
|
|
127
|
+
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ValidationError(CliError):
|
|
132
|
+
"""Error class for validation failures."""
|
|
133
|
+
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class FileError(CliError):
|
|
138
|
+
"""Error class for file errors."""
|
|
139
|
+
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class APINotOk(CliWarning):
|
|
144
|
+
"""Warning class for API not returning OK."""
|
|
145
|
+
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TooManyResults(CliWarning):
|
|
150
|
+
"""Warning class for too many results."""
|
|
151
|
+
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class NoHistoryFound(CliWarning):
|
|
156
|
+
"""Warning class for no history found."""
|
|
157
|
+
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class EntityNotFound(CliWarning):
|
|
162
|
+
"""Warning class for an entity that was not found."""
|
|
163
|
+
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class EntityAlreadyExists(CliWarning):
|
|
168
|
+
"""Warning class for an entity that already exists."""
|
|
169
|
+
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class MultipleEntititesFound(CliWarning):
|
|
174
|
+
"""Warning class for multiple entities found."""
|
|
175
|
+
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class EntityOwnershipMismatch(CliWarning):
|
|
180
|
+
"""Warning class for an entity that already exists but owned by someone else."""
|
|
181
|
+
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class InputFailure(CliWarning):
|
|
186
|
+
"""Warning class for input failure."""
|
|
187
|
+
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ForceMissing(CliWarning):
|
|
192
|
+
"""Warning class for missing force flag."""
|
|
193
|
+
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class InvalidIPAddress(CliWarning):
|
|
198
|
+
"""Warning class for an entity that is not an IP address."""
|
|
199
|
+
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class InvalidIPv4Address(CliWarning):
|
|
204
|
+
"""Warning class for an entity that is not an IPv4 address."""
|
|
205
|
+
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class InvalidIPv6Address(CliWarning):
|
|
210
|
+
"""Warning class for an entity that is not an IPv6 address."""
|
|
211
|
+
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class InvalidNetwork(CliWarning):
|
|
216
|
+
"""Warning class for an entity that is not a network."""
|
|
217
|
+
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class NetworkOverlap(CliWarning):
|
|
222
|
+
"""Warning class for a networkthat overlaps with another network."""
|
|
223
|
+
|
|
224
|
+
pass
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class LoginFailedError(CliException):
|
|
228
|
+
"""Error class for login failure."""
|
|
229
|
+
|
|
230
|
+
def formatted_exception(self) -> str:
|
|
231
|
+
"""Return a string formatted with 'Login failed:' prefixing the error message.
|
|
232
|
+
|
|
233
|
+
:returns: Formatted error message.
|
|
234
|
+
"""
|
|
235
|
+
return f"Login failed: {self.escape()}"
|
|
236
|
+
|
|
237
|
+
pass
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""A formatter for the CLI that adds extra information and improves command formatting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CustomHelpFormatter(argparse.HelpFormatter):
|
|
10
|
+
"""Custom help formatter to add extra information and improve command formatting."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
13
|
+
"""Initialize the formatter."""
|
|
14
|
+
super().__init__(*args, **kwargs)
|
|
15
|
+
self._max_help_position = 35 # Adjust this to control the indentation of help text
|
|
16
|
+
|
|
17
|
+
def _format_action(self, action: argparse.Action) -> str:
|
|
18
|
+
"""Format the action with custom width.
|
|
19
|
+
|
|
20
|
+
:param action: The argparse Action being formatted.
|
|
21
|
+
|
|
22
|
+
:returns: Formatted action string.
|
|
23
|
+
"""
|
|
24
|
+
# Check if this is a _SubParsersAction (used for subcommands)
|
|
25
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
26
|
+
# Use a custom heading for subcommands
|
|
27
|
+
formatted_action = "commands:\n"
|
|
28
|
+
|
|
29
|
+
# Format each subcommand
|
|
30
|
+
for subcommand, parser in action.choices.items():
|
|
31
|
+
subcommand_help = parser.description if parser.description else ""
|
|
32
|
+
formatted_subcommand = f" {subcommand:<20} {subcommand_help}"
|
|
33
|
+
formatted_action += formatted_subcommand + "\n"
|
|
34
|
+
|
|
35
|
+
return formatted_action
|
|
36
|
+
|
|
37
|
+
# Handle non-_SubParsersAction actions with default formatting
|
|
38
|
+
return super()._format_action(action)
|
mreg_cli/main.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Main entry point for mreg_cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import getpass
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from prompt_toolkit import HTML
|
|
10
|
+
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
|
|
11
|
+
|
|
12
|
+
import mreg_cli.utilities.api as api
|
|
13
|
+
from mreg_cli.__about__ import __version__
|
|
14
|
+
from mreg_cli.cli import cli, source
|
|
15
|
+
from mreg_cli.config import MregCliConfig
|
|
16
|
+
from mreg_cli.exceptions import LoginFailedError
|
|
17
|
+
from mreg_cli.outputmanager import OutputManager
|
|
18
|
+
from mreg_cli.types import LogLevel
|
|
19
|
+
from mreg_cli.utilities.api import try_token_or_login
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
"""Entry point for the mreg cli."""
|
|
26
|
+
# Read config file first, to provide defaults
|
|
27
|
+
config = MregCliConfig()
|
|
28
|
+
|
|
29
|
+
parser = argparse.ArgumentParser(description="The MREG cli")
|
|
30
|
+
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--version",
|
|
33
|
+
help="Show version and exit.",
|
|
34
|
+
action="store_true",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
connect_args = parser.add_argument_group("connection settings")
|
|
38
|
+
connect_args.add_argument(
|
|
39
|
+
"--url",
|
|
40
|
+
default=config.get_url(),
|
|
41
|
+
help="use mreg server at %(metavar)s (default: %(default)s)",
|
|
42
|
+
metavar="URL",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
connect_args.add_argument(
|
|
46
|
+
"-u",
|
|
47
|
+
"--user",
|
|
48
|
+
default=config.get("user", getpass.getuser()),
|
|
49
|
+
help="authenticate as %(metavar)s (default: %(default)s)",
|
|
50
|
+
metavar="USER",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
mreg_args = parser.add_argument_group("mreg settings")
|
|
54
|
+
mreg_args.add_argument(
|
|
55
|
+
"-d",
|
|
56
|
+
"--domain",
|
|
57
|
+
default=config.get("domain", config.get_default_domain()),
|
|
58
|
+
help="default %(metavar)s (default: %(default)s)",
|
|
59
|
+
metavar="DOMAIN",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
mreg_args.add_argument(
|
|
63
|
+
"-p",
|
|
64
|
+
"--prompt",
|
|
65
|
+
help="default %(metavar)s), defaults to the server name if not set.",
|
|
66
|
+
metavar="PROMPT",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
output_args = parser.add_argument_group("output settings")
|
|
70
|
+
output_args.add_argument(
|
|
71
|
+
"-v",
|
|
72
|
+
"--log-level",
|
|
73
|
+
dest="loglevel",
|
|
74
|
+
default="INFO",
|
|
75
|
+
choices=LogLevel.choices(),
|
|
76
|
+
help="Log level for logging.",
|
|
77
|
+
)
|
|
78
|
+
output_args.add_argument(
|
|
79
|
+
"-l",
|
|
80
|
+
"--logfile",
|
|
81
|
+
dest="logfile",
|
|
82
|
+
help="write log to %(metavar)s",
|
|
83
|
+
metavar="LOGFILE",
|
|
84
|
+
)
|
|
85
|
+
output_args.add_argument(
|
|
86
|
+
"--show-token",
|
|
87
|
+
dest="show_token",
|
|
88
|
+
action="store_true",
|
|
89
|
+
help="show API token after login",
|
|
90
|
+
)
|
|
91
|
+
output_args.add_argument(
|
|
92
|
+
"--record",
|
|
93
|
+
dest="record_traffic",
|
|
94
|
+
help="Record all server/client traffic to %(metavar)s",
|
|
95
|
+
metavar="RECFILE",
|
|
96
|
+
)
|
|
97
|
+
output_args.add_argument(
|
|
98
|
+
"--record-without-timestamps",
|
|
99
|
+
dest="record_traffic_without_timestamps",
|
|
100
|
+
action="store_true",
|
|
101
|
+
help="Do not apply timestamps to the recording file",
|
|
102
|
+
)
|
|
103
|
+
output_args.add_argument(
|
|
104
|
+
"--source",
|
|
105
|
+
dest="source",
|
|
106
|
+
help="Read commands from %(metavar)s",
|
|
107
|
+
metavar="SOURCE",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
output_args.add_argument(
|
|
111
|
+
"--token-only",
|
|
112
|
+
dest="token_only",
|
|
113
|
+
action="store_true",
|
|
114
|
+
default=False,
|
|
115
|
+
help="Only attempt token login, this will avoid interactive prompts.",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
output_args.add_argument(
|
|
119
|
+
"command", metavar="command", nargs="*", help="Oneshot command to issue to the cli."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
args = parser.parse_args()
|
|
123
|
+
|
|
124
|
+
if args.version:
|
|
125
|
+
print(f"mreg-cli {__version__}")
|
|
126
|
+
raise SystemExit() from None
|
|
127
|
+
|
|
128
|
+
logfile = config.get_default_logfile()
|
|
129
|
+
conf = {k: v for k, v in vars(args).items() if v}
|
|
130
|
+
config.set_cmd_config(conf)
|
|
131
|
+
|
|
132
|
+
if "logfile" in conf:
|
|
133
|
+
logfile = conf["logfile"]
|
|
134
|
+
|
|
135
|
+
config.start_logging(logfile, args.loglevel)
|
|
136
|
+
|
|
137
|
+
logger.debug(f"args: {args}")
|
|
138
|
+
|
|
139
|
+
if "record_traffic" in conf:
|
|
140
|
+
OutputManager().recording_start(conf["record_traffic"])
|
|
141
|
+
|
|
142
|
+
if "record_traffic_without_timestamps" in conf:
|
|
143
|
+
OutputManager().record_timestamps(False)
|
|
144
|
+
|
|
145
|
+
if config.get("user") is None:
|
|
146
|
+
print("Username not set in config or as argument")
|
|
147
|
+
return
|
|
148
|
+
elif config.get("url") is None:
|
|
149
|
+
print("mreg url not set in config or as argument")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
try_token_or_login(
|
|
154
|
+
str(config.get("user")),
|
|
155
|
+
str(config.get("url")),
|
|
156
|
+
fail_without_token=args.token_only,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
except (EOFError, KeyboardInterrupt, LoginFailedError) as e:
|
|
160
|
+
print(e)
|
|
161
|
+
raise SystemExit() from None
|
|
162
|
+
if args.show_token:
|
|
163
|
+
print(api.get_session_token() or "Token not found.")
|
|
164
|
+
raise SystemExit() from None
|
|
165
|
+
|
|
166
|
+
# Define a function that returns the prompt message
|
|
167
|
+
def get_prompt_message():
|
|
168
|
+
"""Return the prompt message."""
|
|
169
|
+
manager = OutputManager()
|
|
170
|
+
|
|
171
|
+
if args.prompt:
|
|
172
|
+
prompt = args.prompt
|
|
173
|
+
elif config.get("prompt"):
|
|
174
|
+
prompt = config.get("prompt")
|
|
175
|
+
else:
|
|
176
|
+
host = str(config.get("url")).replace("https://", "").replace("http://", "")
|
|
177
|
+
user = args.user or config.get("user", "?")
|
|
178
|
+
prompt = f"{user}@{host}"
|
|
179
|
+
|
|
180
|
+
prefix: list[str] = []
|
|
181
|
+
|
|
182
|
+
if manager.recording_active():
|
|
183
|
+
prefix.append(f">'{manager.recording_filename()}'")
|
|
184
|
+
|
|
185
|
+
if prefix:
|
|
186
|
+
prefix_str = ",".join(prefix)
|
|
187
|
+
return HTML(f"[{prefix_str}] {prompt}> ")
|
|
188
|
+
return HTML(f"{prompt}> ")
|
|
189
|
+
|
|
190
|
+
# session is a PromptSession object from prompt_toolkit which handles
|
|
191
|
+
# some configurations of the prompt for us: the text of the prompt; the
|
|
192
|
+
# completer; and other visual things.
|
|
193
|
+
session = PromptSession(
|
|
194
|
+
message=get_prompt_message,
|
|
195
|
+
search_ignore_case=True,
|
|
196
|
+
completer=cli,
|
|
197
|
+
complete_while_typing=True,
|
|
198
|
+
complete_style=CompleteStyle.MULTI_COLUMN,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Welcome text for the app
|
|
202
|
+
print("Type -h for help.")
|
|
203
|
+
|
|
204
|
+
# if the --source parameter was given, read commands from the source file and then exit
|
|
205
|
+
if "source" in conf:
|
|
206
|
+
logger.info("Reading commands from %s", conf["source"])
|
|
207
|
+
for command in source([conf["source"]], "verbosity" in conf, False):
|
|
208
|
+
cli.process_command_line(command)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
# Check if we got a oneshot command. If so, execute it and exit.
|
|
212
|
+
if args.command:
|
|
213
|
+
cmd = " ".join(args.command)
|
|
214
|
+
try:
|
|
215
|
+
cli.process_command_line(cmd)
|
|
216
|
+
except ValueError as e:
|
|
217
|
+
print(e)
|
|
218
|
+
|
|
219
|
+
raise SystemExit() from None
|
|
220
|
+
|
|
221
|
+
# The app runs in an infinite loop and is expected to exit using sys.exit()
|
|
222
|
+
logger.debug("Entering main loop")
|
|
223
|
+
while True:
|
|
224
|
+
try:
|
|
225
|
+
lines = session.prompt()
|
|
226
|
+
except KeyboardInterrupt:
|
|
227
|
+
continue
|
|
228
|
+
except EOFError:
|
|
229
|
+
raise SystemExit() from None
|
|
230
|
+
try:
|
|
231
|
+
for line in lines.splitlines():
|
|
232
|
+
cli.process_command_line(line)
|
|
233
|
+
except ValueError as e:
|
|
234
|
+
print(e)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
main()
|