arcane-mage 2.2.0__tar.gz → 2.2.1__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 (82) hide show
  1. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/PKG-INFO +1 -1
  2. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/pyproject.toml +1 -1
  3. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/config.py +4 -1
  4. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/network.py +21 -13
  5. arcane_mage-2.2.1/tests/test_models/test_chain_sources.py +79 -0
  6. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/uv.lock +1 -1
  7. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/.claude/settings.local.json +0 -0
  8. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/.gitignore +0 -0
  9. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/CHANGELOG.md +0 -0
  10. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/LICENSE +0 -0
  11. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/README.md +0 -0
  12. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/README.md +0 -0
  13. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/galaxy.yml +0 -0
  14. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/meta/runtime.yml +0 -0
  15. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/plugins/modules/deprovision.py +0 -0
  16. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/plugins/modules/iso_info.py +0 -0
  17. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/plugins/modules/provision.py +0 -0
  18. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/plugins/modules/status.py +0 -0
  19. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py +0 -0
  20. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/docs/configuration.md +0 -0
  21. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/docs/keyring.md +0 -0
  22. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/docs/proxmox-setup.md +0 -0
  23. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/examples/all_settings.yaml +0 -0
  24. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/examples/merge_anchors.yaml +0 -0
  25. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/examples/minimal.yaml +0 -0
  26. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/examples/typical.yaml +0 -0
  27. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/proxmox_local_storage.png +0 -0
  28. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/proxmox_permissions.png +0 -0
  29. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/proxmox_user.png +0 -0
  30. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/proxmox_user_api_token.png +0 -0
  31. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/__init__.py +0 -0
  32. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/__main__.py +0 -0
  33. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/arcane_mage.py +0 -0
  34. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/arcane_mage.tcss +0 -0
  35. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/fat_writer.py +0 -0
  36. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/helpers.py +0 -0
  37. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/images/__init__.py +0 -0
  38. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/images/arcane_config.raw.gz +0 -0
  39. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/images/arcane_efi.raw.gz +0 -0
  40. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/log.py +0 -0
  41. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/messages.py +0 -0
  42. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/__init__.py +0 -0
  43. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/delegate.py +0 -0
  44. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/fluxnode.py +0 -0
  45. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/hypervisor.py +0 -0
  46. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/identity.py +0 -0
  47. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/installer.py +0 -0
  48. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/notifications.py +0 -0
  49. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/models/system.py +0 -0
  50. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/password.py +0 -0
  51. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/provisioner.py +0 -0
  52. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/proxmox.py +0 -0
  53. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/py.typed +0 -0
  54. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/screens/__init__.py +0 -0
  55. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/screens/confirm_screen.py +0 -0
  56. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/screens/hypervisor.py +0 -0
  57. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/screens/provision_node.py +0 -0
  58. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/screens/provisioning_info.py +0 -0
  59. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/screens/welcome.py +0 -0
  60. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/screens/welcome_proxmox.py +0 -0
  61. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/tpm_keyring.py +0 -0
  62. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/widgets/__init__.py +0 -0
  63. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/src/arcane_mage/widgets/spinner.py +0 -0
  64. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/testing123.yaml +0 -0
  65. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/__init__.py +0 -0
  66. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/conftest.py +0 -0
  67. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_fat_writer.py +0 -0
  68. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_helpers.py +0 -0
  69. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/__init__.py +0 -0
  70. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_config_group.py +0 -0
  71. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_delegate.py +0 -0
  72. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_fluxnode.py +0 -0
  73. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_hypervisor.py +0 -0
  74. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_identity.py +0 -0
  75. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_installer.py +0 -0
  76. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_network.py +0 -0
  77. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_notifications.py +0 -0
  78. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_models/test_system.py +0 -0
  79. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_password.py +0 -0
  80. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_provisioner.py +0 -0
  81. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_proxmox.py +0 -0
  82. {arcane_mage-2.2.0 → arcane_mage-2.2.1}/tests/test_public_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: arcane-mage
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Automation helper for Fluxnodes
5
5
  Author-email: David White <david@runonflux.io>
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arcane-mage"
3
- version = "2.2.0"
3
+ version = "2.2.1"
4
4
  description = "Automation helper for Fluxnodes"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -59,7 +59,10 @@ class ArcaneOsConfig:
