mreg-cli 1.1.0__tar.gz → 1.2.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.
Files changed (85) hide show
  1. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/.github/workflows/publish.yml +1 -1
  2. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/.github/workflows/test.yml +11 -1
  3. mreg_cli-1.2.0/.markdownlint.json +9 -0
  4. mreg_cli-1.1.0/CHANGELOG → mreg_cli-1.2.0/CHANGELOG.md +21 -5
  5. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/PKG-INFO +1 -9
  6. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/README.md +0 -8
  7. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/ci/testsuite +1 -0
  8. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/ci/testsuite-result.json +122 -1
  9. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/_version.py +2 -2
  10. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/api/endpoints.py +7 -0
  11. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/api/models.py +214 -30
  12. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/cli.py +1 -0
  13. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/help.py +51 -2
  14. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/host_submodules/core.py +16 -9
  15. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/errorbuilder.py +17 -15
  16. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/exceptions.py +2 -2
  17. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/prompt.py +3 -5
  18. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/tokenfile.py +7 -0
  19. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/utilities/api.py +5 -3
  20. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli.egg-info/PKG-INFO +1 -9
  21. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli.egg-info/SOURCES.txt +2 -1
  22. mreg_cli-1.2.0/tests/test_errorbuilder.py +64 -0
  23. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/test_prompt.py +18 -0
  24. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/test_tokenfile.py +34 -0
  25. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/utilities/test_api.py +28 -4
  26. mreg_cli-1.1.0/tests/test_errorbuilder.py +0 -56
  27. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/.gitignore +0 -0
  28. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/AUTHORS +0 -0
  29. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/CONTRIBUTING +0 -0
  30. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/LICENSE +0 -0
  31. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/ci/diff.py +0 -0
  32. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/ci/docker-compose.yml +0 -0
  33. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/ci/run_testsuite_and_record.sh +0 -0
  34. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/data/mreg-cli.conf +0 -0
  35. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/__about__.py +0 -0
  36. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/__init__.py +0 -0
  37. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/__main__.py +0 -0
  38. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/api/__init__.py +0 -0
  39. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/api/abstracts.py +0 -0
  40. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/api/fields.py +0 -0
  41. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/api/history.py +0 -0
  42. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/__init__.py +0 -0
  43. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/base.py +0 -0
  44. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/dhcp.py +0 -0
  45. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/group.py +0 -0
  46. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/host.py +0 -0
  47. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/host_submodules/__init__.py +0 -0
  48. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/host_submodules/a_aaaa.py +0 -0
  49. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/host_submodules/bacnet.py +0 -0
  50. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/host_submodules/cname.py +0 -0
  51. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/host_submodules/rr.py +0 -0
  52. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/label.py +0 -0
  53. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/logging.py +0 -0
  54. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/network.py +0 -0
  55. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/permission.py +0 -0
  56. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/policy.py +0 -0
  57. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/recording.py +0 -0
  58. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/registry.py +0 -0
  59. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/root.py +0 -0
  60. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/commands/zone.py +0 -0
  61. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/config.py +0 -0
  62. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/help_formatter.py +0 -0
  63. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/main.py +0 -0
  64. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/outputmanager.py +0 -0
  65. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/py.typed +0 -0
  66. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/tags.txt +0 -0
  67. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/types.py +0 -0
  68. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/utilities/__init__.py +0 -0
  69. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/utilities/shared.py +0 -0
  70. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli/utilities/validators.py +0 -0
  71. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli.egg-info/dependency_links.txt +0 -0
  72. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli.egg-info/entry_points.txt +0 -0
  73. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli.egg-info/requires.txt +0 -0
  74. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/mreg_cli.egg-info/top_level.txt +0 -0
  75. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/pyproject.toml +0 -0
  76. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/run.py +0 -0
  77. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/setup.cfg +0 -0
  78. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/__init__.py +0 -0
  79. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/api/__init__.py +0 -0
  80. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/api/test_fields.py +0 -0
  81. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/api/test_models.py +0 -0
  82. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/conftest.py +0 -0
  83. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tests/utilities/__init__.py +0 -0
  84. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/tox.ini +0 -0
  85. {mreg_cli-1.1.0 → mreg_cli-1.2.0}/uv.lock +0 -0
@@ -130,8 +130,8 @@ jobs:
130
130
  publish_github:
131
131
  name: Publish GitHub release
132
132
  needs:
133
- - build_pypi
134
133
  - build_pyinstaller
134
+ - publish_pypi
135
135
  runs-on: ubuntu-latest
136
136
 
137
137
  steps:
@@ -48,7 +48,17 @@ jobs:
48
48
  run: uv sync --group ci
49
49
  - name: Test building with tox
50
50
  run: uv run tox r
51
-
51
+
52
+ lint:
53
+ name: Lint
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: DavidAnson/markdownlint-cli2-action@v18
58
+ with:
59
+ config: '.markdownlint.json'
60
+ globs: '**/*.md'
61
+
52
62
  unit:
53
63
  name: Unit tests
54
64
  runs-on: ubuntu-latest
@@ -0,0 +1,9 @@
1
+ {
2
+ "default": true,
3
+ "no-duplicate-heading": {
4
+ "siblings_only": true
5
+ },
6
+ "line-length": false,
7
+ "no-bare-urls": false,
8
+ "no-inline-html": false
9
+ }
@@ -7,7 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- ## [Unreleased] -->
9
9
 
10
- ## [1.1.0] - 2024-11-19
10
+ ## [1.2.0](https://github.com/unioslo/mreg-cli/releases/tag/1.2.0) - 2024-11-25
11
+
12
+ ### Added
13
+
14
+ - Support for `help versions` which shows versions of the CLI and the server for servers that have implemented the `/meta/version` (and `/meta/libraries` endpoints for superusers).
15
+ - Support for `help whoami` which shows information about the current user as the server sees them. This includes the username, attributes, groups, and permissions. This command is only available if the server has implemented the `/meta/users` endpoint.
16
+ - Support for `help whois <username>` which allows elevated users to see information about the given user. Also requires the `/meta/users` endpoint to be implemented on the server.
17
+
18
+ ### Fixed
19
+
20
+ - REPL command completion not showing descriptions for commands.
21
+ - `host info` with MAC address argument not working for hosts with multiple IPs associated with the same MAC address.
22
+ - Failed token file write causing the application to crash.
23
+ - Connection strings without domains and with ports are now parsed properly for prompt interpolation.
24
+
25
+ ## [1.1.0](https://github.com/unioslo/mreg-cli/releases/tag/1.1.0) - 2024-11-19
11
26
 
12
27
  ### Added
13
28
 
@@ -21,18 +36,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
36
 
22
37
  ### Fixed
23
38
 
24
- - `host add`: Re-introduced support for network/ip ending with `/`.
39
+ - MAC adress validation. Now supports all common formats. See the [Pydantic docs](https://docs.pydantic.dev/latest/api/pydantic_extra_types_mac_address/) for more information.
40
+ - `host add`: Re-introduced support for network/IP ending with `/`.
25
41
  - Automatically deduces the correct network to assign an IP from.
26
- - `host add`: No longer possible to assign the network ID (first address of subnet) to a host.
42
+ - `host add`: No longer possible to assign network address (first address of subnet) or broadcast address (last address of subnet) to a host.
27
43
 
28
- ## [1.0.1] - 2024-10-21
44
+ ## [1.0.1](https://github.com/unioslo/mreg-cli/releases/tag/1.0.1) - 2024-10-21
29
45
 
30
46
  ### Fixed
31
47
 
32
48
  - Rendering of top level command autocompletion in REPL.
33
49
  - `OK:` messages not being displayed outside of `--record` mode
34
50
 
35
- ## [1.0.0] - 2024-10-12
51
+ ## [1.0.0](https://github.com/unioslo/mreg-cli/releases/tag/1.0.0) - 2024-10-12
36
52
 
37
53
  The big Pydantic update. The entire codebase has been rewritten to use Pydantic for request and response validation. This brings with it a huge improvement to the development experience and the robustness of the code.
38
54
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mreg-cli
3
- Version: 1.1.0
3
+ Version: 1.2.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>
@@ -726,15 +726,7 @@ Requires-Dist: platformdirs>=4.3.0
726
726
  - [DHCP](#dhcp)
727
727
  - [subnet](#subnet)
728
728
  - [zone](#zone)
729
- - [history](#history)
730
729
  - [other](#other)
731
- - [Devhelp](#devhelp)
732
- - [client.py](#clientpy)
733
- - [commands.py](#commandspy)
734
- - [util.py](#utilpy)
735
- - [history.py](#historypy)
736
- - [config.py](#configpy)
737
- - [log.py](#logpy)
738
730
 
739
731
  ## Setup
740
732
 
@@ -22,15 +22,7 @@
22
22
  - [DHCP](#dhcp)
23
23
  - [subnet](#subnet)
24
24
  - [zone](#zone)
25
- - [history](#history)
26
25
  - [other](#other)
27
- - [Devhelp](#devhelp)
28
- - [client.py](#clientpy)
29
- - [commands.py](#commandspy)
30
- - [util.py](#utilpy)
31
- - [history.py](#historypy)
32
- - [config.py](#configpy)
33
- - [log.py](#logpy)
34
26
 
35
27
  ## Setup
36
28
 
@@ -203,6 +203,7 @@ dhcp assoc foo aa:bb:cc:dd:ee:ff # should fail, the host has two IPs of differen
203
203
  host aaaa_remove foo 2001:db9::5
204
204
  host aaaa_add foo 2001:db8::5 -f
205
205
  dhcp assoc foo aa:bb:cc:dd:ee:ff # should work, the host now has two IPs of different types on the same VLAN.
206
+ host info foo # should work and show both IPs
206
207
  host remove foo -f
207
208
  network remove 10.0.0.0/24 -f
208
209
  network remove 2001:db8::/64 -f
@@ -20022,6 +20022,127 @@
20022
20022
  ],
20023
20023
  "time": null
20024
20024
  },
20025
+ {
20026
+ "command": "host info foo",
20027
+ "command_filter": null,
20028
+ "command_filter_negate": false,
20029
+ "command_issued": "host info foo # should work and show both IPs",
20030
+ "ok": [],
20031
+ "warning": [],
20032
+ "error": [],
20033
+ "output": [
20034
+ "Name: foo.example.org",
20035
+ "Contact: ",
20036
+ "A_Records IP MAC",
20037
+ " 10.0.0.5 aa:bb:cc:dd:ee:ff",
20038
+ "AAAA_Records IP MAC",
20039
+ " 2001:db8::5 <not set>",
20040
+ "TTL: (Default)",
20041
+ "TXT: v=spf1 -all",
20042
+ "Created: Thu Nov 21 10:34:55 2024",
20043
+ "Updated: Thu Nov 21 10:34:55 2024"
20044
+ ],
20045
+ "api_requests": [
20046
+ {
20047
+ "method": "GET",
20048
+ "url": "/api/v1/hosts/foo.example.org",
20049
+ "data": {},
20050
+ "status": 200,
20051
+ "response": {
20052
+ "ipaddresses": [
20053
+ {
20054
+ "macaddress": "aa:bb:cc:dd:ee:ff",
20055
+ "ipaddress": "10.0.0.5",
20056
+ "host": 13
20057
+ },
20058
+ {
20059
+ "macaddress": "",
20060
+ "ipaddress": "2001:db8::5",
20061
+ "host": 13
20062
+ }
20063
+ ],
20064
+ "cnames": [],
20065
+ "mxs": [],
20066
+ "txts": [
20067
+ {
20068
+ "txt": "v=spf1 -all",
20069
+ "host": 13
20070
+ }
20071
+ ],
20072
+ "ptr_overrides": [],
20073
+ "hinfo": null,
20074
+ "loc": null,
20075
+ "bacnetid": null,
20076
+ "name": "foo.example.org",
20077
+ "contact": "",
20078
+ "ttl": null,
20079
+ "comment": "",
20080
+ "zone": 1
20081
+ }
20082
+ },
20083
+ {
20084
+ "method": "GET",
20085
+ "url": "/api/v1/srvs/?host=13",
20086
+ "data": {},
20087
+ "status": 200,
20088
+ "response": {
20089
+ "count": 0,
20090
+ "next": null,
20091
+ "previous": null,
20092
+ "results": []
20093
+ }
20094
+ },
20095
+ {
20096
+ "method": "GET",
20097
+ "url": "/api/v1/naptrs/?host=13",
20098
+ "data": {},
20099
+ "status": 200,
20100
+ "response": {
20101
+ "count": 0,
20102
+ "next": null,
20103
+ "previous": null,
20104
+ "results": []
20105
+ }
20106
+ },
20107
+ {
20108
+ "method": "GET",
20109
+ "url": "/api/v1/sshfps/?host=13",
20110
+ "data": {},
20111
+ "status": 200,
20112
+ "response": {
20113
+ "count": 0,
20114
+ "next": null,
20115
+ "previous": null,
20116
+ "results": []
20117
+ }
20118
+ },
20119
+ {
20120
+ "method": "GET",
20121
+ "url": "/api/v1/hostpolicy/roles/?hosts=13",
20122
+ "data": {},
20123
+ "status": 200,
20124
+ "response": {
20125
+ "count": 0,
20126
+ "next": null,
20127
+ "previous": null,
20128
+ "results": []
20129
+ }
20130
+ },
20131
+ {
20132
+ "method": "GET",
20133
+ "url": "/api/v1/hostgroups/?hosts=13",
20134
+ "data": {},
20135
+ "status": 200,
20136
+ "response": {
20137
+ "count": 0,
20138
+ "next": null,
20139
+ "previous": null,
20140
+ "results": []
20141
+ }
20142
+ }
20143
+ ],
20144
+ "time": null
20145
+ },
20025
20146
  {
20026
20147
  "command": "host remove foo -f",
20027
20148
  "command_filter": null,
@@ -24928,7 +25049,7 @@
24928
25049
  "api_requests": [
24929
25050
  {
24930
25051
  "method": "GET",
24931
- "url": "/api/v1/hosts/?ipaddresses__ipaddress=10.0.0.20&ordering=name",
25052
+ "url": "/api/v1/hosts/?ipaddresses__ipaddress=10.0.0.20",
24932
25053
  "data": {},
24933
25054
  "status": 200,
24934
25055
  "response": {
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.1.0'
16
- __version_tuple__ = version_tuple = (1, 1, 0)
15
+ __version__ = version = '1.2.0'
16
+ __version_tuple__ = version_tuple = (1, 2, 0)
@@ -77,6 +77,13 @@ class Endpoint(str, Enum):
77
77
 
78
78
  ForwardZoneForHost = f"{ForwardZones}hostname/"
79
79
 
80
+ TokenIsValid = "/api/token-is-valid/"
81
+
82
+ MetaUser = "/api/meta/user"
83
+ MetaVersion = "/api/meta/version"
84
+ MetaLibraries = "/api/meta/libraries"
85
+ MetaHeartbeat = "/api/meta/heartbeat"
86
+
80
87
  def __str__(self):
81
88
  """Prevent direct usage without parameters where needed."""
82
89
  if "{}" in self.value:
@@ -7,16 +7,9 @@ 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, Literal, Self, cast, overload
11
-
12
- from pydantic import (
13
- AliasChoices,
14
- BaseModel,
15
- ConfigDict,
16
- Field,
17
- computed_field,
18
- field_validator,
19
- )
10
+ from typing import Any, Callable, ClassVar, List, Literal, Self, cast, overload
11
+
12
+ from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field, field_validator
20
13
  from typing_extensions import Unpack
21
14
 
22
15
  from mreg_cli.api.abstracts import APIMixin, FrozenModel, FrozenModelWithTimestamps
@@ -38,7 +31,7 @@ from mreg_cli.exceptions import (
38
31
  InvalidIPv6Address,
39
32
  InvalidNetwork,
40
33
  IPNetworkWarning,
41
- MultipleEntititesFound,
34
+ MultipleEntitiesFound,
42
35
  PatchError,
43
36
  UnexpectedDataError,
44
37
  ValidationError,
@@ -118,6 +111,7 @@ class NetworkOrIP(BaseModel):
118
111
  :param value:The value to parse.
119
112
  :param mode: The mode to validate the input as.
120
113
  :returns: The parsed value as an IP address or network.
114
+ :raises IPNetworkWarning: If the value is not an IP address or network.
121
115
  """
122
116
  ipnet = cls.validate(value)
123
117
  funcmap: dict[IPNetMode, Callable[..., IP_AddressT | IP_NetworkT]] = {
@@ -2185,7 +2179,7 @@ class CNAME(FrozenModelWithTimestamps, WithHost, WithZone, WithTTL, APIMixin):
2185
2179
  raise EntityNotFound(f"CNAME record for {name} not found for {target_hostname}.")
2186
2180
 
2187
2181
  if len(results) > 1:
2188
- raise MultipleEntititesFound(f"Multiple CNAME records found for {host} with {name}!")
2182
+ raise MultipleEntitiesFound(f"Multiple CNAME records found for {host} with {name}!")
2189
2183
 
2190
2184
  return results[0]
2191
2185
 
@@ -2684,28 +2678,22 @@ class Host(FrozenModelWithTimestamps, WithTTL, WithHistory, APIMixin):
2684
2678
  try:
2685
2679
  ptr = False
2686
2680
  ipaddress.ip_address(identifier)
2681
+ NetworkOrIP.parse(identifier, mode="ip")
2687
2682
 
2688
- hosts = Host.get_list_by_field(
2689
- "ipaddresses__ipaddress", identifier, ordering="name"
2690
- )
2691
-
2692
- if not hosts:
2693
- hosts = Host.get_list_by_field("ptr_overrides__ipaddress", identifier)
2683
+ host = Host.get_by_field("ipaddresses__ipaddress", identifier)
2684
+ if not host:
2685
+ host = Host.get_by_field("ptr_overrides__ipaddress", identifier)
2694
2686
  ptr = True
2695
2687
 
2696
- if len(hosts) == 1:
2688
+ if host:
2697
2689
  if ptr and inform_as_ptr:
2698
- OutputManager().add_line(
2699
- f"{identifier} is a PTR override for {hosts[0].name}"
2700
- )
2701
- return hosts[0]
2702
-
2703
- if len(hosts) > 1:
2704
- raise MultipleEntititesFound(
2705
- f"Multiple hosts found with IP address or PTR {identifier}."
2706
- )
2707
-
2708
- except ValueError:
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
2709
2697
  pass
2710
2698
 
2711
2699
  try:
@@ -3501,3 +3489,199 @@ class HostGroup(FrozenModelWithTimestamps, WithName, WithHistory, APIMixin):
3501
3489
 
3502
3490
  for host in self.hosts:
3503
3491
  manager.add_formatted_line_with_source("host", host, self.name)
3492
+
3493
+
3494
+ ### Meta models
3495
+
3496
+
3497
+ class UserDjangoStatus(BaseModel):
3498
+ """Model for Django status in the user response."""
3499
+
3500
+ superuser: bool
3501
+ staff: bool
3502
+ active: bool
3503
+
3504
+
3505
+ class UserMregStatus(BaseModel):
3506
+ """Model for Mreg status in the user response."""
3507
+
3508
+ superuser: bool
3509
+ admin: bool
3510
+ group_admin: bool
3511
+ network_admin: bool
3512
+ hostpolicy_admin: bool
3513
+ dns_wildcard_admin: bool
3514
+ underscore_admin: bool
3515
+
3516
+
3517
+ class UserPermission(BaseModel):
3518
+ """Model for permissions in the user response."""
3519
+
3520
+ group: str
3521
+ range: str
3522
+ regex: str
3523
+ labels: List[str]
3524
+
3525
+
3526
+ class ServerVersion(BaseModel):
3527
+ """Model for server version metadata."""
3528
+
3529
+ version: str
3530
+
3531
+ @classmethod
3532
+ def endpoint(cls) -> str:
3533
+ """Return the endpoint for the class."""
3534
+ return Endpoint.MetaVersion
3535
+
3536
+ @classmethod
3537
+ def fetch(cls, ignore_errors: bool = True) -> "ServerVersion":
3538
+ """Fetch the server version from the endpoint.
3539
+
3540
+ :param ignore_errors: Whether to ignore errors.
3541
+ :raises ValidationError: If the response data is invalid and ignore_errors is False.
3542
+ :raises requests.RequestException: If the HTTP request fails and ignore_errors is False.
3543
+ :returns: An instance of ServerVersion with the fetched data.
3544
+ """
3545
+ try:
3546
+ response = get(cls.endpoint())
3547
+ return cls.model_validate(response.json())
3548
+ except Exception as e:
3549
+ if ignore_errors:
3550
+ return cls(version="Unknown")
3551
+ raise e
3552
+
3553
+ def output(self) -> None:
3554
+ """Output the server version to the console."""
3555
+ manager = OutputManager()
3556
+ manager.add_line(f"mreg-server: {self.version}")
3557
+
3558
+
3559
+ class Library(BaseModel):
3560
+ """Model for library metadata."""
3561
+
3562
+ name: str
3563
+ version: str
3564
+
3565
+
3566
+ class ServerLibraries(BaseModel):
3567
+ """Model for server libraries metadata."""
3568
+
3569
+ libraries: List[Library]
3570
+
3571
+ @classmethod
3572
+ def endpoint(cls) -> str:
3573
+ """Return the endpoint for the class."""
3574
+ return Endpoint.MetaLibraries
3575
+
3576
+ @classmethod
3577
+ def fetch(cls, ignore_errors: bool = True) -> "ServerLibraries":
3578
+ """Fetch the server libraries from the endpoint.
3579
+
3580
+ :param ignore_errors: Whether to ignore errors.
3581
+ :raises ValidationError: If the response data is invalid and ignore_errors is False.
3582
+ :raises requests.RequestException: If the HTTP request fails and ignore_errors is False.
3583
+ :returns: An instance of ServerLibraries with the fetched data.
3584
+ """
3585
+ try:
3586
+ response = get(cls.endpoint())
3587
+ libraries: List[Library] = []
3588
+
3589
+ for name, version in response.json().items():
3590
+ libraries.append(Library(name=name, version=version))
3591
+ return cls(libraries=libraries)
3592
+ except Exception as e:
3593
+ if ignore_errors:
3594
+ return cls(libraries=[])
3595
+ raise e
3596
+
3597
+ def output(self, indent: int = 4) -> None:
3598
+ """Output the server libraries to the console."""
3599
+ manager = OutputManager()
3600
+ if not self.libraries:
3601
+ return
3602
+
3603
+ manager.add_line("Libraries:")
3604
+ for lib in self.libraries:
3605
+ manager.add_line(f"{' ' * indent}{lib.name}: {lib.version}")
3606
+
3607
+
3608
+ class UserInfo(BaseModel):
3609
+ """Model for the user information."""
3610
+
3611
+ username: str
3612
+ django_status: UserDjangoStatus
3613
+ mreg_status: UserMregStatus
3614
+ groups: List[str]
3615
+ permissions: List[UserPermission]
3616
+
3617
+ @classmethod
3618
+ def endpoint(cls) -> str:
3619
+ """Return the endpoint for the class."""
3620
+ return Endpoint.MetaUser
3621
+
3622
+ @classmethod
3623
+ def fetch(cls, ignore_errors: bool = True, user: str | None = None) -> "UserInfo":
3624
+ """Fetch the user information from the endpoint.
3625
+
3626
+ :param ignore_errors: Whether to ignore errors.
3627
+ :param user: The username to fetch information for. If None, fetch information for the
3628
+ current user.
3629
+
3630
+ :raises ValidationError: If the response data is invalid and ignore_errors is False.
3631
+ :raises requests.RequestException: If the HTTP request fails and ignore_errors is False.
3632
+ :returns: An instance of UserInfo with the fetched data.
3633
+ """
3634
+ try:
3635
+ endpoint = cls.endpoint()
3636
+ if user:
3637
+ endpoint = f"{endpoint}?username={user}"
3638
+
3639
+ response = get(endpoint)
3640
+ return cls.model_validate(response.json())
3641
+ except Exception as e:
3642
+ if ignore_errors:
3643
+ return cls(
3644
+ username="Unknown",
3645
+ django_status=UserDjangoStatus(superuser=False, staff=False, active=False),
3646
+ mreg_status=UserMregStatus(
3647
+ superuser=False,
3648
+ admin=False,
3649
+ group_admin=False,
3650
+ network_admin=False,
3651
+ hostpolicy_admin=False,
3652
+ dns_wildcard_admin=False,
3653
+ underscore_admin=False,
3654
+ ),
3655
+ groups=[],
3656
+ permissions=[],
3657
+ )
3658
+ raise e
3659
+
3660
+ def output(self) -> None:
3661
+ """Output the user information to the console."""
3662
+ outputmanager = OutputManager()
3663
+ outputmanager.add_line(f"Username: {self.username}")
3664
+ outputmanager.add_line("Django status:")
3665
+ outputmanager.add_line(f" Superuser: {self.django_status.superuser}")
3666
+ outputmanager.add_line(f" Staff: {self.django_status.staff}")
3667
+ outputmanager.add_line(f" Active: {self.django_status.active}")
3668
+ outputmanager.add_line("Mreg status:")
3669
+ outputmanager.add_line(f" Superuser: {self.mreg_status.superuser}")
3670
+ outputmanager.add_line(f" Admin: {self.mreg_status.admin}")
3671
+ outputmanager.add_line(f" Group admin: {self.mreg_status.group_admin}")
3672
+ outputmanager.add_line(f" Network admin: {self.mreg_status.network_admin}")
3673
+ outputmanager.add_line(f" Hostpolicy admin: {self.mreg_status.hostpolicy_admin}")
3674
+ outputmanager.add_line(f" DNS wildcard admin: {self.mreg_status.dns_wildcard_admin}")
3675
+ outputmanager.add_line(f" Underscore admin: {self.mreg_status.underscore_admin}")
3676
+ outputmanager.add_line("Groups:")
3677
+ for group in self.groups:
3678
+ outputmanager.add_line(f" {group}")
3679
+ outputmanager.add_line("Permissions:")
3680
+ if not self.permissions:
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)}")
@@ -222,6 +222,7 @@ class Command(Completer):
222
222
  yield Completion(
223
223
  name,
224
224
  start_position=-len(cur),
225
+ display_meta=self.children[name].short_desc,
225
226
  )
226
227
 
227
228
  # if the line starts with one of the sub commands, pass it along
@@ -5,9 +5,13 @@ from __future__ import annotations
5
5
  import argparse
6
6
  from typing import Any
7
7
 
8
+ from mreg_cli.__about__ import __version__ as mreg_cli_version
9
+ from mreg_cli.api.models import ServerLibraries, ServerVersion, UserInfo
8
10
  from mreg_cli.commands.base import BaseCommand
9
11
  from mreg_cli.commands.registry import CommandRegistry
10
12
  from mreg_cli.config import MregCliConfig
13
+ from mreg_cli.outputmanager import OutputManager
14
+ from mreg_cli.types import Flag
11
15
 
12
16
  command_registry = CommandRegistry()
13
17
 
@@ -27,7 +31,9 @@ class HelpCommands(BaseCommand):
27
31
  )
28
32
 
29
33
 
30
- @command_registry.register_command("filtering", "Show help for filtering.", "Filtering Help")
34
+ @command_registry.register_command(
35
+ prog="filtering", description="Show help for filtering.", short_desc="Filtering Help"
36
+ )
31
37
  def filtering_help(_: argparse.Namespace) -> None:
32
38
  """Show help for filtering."""
33
39
  print(
@@ -71,7 +77,50 @@ TXT: v=spf1 -all
71
77
  )
72
78
 
73
79
 
74
- @command_registry.register_command("configuration", "Show configuration", "Show configuration")
80
+ @command_registry.register_command(
81
+ prog="configuration", description="Show configuration", short_desc="Show configuration"
82
+ )
75
83
  def configuration_help(_: argparse.Namespace) -> None:
76
84
  """Show configuration."""
77
85
  MregCliConfig().print_config_table()
86
+
87
+
88
+ @command_registry.register_command(
89
+ prog="versions",
90
+ description="Show versions of client and server as much as possible",
91
+ short_desc="Show versions",
92
+ )
93
+ def versions_help(_: argparse.Namespace) -> None:
94
+ """Show versions of client and server as much as possible."""
95
+ output_manager = OutputManager()
96
+ output_manager.add_line(f"mreg-cli: {mreg_cli_version}")
97
+
98
+ ServerVersion.fetch().output()
99
+ ServerLibraries.fetch().output()
100
+
101
+
102
+ @command_registry.register_command(
103
+ prog="whoami",
104
+ description="Show information about the the current user",
105
+ short_desc="Show self info",
106
+ )
107
+ def whoami_help(_: argparse.Namespace) -> None:
108
+ """Show information about the current user."""
109
+ try:
110
+ UserInfo.fetch(ignore_errors=False).output()
111
+ except Exception as e:
112
+ print("Failed to fetch user info:", e)
113
+
114
+
115
+ @command_registry.register_command(
116
+ prog="whois",
117
+ description="Show information about a user",
118
+ short_desc="Show user info",
119
+ flags=[Flag("user", description="The user to show information about")],
120
+ )
121
+ def whois_help(args: argparse.Namespace) -> None:
122
+ """Show information about a user."""
123
+ try:
124
+ UserInfo.fetch(ignore_errors=False, user=args.user).output()
125
+ except Exception as e:
126
+ print("Failed to fetch user info:", e)