mreg-cli 1.2.0__tar.gz → 1.2.1__tar.gz
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-1.2.0 → mreg_cli-1.2.1}/CHANGELOG.md +11 -1
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/PKG-INFO +1 -1
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/ci/testsuite-result.json +4 -12
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/_version.py +2 -2
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/api/fields.py +26 -11
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/api/models.py +171 -63
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/cli.py +2 -2
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/help.py +16 -6
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/host_submodules/a_aaaa.py +3 -3
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/host_submodules/rr.py +4 -4
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/network.py +1 -1
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/permission.py +2 -2
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/config.py +5 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/errorbuilder.py +23 -23
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/exceptions.py +18 -12
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/main.py +2 -2
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/outputmanager.py +4 -5
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/utilities/api.py +2 -2
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli.egg-info/PKG-INFO +1 -1
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/api/test_models.py +26 -2
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/test_errorbuilder.py +13 -2
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/uv.lock +1 -1
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/.github/workflows/publish.yml +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/.github/workflows/test.yml +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/.gitignore +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/.markdownlint.json +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/AUTHORS +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/CONTRIBUTING +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/LICENSE +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/README.md +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/ci/diff.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/ci/docker-compose.yml +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/ci/run_testsuite_and_record.sh +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/ci/testsuite +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/data/mreg-cli.conf +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/__about__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/__main__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/api/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/api/abstracts.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/api/endpoints.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/api/history.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/base.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/dhcp.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/group.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/host.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/host_submodules/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/host_submodules/bacnet.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/host_submodules/cname.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/host_submodules/core.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/label.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/logging.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/policy.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/recording.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/registry.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/root.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/commands/zone.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/help_formatter.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/prompt.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/py.typed +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/tags.txt +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/tokenfile.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/types.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/utilities/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/utilities/shared.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli/utilities/validators.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli.egg-info/SOURCES.txt +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli.egg-info/dependency_links.txt +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli.egg-info/entry_points.txt +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli.egg-info/requires.txt +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/mreg_cli.egg-info/top_level.txt +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/pyproject.toml +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/run.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/setup.cfg +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/api/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/api/test_fields.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/conftest.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/test_prompt.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/test_tokenfile.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/utilities/__init__.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tests/utilities/test_api.py +0 -0
- {mreg_cli-1.2.0 → mreg_cli-1.2.1}/tox.ini +0 -0
|
@@ -5,7 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
<!-- ##
|
|
8
|
+
<!-- ## Unreleased -->
|
|
9
|
+
|
|
10
|
+
## [1.2.1](https://github.com/unioslo/mreg-cli/releases/tag/1.2.1) - 2024-12-02
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Host lookup with IPv6 addresses raising `ValidationError` when no host can be found with the given address.
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- Support for fetching hosts that have a valid IP/MAC address as their hostname.
|
|
9
19
|
|
|
10
20
|
## [1.2.0](https://github.com/unioslo/mreg-cli/releases/tag/1.2.0) - 2024-11-25
|
|
11
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mreg-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: MREG interactive command line client
|
|
5
5
|
Author: Others (see AUTHORS)
|
|
6
6
|
Author-email: Øyvind Kolbu <oyvind.kolbu@usit.uio.no>, Safet A <amedov@usit.uio.no>, Magnus Hirth <mhmorgan42@gmail.com>, Nico <nicolaydmohebi@gmail.com>, Marius Bakke <marius.bakke@usit.uio.no>, Fredrik Larsen <fredrik.larsen@usit.uio.no>, Paal Braathen <paal.braathen@usit.uio.no>, Nils Hiorth <nils@nls.me>, Dmytro Karpenko <dmytrok@usit.uio.no>
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
"command_issued": "zone create example.org hostmaster@example.org ns1.example.org # should require force because ns1 is unknown",
|
|
7
7
|
"ok": [],
|
|
8
8
|
"warning": [
|
|
9
|
-
"Host ns1.example.org not found.",
|
|
10
9
|
"ns1.example.org is not in mreg, must force"
|
|
11
10
|
],
|
|
12
11
|
"error": [],
|
|
@@ -41,9 +40,7 @@
|
|
|
41
40
|
"ok": [
|
|
42
41
|
"Created zone example.org"
|
|
43
42
|
],
|
|
44
|
-
"warning": [
|
|
45
|
-
"Host ns1.example.org not found."
|
|
46
|
-
],
|
|
43
|
+
"warning": [],
|
|
47
44
|
"error": [],
|
|
48
45
|
"output": [],
|
|
49
46
|
"api_requests": [
|
|
@@ -210,10 +207,9 @@
|
|
|
210
207
|
"command": "zone set_ns example.org ns2.example.org",
|
|
211
208
|
"command_filter": null,
|
|
212
209
|
"command_filter_negate": false,
|
|
213
|
-
"command_issued": "zone set_ns example.org ns2.example.org
|
|
210
|
+
"command_issued": "zone set_ns example.org ns2.example.org # requires force because ns2 is unknown",
|
|
214
211
|
"ok": [],
|
|
215
212
|
"warning": [
|
|
216
|
-
"Host ns2.example.org not found.",
|
|
217
213
|
"ns2.example.org is not in mreg, must force"
|
|
218
214
|
],
|
|
219
215
|
"error": [],
|
|
@@ -271,9 +267,7 @@
|
|
|
271
267
|
"ok": [
|
|
272
268
|
"Updated nameservers for example.org"
|
|
273
269
|
],
|
|
274
|
-
"warning": [
|
|
275
|
-
"Host ns2.example.org not found."
|
|
276
|
-
],
|
|
270
|
+
"warning": [],
|
|
277
271
|
"error": [],
|
|
278
272
|
"output": [],
|
|
279
273
|
"api_requests": [
|
|
@@ -15283,9 +15277,7 @@
|
|
|
15283
15277
|
"ok": [
|
|
15284
15278
|
"Created zone delegation wut.example.org"
|
|
15285
15279
|
],
|
|
15286
|
-
"warning": [
|
|
15287
|
-
"Host ns2.example.org not found."
|
|
15288
|
-
],
|
|
15280
|
+
"warning": [],
|
|
15289
15281
|
"error": [],
|
|
15290
15282
|
"output": [],
|
|
15291
15283
|
"api_requests": [
|
|
@@ -21,25 +21,40 @@ class MACAddressField(FrozenModel):
|
|
|
21
21
|
|
|
22
22
|
address: MacAddress
|
|
23
23
|
|
|
24
|
-
@classmethod
|
|
25
|
-
def validate(cls, value: str | MacAddress | Self) -> Self:
|
|
26
|
-
"""Validate a MAC address and return it as a string."""
|
|
27
|
-
try:
|
|
28
|
-
return cls.validate_naive(value)
|
|
29
|
-
except ValueError as e:
|
|
30
|
-
raise InputFailure(e) from e
|
|
31
|
-
|
|
32
24
|
# HACK: extremely hacky workaround for our custom exceptions always
|
|
33
25
|
# logging errors/warnings even when caught.
|
|
34
26
|
@classmethod
|
|
35
|
-
def
|
|
27
|
+
def validate(cls, value: str | MacAddress | Self) -> Self:
|
|
36
28
|
"""Validate but raise built-in exceptions on failure."""
|
|
37
29
|
if isinstance(value, MACAddressField):
|
|
38
|
-
return cls.
|
|
30
|
+
return cls.validate(value.address)
|
|
39
31
|
try:
|
|
40
32
|
return cls(address=value) # pyright: ignore[reportArgumentType]
|
|
41
33
|
except ValidationError as e:
|
|
42
|
-
raise
|
|
34
|
+
raise InputFailure(f"Invalid MAC address '{value}'") from e
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def parse_or_raise(cls, obj: Any) -> MacAddress:
|
|
38
|
+
"""Parse a MAC address from a string. Returns the MAC address as a string.
|
|
39
|
+
|
|
40
|
+
:param obj: The object to parse.
|
|
41
|
+
:returns: The MAC address as a string.
|
|
42
|
+
:raises ValueError: If the object is not a valid MAC address.
|
|
43
|
+
"""
|
|
44
|
+
# Match interface of NetworkOrIP.parse_or_raise
|
|
45
|
+
return cls.validate(obj).address
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def parse(cls, obj: Any) -> MacAddress | None:
|
|
49
|
+
"""Parse a MAC address from a string. Returns None if the MAC address is invalid.
|
|
50
|
+
|
|
51
|
+
:param obj: The object to parse.
|
|
52
|
+
:returns: The MAC address as a string or None if it is invalid.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
return cls.parse_or_raise(obj)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return None
|
|
43
58
|
|
|
44
59
|
def __str__(self) -> str:
|
|
45
60
|
"""Return the MAC address as a string."""
|
|
@@ -7,9 +7,11 @@ import logging
|
|
|
7
7
|
import re
|
|
8
8
|
from datetime import date, datetime
|
|
9
9
|
from functools import cached_property
|
|
10
|
-
from typing import Any, Callable, ClassVar, List, Literal, Self, cast, overload
|
|
10
|
+
from typing import Any, Callable, ClassVar, Iterable, List, Literal, Self, cast, overload
|
|
11
11
|
|
|
12
12
|
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator
|
|
13
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
14
|
+
from pydantic_extra_types.mac_address import MacAddress
|
|
13
15
|
from typing_extensions import Unpack
|
|
14
16
|
|
|
15
17
|
from mreg_cli.api.abstracts import APIMixin, FrozenModel, FrozenModelWithTimestamps
|
|
@@ -75,35 +77,41 @@ class NetworkOrIP(BaseModel):
|
|
|
75
77
|
return cls.validate(value.ip_or_network)
|
|
76
78
|
try:
|
|
77
79
|
return cls(ip_or_network=value) # pyright: ignore[reportArgumentType] # validator handles this
|
|
78
|
-
except
|
|
80
|
+
except PydanticValidationError as e:
|
|
79
81
|
raise InputFailure(f"Invalid IP address or network: {value}") from e
|
|
80
82
|
|
|
81
83
|
@overload
|
|
82
84
|
@classmethod
|
|
83
|
-
def
|
|
85
|
+
def parse_or_raise(cls, value: Any, mode: None = None) -> IP_AddressT | IP_NetworkT: ...
|
|
84
86
|
|
|
85
87
|
@overload
|
|
86
88
|
@classmethod
|
|
87
|
-
def
|
|
89
|
+
def parse_or_raise(cls, value: Any, mode: Literal["ip"]) -> IP_AddressT: ...
|
|
88
90
|
|
|
89
91
|
@overload
|
|
90
92
|
@classmethod
|
|
91
|
-
def
|
|
93
|
+
def parse_or_raise(cls, value: Any, mode: Literal["ipv4"]) -> ipaddress.IPv4Address: ...
|
|
92
94
|
|
|
93
95
|
@overload
|
|
94
96
|
@classmethod
|
|
95
|
-
def
|
|
97
|
+
def parse_or_raise(cls, value: Any, mode: Literal["ipv6"]) -> ipaddress.IPv6Address: ...
|
|
96
98
|
|
|
97
99
|
@overload
|
|
98
100
|
@classmethod
|
|
99
|
-
def
|
|
101
|
+
def parse_or_raise(cls, value: Any, mode: Literal["network"]) -> IP_NetworkT: ...
|
|
100
102
|
|
|
101
103
|
@overload
|
|
102
104
|
@classmethod
|
|
103
|
-
def
|
|
105
|
+
def parse_or_raise(cls, value: Any, mode: Literal["networkv4"]) -> ipaddress.IPv4Network: ...
|
|
106
|
+
|
|
107
|
+
@overload
|
|
108
|
+
@classmethod
|
|
109
|
+
def parse_or_raise(cls, value: Any, mode: Literal["networkv6"]) -> ipaddress.IPv6Network: ...
|
|
104
110
|
|
|
105
111
|
@classmethod
|
|
106
|
-
def
|
|
112
|
+
def parse_or_raise(
|
|
113
|
+
cls, value: Any, mode: IPNetMode | None = None
|
|
114
|
+
) -> IP_AddressT | IP_NetworkT:
|
|
107
115
|
"""Parse a value as an IP address or network.
|
|
108
116
|
|
|
109
117
|
Optionally specify the mode to validate the input as.
|
|
@@ -126,6 +134,49 @@ class NetworkOrIP(BaseModel):
|
|
|
126
134
|
return func(ipnet)
|
|
127
135
|
return ipnet.ip_or_network
|
|
128
136
|
|
|
137
|
+
@overload
|
|
138
|
+
@classmethod
|
|
139
|
+
def parse(cls, value: Any, mode: None = None) -> IP_AddressT | IP_NetworkT | None: ...
|
|
140
|
+
|
|
141
|
+
@overload
|
|
142
|
+
@classmethod
|
|
143
|
+
def parse(cls, value: Any, mode: Literal["ip"]) -> IP_AddressT | None: ...
|
|
144
|
+
|
|
145
|
+
@overload
|
|
146
|
+
@classmethod
|
|
147
|
+
def parse(cls, value: Any, mode: Literal["ipv4"]) -> ipaddress.IPv4Address | None: ...
|
|
148
|
+
|
|
149
|
+
@overload
|
|
150
|
+
@classmethod
|
|
151
|
+
def parse(cls, value: Any, mode: Literal["ipv6"]) -> ipaddress.IPv6Address | None: ...
|
|
152
|
+
|
|
153
|
+
@overload
|
|
154
|
+
@classmethod
|
|
155
|
+
def parse(cls, value: Any, mode: Literal["network"]) -> IP_NetworkT | None: ...
|
|
156
|
+
|
|
157
|
+
@overload
|
|
158
|
+
@classmethod
|
|
159
|
+
def parse(cls, value: Any, mode: Literal["networkv4"]) -> ipaddress.IPv4Network | None: ...
|
|
160
|
+
|
|
161
|
+
@overload
|
|
162
|
+
@classmethod
|
|
163
|
+
def parse(cls, value: Any, mode: Literal["networkv6"]) -> ipaddress.IPv6Network | None: ...
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def parse(cls, value: Any, mode: IPNetMode | None = None) -> IP_AddressT | IP_NetworkT | None:
|
|
167
|
+
"""Parse a value as an IP address or network, or None if parsing fails.
|
|
168
|
+
|
|
169
|
+
Optionally specify the mode to validate the input as.
|
|
170
|
+
|
|
171
|
+
:param value:The value to parse.
|
|
172
|
+
:param mode: The mode to validate the input as.
|
|
173
|
+
:returns: The parsed value as an IP address or network, or None.
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
return cls.parse_or_raise(value, mode)
|
|
177
|
+
except ValueError:
|
|
178
|
+
return None
|
|
179
|
+
|
|
129
180
|
@field_validator("ip_or_network", mode="before")
|
|
130
181
|
@classmethod
|
|
131
182
|
def validate_ip_or_network(cls, value: Any) -> IP_AddressT | IP_NetworkT:
|
|
@@ -1580,7 +1631,7 @@ class Network(FrozenModelWithTimestamps, APIMixin):
|
|
|
1580
1631
|
def ip_network(self) -> IP_NetworkT:
|
|
1581
1632
|
"""IP network object for the network."""
|
|
1582
1633
|
try:
|
|
1583
|
-
return NetworkOrIP.
|
|
1634
|
+
return NetworkOrIP.parse_or_raise(self.network, mode="network")
|
|
1584
1635
|
except IPNetworkWarning as e:
|
|
1585
1636
|
logger.error(
|
|
1586
1637
|
"Invalid network address %s for network with ID %s", self.network, self.id
|
|
@@ -1707,7 +1758,7 @@ class Network(FrozenModelWithTimestamps, APIMixin):
|
|
|
1707
1758
|
def fmt(label: str, value: Any) -> None:
|
|
1708
1759
|
manager.add_line(f"{label:<{padding}}{value}")
|
|
1709
1760
|
|
|
1710
|
-
ipnet = NetworkOrIP.
|
|
1761
|
+
ipnet = NetworkOrIP.parse_or_raise(self.network, mode="network")
|
|
1711
1762
|
reserved_ips = self.get_reserved_ips()
|
|
1712
1763
|
# Remove network address and broadcast address from reserved IPs
|
|
1713
1764
|
reserved_ips_filtered = [
|
|
@@ -1799,9 +1850,9 @@ class Network(FrozenModelWithTimestamps, APIMixin):
|
|
|
1799
1850
|
if isinstance(other, Network):
|
|
1800
1851
|
other = other.network
|
|
1801
1852
|
if isinstance(other, str):
|
|
1802
|
-
other = NetworkOrIP.
|
|
1853
|
+
other = NetworkOrIP.parse_or_raise(other, mode="network")
|
|
1803
1854
|
|
|
1804
|
-
self_net = NetworkOrIP.
|
|
1855
|
+
self_net = NetworkOrIP.parse_or_raise(self.network, mode="network")
|
|
1805
1856
|
return self_net.overlaps(other)
|
|
1806
1857
|
|
|
1807
1858
|
def get_first_available_ip(self) -> IP_AddressT:
|
|
@@ -2610,6 +2661,58 @@ class Host(FrozenModelWithTimestamps, WithTTL, WithHistory, APIMixin):
|
|
|
2610
2661
|
"""Return the endpoint for the class."""
|
|
2611
2662
|
return Endpoint.Hosts
|
|
2612
2663
|
|
|
2664
|
+
@classmethod
|
|
2665
|
+
def get_by_ip(cls, ip: IP_AddressT, inform_as_ptr: bool = True) -> Host | None:
|
|
2666
|
+
"""Get a host by IP address.
|
|
2667
|
+
|
|
2668
|
+
:param ip: The IP address to search for.
|
|
2669
|
+
:param check_ptr: If True, check for PTR overrides as well.
|
|
2670
|
+
:returns: The Host object if found, None otherwise.
|
|
2671
|
+
"""
|
|
2672
|
+
try:
|
|
2673
|
+
host = cls.get_by_field("ipaddresses__ipaddress", str(ip))
|
|
2674
|
+
if not host:
|
|
2675
|
+
host = cls.get_by_field("ptr_overrides__ipaddress", str(ip))
|
|
2676
|
+
if host and inform_as_ptr:
|
|
2677
|
+
OutputManager().add_line(f"{ip} is a PTR override for {host.name}")
|
|
2678
|
+
return host
|
|
2679
|
+
except MultipleEntitiesFound as e:
|
|
2680
|
+
raise MultipleEntitiesFound(f"Multiple hosts found with IP address {ip}.") from e
|
|
2681
|
+
|
|
2682
|
+
@classmethod
|
|
2683
|
+
def get_by_ip_or_raise(cls, ip: IP_AddressT, inform_as_ptr: bool = True) -> Host:
|
|
2684
|
+
"""Get a host by IP address or raise EntityNotFound.
|
|
2685
|
+
|
|
2686
|
+
:param ip: The IP address to search for.
|
|
2687
|
+
:returns: The Host object if found.
|
|
2688
|
+
:param check_ptr: If True, check for PTR overrides as well.
|
|
2689
|
+
"""
|
|
2690
|
+
host = cls.get_by_ip(ip, inform_as_ptr=inform_as_ptr)
|
|
2691
|
+
if not host:
|
|
2692
|
+
raise EntityNotFound(f"Host with IP address {ip} not found.")
|
|
2693
|
+
return host
|
|
2694
|
+
|
|
2695
|
+
@classmethod
|
|
2696
|
+
def get_by_mac(cls, mac: MacAddress) -> Host | None:
|
|
2697
|
+
"""Get a host by MAC address.
|
|
2698
|
+
|
|
2699
|
+
:param ip: The MAC address to search for.
|
|
2700
|
+
:returns: The Host object if found, None otherwise.
|
|
2701
|
+
"""
|
|
2702
|
+
return cls.get_by_field("ipaddresses__macaddress", str(mac))
|
|
2703
|
+
|
|
2704
|
+
@classmethod
|
|
2705
|
+
def get_by_mac_or_raise(cls, mac: MacAddress) -> Host:
|
|
2706
|
+
"""Get a host by MAC address or raise EntityNotFound.
|
|
2707
|
+
|
|
2708
|
+
:param ip: The MAC address to search for.
|
|
2709
|
+
:returns: The Host object if found.
|
|
2710
|
+
"""
|
|
2711
|
+
host = cls.get_by_mac(mac)
|
|
2712
|
+
if not host:
|
|
2713
|
+
raise EntityNotFound(f"Host with MAC address {mac} not found.")
|
|
2714
|
+
return host
|
|
2715
|
+
|
|
2613
2716
|
@classmethod
|
|
2614
2717
|
def get_by_any_means_or_raise(
|
|
2615
2718
|
cls, identifier: str | HostT, inform_as_cname: bool = True, inform_as_ptr: bool = True
|
|
@@ -2670,56 +2773,31 @@ class Host(FrozenModelWithTimestamps, WithTTL, WithHistory, APIMixin):
|
|
|
2670
2773
|
|
|
2671
2774
|
:returns: A Host object if the host was found, otherwise None.
|
|
2672
2775
|
"""
|
|
2673
|
-
host = None
|
|
2674
2776
|
if not isinstance(identifier, HostT):
|
|
2675
2777
|
if identifier.isdigit():
|
|
2676
2778
|
return Host.get_by_id(int(identifier))
|
|
2677
2779
|
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
NetworkOrIP.parse(identifier, mode="ip")
|
|
2682
|
-
|
|
2683
|
-
host = Host.get_by_field("ipaddresses__ipaddress", identifier)
|
|
2684
|
-
if not host:
|
|
2685
|
-
host = Host.get_by_field("ptr_overrides__ipaddress", identifier)
|
|
2686
|
-
ptr = True
|
|
2687
|
-
|
|
2688
|
-
if host:
|
|
2689
|
-
if ptr and inform_as_ptr:
|
|
2690
|
-
OutputManager().add_line(f"{identifier} is a PTR override for {host.name}")
|
|
2691
|
-
return host
|
|
2692
|
-
except MultipleEntitiesFound as e:
|
|
2693
|
-
raise MultipleEntitiesFound(
|
|
2694
|
-
f"Multiple hosts found with IP address or PTR {identifier}."
|
|
2695
|
-
) from e
|
|
2696
|
-
except ValueError: # invalid IP
|
|
2697
|
-
pass
|
|
2780
|
+
if ip := NetworkOrIP.parse(identifier, mode="ip"):
|
|
2781
|
+
host = cls.get_by_ip_or_raise(ip, inform_as_ptr=inform_as_ptr)
|
|
2782
|
+
return host
|
|
2698
2783
|
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
return Host.get_by_field("ipaddresses__macaddress", mac.address)
|
|
2702
|
-
except ValueError:
|
|
2703
|
-
pass
|
|
2784
|
+
if mac := MACAddressField.parse(identifier):
|
|
2785
|
+
return cls.get_by_mac_or_raise(mac)
|
|
2704
2786
|
|
|
2705
2787
|
# Let us try to find the host by name...
|
|
2706
|
-
|
|
2707
|
-
else:
|
|
2708
|
-
name = identifier
|
|
2709
|
-
|
|
2710
|
-
host = Host.get_by_field("name", name.hostname)
|
|
2788
|
+
identifier = HostT(hostname=identifier)
|
|
2711
2789
|
|
|
2712
|
-
if host:
|
|
2790
|
+
if host := cls.get_by_field("name", identifier.hostname):
|
|
2713
2791
|
return host
|
|
2714
2792
|
|
|
2715
|
-
cname = CNAME.get_by_field("name",
|
|
2793
|
+
cname = CNAME.get_by_field("name", identifier.hostname)
|
|
2716
2794
|
# If we found a CNAME, get the host it points to. We're not interested in the
|
|
2717
2795
|
# CNAME itself.
|
|
2718
2796
|
if cname is not None:
|
|
2719
2797
|
host = Host.get_by_id(cname.host)
|
|
2720
2798
|
|
|
2721
2799
|
if host and inform_as_cname:
|
|
2722
|
-
OutputManager().add_line(f"{
|
|
2800
|
+
OutputManager().add_line(f"{identifier.hostname} is a CNAME for {host.name}")
|
|
2723
2801
|
|
|
2724
2802
|
return host
|
|
2725
2803
|
|
|
@@ -2797,7 +2875,7 @@ class Host(FrozenModelWithTimestamps, WithTTL, WithHistory, APIMixin):
|
|
|
2797
2875
|
:returns: The IP address object if found, None otherwise.
|
|
2798
2876
|
"""
|
|
2799
2877
|
if not isinstance(arg_mac, MACAddressField):
|
|
2800
|
-
arg_mac = MACAddressField.
|
|
2878
|
+
arg_mac = MACAddressField.validate(arg_mac)
|
|
2801
2879
|
return next((ip for ip in self.ipaddresses if ip.macaddress == arg_mac), None)
|
|
2802
2880
|
|
|
2803
2881
|
def ips_with_macaddresses(self) -> list[IPAddress]:
|
|
@@ -3522,6 +3600,39 @@ class UserPermission(BaseModel):
|
|
|
3522
3600
|
regex: str
|
|
3523
3601
|
labels: List[str]
|
|
3524
3602
|
|
|
3603
|
+
# NOTE: _needs_ to be a computed field in order to use it in
|
|
3604
|
+
# OutputManager.add_formatted_table, since we dump the model to a dict
|
|
3605
|
+
# inside that method.
|
|
3606
|
+
@computed_field
|
|
3607
|
+
@property
|
|
3608
|
+
def labels_str(self) -> str:
|
|
3609
|
+
"""Return the labels as a string."""
|
|
3610
|
+
return ", ".join(self.labels)
|
|
3611
|
+
|
|
3612
|
+
@classmethod
|
|
3613
|
+
def output_multiple(cls, permissions: Iterable[Self]) -> None:
|
|
3614
|
+
"""Output multiple permissions to the console.
|
|
3615
|
+
|
|
3616
|
+
:param permissions: List of UserPermission records to output.
|
|
3617
|
+
"""
|
|
3618
|
+
# NOTE: this is more or less identical to `Permission.output_multiple()`
|
|
3619
|
+
# with the addition of printing labels.
|
|
3620
|
+
# Since UserPermission is a different model from Permission, sharing a
|
|
3621
|
+
# common method is difficult to make type safe without declaring some sort
|
|
3622
|
+
# of protocol class, which seems a bit overkill.
|
|
3623
|
+
manager = OutputManager()
|
|
3624
|
+
manager.add_line("Permissions:")
|
|
3625
|
+
if not permissions:
|
|
3626
|
+
manager.add_line(" None")
|
|
3627
|
+
return
|
|
3628
|
+
|
|
3629
|
+
OutputManager().add_formatted_table(
|
|
3630
|
+
("IP range", "Group", "Reg.exp.", "Labels"),
|
|
3631
|
+
("range", "group", "regex", "labels_str"),
|
|
3632
|
+
permissions,
|
|
3633
|
+
indent=2,
|
|
3634
|
+
)
|
|
3635
|
+
|
|
3525
3636
|
|
|
3526
3637
|
class ServerVersion(BaseModel):
|
|
3527
3638
|
"""Model for server version metadata."""
|
|
@@ -3657,15 +3768,18 @@ class UserInfo(BaseModel):
|
|
|
3657
3768
|
)
|
|
3658
3769
|
raise e
|
|
3659
3770
|
|
|
3660
|
-
def output(self) -> None:
|
|
3771
|
+
def output(self, django: bool = False) -> None:
|
|
3661
3772
|
"""Output the user information to the console."""
|
|
3662
3773
|
outputmanager = OutputManager()
|
|
3663
3774
|
outputmanager.add_line(f"Username: {self.username}")
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3775
|
+
|
|
3776
|
+
if django:
|
|
3777
|
+
outputmanager.add_line("Django roles:")
|
|
3778
|
+
outputmanager.add_line(f" Superuser: {self.django_status.superuser}")
|
|
3779
|
+
outputmanager.add_line(f" Staff: {self.django_status.staff}")
|
|
3780
|
+
outputmanager.add_line(f" Active: {self.django_status.active}")
|
|
3781
|
+
|
|
3782
|
+
outputmanager.add_line("Mreg roles:")
|
|
3669
3783
|
outputmanager.add_line(f" Superuser: {self.mreg_status.superuser}")
|
|
3670
3784
|
outputmanager.add_line(f" Admin: {self.mreg_status.admin}")
|
|
3671
3785
|
outputmanager.add_line(f" Group admin: {self.mreg_status.group_admin}")
|
|
@@ -3673,15 +3787,9 @@ class UserInfo(BaseModel):
|
|
|
3673
3787
|
outputmanager.add_line(f" Hostpolicy admin: {self.mreg_status.hostpolicy_admin}")
|
|
3674
3788
|
outputmanager.add_line(f" DNS wildcard admin: {self.mreg_status.dns_wildcard_admin}")
|
|
3675
3789
|
outputmanager.add_line(f" Underscore admin: {self.mreg_status.underscore_admin}")
|
|
3790
|
+
|
|
3676
3791
|
outputmanager.add_line("Groups:")
|
|
3677
3792
|
for group in self.groups:
|
|
3678
3793
|
outputmanager.add_line(f" {group}")
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
outputmanager.add_line(" None")
|
|
3682
|
-
|
|
3683
|
-
for perm in self.permissions:
|
|
3684
|
-
outputmanager.add_line(f" Group: {perm.group}")
|
|
3685
|
-
outputmanager.add_line(f" Range: {perm.range}")
|
|
3686
|
-
outputmanager.add_line(f" Regex: {perm.regex}")
|
|
3687
|
-
outputmanager.add_line(f" Labels: {', '.join(perm.labels)}")
|
|
3794
|
+
|
|
3795
|
+
UserPermission.output_multiple(self.permissions)
|
|
@@ -167,7 +167,7 @@ class Command(Completer):
|
|
|
167
167
|
self.last_errno = e.code
|
|
168
168
|
|
|
169
169
|
except (CliWarning, CliError) as exc:
|
|
170
|
-
exc.
|
|
170
|
+
exc.print_and_log()
|
|
171
171
|
|
|
172
172
|
except CliExit:
|
|
173
173
|
# If we have active recordings going on, save them before exiting
|
|
@@ -263,7 +263,7 @@ class Command(Completer):
|
|
|
263
263
|
try:
|
|
264
264
|
cmd = output.from_command(line)
|
|
265
265
|
except (CliWarning, CliError) as exc:
|
|
266
|
-
exc.
|
|
266
|
+
exc.print_and_log()
|
|
267
267
|
return
|
|
268
268
|
# Create and set the corrolation id, using the cleaned command
|
|
269
269
|
# as the suffix. This is used to track the command in the logs
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import logging
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from mreg_cli.__about__ import __version__ as mreg_cli_version
|
|
@@ -10,6 +11,7 @@ from mreg_cli.api.models import ServerLibraries, ServerVersion, UserInfo
|
|
|
10
11
|
from mreg_cli.commands.base import BaseCommand
|
|
11
12
|
from mreg_cli.commands.registry import CommandRegistry
|
|
12
13
|
from mreg_cli.config import MregCliConfig
|
|
14
|
+
from mreg_cli.exceptions import CliError
|
|
13
15
|
from mreg_cli.outputmanager import OutputManager
|
|
14
16
|
from mreg_cli.types import Flag
|
|
15
17
|
|
|
@@ -103,24 +105,32 @@ def versions_help(_: argparse.Namespace) -> None:
|
|
|
103
105
|
prog="whoami",
|
|
104
106
|
description="Show information about the the current user",
|
|
105
107
|
short_desc="Show self info",
|
|
108
|
+
flags=[
|
|
109
|
+
Flag("-django", action="store_true", description="Show Django internal roles"),
|
|
110
|
+
],
|
|
106
111
|
)
|
|
107
|
-
def whoami_help(
|
|
112
|
+
def whoami_help(args: argparse.Namespace) -> None:
|
|
108
113
|
"""Show information about the current user."""
|
|
109
114
|
try:
|
|
110
|
-
UserInfo.fetch(ignore_errors=False).output()
|
|
115
|
+
UserInfo.fetch(ignore_errors=False).output(django=args.django)
|
|
111
116
|
except Exception as e:
|
|
112
|
-
|
|
117
|
+
raise CliError(
|
|
118
|
+
f"Failed to display user info for current user {MregCliConfig().get_user()!r}: {e}"
|
|
119
|
+
) from e
|
|
113
120
|
|
|
114
121
|
|
|
115
122
|
@command_registry.register_command(
|
|
116
123
|
prog="whois",
|
|
117
124
|
description="Show information about a user",
|
|
118
125
|
short_desc="Show user info",
|
|
119
|
-
flags=[
|
|
126
|
+
flags=[
|
|
127
|
+
Flag("user", description="The user to show information about"),
|
|
128
|
+
Flag("-django", action="store_true", description="Show Django internal roles"),
|
|
129
|
+
],
|
|
120
130
|
)
|
|
121
131
|
def whois_help(args: argparse.Namespace) -> None:
|
|
122
132
|
"""Show information about a user."""
|
|
123
133
|
try:
|
|
124
|
-
UserInfo.fetch(ignore_errors=False, user=args.user).output()
|
|
134
|
+
UserInfo.fetch(ignore_errors=False, user=args.user).output(django=args.django)
|
|
125
135
|
except Exception as e:
|
|
126
|
-
|
|
136
|
+
raise CliError(f"Failed to display user info for {args.user!r}: {e}") from e
|
|
@@ -49,7 +49,7 @@ def _ip_change(args: argparse.Namespace, ipversion: IP_Version) -> None:
|
|
|
49
49
|
if args.old == args.new:
|
|
50
50
|
raise EntityAlreadyExists("New and old IP are equal")
|
|
51
51
|
|
|
52
|
-
old_ip = NetworkOrIP.
|
|
52
|
+
old_ip = NetworkOrIP.parse_or_raise(args.old, mode="ip")
|
|
53
53
|
|
|
54
54
|
new_ip = NetworkOrIP.validate(args.new)
|
|
55
55
|
if new_ip.is_network():
|
|
@@ -88,7 +88,7 @@ def _ip_move(args: argparse.Namespace, ipversion: IP_Version) -> None:
|
|
|
88
88
|
:param args: argparse.Namespace (ip, fromhost, tohost)
|
|
89
89
|
:param ipversion: 4 or 6
|
|
90
90
|
"""
|
|
91
|
-
ip = NetworkOrIP.
|
|
91
|
+
ip = NetworkOrIP.parse_or_raise(args.ip, mode="ip")
|
|
92
92
|
if ip.version != ipversion:
|
|
93
93
|
raise InputFailure(
|
|
94
94
|
f"IP version {ip.version} does not match the requested version {ipversion}"
|
|
@@ -123,7 +123,7 @@ def _ip_remove(args: argparse.Namespace, ipversion: IP_Version) -> None:
|
|
|
123
123
|
:param args: argparse.Namespace (name, ip)
|
|
124
124
|
"""
|
|
125
125
|
host = Host.get_by_any_means_or_raise(args.name)
|
|
126
|
-
ip = NetworkOrIP.
|
|
126
|
+
ip = NetworkOrIP.parse_or_raise(args.ip, mode="ip")
|
|
127
127
|
if ip.version != ipversion:
|
|
128
128
|
raise InputFailure(
|
|
129
129
|
f"IP version {ip.version} does not match the requested version {ipversion}"
|
|
@@ -474,7 +474,7 @@ def ptr_change(args: argparse.Namespace) -> None:
|
|
|
474
474
|
if not old_host.ptr_overrides:
|
|
475
475
|
raise EntityNotFound(f"No PTR records for {old_host}")
|
|
476
476
|
|
|
477
|
-
ip = NetworkOrIP.
|
|
477
|
+
ip = NetworkOrIP.parse_or_raise(args.ip, mode="ip")
|
|
478
478
|
ptr_override = old_host.get_ptr_override(ip)
|
|
479
479
|
if not ptr_override:
|
|
480
480
|
raise EntityNotFound(f"No PTR record for {old_host} with IP {ip}")
|
|
@@ -503,7 +503,7 @@ def ptr_remove(args: argparse.Namespace) -> None:
|
|
|
503
503
|
:param args: argparse.Namespace (ip, name)
|
|
504
504
|
"""
|
|
505
505
|
host = Host.get_by_any_means_or_raise(args.name)
|
|
506
|
-
ip = NetworkOrIP.
|
|
506
|
+
ip = NetworkOrIP.parse_or_raise(args.ip, mode="ip")
|
|
507
507
|
ptr_override = host.get_ptr_override(ip)
|
|
508
508
|
if not ptr_override:
|
|
509
509
|
raise EntityNotFound(f"No PTR record for {host} with IP {ip}")
|
|
@@ -529,7 +529,7 @@ def ptr_add(args: argparse.Namespace) -> None:
|
|
|
529
529
|
|
|
530
530
|
:param args: argparse.Namespace (ip, name, force)
|
|
531
531
|
"""
|
|
532
|
-
ip = NetworkOrIP.
|
|
532
|
+
ip = NetworkOrIP.parse_or_raise(args.ip, mode="ip")
|
|
533
533
|
|
|
534
534
|
host = Host.get_by_any_means_or_raise(args.name)
|
|
535
535
|
existing_ptrs = PTR_override.get_list_by_field("ipaddress", str(ip))
|
|
@@ -563,7 +563,7 @@ def ptr_show(args: argparse.Namespace) -> None:
|
|
|
563
563
|
|
|
564
564
|
:param args: argparse.Namespace (ip)
|
|
565
565
|
"""
|
|
566
|
-
ip = NetworkOrIP.
|
|
566
|
+
ip = NetworkOrIP.parse_or_raise(args.ip, mode="ip")
|
|
567
567
|
host = Host.get_by_any_means_or_raise(str(ip), inform_as_ptr=False)
|
|
568
568
|
if not host.ptr_overrides:
|
|
569
569
|
OutputManager().add_line(f"No PTR records for {host.name.hostname}")
|
|
@@ -67,7 +67,7 @@ def create(args: argparse.Namespace) -> None:
|
|
|
67
67
|
if args.location and not is_valid_location_tag(args.location):
|
|
68
68
|
raise InputFailure("Not a valid location tag")
|
|
69
69
|
|
|
70
|
-
arg_network = NetworkOrIP.
|
|
70
|
+
arg_network = NetworkOrIP.parse_or_raise(args.network, mode="network")
|
|
71
71
|
networks = Network.get_list()
|
|
72
72
|
for network in networks:
|
|
73
73
|
if network.overlaps(arg_network):
|
|
@@ -56,7 +56,7 @@ def network_list(args: argparse.Namespace) -> None:
|
|
|
56
56
|
permissions = Permission.get_by_query(query=params, ordering="range,group", limit=None)
|
|
57
57
|
|
|
58
58
|
if args.range is not None:
|
|
59
|
-
argnetwork = NetworkOrIP.
|
|
59
|
+
argnetwork = NetworkOrIP.parse_or_raise(args.range, mode="network")
|
|
60
60
|
|
|
61
61
|
for permission in permissions:
|
|
62
62
|
permnet = permission.range
|
|
@@ -107,7 +107,7 @@ def network_add(args: argparse.Namespace) -> None:
|
|
|
107
107
|
|
|
108
108
|
:param args: argparse.Namespace (range, group, regex)
|
|
109
109
|
"""
|
|
110
|
-
NetworkOrIP.
|
|
110
|
+
NetworkOrIP.parse_or_raise(args.range, mode="network")
|
|
111
111
|
|
|
112
112
|
query = {
|
|
113
113
|
"range": args.range,
|