59
59
  ]
60
60
 
61
61
  async def write_installer_config(self, file_path: Path) -> bool:
62
- res = await self.fluxnode.network.write_installer_config(file_path)
62
+ chain = self.fluxnode.chain
63
+ res = await self.fluxnode.network.write_installer_config(
64
+ file_path, labnet=chain is not None and chain.network == "labnet"
65
+ )
63
66
 
64
67
  return res
65
68
 
@@ -67,12 +67,7 @@ class Link:
67
67
  @property
68
68
  def ethernet(self) -> bool:
69
69
  # not sure how this applies for wlan interfaces as I don't have one to test
70
- return (
71
- self.kind is None
72
- and self.name != "lo"
73
- and bool(self.address)
74
- and self.address != "00:00:00:00:00:00"
75
- )
70
+ return self.kind is None and self.name != "lo" and bool(self.address) and self.address != "00:00:00:00:00:00"
76
71
 
77
72
 
78
73
  @py_dataclass
@@ -137,11 +132,7 @@ class Route:
137
132
  if not isinstance(other, Route):
138
133
  return False
139
134
 
140
- return (
141
- self.dst == other.dst
142
- and self.link == other.link
143
- and self.gateway == other.gateway
144
- )
135
+ return self.dst == other.dst and self.link == other.link and self.gateway == other.gateway
145
136
 
146
137
 
147
138
  @py_dataclass
@@ -264,6 +255,20 @@ class NetworkConfig:
264
255
  return confs
265
256
 
266
257
 
258
+ def chain_source_address(ip: IPv4Address, *, labnet: bool) -> bool:
259
+ """Whether a node may stream its chain from `ip`: a unicast address on a LAN,
260
+ or, installing onto labnet, on labnet's internet too. Loopback, link-local,
261
+ multicast, unspecified, reserved and broadcast addresses are never a source,
262
+ though Python counts several of them private."""
263
+ if ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified or ip.is_reserved:
264
+ return False
265
+ if ip == IPv4Address("255.255.255.255"):
266
+ return False
267
+ if ip.is_private:
268
+ return True
269
+ return labnet and ip.is_global
270
+
271
+
267
272
  @py_dataclass
268
273
  class FluxnodeNetworkConfig:
269
274
  """Fluxnode-specific network settings such as UPnP and private chain sources."""
@@ -275,7 +280,10 @@ class FluxnodeNetworkConfig:
275
280
  def to_dict(self) -> dict:
276
281
  return TypeAdapter(type(self)).dump_python(self, mode="json", exclude_defaults=True)
277
282
 
278
- async def write_installer_config(self, file_path: Path | str) -> bool:
283
+ async def write_installer_config(self, file_path: Path | str, *, labnet: bool = False) -> bool:
284
+ """Writes the installer's network section. A chain source is kept only at a
285
+ unicast address a node can stream from: on a LAN, or, for a node installing
286
+ onto labnet, also on labnet's internet, where the lab's donor answers."""
279
287
  # we let the configure app set the local chain sources
280
288
  ip_pattern = r"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"
