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
@@ -0,0 +1,166 @@
1
+ """Sub module for the 'host' command handling CNAME records."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from mreg_cli.api.models import CNAME, ForwardZone, Host, HostT
8
+ from mreg_cli.commands.host import registry as command_registry
9
+ from mreg_cli.exceptions import (
10
+ CreateError,
11
+ DeleteError,
12
+ EntityAlreadyExists,
13
+ EntityNotFound,
14
+ EntityOwnershipMismatch,
15
+ InputFailure,
16
+ PatchError,
17
+ )
18
+ from mreg_cli.outputmanager import OutputManager
19
+ from mreg_cli.types import Flag
20
+
21
+
22
+ @command_registry.register_command(
23
+ prog="cname_add",
24
+ description=(
25
+ "Add a CNAME record to host. If NAME is an alias "
26
+ "the cname host is used as target for ALIAS."
27
+ ),
28
+ short_desc="Add CNAME.",
29
+ flags=[
30
+ Flag("name", description="Name of target host.", metavar="NAME"),
31
+ Flag("alias", description="Name of CNAME host.", metavar="ALIAS"),
32
+ Flag("-force", action="store_true", description="Enable force."),
33
+ ],
34
+ )
35
+ def cname_add(args: argparse.Namespace) -> None:
36
+ """Add a CNAME record to host.
37
+
38
+ :param args: argparse.Namespace (name, alias, force)
39
+ """
40
+ # Get host info or raise exception
41
+ host = Host.get_by_any_means_or_raise(args.name)
42
+ alias = HostT(hostname=args.alias)
43
+
44
+ alias_in_use = Host.get_by_any_means(alias, inform_as_cname=False)
45
+ if alias_in_use:
46
+ if alias_in_use.id == host.id:
47
+ raise EntityAlreadyExists(f"The alias {alias} is already active for {host}.")
48
+
49
+ if alias_in_use.name.hostname != alias.hostname:
50
+ raise EntityOwnershipMismatch(
51
+ f"The alias {alias} is already in use as a CNAME for {alias_in_use.name.hostname}."
52
+ )
53
+
54
+ # Catchall for any other case, should not be possible.
55
+ raise EntityOwnershipMismatch(
56
+ "The alias name is in use by an existing host. Find a new alias."
57
+ )
58
+
59
+ zone = ForwardZone.get_from_hostname(alias)
60
+ if not zone:
61
+ raise EntityNotFound(f"Could not find a zone for the alias {alias}.")
62
+
63
+ CNAME.create(params={"host": str(host.id), "name": alias.hostname})
64
+ cname = CNAME.get_by_host_and_name(host.name, alias)
65
+
66
+ if cname:
67
+ OutputManager().add_ok(f"Added CNAME {cname.name} for {host.name.hostname}.")
68
+ else:
69
+ raise CreateError(f"Failed to add CNAME {alias} for {host.name.hostname}.")
70
+
71
+
72
+ @command_registry.register_command(
73
+ prog="cname_remove",
74
+ description="Remove CNAME record.",
75
+ short_desc="Remove CNAME.",
76
+ flags=[
77
+ Flag("name", description="Name of the target host.", metavar="NAME"),
78
+ Flag("alias", description="Name of CNAME to remove.", metavar="CNAME"),
79
+ ],
80
+ )
81
+ def cname_remove(args: argparse.Namespace) -> None:
82
+ """Remove CNAME record.
83
+
84
+ :param args: argparse.Namespace (name, alias)
85
+ """
86
+ host = Host.get_by_any_means_or_raise(args.name)
87
+ alias = HostT(hostname=args.alias)
88
+
89
+ alias_as_host = Host.get_by_field("name", alias.hostname)
90
+ if alias_as_host:
91
+ raise InputFailure(f"The alias {alias} is a host, did you mix up the arguments?")
92
+
93
+ cname = CNAME.get_by_field("name", alias.hostname)
94
+ if not cname:
95
+ raise EntityNotFound(f"No CNAME record found for {alias}.")
96
+
97
+ # Handle situation where the CNAME is not associated with the host we are removing it from.
98
+ if cname.host != host.id:
99
+ cname_host = Host.get_by_id(cname.host)
100
+ if not cname_host:
101
+ raise EntityNotFound(f"Could not find the host for the CNAME {alias}.")
102
+ actual = cname_host.name.hostname
103
+ desired = host.name.hostname
104
+ raise EntityOwnershipMismatch(
105
+ f"The CNAME {cname.name} is associated with {actual}, NOT {desired}."
106
+ )
107
+
108
+ if cname.delete():
109
+ OutputManager().add_line(f"Removed CNAME {cname.name} for {host.name}.")
110
+ else:
111
+ raise DeleteError(f"Failed to remove CNAME {cname.name} for {host.name}.")
112
+
113
+
114
+ @command_registry.register_command(
115
+ prog="cname_replace",
116
+ description="Move a CNAME record from one host to another.",
117
+ short_desc="Replace a CNAME record.",
118
+ flags=[
119
+ Flag("cname", description="The CNAME to modify.", metavar="CNAME"),
120
+ Flag("host", description="The new host for the CNAME.", metavar="HOST"),
121
+ ],
122
+ )
123
+ def cname_replace(args: argparse.Namespace) -> None:
124
+ """Move a CNAME entry from one host to another.
125
+
126
+ :param args: argparse.Namespace (cname, host)
127
+ """
128
+ cname = HostT(hostname=args.cname)
129
+ host = Host.get_by_any_means_or_raise(args.host)
130
+
131
+ cname_obj = CNAME.get_by_name(cname)
132
+
133
+ if not cname_obj:
134
+ raise EntityNotFound(f"No CNAME record found for {cname}.")
135
+
136
+ old_host = Host.get_by_id(cname_obj.host)
137
+ if not old_host:
138
+ raise EntityNotFound(f"Could not find the host for the CNAME {cname}.")
139
+
140
+ updated_cname = cname_obj.patch({"host": host.id})
141
+ if updated_cname:
142
+ OutputManager().add_ok(
143
+ f"Moved CNAME alias {cname}: {old_host.name.hostname} -> {host.name}."
144
+ )
145
+ else:
146
+ raise PatchError(f"Failed to move CNAME alias {cname}.")
147
+
148
+
149
+ @command_registry.register_command(
150
+ prog="cname_show",
151
+ description=(
152
+ "Show CNAME records for host. If NAME is an alias the cname hosts' aliases are shown."
153
+ ),
154
+ short_desc="Show CNAME records.",
155
+ flags=[
156
+ Flag("name", description="Name of the target host.", metavar="NAME"),
157
+ ],
158
+ )
159
+ def cname_show(args: argparse.Namespace) -> None:
160
+ """Show CNAME records for host.
161
+
162
+ If <name> is an alias the cname hosts aliases are shown.
163
+
164
+ :param args: argparse.Namespace (name)
165
+ """
166
+ Host.get_by_any_means_or_raise(args.name).output_cnames()
@@ -0,0 +1,507 @@
1
+ """Core commands for the host sub-module.
2
+
3
+ Commands implemented:
4
+
5
+ - add
6
+ - remove
7
+ - rename
8
+ - info
9
+ - find
10
+ - set_comment
11
+ - set_contact
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+
18
+ from mreg_cli.api.models import (
19
+ ForwardZone,
20
+ Host,
21
+ HostList,
22
+ HostT,
23
+ MACAddressField,
24
+ Network,
25
+ NetworkOrIP,
26
+ )
27
+ from mreg_cli.commands.host import registry as command_registry
28
+ from mreg_cli.exceptions import (
29
+ CreateError,
30
+ DeleteError,
31
+ EntityAlreadyExists,
32
+ EntityNotFound,
33
+ EntityOwnershipMismatch,
34
+ ForceMissing,
35
+ InputFailure,
36
+ PatchError,
37
+ )
38
+ from mreg_cli.outputmanager import OutputManager
39
+ from mreg_cli.types import Flag, JsonMapping, QueryParams
40
+ from mreg_cli.utilities.shared import convert_wildcard_to_regex
41
+
42
+
43
+ @command_registry.register_command(
44
+ prog="add",
45
+ description=(
46
+ "Add a new host with the given name, ip or network and contact. comment is optional."
47
+ ),
48
+ short_desc="Add a new host",
49
+ flags=[
50
+ Flag(
51
+ "name",
52
+ short_desc="Name of new host (req)",
53
+ description="Name of new host (req)",
54
+ ),
55
+ Flag(
56
+ "-ip",
57
+ short_desc="An ip or net",
58
+ description=(
59
+ "The hosts ip or a network. If it's a network the first free IP is "
60
+ "selected from the network"
61
+ ),
62
+ metavar="IP/NET",
63
+ ),
64
+ Flag(
65
+ "-contact",
66
+ short_desc="Contact mail for the host",
67
+ description="Contact mail for the host",
68
+ ),
69
+ Flag("-comment", short_desc="A comment.", description="A comment."),
70
+ Flag("-macaddress", description="Mac address", metavar="MACADDRESS"),
71
+ Flag("-force", action="store_true", description="Enable force."),
72
+ ],
73
+ )
74
+ def add(args: argparse.Namespace) -> None:
75
+ """Add a new host with the given name.
76
+
77
+ Required arguments in argparse:
78
+ - name: Name of new host
79
+ - ip: An ip or network
80
+
81
+ Optional arguments in argparse:
82
+ - contact: Contact mail for the host
83
+ - comment: A comment
84
+ - macaddress: Mac address for assocation to the IP
85
+
86
+ :param args: argparse.Namespace (name, ip, contact, comment, force, macaddress)
87
+
88
+ """
89
+ ip = args.ip
90
+ hname = HostT(hostname=args.name)
91
+ macaddress = args.macaddress
92
+
93
+ if macaddress is not None:
94
+ try:
95
+ macaddress = MACAddressField(address=macaddress).address
96
+ except ValueError as e:
97
+ raise InputFailure(f"invalid MAC address: {macaddress}") from e
98
+
99
+ host = Host.get_by_any_means(hname)
100
+ if host:
101
+ if host.name.hostname != hname.hostname:
102
+ raise EntityOwnershipMismatch(f"{hname} is a CNAME pointing to {host.name}")
103
+ else:
104
+ raise EntityAlreadyExists(f"Host {hname} already exists.")
105
+
106
+ zone = ForwardZone.get_from_hostname(hname)
107
+ if not zone and not args.force:
108
+ raise ForceMissing(f"{hname} isn't in a zone controlled by MREG, must force")
109
+ if zone and zone.is_delegated() and not args.force:
110
+ raise ForceMissing(f"{hname} is in zone delegation {zone.name}, must force")
111
+
112
+ if "*" in hname.hostname and not args.force:
113
+ raise ForceMissing("Wildcards must be forced.")
114
+
115
+ if macaddress is not None:
116
+ try:
117
+ macaddress = MACAddressField(address=macaddress)
118
+ except ValueError as e:
119
+ raise InputFailure(f"invalid MAC address: {macaddress}") from e
120
+
121
+ data: JsonMapping = {
122
+ "name": hname.hostname,
123
+ "contact": args.contact or None,
124
+ "comment": args.comment or None,
125
+ }
126
+
127
+ if args.ip:
128
+ network = None
129
+ net_or_ip = NetworkOrIP(ip_or_network=args.ip)
130
+ if net_or_ip.is_ip():
131
+ data["ipaddress"] = str(net_or_ip)
132
+ network = Network.get_by_ip(net_or_ip.as_ip())
133
+ elif net_or_ip.is_network():
134
+ data["network"] = str(net_or_ip)
135
+ network = Network.get_by_network(str(net_or_ip))
136
+ else:
137
+ raise EntityNotFound(f"Invalid ip or network: {args.ip}")
138
+ if network and network.frozen and not args.force:
139
+ raise ForceMissing(f"Network {network.network} is frozen, must force")
140
+
141
+ host = Host.create(data)
142
+ if not host:
143
+ raise CreateError("Failed to add host.")
144
+ OutputManager().add_ok(f"Created host {host.name}")
145
+
146
+ if macaddress is not None:
147
+ if ip:
148
+ host = host.associate_mac_to_ip(macaddress, str(ip))
149
+ else:
150
+ # We passed a network to create the host, so we need to find the IP
151
+ # that was assigned to the host. We don't get that in the response
152
+ # per se, so we check to see if there is only one IP in the host and
153
+ # use that. If there are more than one, we can't know which one was
154
+ # assigned to the host during create, so we abort.
155
+ if len(host.ipaddresses) == 1:
156
+ host = host.associate_mac_to_ip(macaddress, host.ipaddresses[0].ipaddress)
157
+ else:
158
+ OutputManager().add_ok(
159
+ "Failed to associate MAC address to IP, multiple IP addresses after creation."
160
+ )
161
+
162
+ host.output()
163
+
164
+
165
+ @command_registry.register_command(
166
+ prog="remove",
167
+ description="Remove the given host.",
168
+ short_desc="Remove a host",
169
+ flags=[
170
+ Flag(
171
+ "name",
172
+ short_desc="Name or ip.",
173
+ description="Name of host or an ip belonging to the host.",
174
+ metavar="NAME/IP",
175
+ ),
176
+ Flag("-force", action="store_true", description="Enable force."),
177
+ Flag(
178
+ "-override",
179
+ short_desc="Comma separated override list, requires -force.",
180
+ description=(
181
+ "Comma separated overrides for forced removal. Requires -force."
182
+ "Accepted overrides: 'cname', 'ipadress', 'mx', 'srv', 'ptr', 'naptr'."
183
+ "Example usage: '-override cname,ipaddress,mx'"
184
+ ),
185
+ metavar="OVERRIDE",
186
+ ),
187
+ ],
188
+ )
189
+ def remove(args: argparse.Namespace) -> None:
190
+ """Remove host.
191
+
192
+ :param args: argparse.Namespace (name, force, override)
193
+ """
194
+ hostname = args.name
195
+ host = Host.get_by_any_means_or_raise(hostname, inform_as_cname=True)
196
+
197
+ overrides: list[str] = args.override.split(",") if args.override else []
198
+
199
+ accepted_overrides = ["cname", "ipaddress", "mx", "srv", "ptr", "naptr"]
200
+ for override in overrides:
201
+ if override not in accepted_overrides:
202
+ raise InputFailure(
203
+ f"Invalid override: {override}. Accepted overrides: {accepted_overrides}"
204
+ )
205
+
206
+ def forced(override_required: str | None = None) -> bool:
207
+ # If we require an override, check if it's in the list of provided overrides.
208
+ if override_required:
209
+ return override_required in overrides
210
+
211
+ # We didn't require an override, so we only need to check for force.
212
+ if args.force:
213
+ return True
214
+
215
+ # And the fallback is "no".
216
+ return False
217
+
218
+ warnings: list[str] = []
219
+ # Require force if host has any cnames.
220
+ if host.cnames and not args.force:
221
+ warnings.append(f" {len(host.cnames)} cnames, override with 'cname'")
222
+ for cname in host.cnames:
223
+ warnings.append(f" - {cname.name}")
224
+
225
+ # Require force if host has multiple A/AAAA records and they are not in the same VLAN.
226
+ if len(host.ipaddresses) > 1:
227
+ host_vlans = host.vlans()
228
+ same_vlan = len(host_vlans) == 1
229
+
230
+ if same_vlan and not forced():
231
+ warnings.append(" multiple ipaddresses on the same VLAN. Must use 'force'.")
232
+ elif not same_vlan and not forced("ipaddresses"):
233
+ warnings.append(
234
+ " {} ipaddresses on distinct VLANs, override with 'ipadress'".format(
235
+ len(host.ipaddresses)
236
+ )
237
+ )
238
+ for vlan in host_vlans:
239
+ vlan = host_vlans[vlan]
240
+ ip_strings = [str(ip.ipaddress.address) for ip in vlan]
241
+ ip_strings.sort()
242
+ warnings.append(f" - {', '.join(ip_strings)} (vlan: {vlan})")
243
+
244
+ if host.mxs and not forced("mx"):
245
+ warnings.append(f" {len(host.mxs)} MX records, override with 'mx'")
246
+ for mx in host.mxs:
247
+ warnings.append(f" - {mx.mx} (priority: {mx.priority})")
248
+
249
+ # Require force if host has any NAPTR records. Delete the NAPTR records if
250
+ # force
251
+ naptrs = host.naptrs()
252
+ if len(naptrs) > 0:
253
+ if not forced("naptr"):
254
+ warnings.append(f" {len(naptrs)} NAPTR records, override with 'naptr'")
255
+ for naptr in naptrs:
256
+ warnings.append(f" - {naptr.replacement}")
257
+ else:
258
+ for naptr in naptrs:
259
+ OutputManager().add_ok(
260
+ "deleted NAPTR record {} when removing {}".format(
261
+ naptr.replacement,
262
+ host.name,
263
+ )
264
+ )
265
+
266
+ # Require force if host has any SRV records. Delete the SRV records if force
267
+ srvs = host.srvs()
268
+ if len(srvs) > 0:
269
+ if not forced("srv"):
270
+ warnings.append(f" {len(srvs)} SRV records, override with 'srv'")
271
+ for srv in srvs:
272
+ warnings.append(f" - {srv.name}")
273
+ else:
274
+ for srv in srvs:
275
+ OutputManager().add_ok(
276
+ "deleted SRV record {} when removing {}".format(
277
+ srv.name,
278
+ host.name,
279
+ )
280
+ )
281
+
282
+ # Require force if host has any PTR records. Delete the PTR records if force
283
+ if len(host.ptr_overrides) > 0:
284
+ if not forced("ptr"):
285
+ warnings.append(f" {len(host.ptr_overrides)} PTR records, override with 'ptr'")
286
+ for ptr in host.ptr_overrides:
287
+ warnings.append(f" - {ptr.ipaddress}")
288
+ else:
289
+ for ptr in host.ptr_overrides:
290
+ OutputManager().add_ok(
291
+ "deleted PTR record {} when removing {}".format(
292
+ ptr.ipaddress,
293
+ host.name,
294
+ )
295
+ )
296
+
297
+ # Warn user and raise exception if any force requirements was found
298
+ if warnings:
299
+ warn_msg = "\n".join(warnings)
300
+ raise ForceMissing(f"{host.name} requires force and override for deletion:\n{warn_msg}")
301
+
302
+ if host.delete():
303
+ OutputManager().add_ok(f"removed {host.name}")
304
+ else:
305
+ raise DeleteError(f"failed to remove {host.name}")
306
+
307
+
308
+ @command_registry.register_command(
309
+ prog="info",
310
+ description="Print info about one or more hosts.",
311
+ short_desc="Print info about one or more hosts.",
312
+ flags=[
313
+ Flag(
314
+ "hosts",
315
+ description="One or more hosts given by their name, ip or mac.",
316
+ short_desc="One or more names, ips or macs.",
317
+ nargs="+",
318
+ metavar="NAME/IP/MAC",
319
+ ),
320
+ Flag(
321
+ "-traverse-hostgroups",
322
+ action="store_true",
323
+ description="Show memberships of all parent groups as well as direct groups.",
324
+ short_desc="Traverse hostgroups.",
325
+ ),
326
+ ],
327
+ )
328
+ def host_info(args: argparse.Namespace) -> None:
329
+ """Print information about host.
330
+
331
+ :param args: argparse.Namespace (hosts, traverse_hostgroups)
332
+
333
+ Setting traverse hostgroups will show memberships of all parent groups as well as
334
+ direct groups.
335
+ """
336
+ for host in args.hosts:
337
+ Host.get_by_any_means_or_raise(host, inform_as_cname=True).output(
338
+ traverse_hostgroups=args.traverse_hostgroups
339
+ )
340
+
341
+
342
+ @command_registry.register_command(
343
+ prog="find",
344
+ description="Lists hosts matching search criteria",
345
+ short_desc="Lists hosts matching search criteria",
346
+ flags=[
347
+ Flag(
348
+ "-name",
349
+ description="Name or part of name",
350
+ short_desc="Name or part of name",
351
+ metavar="NAME",
352
+ ),
353
+ Flag(
354
+ "-comment",
355
+ description="Comment or part of comment",
356
+ short_desc="Comment or part of comment",
357
+ metavar="COMMENT",
358
+ ),
359
+ Flag(
360
+ "-contact",
361
+ description="Contact or part of contact",
362
+ short_desc="Contact or part of contact",
363
+ metavar="CONTACT",
364
+ ),
365
+ ],
366
+ )
367
+ def find(args: argparse.Namespace) -> None:
368
+ """List hosts maching search criteria.
369
+
370
+ :param args: argparse.Namespace (name, comment, contact)
371
+ """
372
+
373
+ def _add_param(param: str, value: str) -> None:
374
+ param, value = convert_wildcard_to_regex(param, value, True)
375
+ params[param] = value
376
+
377
+ if not any([args.name, args.comment, args.contact]):
378
+ raise InputFailure("Need at least one search critera")
379
+
380
+ params: QueryParams = {
381
+ "ordering": "name",
382
+ }
383
+
384
+ for param in ("contact", "comment", "name"):
385
+ value = getattr(args, param)
386
+ if value:
387
+ _add_param(param, value)
388
+
389
+ HostList.get(params=params).output()
390
+
391
+
392
+ @command_registry.register_command(
393
+ prog="rename",
394
+ description="Rename host. If the old name is an alias then the alias is renamed.",
395
+ short_desc="Rename a host",
396
+ flags=[
397
+ Flag(
398
+ "old_name",
399
+ description=(
400
+ "Host name of the host to rename. May be an alias. "
401
+ "If it is an alias then the alias is renamed."
402
+ ),
403
+ short_desc="Existing host name.",
404
+ metavar="OLD",
405
+ ),
406
+ Flag(
407
+ "new_name",
408
+ description="New name for the host, or alias.",
409
+ short_desc="New name",
410
+ metavar="NEW",
411
+ ),
412
+ Flag("-force", action="store_true", description="Enable force."),
413
+ ],
414
+ )
415
+ def rename(args: argparse.Namespace) -> None:
416
+ """Rename host. If <old-name> is an alias then the alias is renamed.
417
+
418
+ :param args: argparse.Namespace (old_name, new_name, force)
419
+
420
+ :return: The updated Host or None
421
+ """
422
+ old_host = Host.get_by_any_means_or_raise(args.old_name)
423
+ new_name = HostT(hostname=args.new_name)
424
+ new_host = Host.get_by_any_means(new_name, inform_as_cname=True)
425
+ if new_host:
426
+ raise EntityAlreadyExists(f"host {new_host} already exists")
427
+
428
+ # Require force if FQDN not in MREG zone
429
+ zone = ForwardZone.get_from_hostname(new_name)
430
+ if not zone and not args.force:
431
+ raise ForceMissing(f"{new_name} isn't in a zone controlled by MREG, must force")
432
+
433
+ if "*" in new_name.hostname and not args.force:
434
+ raise ForceMissing("Wildcards must be forced.")
435
+
436
+ new_host = old_host.rename(new_name)
437
+ OutputManager().add_ok(f"renamed {old_host} to {new_name}")
438
+
439
+
440
+ # Add 'set_comment' as a sub command to the 'host' command
441
+ @command_registry.register_command(
442
+ prog="set_comment",
443
+ description="Set comment for host. If NAME is an alias the cname host is updated.",
444
+ short_desc="Set comment.",
445
+ flags=[
446
+ Flag("name", description="Name of the target host.", metavar="NAME"),
447
+ Flag(
448
+ "comment",
449
+ description=(
450
+ "The new comment. If it contains spaces then it must be enclosed in quotes."
451
+ ),
452
+ metavar="COMMENT",
453
+ ),
454
+ ],
455
+ )
456
+ def set_comment(args: argparse.Namespace) -> None:
457
+ """Set comment for host. If <name> is an alias the cname host is updated.
458
+
459
+ :param args: argparse.Namespace (name, comment)
460
+ """
461
+ host = Host.get_by_any_means_or_raise(args.name, inform_as_cname=True)
462
+ updated_host = host.set_comment(args.comment)
463
+
464
+ if not updated_host:
465
+ raise PatchError(f"Failed to update comment of {host.name}")
466
+
467
+ OutputManager().add_ok(f"Updated comment of {host} to {args.comment}")
468
+
469
+
470
+ @command_registry.register_command(
471
+ prog="set_contact",
472
+ description="Set contact for host. If <name> is an alias the cname host is updated.",
473
+ short_desc="Set contact.",
474
+ flags=[
475
+ Flag("name", description="Name of the target host.", metavar="NAME"),
476
+ Flag("contact", description="Mail address of the contact.", metavar="CONTACT"),
477
+ ],
478
+ )
479
+ def set_contact(args: argparse.Namespace) -> None:
480
+ """Set contact for host. If <name> is an alias the cname host is updated.
481
+
482
+ :param args: argparse.Namespace (name, contact)
483
+ """
484
+ host = Host.get_by_any_means_or_raise(args.name, inform_as_cname=True)
485
+ updated_host = host.set_contact(args.contact)
486
+
487
+ if not updated_host:
488
+ raise PatchError(f"Failed to update contact of {host.name}")
489
+
490
+ OutputManager().add_ok(f"Updated contact of {host} to {args.contact}")
491
+
492
+
493
+ @command_registry.register_command(
494
+ prog="history",
495
+ description="Show history for host.",
496
+ short_desc="Show history.",
497
+ flags=[
498
+ Flag("name", description="Host name", metavar="NAME"),
499
+ ],
500
+ )
501
+ def history(args: argparse.Namespace) -> None:
502
+ """Show host history for name.
503
+
504
+ :param args: argparse.Namespace (name)
505
+ """
506
+ hostname = HostT(hostname=args.name)
507
+ Host.output_history(hostname.hostname)