mreg-cli 1.4.0__tar.gz → 1.4.2__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 (90) hide show
  1. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/CHANGELOG.md +13 -0
  2. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/PKG-INFO +1 -1
  3. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/ci/testsuite-result.json +22 -22
  4. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/_version.py +2 -2
  5. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/api/models.py +72 -10
  6. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/host_submodules/rr.py +7 -8
  7. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/network.py +4 -1
  8. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli.egg-info/PKG-INFO +1 -1
  9. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/api/test_models.py +82 -1
  10. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/.github/workflows/publish.yml +0 -0
  11. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/.github/workflows/test.yml +0 -0
  12. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/.gitignore +0 -0
  13. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/.markdownlint.json +0 -0
  14. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/AUTHORS +0 -0
  15. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/CONTRIBUTING +0 -0
  16. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/LICENSE +0 -0
  17. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/README.md +0 -0
  18. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/ci/Dockerfile +0 -0
  19. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/ci/diff.py +0 -0
  20. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/ci/docker-compose.yml +0 -0
  21. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/ci/run_testsuite_and_record.sh +0 -0
  22. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/ci/run_testsuite_and_record_V2.sh +0 -0
  23. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/ci/testsuite +0 -0
  24. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/data/mreg-cli.conf +0 -0
  25. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/__about__.py +0 -0
  26. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/__init__.py +0 -0
  27. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/__main__.py +0 -0
  28. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/api/__init__.py +0 -0
  29. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/api/abstracts.py +0 -0
  30. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/api/endpoints.py +0 -0
  31. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/api/errors.py +0 -0
  32. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/api/fields.py +0 -0
  33. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/api/history.py +0 -0
  34. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/cli.py +0 -0
  35. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/__init__.py +0 -0
  36. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/base.py +0 -0
  37. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/dhcp.py +0 -0
  38. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/group.py +0 -0
  39. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/help.py +0 -0
  40. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/host.py +0 -0
  41. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/host_submodules/__init__.py +0 -0
  42. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/host_submodules/a_aaaa.py +0 -0
  43. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/host_submodules/bacnet.py +0 -0
  44. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/host_submodules/cname.py +0 -0
  45. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/host_submodules/core.py +0 -0
  46. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/label.py +0 -0
  47. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/logging.py +0 -0
  48. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/permission.py +0 -0
  49. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/policy.py +0 -0
  50. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/recording.py +0 -0
  51. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/registry.py +0 -0
  52. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/root.py +0 -0
  53. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/commands/zone.py +0 -0
  54. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/config.py +0 -0
  55. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/errorbuilder.py +0 -0
  56. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/exceptions.py +0 -0
  57. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/help_formatter.py +0 -0
  58. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/main.py +0 -0
  59. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/outputmanager.py +0 -0
  60. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/prompt.py +0 -0
  61. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/py.typed +0 -0
  62. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/tags.txt +0 -0
  63. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/tokenfile.py +0 -0
  64. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/types.py +0 -0
  65. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/utilities/__init__.py +0 -0
  66. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/utilities/api.py +0 -0
  67. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/utilities/shared.py +0 -0
  68. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli/utilities/validators.py +0 -0
  69. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli.egg-info/SOURCES.txt +0 -0
  70. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli.egg-info/dependency_links.txt +0 -0
  71. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli.egg-info/entry_points.txt +0 -0
  72. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli.egg-info/requires.txt +0 -0
  73. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/mreg_cli.egg-info/top_level.txt +0 -0
  74. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/pyproject.toml +0 -0
  75. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/run.py +0 -0
  76. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/setup.cfg +0 -0
  77. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/__init__.py +0 -0
  78. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/api/__init__.py +0 -0
  79. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/api/test_fields.py +0 -0
  80. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/conftest.py +0 -0
  81. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/test_errorbuilder.py +0 -0
  82. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/test_exceptions.py +0 -0
  83. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/test_outputmanager.py +0 -0
  84. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/test_prompt.py +0 -0
  85. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/test_tokenfile.py +0 -0
  86. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/test_types.py +0 -0
  87. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/utilities/__init__.py +0 -0
  88. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tests/utilities/test_api.py +0 -0
  89. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/tox.ini +0 -0
  90. {mreg_cli-1.4.0 → mreg_cli-1.4.2}/uv.lock +0 -0
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- ## Unreleased -->
9
9
 
10
+ ## [1.4.2](https://github.com/unioslo/mreg-cli/releases/tag/1.4.2) - 2025-08-15
11
+
12
+ ### Fixed
13
+
14
+ - `host info` for hosts with A(AAA)-records that point to networks not managed by MREG.
15
+ - `network community_host_add` raising a 404 error for hosts with IPs in networks not managed by MREG.
16
+
17
+ ## [1.4.1](https://github.com/unioslo/mreg-cli/releases/tag/1.4.1) - 2025-08-06
18
+
19
+ ### Changed
20
+
21
+ - `ptr add -force` now allows adding PTR records for IPs that are not in a network managed by MREG.
22
+
10
23
  ## [1.4.0](https://github.com/unioslo/mreg-cli/releases/tag/1.4.0) - 2025-06-20
11
24
 
12
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mreg-cli
3
- Version: 1.4.0
3
+ Version: 1.4.2
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>
@@ -23044,7 +23044,7 @@
23044
23044
  "command_issued": "dhcp assoc foo aa:bb:cc:dd:ee:ff # should fail, two IPs of same type",
23045
23045
  "ok": [],
23046
23046
  "warning": [
23047
- "Host foo.example.org has multiple IPs, cannot determine which one to use."
23047
+ "Host foo.example.org has multiple IPs, cannot determine which one to use: 10.0.0.5, 10.0.0.6."
23048
23048
  ],
23049
23049
  "error": [],
23050
23050
  "output": [],