281
289
  port_pattern = (
@@ -294,7 +302,7 @@ class FluxnodeNetworkConfig:
294
302
  if not re.match(ip_pattern, ip) or not re.match(port_pattern, port):
295
303
  continue
296
304
 
297
- if not IPv4Address(ip).is_private:
305
+ if not chain_source_address(IPv4Address(ip), labnet=labnet):
298
306
  continue
299
307
 
300
308
  filtered_chain_sources.append(chain_source)
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+ import yaml
7
+
8
+ from arcane_mage.models.config import ArcaneOsConfig
9
+
10
+ LAB_KEY = "020483178bc32818eb9922b68ac0771fe95136a6edf746016125270cfc2111fd5a"
11
+
12
+
13
+ async def written_sources(tmp_path: Path, node: dict, sources: list[str], chain: dict | None) -> list[str]:
14
+ node["fluxnode"]["network"] = {"private_chain_sources": sources}
15
+ if chain is not None:
16
+ node["fluxnode"]["chain"] = chain
17
+ path = tmp_path / "installer.yaml"
18
+ assert await ArcaneOsConfig.from_dict(node).write_installer_config(path)
19
+ return yaml.safe_load(path.read_text())["network"]["private_chain_sources"]
20
+
21
+
22
+ async def test_a_mainnet_install_keeps_only_private_chain_sources(tmp_path: Path, minimal_node_dict: dict):
23
+ sources = ["192.168.1.20:16127", "10.0.0.5:16137", "25.47.0.2:16127", "8.8.8.8:16127"]
24
+
25
+ kept = await written_sources(tmp_path, minimal_node_dict, sources, None)
26
+
27
+ assert kept == ["192.168.1.20:16127", "10.0.0.5:16137"]
28
+
29
+
30
+ async def test_a_labnet_install_keeps_the_labs_donor_on_its_internet(tmp_path: Path, minimal_node_dict: dict):
31
+ sources = ["25.47.0.2:16127", "192.168.147.10:16127"]
32
+
33
+ kept = await written_sources(tmp_path, minimal_node_dict, sources, {"network": "labnet", "key": LAB_KEY})
34
+
35
+ assert kept == ["25.47.0.2:16127", "192.168.147.10:16127"]
36
+
37
+
38
+ @pytest.mark.parametrize(
39
+ "source",
40
+ [
41
+ "127.0.0.1:16127",
42
+ "0.0.0.0:16127",
43
+ "169.254.10.1:16127",
44
+ "224.0.0.1:16127",
45
+ "255.255.255.255:16127",
46
+ "240.0.0.1:16127",
47
+ "25.47.0.2:99999",
48
+ "25.47.0.2",
49
+ "host.example:16127",
50
+ ],
51
+ )
52
+ async def test_a_labnet_install_still_drops_what_is_no_unicast_source(
53
+ tmp_path: Path, minimal_node_dict: dict, source: str
54
+ ):
55
+ kept = await written_sources(tmp_path, minimal_node_dict, [source], {"network": "labnet", "key": LAB_KEY})
56
+
57
+ assert kept == []
58
+
59
+
60
+ async def test_an_explicit_mainnet_chain_keeps_only_private_chain_sources(tmp_path: Path, minimal_node_dict: dict):
61
+ kept = await written_sources(tmp_path, minimal_node_dict, ["25.47.0.2:16127"], {"network": "mainnet"})
62
+
63
+ assert kept == []
64
+
65
+
66
+ @pytest.mark.parametrize(
67
+ "source",
68
+ [
69
+ "127.0.0.1:16127",
70
+ "0.0.0.0:16127",
71
+ "169.254.10.1:16127",
72
+ "255.255.255.255:16127",
73
+ "240.0.0.1:16127",
74
+ ],
75
+ )
76
+ async def test_a_mainnet_install_drops_what_is_no_lan_source(tmp_path: Path, minimal_node_dict: dict, source: str):
77
+ kept = await written_sources(tmp_path, minimal_node_dict, [source], None)
78
+
79
+ assert kept == []
@@ -146,7 +146,7 @@ wheels = [
146
146
 
147
147
  [[package]]
148
148
  name = "arcane-mage"
149
- version = "2.2.0"
149
+ version = "2.2.1"
150
150
  source = { editable = "." }
151
151
  dependencies = [
152
152
  { name = "aiofiles" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes