mreg-cli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. mreg_cli/__about__.py +7 -0
  2. mreg_cli/__init__.py +5 -0
  3. mreg_cli/__main__.py +8 -0
  4. mreg_cli/_version.py +16 -0
  5. mreg_cli/api/__init__.py +7 -0
  6. mreg_cli/api/abstracts.py +486 -0
  7. mreg_cli/api/endpoints.py +130 -0
  8. mreg_cli/api/fields.py +104 -0
  9. mreg_cli/api/history.py +152 -0
  10. mreg_cli/api/models.py +3416 -0
  11. mreg_cli/cli.py +382 -0
  12. mreg_cli/commands/__init__.py +1 -0
  13. mreg_cli/commands/base.py +62 -0
  14. mreg_cli/commands/dhcp.py +136 -0
  15. mreg_cli/commands/group.py +326 -0
  16. mreg_cli/commands/help.py +77 -0
  17. mreg_cli/commands/host.py +54 -0
  18. mreg_cli/commands/host_submodules/__init__.py +28 -0
  19. mreg_cli/commands/host_submodules/a_aaaa.py +452 -0
  20. mreg_cli/commands/host_submodules/bacnet.py +126 -0
  21. mreg_cli/commands/host_submodules/cname.py +166 -0
  22. mreg_cli/commands/host_submodules/core.py +507 -0
  23. mreg_cli/commands/host_submodules/rr.py +973 -0
  24. mreg_cli/commands/label.py +146 -0
  25. mreg_cli/commands/logging.py +112 -0
  26. mreg_cli/commands/network.py +516 -0
  27. mreg_cli/commands/permission.py +202 -0
  28. mreg_cli/commands/policy.py +519 -0
  29. mreg_cli/commands/recording.py +59 -0
  30. mreg_cli/commands/registry.py +56 -0
  31. mreg_cli/commands/root.py +58 -0
  32. mreg_cli/commands/zone.py +288 -0
  33. mreg_cli/config.py +253 -0
  34. mreg_cli/errorbuilder.py +193 -0
  35. mreg_cli/exceptions.py +237 -0
  36. mreg_cli/help_formatter.py +38 -0
  37. mreg_cli/main.py +238 -0
  38. mreg_cli/outputmanager.py +466 -0
  39. mreg_cli/py.typed +0 -0
  40. mreg_cli/tags.txt +55 -0
  41. mreg_cli/tokenfile.py +89 -0
  42. mreg_cli/types.py +160 -0
  43. mreg_cli/utilities/__init__.py +5 -0
  44. mreg_cli/utilities/api.py +595 -0
  45. mreg_cli/utilities/shared.py +65 -0
  46. mreg_cli/utilities/validators.py +19 -0
  47. mreg_cli-1.0.0.dist-info/AUTHORS +12 -0
  48. mreg_cli-1.0.0.dist-info/LICENSE +674 -0
  49. mreg_cli-1.0.0.dist-info/METADATA +1079 -0
  50. mreg_cli-1.0.0.dist-info/RECORD +53 -0
  51. mreg_cli-1.0.0.dist-info/WHEEL +5 -0
  52. mreg_cli-1.0.0.dist-info/entry_points.txt +2 -0
  53. mreg_cli-1.0.0.dist-info/top_level.txt +1 -0
mreg_cli/api/fields.py ADDED
@@ -0,0 +1,104 @@
1
+ """Fields for models of the API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import re
7
+ from typing import Annotated, Any
8
+
9
+ from pydantic import BeforeValidator, field_validator
10
+
11
+ from mreg_cli.api.abstracts import FrozenModel
12
+ from mreg_cli.exceptions import InputFailure
13
+ from mreg_cli.types import IP_AddressT
14
+
15
+ _mac_regex = re.compile(r"^([0-9A-Fa-f]{2}[.:-]){5}([0-9A-Fa-f]{2})$")
16
+
17
+
18
+ class MACAddressField(FrozenModel):
19
+ """Represents a MAC address."""
20
+
21
+ address: str
22
+
23
+ @field_validator("address", mode="after")
24
+ @classmethod
25
+ def validate_and_format_mac(cls, v: str) -> str:
26
+ """Validate and normalize MAC address to 'aa:bb:cc:dd:ee:ff' format.
27
+
28
+ :param v: The input MAC address string.
29
+ :raises ValueError: If the input does not match the expected MAC address pattern.
30
+ :returns: The normalized MAC address.
31
+ """
32
+ # Validate input format
33
+ if not _mac_regex.match(v):
34
+ raise ValueError("Invalid MAC address format")
35
+
36
+ # Normalize MAC address
37
+ v = re.sub(r"[.:-]", "", v).lower()
38
+ return ":".join(v[i : i + 2] for i in range(0, 12, 2))
39
+
40
+ def __str__(self) -> str:
41
+ """Return the MAC address as a string."""
42
+ return self.address
43
+
44
+
45
+ class IPAddressField(FrozenModel):
46
+ """Represents an IP address, automatically determines if it's IPv4 or IPv6."""
47
+
48
+ address: IP_AddressT
49
+
50
+ @classmethod
51
+ def from_string(cls, address: str) -> IPAddressField:
52
+ """Create an IPAddressField from a string.
53
+
54
+ Shortcut for creating an IPAddressField from a string,
55
+ without having to convince the type checker that we can
56
+ pass in a string to the address field each time.
57
+ """
58
+ return cls(address=address) # pyright: ignore[reportArgumentType] # validator handles this
59
+
60
+ @field_validator("address", mode="before")
61
+ @classmethod
62
+ def parse_ip_address(cls, value: Any) -> IP_AddressT:
63
+ """Parse and validate the IP address."""
64
+ try:
65
+ return ipaddress.ip_address(value)
66
+ except ValueError as e:
67
+ raise InputFailure(f"Invalid IP address '{value}'.") from e
68
+
69
+ def is_ipv4(self) -> bool:
70
+ """Check if the IP address is IPv4."""
71
+ return isinstance(self.address, ipaddress.IPv4Address)
72
+
73
+ def is_ipv6(self) -> bool:
74
+ """Check if the IP address is IPv6."""
75
+ return isinstance(self.address, ipaddress.IPv6Address)
76
+
77
+ @staticmethod
78
+ def is_valid(value: str) -> bool:
79
+ """Check if the value is a valid IP address."""
80
+ try:
81
+ ipaddress.ip_address(value)
82
+ return True
83
+ except ValueError:
84
+ return False
85
+
86
+ def __str__(self) -> str:
87
+ """Return the IP address as a string."""
88
+ return str(self.address)
89
+
90
+ def __hash__(self):
91
+ """Return a hash of the IP address."""
92
+ return hash(self.address)
93
+
94
+
95
+ def _extract_name(value: dict[str, Any]) -> str:
96
+ """Extract the name from the dictionary.
97
+
98
+ :param v: Dictionary containing the name.
99
+ :returns: Extracted name as a string.
100
+ """
101
+ return value["name"]
102
+
103
+
104
+ NameList = list[Annotated[str, BeforeValidator(_extract_name)]]
@@ -0,0 +1,152 @@
1
+ """History abstractions for mreg-cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ import json
7
+ from enum import Enum
8
+ from typing import Any, Self
9
+
10
+ from pydantic import BaseModel, Field, field_validator
11
+
12
+ from mreg_cli.api.endpoints import Endpoint
13
+ from mreg_cli.exceptions import EntityNotFound, InternalError
14
+ from mreg_cli.outputmanager import OutputManager
15
+ from mreg_cli.types import QueryParams
16
+ from mreg_cli.utilities.api import get_typed
17
+
18
+
19
+ class HistoryResource(str, Enum):
20
+ """History resources for the API.
21
+
22
+ Names represent resource names.
23
+ Values represent resource relations.
24
+
25
+ Access resource names and relation with the `resource()` and `relation()` methods.
26
+ """
27
+
28
+ Host = "hosts"
29
+ Group = "groups"
30
+ HostPolicy_Role = "roles"
31
+ HostPolicy_Atom = "atoms"
32
+
33
+ @classmethod
34
+ def _missing_(cls, value: Any) -> HistoryResource:
35
+ v = str(value).lower()
36
+ for resource in cls:
37
+ if resource.value == v:
38
+ return resource
39
+ elif resource.name.lower() == v:
40
+ return resource
41
+ raise ValueError(f"Unknown resource {value}")
42
+
43
+ def relation(self) -> str:
44
+ """Get the resource relation."""
45
+ return self.value
46
+
47
+ def resource(self) -> str:
48
+ """Get the resource name."""
49
+ return self.name.lower()
50
+
51
+
52
+ class HistoryItem(BaseModel):
53
+ """Represents a history item."""
54
+
55
+ id: int # noqa: A003
56
+ timestamp: datetime.datetime
57
+ user: str
58
+ resource: HistoryResource
59
+ name: str
60
+ mid: int = Field(alias="model_id") # model_ is an internal pydantic namespace.
61
+ model: str
62
+ action: str
63
+ data: dict[str, Any]
64
+
65
+ @field_validator("data", mode="before")
66
+ def parse_json_data(cls, v: Any) -> Any:
67
+ """Ensure that non-dict values are treated as JSON."""
68
+ if isinstance(v, dict):
69
+ return v # pyright: ignore[reportUnknownVariableType]
70
+ try:
71
+ return json.loads(v)
72
+ except json.JSONDecodeError as e:
73
+ raise ValueError("Failed to parse history data as JSON") from e
74
+
75
+ def clean_timestamp(self) -> str:
76
+ """Clean up the timestamp for output."""
77
+ return self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
78
+
79
+ def msg(self, basename: str) -> str:
80
+ """Attempt to make a history item human readable."""
81
+ msg = ""
82
+ action = self.action
83
+ model = self.model
84
+ if action in ("add", "remove"):
85
+ if action == "add":
86
+ direction = "to"
87
+ elif action == "remove":
88
+ direction = "from"
89
+ else:
90
+ raise InternalError(f"Unhandled history entry: {action}")
91
+ rel = self.data["relation"][:-1]
92
+ cls = str(self.resource)
93
+ if "." in cls:
94
+ cls = cls[cls.rindex(".") + 1 :]
95
+ cls = cls.replace("HostPolicy_", "")
96
+ cls = cls.lower()
97
+ msg = f"{rel} {self.data['name']} {direction} {cls} {self.name}"
98
+ elif action == "create":
99
+ msg = ", ".join(f"{k} = '{v}'" for k, v in self.data.items())
100
+ elif action == "update":
101
+ if model in ("Ipaddress",):
102
+ msg = self.data["current_data"]["ipaddress"] + ", "
103
+ changes: list[str] = []
104
+ for key, newval in self.data["update"].items():
105
+ oldval = self.data["current_data"][key] or "not set"
106
+ newval = newval or "not set"
107
+ changes.append(f"{key}: {oldval} -> {newval}")
108
+ msg += ",".join(changes)
109
+ elif action == "destroy":
110
+ if model == "Host":
111
+ msg = "deleted " + self.name
112
+ else:
113
+ msg = ", ".join(f"{k} = '{v}'" for k, v in self.data.items())
114
+ else:
115
+ raise InternalError(f"Unhandled history entry: {action}")
116
+
117
+ return msg
118
+
119
+ def output(self, basename: str) -> None:
120
+ """Output the history item."""
121
+ ts = self.clean_timestamp()
122
+ msg = self.msg(basename)
123
+ OutputManager().add_line(f"{ts} [{self.user}]: {self.model} {self.action}: {msg}")
124
+
125
+ @classmethod
126
+ def output_multiple(cls, basename: str, items: list[HistoryItem]) -> None:
127
+ """Output multiple history items."""
128
+ for item in sorted(items, key=lambda i: i.timestamp):
129
+ item.output(basename)
130
+
131
+ @classmethod
132
+ def get(cls, name: str, resource: HistoryResource) -> list[Self]:
133
+ """Get history items for a resource."""
134
+ params: QueryParams = {"resource": resource.resource(), "name": name}
135
+ ret = get_typed(Endpoint.History, list[cls], params=params)
136
+ if len(ret) == 0:
137
+ raise EntityNotFound(f"No history found for {name}")
138
+
139
+ model_ids = ",".join({str(i.mid) for i in ret})
140
+ params = {
141
+ "resource": resource.resource(),
142
+ "model_id__in": model_ids,
143
+ }
144
+ ret = get_typed(Endpoint.History, list[cls], params=params)
145
+
146
+ params = {
147
+ "data__relation": resource.relation(),
148
+ "data__id__in": model_ids,
149
+ }
150
+ ret.extend(get_typed(Endpoint.History, list[cls], params=params))
151
+
152
+ return ret