@@ -23070,15 +23070,15 @@
23070
23070
  "ipaddresses": [
23071
23071
  {
23072
23072
  "macaddress": "",
23073
- "created_at": "2025-06-19T13:09:08.251883+02:00",
23074
- "updated_at": "2025-06-19T13:09:08.251892+02:00",
23073
+ "created_at": "2025-08-13T10:41:00.041467+02:00",
23074
+ "updated_at": "2025-08-13T10:41:00.041474+02:00",
23075
23075
  "ipaddress": "10.0.0.5",
23076
23076
  "host": 18
23077
23077
  },
23078
23078
  {
23079
23079
  "macaddress": "",
23080
- "created_at": "2025-06-19T13:09:08.382145+02:00",
23081
- "updated_at": "2025-06-19T13:09:08.382151+02:00",
23080
+ "created_at": "2025-08-13T10:41:00.185731+02:00",
23081
+ "updated_at": "2025-08-13T10:41:00.185738+02:00",
23082
23082
  "ipaddress": "10.0.0.6",
23083
23083
  "host": 18
23084
23084
  }
@@ -23087,8 +23087,8 @@
23087
23087
  "mxs": [],
23088
23088
  "txts": [
23089
23089
  {
23090
- "created_at": "2025-06-19T13:09:07.019259+02:00",
23091
- "updated_at": "2025-06-19T13:09:07.019265+02:00",
23090
+ "created_at": "2025-08-13T10:40:58.935125+02:00",
23091
+ "updated_at": "2025-08-13T10:40:58.935130+02:00",
23092
23092
  "txt": "v=spf1 -all",
23093
23093
  "host": 18
23094
23094
  }
@@ -23103,8 +23103,8 @@
23103
23103
  "loc": null,
23104
23104
  "bacnetid": null,
23105
23105
  "communities": [],
23106
- "created_at": "2025-06-19T13:09:07.017552+02:00",
23107
- "updated_at": "2025-06-19T13:09:07.017559+02:00",
23106
+ "created_at": "2025-08-13T10:40:58.933270+02:00",
23107
+ "updated_at": "2025-08-13T10:40:58.933280+02:00",
23108
23108
  "name": "foo.example.org",
23109
23109
  "contact": "",
23110
23110
  "ttl": null,
@@ -23513,7 +23513,7 @@
23513
23513
  "command_issued": "dhcp assoc foo aa:bb:cc:dd:ee:ff # should fail, the host has two IPs of different types on different VLANs.",
23514
23514
  "ok": [],
23515
23515
  "warning": [
23516
- "Host foo.example.org has multiple IPs, cannot determine which one to use."
23516
+ "Host foo.example.org has multiple IPs, cannot determine which one to use: 10.0.0.5, 2001:db9::5."
23517
23517
  ],
23518
23518
  "error": [],
23519
23519
  "output": [],
@@ -23539,15 +23539,15 @@
23539
23539
  "ipaddresses": [
23540
23540
  {
23541
23541
  "macaddress": "",
23542
- "created_at": "2025-06-19T13:09:08.251883+02:00",
23543
- "updated_at": "2025-06-19T13:09:08.251892+02:00",
23542
+ "created_at": "2025-08-13T10:41:00.041467+02:00",
23543
+ "updated_at": "2025-08-13T10:41:00.041474+02:00",
23544
23544
  "ipaddress": "10.0.0.5",
23545
23545
  "host": 18
23546
23546
  },
23547
23547
  {
23548
23548
  "macaddress": "",
23549
- "created_at": "2025-06-19T13:09:08.758513+02:00",
23550
- "updated_at": "2025-06-19T13:09:08.758520+02:00",
23549
+ "created_at": "2025-08-13T10:41:00.786601+02:00",
23550
+ "updated_at": "2025-08-13T10:41:00.786608+02:00",
23551
23551
  "ipaddress": "2001:db9::5",
23552
23552
  "host": 18
23553
23553
  }
@@ -23556,8 +23556,8 @@
23556
23556
  "mxs": [],
23557
23557
  "txts": [
23558
23558
  {
23559
- "created_at": "2025-06-19T13:09:07.019259+02:00",
23560
- "updated_at": "2025-06-19T13:09:07.019265+02:00",
23559
+ "created_at": "2025-08-13T10:40:58.935125+02:00",
23560
+ "updated_at": "2025-08-13T10:40:58.935130+02:00",
23561
23561
  "txt": "v=spf1 -all",
23562
23562
  "host": 18
23563
23563
  }
@@ -23572,8 +23572,8 @@
23572
23572
  "loc": null,
23573
23573
  "bacnetid": null,
23574
23574
  "communities": [],
23575
- "created_at": "2025-06-19T13:09:07.017552+02:00",
23576
- "updated_at": "2025-06-19T13:09:07.017559+02:00",
23575
+ "created_at": "2025-08-13T10:40:58.933270+02:00",
23576
+ "updated_at": "2025-08-13T10:40:58.933280+02:00",
23577
23577
  "name": "foo.example.org",
23578
23578
  "contact": "",
23579
23579
  "ttl": null,
@@ -23590,8 +23590,8 @@
23590
23590
  "excluded_ranges": [],
23591
23591
  "policy": null,
23592
23592
  "communities": [],
23593
- "created_at": "2025-06-19T13:09:06.902388+02:00",
23594
- "updated_at": "2025-06-19T13:09:06.902398+02:00",
23593
+ "created_at": "2025-08-13T10:40:58.822374+02:00",
23594
+ "updated_at": "2025-08-13T10:40:58.822385+02:00",
23595
23595
  "network": "10.0.0.0/24",
23596
23596
  "description": "foo",
23597
23597
  "vlan": 1234,
@@ -23611,8 +23611,8 @@
23611
23611
  "excluded_ranges": [],
23612
23612
  "policy": null,
23613
23613
  "communities": [],
23614
- "created_at": "2025-06-19T13:09:08.663889+02:00",
23615
- "updated_at": "2025-06-19T13:09:08.663898+02:00",
23614
+ "created_at": "2025-08-13T10:41:00.673383+02:00",
23615
+ "updated_at": "2025-08-13T10:41:00.673397+02:00",
23616
23616
  "network": "2001:db9::/64",
23617
23617
  "description": "notfoo_ipv6",
23618
23618
  "vlan": 1235,
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '1.4.0'
21
- __version_tuple__ = version_tuple = (1, 4, 0)
20
+ __version__ = version = '1.4.2'
21
+ __version_tuple__ = version_tuple = (1, 4, 2)
@@ -24,6 +24,7 @@ from mreg_cli.api.endpoints import Endpoint
24
24
  from mreg_cli.api.fields import HostName, MacAddress, NameList
25
25
  from mreg_cli.api.history import HistoryItem, HistoryResource
26
26
  from mreg_cli.exceptions import (
27
+ APINotOk,
27
28
  CreateError,
28
29
  DeleteError,
29
30
  EntityAlreadyExists,
@@ -1615,6 +1616,39 @@ class Network(FrozenModelWithTimestamps, APIMixin):
1615
1616
  """Return the endpoint for the class."""
1616
1617
  return Endpoint.Networks
1617
1618
 
1619
+ @classmethod
1620
+ def dummy_network_from_ip(cls, ip: IPAddress) -> Self:
1621
+ """Create a Network object for an unknown network given an IP.
1622
+
1623
+ NOTE: Does not perform any API calls. This is purely to work around
1624
+ the fact that MREG supports creating IP addresses for networks that
1625
+ are not registered in MREG.
1626
+ """
1627
+ if ip.is_ipv4():
1628
+ network_addr = "0.0.0.0/24"
1629
+ description = "Unknown IPv4 network"
1630
+ else:
1631
+ network_addr = "::/64"
1632
+ description = "Unknown IPv6 network"
1633
+
1634
+ return cls(
1635
+ id=0,
1636
+ excluded_ranges=[],
1637
+ network=network_addr,
1638
+ description=description,
1639
+ vlan=None,
1640
+ dns_delegated=False,
1641
+ category="",
1642
+ location="",
1643
+ frozen=False,
1644
+ reserved=0,
1645
+ policy=None,
1646
+ communities=[],
1647
+ # epoch time
1648
+ created_at=datetime.fromtimestamp(0),
1649
+ updated_at=datetime.fromtimestamp(0),
1650
+ )
1651
+
1618
1652
  @classmethod
1619
1653
  def get_by_any_means(cls, identifier: str) -> Self | None:
1620
1654
  """Get a network by the given identifier.
@@ -2412,14 +2446,18 @@ class IPAddress(FrozenModelWithTimestamps, WithHost, APIMixin):
2412
2446
  """Return True if the IP address is IPv6."""
2413
2447
  return self.ipaddress.version == 6
2414
2448
 
2415
- def network(self) -> Network:
2449
+ def network(self) -> Network | None:
2416
2450
  """Return the network of the IP address."""
2417
- data = get(Endpoint.NetworksByIP.with_id(str(self.ipaddress)))
2418
- return Network.model_validate(data.json())
2451
+ try:
2452
+ return get_typed(Endpoint.NetworksByIP.with_id(str(self.ipaddress)), Network)
2453
+ except APINotOk:
2454
+ return None
2419
2455
 
2420
2456
  def vlan(self) -> int | None:
2421
2457
  """Return the VLAN of the IP address."""
2422
- return self.network().vlan
2458
+ if net := self.network():
2459
+ return net.vlan
2460
+ return None
2423
2461
 
2424
2462
  def associate_mac(self, mac: MacAddress, force: bool = False) -> IPAddress:
2425
2463
  """Associate a MAC address with the IP address.
@@ -3394,14 +3432,32 @@ class Host(FrozenModelWithTimestamps, WithTTL, WithHistory, APIMixin):
3394
3432
  ipv6s = self.ipv6_addresses()
3395
3433
 
3396
3434
  if len(ipv4s) == 1 and len(ipv6s) == 1:
3397
- vlan4 = ipv4s[0].network().vlan
3398
- vlan6 = ipv6s[0].network().vlan
3399
-
3400
- if vlan4 == vlan6:
3401
- return ipv4s[0]
3435
+ ipv4_address = ipv4s[0]
3436
+ ipv6_address = ipv6s[0]
3437
+ ipv4_network = ipv4_address.network()
3438
+ ipv6_network = ipv6_address.network()
3439
+
3440
+ if ipv4_network and ipv6_network:
3441
+ if ipv4_network.vlan == ipv6_network.vlan:
3442
+ return ipv4_address
3443
+ elif ipv4_network: # only IPv4 is in mreg
3444
+ logger.warning(
3445
+ "Host '%s' has IPv6 address not in MREG: %s",
3446
+ self.name,
3447
+ str(ipv6_address.ipaddress),
3448
+ )
3449
+ return ipv4_address
3450
+ elif ipv6_network: # only IPv6 is in mreg
3451
+ logger.warning(
3452
+ "Host '%s' has IPv4 address not in MREG: %s",
3453
+ self.name,
3454
+ str(ipv4_address.ipaddress),
3455
+ )
3456
+ return ipv6_address
3402
3457
 
3458
+ ips = ", ".join(str(ip.ipaddress) for ip in self.ipaddresses)
3403
3459
  raise EntityOwnershipMismatch(
3404
- f"Host {self} has multiple IPs, cannot determine which one to use."
3460
+ f"Host {self} has multiple IPs, cannot determine which one to use: {ips}."
3405
3461
  )
3406
3462
 
3407
3463
  def has_ptr_override(self, arg_ip: IP_AddressT) -> bool:
@@ -3554,12 +3610,18 @@ class Host(FrozenModelWithTimestamps, WithTTL, WithHistory, APIMixin):
3554
3610
  def networks(self) -> dict[Network, list[IPAddress]]:
3555
3611
  """Return a dict of unique networks and a list of associated IP addresses for the host.
3556
3612
 
3613
+ Does not return networks that are not registered in MREG.
3614
+
3557
3615
  :returns: A dictionary of networks and the associated IP addresses.
3558
3616
  """
3559
3617
  ret_dict: dict[Network, list[IPAddress]] = {}
3560
3618
 
3561
3619
  for ip in self.ipaddresses:
3562
3620
  network = ip.network()
3621
+ if not network:
3622
+ # If network is not in MREG, we create a placeholder network
3623
+ network = Network.dummy_network_from_ip(ip)
3624
+
3563
3625
  if network not in ret_dict:
3564
3626
  ret_dict[network] = []
3565
3627
 
@@ -534,15 +534,14 @@ def ptr_add(args: argparse.Namespace) -> None:
534
534
  if existing_ptrs:
535
535
  raise EntityAlreadyExists(f"{ip} already exists in a PTR record.")
536
536
 
537
- if host.zone is None and not args.force:
538
- raise ForceMissing(f"{host} isn't in a zone controlled by MREG, must force")
539
-
540
537
  network = Network.get_by_ip(ip)
541
- if not network:
542
- raise EntityNotFound(f"{ip} isn't in a network controlled by MREG")
543
-
544
- if network.is_reserved_ip(ip) and not args.force:
545
- raise ForceMissing("Address is reserved. Requires force")
538
+ if not args.force:
539
+ if host.zone is None:
540
+ raise ForceMissing(f"{host} isn't in a zone controlled by MREG, must force")
541
+ elif not network:
542
+ raise ForceMissing(f"{ip} isn't in a network controlled by MREG, must force")
543
+ elif network and network.is_reserved_ip(ip):
544
+ raise ForceMissing(f"{ip} is reserved, must force")
546
545
 
547
546
  PTR_override.create({"host": host.id, "ipaddress": str(ip)})
548
547
  OutputManager().add_ok(f"Added PTR record {ip} to {host.name}.")
@@ -1223,7 +1223,10 @@ def community_host_add(args: argparse.Namespace) -> None:
1223
1223
 
1224
1224
  h = Host.get_by_any_means_or_raise(host)
1225
1225
  ipaddr = _check_host_ip(h, ip)
1226
- net = ipaddr.network()
1226
+
1227
+ if not (net := ipaddr.network()):
1228
+ raise EntityNotFound(f"{h.name!r} is not in a network controlled by MREG.")
1229
+
1227
1230
  com = net.get_community_or_raise(community)
1228
1231
 
1229
1232
  com.add_host(h, ipaddress=ipaddr.ipaddress)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mreg-cli
3
- Version: 1.4.0
3
+ Version: 1.4.2
4
4
  Summary: MREG interactive command line client
5
5
  Author: Others (see AUTHORS)
6
6
  Author-email: Øyvind Kolbu <oyvind.kolbu@usit.uio.no>, Safet A <amedov@usit.uio.no>, Magnus Hirth <mhmorgan42@gmail.com>, Nico <nicolaydmohebi@gmail.com>, Marius Bakke <marius.bakke@usit.uio.no>, Fredrik Larsen <fredrik.larsen@usit.uio.no>, Paal Braathen <paal.braathen@usit.uio.no>, Nils Hiorth <nils@nls.me>, Dmytro Karpenko <dmytrok@usit.uio.no>
@@ -6,7 +6,7 @@ from typing import Any, Callable
6
6
 
7
7
  import pytest
8
8
 
9
- from mreg_cli.api.models import IPNetMode, Network, NetworkOrIP
9
+ from mreg_cli.api.models import IPAddress, IPNetMode, Network, NetworkOrIP
10
10
  from mreg_cli.exceptions import (
11
11
  InputFailure,
12
12
  InvalidIPAddress,
@@ -135,3 +135,84 @@ def test_network_ip_network(inp: str, expect: IP_NetworkT) -> None:
135
135
  assert network.ip_network == expect
136
136
  assert network.broadcast_address == expect.broadcast_address
137
137
  assert network.network_address == expect.network_address
138
+
139
+
140
+ @pytest.mark.parametrize(
141
+ "inp",
142
+ [
143
+ "192.168.0.1",
144
+ "0.0.0.0",
145
+ "10.0.0.1",
146
+ ],
147
+ )
148
+ def test_network_dummy_network_from_ip_v4(inp: str) -> None:
149
+ """Test creating a dummy network from an IPv4 address."""
150
+ _test_network_dummy_network(IPv4Address(inp), expected_version=4)
151
+
152
+
153
+ @pytest.mark.parametrize(
154
+ "inp",
155
+ [
156
+ "2001:db8::1",
157
+ "::1",
158
+ "fe80::1",
159
+ ],
160
+ )
161
+ def test_network_dummy_network_from_ip_v6(inp: str) -> None:
162
+ """Test creating a dummy network from an IPv6 address."""
163
+ _test_network_dummy_network(IPv6Address(inp), expected_version=6)
164
+
165
+
166
+ def _test_network_dummy_network(inp: IPv4Address | IPv6Address, expected_version: int) -> None:
167
+ """Helper function to test creating a dummy network from an IP address.""" # noqa: D401
168
+ ip = _get_mreg_ipaddress(inp)
169
+ network = Network.dummy_network_from_ip(ip)
170
+ assert isinstance(network, Network)
171
+ assert network.ip_network.version == expected_version
172
+
173
+ # Ensure all dummy networks for the given IP type are hashed
174
+ # the same and equal to each other, so that all dummy networks
175
+ # of a given IP version are considered the same.
176
+ network2 = Network.dummy_network_from_ip(ip)
177
+ assert network == network2
178
+ assert hash(network) == hash(network2)
179
+
180
+
181
+ def _get_mreg_ipaddress(ip: IPv4Address | IPv6Address) -> IPAddress:
182
+ """Construct an mreg IPAddress object from an IP."""
183
+ return IPAddress(
184
+ id=0,
185
+ host=0,
186
+ created_at=datetime.now(),
187
+ updated_at=datetime.now(),
188
+ ipaddress=ip,
189
+ # no mac
190
+ )
191
+
192
+
193
+ def test_network_dummy_network_from_ip_identity() -> None:
194
+ """Test hashing of dummy networks from identical and differenet IP versions."""
195
+ ipv4_1 = _get_mreg_ipaddress(IPv4Address("192.168.0.1"))
196
+ ipv4_2 = _get_mreg_ipaddress(IPv4Address("192.168.0.2"))
197
+ ipv6_1 = _get_mreg_ipaddress(IPv6Address("2001:db8::1"))
198
+ ipv6_2 = _get_mreg_ipaddress(IPv6Address("2001:db8::2"))
199
+
200
+ network_v4_1 = Network.dummy_network_from_ip(ipv4_1)
201
+ network_v4_2 = Network.dummy_network_from_ip(ipv4_2)
202
+ network_v6_1 = Network.dummy_network_from_ip(ipv6_1)
203
+ network_v6_2 = Network.dummy_network_from_ip(ipv6_2)
204
+
205
+ assert network_v4_1 == network_v4_2
206
+ assert hash(network_v4_1) == hash(network_v4_2)
207
+
208
+ assert network_v6_1 == network_v6_2
209
+ assert hash(network_v6_1) == hash(network_v6_2)
210
+
211
+ assert network_v4_1 != network_v6_1
212
+ assert hash(network_v4_1) != hash(network_v6_1)
213
+
214
+ # Usage in dicts
215
+ d = {network_v4_1: "foo", network_v6_1: "bar"}
216
+ # Use the other two network objects to access the existing entries
217
+ assert d[network_v4_2] == "foo"
218
+ assert d[network_v6_2] == "bar"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes