mreg-cli 1.6.0__tar.gz → 1.7.0__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.6.0 → mreg_cli-1.7.0}/CHANGELOG.md +55 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/PKG-INFO +2 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/_version.py +3 -3
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/api/fields.py +3 -3
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/api/models.py +24 -11
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/cache.py +24 -9
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/cli.py +6 -4
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/cache.py +5 -5
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/help.py +6 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/host_submodules/core.py +2 -2
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/logging.py +49 -18
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/policy.py +23 -5
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/recording.py +3 -2
- mreg_cli-1.7.0/mreg_cli/config.py +363 -0
- mreg_cli-1.7.0/mreg_cli/dirs.py +41 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/exceptions.py +33 -18
- mreg_cli-1.7.0/mreg_cli/log.py +74 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/main.py +55 -27
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/outputmanager.py +16 -17
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/prompt.py +8 -18
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/types.py +3 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/utilities/api.py +13 -11
- mreg_cli-1.7.0/mreg_cli/utilities/fs.py +57 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/utilities/validators.py +3 -3
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli.egg-info/PKG-INFO +2 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli.egg-info/SOURCES.txt +6 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli.egg-info/requires.txt +1 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/pyproject.toml +6 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/run.py +2 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/api/test_fields.py +2 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/conftest.py +31 -20
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/test_cache.py +23 -16
- mreg_cli-1.7.0/tests/test_config.py +237 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/test_exceptions.py +5 -6
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/test_outputmanager.py +1 -1
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/test_prompt.py +10 -12
- mreg_cli-1.7.0/tests/utilities/test_fs.py +140 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/uv.lock +37 -0
- mreg_cli-1.6.0/mreg_cli/config.py +0 -336
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/.github/workflows/publish.yml +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/.github/workflows/test.yml +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/.gitignore +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/.markdownlint.json +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/AUTHORS +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/CONTRIBUTING +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/LICENSE +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/README.md +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/ci/Dockerfile +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/ci/diff.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/ci/docker-compose.yml +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/ci/run_testsuite_and_record.sh +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/ci/run_testsuite_and_record_V2.sh +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/ci/testsuite +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/ci/testsuite-result.json +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/data/mreg-cli.conf +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/__about__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/__main__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/api/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/api/abstracts.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/api/endpoints.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/api/errors.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/api/history.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/base.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/dhcp.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/group.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/host.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/host_submodules/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/host_submodules/a_aaaa.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/host_submodules/bacnet.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/host_submodules/cname.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/host_submodules/rr.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/label.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/network.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/permission.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/registry.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/root.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/commands/zone.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/errorbuilder.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/help_formatter.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/py.typed +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/tags.txt +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/tokenfile.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/utilities/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli/utilities/shared.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli.egg-info/dependency_links.txt +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli.egg-info/entry_points.txt +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/mreg_cli.egg-info/top_level.txt +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/setup.cfg +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/api/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/api/test_models.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/test_errorbuilder.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/test_tokenfile.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/test_types.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/utilities/__init__.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tests/utilities/test_api.py +0 -0
- {mreg_cli-1.6.0 → mreg_cli-1.7.0}/tox.ini +0 -0
|
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
<!-- ## Unreleased -->
|
|
9
9
|
|
|
10
|
+
## [1.7.0](https://github.com/unioslo/mreg-cli/releases/tag/1.7.0) - 2025-11-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `policy list_hosts -exclude EXCLUDEROLE [EXCLUDEROLE ...]` option to exclude hosts that have the given role(s) when listing hosts with a role. Supports multiple arguments and regular expressions.
|
|
15
|
+
- Environment variable support for all config file options.
|
|
16
|
+
- Environment variables are prefixed with `MREG_CLI_` and use uppercase letters and underscores instead of lowercase letters and hyphens.
|
|
17
|
+
- For example, the `http_timeout` config file option can be set with the `MREG_CLI_HTTP_TIMEOUT` environment variable.
|
|
18
|
+
- Environment variables take precedence over config file options.
|
|
19
|
+
- All config file options and their corresponding environment vaiables:
|
|
20
|
+
|
|
21
|
+
| Config FIle | Env |
|
|
22
|
+
|----------------------------------|-----------------------------------------|
|
|
23
|
+
| `url` | `MREG_CLI_URL` |
|
|
24
|
+
| `user` | `MREG_CLI_USER` |
|
|
25
|
+
| `domain` | `MREG_CLI_DOMAIN` |
|
|
26
|
+
| `timeout` | `MREG_CLI_TIMEOUT` |
|
|
27
|
+
| `prompt` | `MREG_CLI_PROMPT` |
|
|
28
|
+
| `category_tags` | `MREG_CLI_CATEGORY_TAGS` |
|
|
29
|
+
| `location_tags` | `MREG_CLI_LOCATION_TAGS` |
|
|
30
|
+
| `cache` | `MREG_CLI_CACHE` |
|
|
31
|
+
| `cache_ttl` | `MREG_CLI_CACHE_TTL` |
|
|
32
|
+
| `http_timeout` | `MREG_CLI_HTTP_TIMEOUT` |
|
|
33
|
+
| `record_traffic` | `MREG_CLI_RECORD_TRAFFIC` |
|
|
34
|
+
| `record_traffic_without_timestamps` | `MREG_CLI_RECORD_TRAFFIC_WITHOUT_TIMESTAMPS` |
|
|
35
|
+
| `token_only` | `MREG_CLI_TOKEN_ONLY` |
|
|
36
|
+
| `source` | `MREG_CLI_SOURCE` |
|
|
37
|
+
| `verbose` | `MREG_CLI_VERBOSE` |
|
|
38
|
+
| `log_file` | `MREG_CLI_LOG_FILE` |
|
|
39
|
+
| `log_level` | `MREG_CLI_LOG_LEVEL` |
|
|
40
|
+
|
|
41
|
+
- _New_ configuration options, previously only available as CLI options, exposed as both config file options and environment variables:
|
|
42
|
+
|
|
43
|
+
| Config File | Env |
|
|
44
|
+
|----------------------------------|-----------------------------------------|
|
|
45
|
+
| `http_timeout` | `MREG_CLI_HTTP_TIMEOUT` |
|
|
46
|
+
| `record_traffic` | `MREG_CLI_RECORD_TRAFFIC` |
|
|
47
|
+
| `record_traffic_without_timestamps` | `MREG_CLI_RECORD_TRAFFIC_WITHOUT_TIMESTAMPS` |
|
|
48
|
+
| `token_only` | `MREG_CLI_TOKEN_ONLY` |
|
|
49
|
+
| `source` | `MREG_CLI_SOURCE` |
|
|
50
|
+
| `verbose` | `MREG_CLI_VERBOSE` |
|
|
51
|
+
| `log_file`\* | `MREG_CLI_LOG_FILE` |
|
|
52
|
+
| `log_level` | `MREG_CLI_LOG_LEVEL` |
|
|
53
|
+
|
|
54
|
+
\*: The old name `logfile` is deprecated, but remains a valid alias for this field.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- Default config file location is now `$XDG_CONFIG_HOME/mreg-cli/mreg-cli.conf` (was `~/.config/mreg-cli.conf`).
|
|
59
|
+
- The old config file location remains supported for backwards compatibility, but will be removed in a future release.
|
|
60
|
+
- `help configuration` now shows environment variables.
|
|
61
|
+
- `logging start` no longer toggles logging on and off. Now aborts if logging is already enabled.
|
|
62
|
+
- `logging stop` now aborts if logging is not enabled.
|
|
63
|
+
- `logging status` now shows if logging is enabled or not.
|
|
64
|
+
|
|
10
65
|
## [1.6.0](https://github.com/unioslo/mreg-cli/releases/tag/1.6.0) - 2025-10-13
|
|
11
66
|
|
|
12
67
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mreg-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
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>
|
|
@@ -702,6 +702,7 @@ Requires-Dist: pydantic[email]>=2.7.1
|
|
|
702
702
|
Requires-Dist: pydantic-extra-types>=2.1.0
|
|
703
703
|
Requires-Dist: platformdirs>=4.3.0
|
|
704
704
|
Requires-Dist: diskcache>=5.6.3
|
|
705
|
+
Requires-Dist: pydantic-settings>=2.10.1
|
|
705
706
|
Dynamic: license-file
|
|
706
707
|
|
|
707
708
|
# MREG CLI [](https://github.com/unioslo/mreg-cli/actions/workflows/test.yml)
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.7.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 7, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g0f525f9e1'
|
|
@@ -73,10 +73,10 @@ class HostName(str):
|
|
|
73
73
|
return value
|
|
74
74
|
|
|
75
75
|
config = MregCliConfig()
|
|
76
|
-
|
|
76
|
+
domain = config.domain
|
|
77
77
|
# Append domain name if in config and it does not end with it
|
|
78
|
-
if
|
|
79
|
-
return f"{value}.{
|
|
78
|
+
if domain and not value.endswith(domain):
|
|
79
|
+
return f"{value}.{domain}"
|
|
80
80
|
return value
|
|
81
81
|
|
|
82
82
|
@classmethod
|
|
@@ -35,7 +35,7 @@ from mreg_cli.api.endpoints import Endpoint
|
|
|
35
35
|
from mreg_cli.api.fields import HostName, MacAddress, NameList
|
|
36
36
|
from mreg_cli.api.history import HistoryItem, HistoryResource
|
|
37
37
|
from mreg_cli.exceptions import (
|
|
38
|
-
|
|
38
|
+
APIError,
|
|
39
39
|
CreateError,
|
|
40
40
|
DeleteError,
|
|
41
41
|
EntityAlreadyExists,
|
|
@@ -1012,11 +1012,11 @@ class ForwardZone(Zone, WithName, APIMixin):
|
|
|
1012
1012
|
:param hostname: The hostname to search for.
|
|
1013
1013
|
:returns: The zone if found, None otherwise.
|
|
1014
1014
|
"""
|
|
1015
|
-
|
|
1016
|
-
if not
|
|
1015
|
+
resp = get(Endpoint.ForwardZoneForHost.with_id(hostname), ok404=True)
|
|
1016
|
+
if not resp:
|
|
1017
1017
|
return None
|
|
1018
1018
|
|
|
1019
|
-
zoneblob =
|
|
1019
|
+
zoneblob = resp.json()
|
|
1020
1020
|
|
|
1021
1021
|
if "delegate" in zoneblob:
|
|
1022
1022
|
return ForwardZoneDelegation.model_validate(zoneblob)
|
|
@@ -1027,7 +1027,7 @@ class ForwardZone(Zone, WithName, APIMixin):
|
|
|
1027
1027
|
if "delegation" in zoneblob:
|
|
1028
1028
|
return ForwardZoneDelegation.model_validate(zoneblob["delegation"])
|
|
1029
1029
|
|
|
1030
|
-
raise UnexpectedDataError(f"Unexpected response from server: {zoneblob}")
|
|
1030
|
+
raise UnexpectedDataError(f"Unexpected response from server: {zoneblob}", resp)
|
|
1031
1031
|
|
|
1032
1032
|
|
|
1033
1033
|
class ReverseZone(Zone, WithName, APIMixin):
|
|
@@ -1268,15 +1268,28 @@ class Role(HostPolicy, WithHistory):
|
|
|
1268
1268
|
for label in labels:
|
|
1269
1269
|
output_manager.add_formatted_line("", label.name, padding)
|
|
1270
1270
|
|
|
1271
|
-
def output_hosts(self, _padding: int = 14) -> None:
|
|
1271
|
+
def output_hosts(self, _padding: int = 14, exclude_roles: list[Role] | None = None) -> None:
|
|
1272
1272
|
"""Output the hosts that use the role.
|
|
1273
1273
|
|
|
1274
1274
|
:param padding: Number of spaces for left-padding the output.
|
|
1275
|
+
:param exclude_roles: List of other roles to exclude hosts with.
|
|
1275
1276
|
"""
|
|
1276
1277
|
manager = OutputManager()
|
|
1277
|
-
|
|
1278
|
+
hosts = self.hosts
|
|
1279
|
+
|
|
1280
|
+
if exclude_roles:
|
|
1281
|
+
# Exclude any hosts that are found in the excluded roles
|
|
1282
|
+
excluded_hosts: set[str] = set()
|
|
1283
|
+
for host in hosts:
|
|
1284
|
+
for role in exclude_roles:
|
|
1285
|
+
if host in role.hosts:
|
|
1286
|
+
excluded_hosts.add(host)
|
|
1287
|
+
break
|
|
1288
|
+
hosts = [host for host in hosts if host not in excluded_hosts]
|
|
1289
|
+
|
|
1290
|
+
if hosts:
|
|
1278
1291
|
manager.add_line("Name:")
|
|
1279
|
-
for host in
|
|
1292
|
+
for host in hosts:
|
|
1280
1293
|
manager.add_line(f" {host}")
|
|
1281
1294
|
else:
|
|
1282
1295
|
manager.add_line("No host uses this role")
|
|
@@ -2463,7 +2476,7 @@ class IPAddress(FrozenModelWithTimestamps, WithHost, APIMixin):
|
|
|
2463
2476
|
"""Return the network of the IP address."""
|
|
2464
2477
|
try:
|
|
2465
2478
|
return get_typed(Endpoint.NetworksByIP.with_id(str(self.ipaddress)), Network)
|
|
2466
|
-
except
|
|
2479
|
+
except APIError:
|
|
2467
2480
|
return None
|
|
2468
2481
|
|
|
2469
2482
|
def vlan(self) -> int | None:
|
|
@@ -4545,13 +4558,13 @@ class LDAPHealth(BaseModel, APIMixin):
|
|
|
4545
4558
|
|
|
4546
4559
|
:param ignore_errors: Ignore non-503 errors. 503 means LDAP is down,
|
|
4547
4560
|
and should not be treated as an error in the traditional sense.
|
|
4548
|
-
:raises requests.
|
|
4561
|
+
:raises requests.APIError: If the response code is not 200 or 503.
|
|
4549
4562
|
:returns: An instance of LDAPStatus.
|
|
4550
4563
|
"""
|
|
4551
4564
|
try:
|
|
4552
4565
|
get(cls.endpoint())
|
|
4553
4566
|
return cls(status="OK")
|
|
4554
|
-
except
|
|
4567
|
+
except APIError as e:
|
|
4555
4568
|
if ignore_errors:
|
|
4556
4569
|
logger.error("Failed to fetch LDAP health: %s", e)
|
|
4557
4570
|
if e.response.status_code == 503:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import functools
|
|
3
4
|
import logging
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from types import ModuleType
|
|
@@ -72,7 +73,7 @@ class NullCache:
|
|
|
72
73
|
tag: str | None = None,
|
|
73
74
|
ignore: tuple[str, ...] = (),
|
|
74
75
|
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
75
|
-
"""Returns a decorator that does nothing"""
|
|
76
|
+
"""Returns a decorator that does nothing."""
|
|
76
77
|
|
|
77
78
|
def decorator(func: Callable[P, T]) -> Callable[P, T]:
|
|
78
79
|
return func # Just return the original function unchanged
|
|
@@ -148,7 +149,7 @@ def _create_cache(config: MregCliConfig) -> CacheLike:
|
|
|
148
149
|
|
|
149
150
|
Falls back to a no-op cache object if the diskcache cache cannot be created.
|
|
150
151
|
"""
|
|
151
|
-
if not config.
|
|
152
|
+
if not config.cache:
|
|
152
153
|
logger.debug("Cache is disabled in configuration, using NullCache.")
|
|
153
154
|
return NullCache()
|
|
154
155
|
|
|
@@ -168,7 +169,7 @@ def configure(config: MregCliConfig) -> None:
|
|
|
168
169
|
cache = _create_cache(config)
|
|
169
170
|
_CACHE = MregCliCache(cache) # pyright: ignore[reportConstantRedefinition]
|
|
170
171
|
|
|
171
|
-
if not config.
|
|
172
|
+
if not config.cache:
|
|
172
173
|
logger.debug("Cache is disabled in configuration, not configuring cache.")
|
|
173
174
|
return
|
|
174
175
|
|
|
@@ -227,7 +228,7 @@ class MregCliCache:
|
|
|
227
228
|
misses=misses,
|
|
228
229
|
items=len(self.cache),
|
|
229
230
|
directory=self.cache.directory,
|
|
230
|
-
ttl=conf.
|
|
231
|
+
ttl=conf.cache_ttl,
|
|
231
232
|
)
|
|
232
233
|
|
|
233
234
|
def _patch_functions(self) -> None:
|
|
@@ -243,10 +244,11 @@ class MregCliCache:
|
|
|
243
244
|
self._patch_functions()
|
|
244
245
|
logger.info("Cache enabled")
|
|
245
246
|
|
|
246
|
-
def disable(self) -> None:
|
|
247
|
+
def disable(self, *, clear: bool = True) -> None:
|
|
247
248
|
"""Disable caching by restoring patched functions and clearing the cache."""
|
|
248
249
|
self._unpatch_functions()
|
|
249
|
-
|
|
250
|
+
if clear:
|
|
251
|
+
self.clear()
|
|
250
252
|
logger.info("Cache disabled")
|
|
251
253
|
|
|
252
254
|
def clear(self) -> None:
|
|
@@ -298,8 +300,6 @@ class MregCliCache:
|
|
|
298
300
|
|
|
299
301
|
def memoize_function(self, memfunc: MemoizedFunction) -> None:
|
|
300
302
|
"""Memoize a given function."""
|
|
301
|
-
from mreg_cli.config import MregCliConfig
|
|
302
|
-
|
|
303
303
|
config = MregCliConfig()
|
|
304
304
|
|
|
305
305
|
mod_path = memfunc.func
|
|
@@ -314,5 +314,20 @@ class MregCliCache:
|
|
|
314
314
|
self.save_original_function(mod_path, mod, func)
|
|
315
315
|
|
|
316
316
|
self.patch_function(
|
|
317
|
-
mod, self.cache.memoize(expire=config.
|
|
317
|
+
mod, self.cache.memoize(expire=config.cache_ttl, tag=memfunc.tag)(func)
|
|
318
318
|
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def disable_cache(func: Callable[P, T]) -> Callable[P, T]:
|
|
322
|
+
"""Disable cache for the duration of the decorated function."""
|
|
323
|
+
|
|
324
|
+
@functools.wraps(func)
|
|
325
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
326
|
+
cache = get_cache()
|
|
327
|
+
try:
|
|
328
|
+
cache.disable(clear=False)
|
|
329
|
+
return func(*args, **kwargs)
|
|
330
|
+
finally:
|
|
331
|
+
cache.enable()
|
|
332
|
+
|
|
333
|
+
return wrapper
|
|
@@ -12,6 +12,7 @@ import os
|
|
|
12
12
|
import shlex
|
|
13
13
|
import sys
|
|
14
14
|
from collections.abc import Generator
|
|
15
|
+
from pathlib import Path
|
|
15
16
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
16
17
|
|
|
17
18
|
from prompt_toolkit import HTML, document, print_formatted_text
|
|
@@ -39,6 +40,7 @@ from mreg_cli.help_formatter import CustomHelpFormatter
|
|
|
39
40
|
from mreg_cli.outputmanager import OutputManager
|
|
40
41
|
from mreg_cli.types import CommandFunc, Flag
|
|
41
42
|
from mreg_cli.utilities.api import create_and_set_corrolation_id, last_request_url
|
|
43
|
+
from mreg_cli.utilities.fs import to_path
|
|
42
44
|
|
|
43
45
|
logger = logging.getLogger(__name__)
|
|
44
46
|
|
|
@@ -307,7 +309,7 @@ for command in commands:
|
|
|
307
309
|
command(cli).register_all_commands()
|
|
308
310
|
|
|
309
311
|
|
|
310
|
-
def source(files: list[
|
|
312
|
+
def source(files: list[Path], ignore_errors: bool, verbose: bool) -> Generator[str]:
|
|
311
313
|
"""Read commands from one or more source files and yield them.
|
|
312
314
|
|
|
313
315
|
:param files: List of file paths to read commands from.
|
|
@@ -317,8 +319,7 @@ def source(files: list[str], ignore_errors: bool, verbose: bool) -> Generator[st
|
|
|
317
319
|
:yields: Command lines from the files.
|
|
318
320
|
"""
|
|
319
321
|
for filename in files:
|
|
320
|
-
|
|
321
|
-
filename = os.path.expanduser(filename)
|
|
322
|
+
filename = filename.expanduser()
|
|
322
323
|
try:
|
|
323
324
|
with open(filename) as f:
|
|
324
325
|
logger.info("Reading commands from %s", filename)
|
|
@@ -357,7 +358,8 @@ def _source(args: argparse.Namespace):
|
|
|
357
358
|
|
|
358
359
|
:param args: The arguments passed to the command.
|
|
359
360
|
"""
|
|
360
|
-
for
|
|
361
|
+
files = [to_path(f) for f in args.files]
|
|
362
|
+
for command in source(files, args.ignore_errors, args.verbose):
|
|
361
363
|
# Process each command here as needed, similar to the main loop
|
|
362
364
|
print(f"Processing command: {command}")
|
|
363
365
|
cli.process_command_line(command)
|
|
@@ -43,7 +43,7 @@ def cache_clear(_: argparse.Namespace) -> None:
|
|
|
43
43
|
def cache_info(_: argparse.Namespace) -> None:
|
|
44
44
|
"""Show cache information."""
|
|
45
45
|
conf = MregCliConfig()
|
|
46
|
-
if not conf.
|
|
46
|
+
if not conf.cache:
|
|
47
47
|
OutputManager().add_line("Cache is disabled")
|
|
48
48
|
return
|
|
49
49
|
|
|
@@ -61,13 +61,13 @@ def cache_info(_: argparse.Namespace) -> None:
|
|
|
61
61
|
def cache_enable(_: argparse.Namespace) -> None:
|
|
62
62
|
"""Enable caching."""
|
|
63
63
|
conf = MregCliConfig()
|
|
64
|
-
if conf.
|
|
64
|
+
if conf.cache:
|
|
65
65
|
OutputManager().add_line("Cache is already enabled")
|
|
66
66
|
return
|
|
67
67
|
|
|
68
68
|
cache = get_cache()
|
|
69
69
|
cache.enable()
|
|
70
|
-
conf.
|
|
70
|
+
conf.cache = True
|
|
71
71
|
OutputManager().add_ok("Enabled cache")
|
|
72
72
|
|
|
73
73
|
|
|
@@ -77,11 +77,11 @@ def cache_enable(_: argparse.Namespace) -> None:
|
|
|
77
77
|
def cache_disable(_: argparse.Namespace) -> None:
|
|
78
78
|
"""Disable caching."""
|
|
79
79
|
conf = MregCliConfig()
|
|
80
|
-
if not conf.
|
|
80
|
+
if not conf.cache:
|
|
81
81
|
OutputManager().add_line("Cache is already disabled")
|
|
82
82
|
return
|
|
83
83
|
|
|
84
84
|
cache = get_cache()
|
|
85
85
|
cache.disable()
|
|
86
|
-
conf.
|
|
86
|
+
conf.cache = False
|
|
87
87
|
OutputManager().add_ok("Cleared and disabled cache")
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
from mreg_cli.__about__ import __version__ as mreg_cli_version
|
|
9
9
|
from mreg_cli.api.models import HealthInfo, ServerLibraries, ServerVersion, UserInfo
|
|
10
|
+
from mreg_cli.cache import disable_cache
|
|
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
|
|
@@ -91,6 +92,7 @@ def configuration_help(_: argparse.Namespace) -> None:
|
|
|
91
92
|
description="Show versions of client and server as much as possible",
|
|
92
93
|
short_desc="Show versions",
|
|
93
94
|
)
|
|
95
|
+
@disable_cache
|
|
94
96
|
def versions_help(_: argparse.Namespace) -> None:
|
|
95
97
|
"""Show versions of client and server as much as possible."""
|
|
96
98
|
output_manager = OutputManager()
|
|
@@ -108,13 +110,14 @@ def versions_help(_: argparse.Namespace) -> None:
|
|
|
108
110
|
Flag("-django", action="store_true", description="Show Django internal roles"),
|
|
109
111
|
],
|
|
110
112
|
)
|
|
113
|
+
@disable_cache
|
|
111
114
|
def whoami_help(args: argparse.Namespace) -> None:
|
|
112
115
|
"""Show information about the current user."""
|
|
113
116
|
try:
|
|
114
117
|
UserInfo.fetch(ignore_errors=False).output(django=args.django)
|
|
115
118
|
except Exception as e:
|
|
116
119
|
raise CliError(
|
|
117
|
-
f"Failed to display user info for current user {MregCliConfig().
|
|
120
|
+
f"Failed to display user info for current user {MregCliConfig().user!r}: {e}"
|
|
118
121
|
) from e
|
|
119
122
|
|
|
120
123
|
|
|
@@ -127,6 +130,7 @@ def whoami_help(args: argparse.Namespace) -> None:
|
|
|
127
130
|
Flag("-django", action="store_true", description="Show Django internal roles"),
|
|
128
131
|
],
|
|
129
132
|
)
|
|
133
|
+
@disable_cache
|
|
130
134
|
def whois_help(args: argparse.Namespace) -> None:
|
|
131
135
|
"""Show information about a user."""
|
|
132
136
|
try:
|
|
@@ -140,6 +144,7 @@ def whois_help(args: argparse.Namespace) -> None:
|
|
|
140
144
|
description="Show information about the health of the server",
|
|
141
145
|
short_desc="Show health info",
|
|
142
146
|
)
|
|
147
|
+
@disable_cache
|
|
143
148
|
def meta_health(_: argparse.Namespace) -> None:
|
|
144
149
|
"""Show information about the health of the server."""
|
|
145
150
|
try:
|
|
@@ -28,7 +28,7 @@ from mreg_cli.api.models import (
|
|
|
28
28
|
)
|
|
29
29
|
from mreg_cli.commands.host import registry as command_registry
|
|
30
30
|
from mreg_cli.exceptions import (
|
|
31
|
-
|
|
31
|
+
APIError,
|
|
32
32
|
CreateError,
|
|
33
33
|
DeleteError,
|
|
34
34
|
EntityAlreadyExists,
|
|
@@ -149,7 +149,7 @@ def add(args: argparse.Namespace) -> None:
|
|
|
149
149
|
raise InvalidIPAddress(
|
|
150
150
|
f"IP {ipaddr} is a broadcast address, not a host address, must force"
|
|
151
151
|
)
|
|
152
|
-
except (EntityNotFound,
|
|
152
|
+
except (EntityNotFound, APIError) as e:
|
|
153
153
|
if not force:
|
|
154
154
|
raise ForceMissing(f"IP {ipaddr} is not in a network, must force") from e
|
|
155
155
|
data["ipaddress"] = str(network_or_ip)
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
-
import os
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
9
8
|
from mreg_cli.commands.base import BaseCommand
|
|
10
9
|
from mreg_cli.commands.registry import CommandRegistry
|
|
11
10
|
from mreg_cli.config import MregCliConfig
|
|
12
|
-
from mreg_cli.exceptions import InputFailure
|
|
11
|
+
from mreg_cli.exceptions import FileError, InputFailure
|
|
12
|
+
from mreg_cli.log import MregCliLogger
|
|
13
13
|
from mreg_cli.outputmanager import OutputManager
|
|
14
14
|
from mreg_cli.types import Flag, LogLevel
|
|
15
|
+
from mreg_cli.utilities.fs import to_path
|
|
15
16
|
from mreg_cli.utilities.shared import sizeof_fmt
|
|
16
17
|
|
|
17
18
|
command_registry = CommandRegistry()
|
|
@@ -37,13 +38,14 @@ class LoggingCommmands(BaseCommand):
|
|
|
37
38
|
short_desc="Start logging",
|
|
38
39
|
flags=[
|
|
39
40
|
Flag(
|
|
40
|
-
"filename",
|
|
41
|
-
description="The filename to log to.",
|
|
41
|
+
"-filename",
|
|
42
|
+
description="The filename to log to. Uses default log file if not given.",
|
|
42
43
|
short_desc="Filename",
|
|
43
44
|
metavar="filename",
|
|
45
|
+
default=None,
|
|
44
46
|
),
|
|
45
47
|
Flag(
|
|
46
|
-
"level",
|
|
48
|
+
"-level",
|
|
47
49
|
description="The logging level to use.",
|
|
48
50
|
short_desc="Level",
|
|
49
51
|
metavar="level",
|
|
@@ -54,12 +56,20 @@ class LoggingCommmands(BaseCommand):
|
|
|
54
56
|
)
|
|
55
57
|
def start_logging(args: argparse.Namespace) -> None:
|
|
56
58
|
"""Start logging to the given file."""
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
+
if args.filename is not None:
|
|
60
|
+
log_file = to_path(args.filename)
|
|
61
|
+
else:
|
|
62
|
+
log_file = MregCliConfig().log_file
|
|
63
|
+
level = LogLevel(args.level)
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
log = MregCliLogger()
|
|
66
|
+
status = log.status
|
|
67
|
+
# Abort if already logging with same params
|
|
68
|
+
if status.enabled and status.file == log_file and status.level == level:
|
|
69
|
+
raise InputFailure(f"Logging already enabled: {level} > {log_file}")
|
|
70
|
+
|
|
71
|
+
log.start_logging(log_file, level)
|
|
72
|
+
OutputManager().add_line(f"Logging started: {level} > {log_file}")
|
|
63
73
|
|
|
64
74
|
|
|
65
75
|
@command_registry.register_command(
|
|
@@ -69,7 +79,12 @@ def start_logging(args: argparse.Namespace) -> None:
|
|
|
69
79
|
)
|
|
70
80
|
def stop_logging(_: argparse.Namespace):
|
|
71
81
|
"""Stop logging."""
|
|
72
|
-
|
|
82
|
+
log = MregCliLogger()
|
|
83
|
+
if not log.status.enabled:
|
|
84
|
+
raise InputFailure("Logging is not enabled.")
|
|
85
|
+
|
|
86
|
+
log.stop_logging()
|
|
87
|
+
OutputManager().add_line("Logging stopped.")
|
|
73
88
|
|
|
74
89
|
|
|
75
90
|
@command_registry.register_command(
|
|
@@ -89,8 +104,15 @@ def stop_logging(_: argparse.Namespace):
|
|
|
89
104
|
def set_logging_level(args: argparse.Namespace) -> None:
|
|
90
105
|
"""Set the logging level."""
|
|
91
106
|
config = MregCliConfig()
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
log = MregCliLogger()
|
|
108
|
+
|
|
109
|
+
if not log.status.enabled:
|
|
110
|
+
raise InputFailure("Logging is not enabled, cannot set level.")
|
|
111
|
+
|
|
112
|
+
# Determine logging params
|
|
113
|
+
log_file = log.status.file or config.log_file # fallback should never be reachable...
|
|
114
|
+
level = LogLevel(args.level)
|
|
115
|
+
log.start_logging(log_file, level)
|
|
94
116
|
|
|
95
117
|
|
|
96
118
|
@command_registry.register_command(
|
|
@@ -100,13 +122,22 @@ def set_logging_level(args: argparse.Namespace) -> None:
|
|
|
100
122
|
)
|
|
101
123
|
def logging_status(_: argparse.Namespace):
|
|
102
124
|
"""Print the logging status."""
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
conf = MregCliConfig()
|
|
126
|
+
|
|
127
|
+
status = MregCliLogger().status
|
|
128
|
+
if not status.enabled:
|
|
129
|
+
raise InputFailure("Logging is disabled.")
|
|
130
|
+
|
|
131
|
+
level = status.level
|
|
132
|
+
file = status.file or conf.log_file # fallback should never be reachable...
|
|
105
133
|
|
|
106
134
|
lines_in_logfile = 0
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
try:
|
|
136
|
+
with open(file) as f:
|
|
137
|
+
lines_in_logfile = sum(1 for _ in f)
|
|
138
|
+
except OSError as e:
|
|
139
|
+
raise FileError(f"Unable to read log file {file}: {e}") from e
|
|
109
140
|
|
|
110
|
-
filesize = sizeof_fmt(
|
|
141
|
+
filesize = sizeof_fmt(file.stat().st_size)
|
|
111
142
|
|
|
112
143
|
OutputManager().add_line(f"{level} > {file} ({lines_in_logfile} lines, {filesize})")
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import itertools
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from mreg_cli.api.models import (
|
|
@@ -13,7 +14,7 @@ from mreg_cli.api.models import (
|
|
|
13
14
|
)
|
|
14
15
|
from mreg_cli.commands.base import BaseCommand
|
|
15
16
|
from mreg_cli.commands.registry import CommandRegistry
|
|
16
|
-
from mreg_cli.exceptions import
|
|
17
|
+
from mreg_cli.exceptions import APIError, CreateError, DeleteError, EntityAlreadyExists, PatchError
|
|
17
18
|
from mreg_cli.outputmanager import OutputManager
|
|
18
19
|
from mreg_cli.types import Flag
|
|
19
20
|
|
|
@@ -259,6 +260,15 @@ def list_roles(args: argparse.Namespace) -> None:
|
|
|
259
260
|
short_desc="List hosts which use the given role",
|
|
260
261
|
flags=[
|
|
261
262
|
Flag("name", description="Role name", metavar="ROLE"),
|
|
263
|
+
Flag(
|
|
264
|
+
"-exclude",
|
|
265
|
+
description=(
|
|
266
|
+
"Exclude hosts that have these roles. "
|
|
267
|
+
"Supports regular expressions and multiple arguments."
|
|
268
|
+
),
|
|
269
|
+
metavar="EXCLUDEROLE",
|
|
270
|
+
nargs="+",
|
|
271
|
+
),
|
|
262
272
|
],
|
|
263
273
|
)
|
|
264
274
|
def list_hosts(args: argparse.Namespace) -> None:
|
|
@@ -267,9 +277,15 @@ def list_hosts(args: argparse.Namespace) -> None:
|
|
|
267
277
|
:param args: argparse.Namespace (name)
|
|
268
278
|
"""
|
|
269
279
|
name: str = args.name
|
|
280
|
+
exclude: list[str] = args.exclude if args.exclude else []
|
|
270
281
|
|
|
271
282
|
role = Role.get_by_name_or_raise(name)
|
|
272
|
-
|
|
283
|
+
|
|
284
|
+
exclude_roles = list(
|
|
285
|
+
itertools.chain.from_iterable(Role.get_list_by_name_regex(r) for r in exclude)
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
role.output_hosts(exclude_roles=exclude_roles)
|
|
273
289
|
|
|
274
290
|
|
|
275
291
|
@command_registry.register_command(
|
|
@@ -312,18 +328,20 @@ def host_add(args: argparse.Namespace) -> None:
|
|
|
312
328
|
hosts = [Host.get_by_any_means_or_raise(host) for host in host_names]
|
|
313
329
|
|
|
314
330
|
for host in hosts:
|
|
331
|
+
# Best-effort approach – try to assign roles to all hosts
|
|
315
332
|
try:
|
|
316
333
|
role.add_host(host.name)
|
|
317
334
|
OutputManager().add_ok(f"Added host {host.name} to role {role_name!r}")
|
|
318
|
-
except
|
|
335
|
+
except APIError as e:
|
|
319
336
|
if e.response.status_code == 409:
|
|
320
337
|
OutputManager().add_line(
|
|
321
338
|
f"Host {host.name} is already a member of role {role_name!r}"
|
|
322
339
|
)
|
|
323
340
|
else:
|
|
324
|
-
|
|
325
|
-
f"Failed to add host {host.name} to role {role_name!r}
|
|
341
|
+
err = PatchError.from_api_error(
|
|
342
|
+
e, f"Failed to add host {host.name} to role {role_name!r}"
|
|
326
343
|
)
|
|
344
|
+
err.print_and_log()
|
|
327
345
|
|
|
328
346
|
|
|
329
347
|
@command_registry.register_command(
|
|
@@ -10,6 +10,7 @@ from mreg_cli.commands.registry import CommandRegistry
|
|
|
10
10
|
from mreg_cli.exceptions import InputFailure
|
|
11
11
|
from mreg_cli.outputmanager import OutputManager
|
|
12
12
|
from mreg_cli.types import Flag
|
|
13
|
+
from mreg_cli.utilities.fs import to_path
|
|
13
14
|
|
|
14
15
|
command_registry = CommandRegistry()
|
|
15
16
|
|
|
@@ -45,8 +46,8 @@ def start_recording(args: argparse.Namespace) -> None:
|
|
|
45
46
|
"""Start recording commands and output to the given file."""
|
|
46
47
|
if not args.filename:
|
|
47
48
|
raise InputFailure("No filename given.")
|
|
48
|
-
|
|
49
|
-
OutputManager().recording_start(
|
|
49
|
+
path = to_path(args.filename)
|
|
50
|
+
OutputManager().recording_start(path)
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
@command_registry.register_command(
|