arcane-mage 2.0.0__tar.gz → 2.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 (83) hide show
  1. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/.gitignore +1 -0
  2. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/CHANGELOG.md +6 -0
  3. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/PKG-INFO +2 -2
  4. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/README.md +108 -0
  5. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/galaxy.yml +21 -0
  6. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/meta/runtime.yml +1 -0
  7. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/deprovision.py +165 -0
  8. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/iso_info.py +178 -0
  9. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/provision.py +456 -0
  10. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/status.py +153 -0
  11. arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py +186 -0
  12. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/pyproject.toml +1 -1
  13. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/fluxnode.py +32 -4
  14. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/network.py +3 -5
  15. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_fluxnode.py +40 -3
  16. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/uv.lock +689 -689
  17. arcane_mage-2.0.0/arcane_mage.log +0 -4
  18. arcane_mage-2.0.0/src/arcane_mage/images/arcane_mage.log +0 -0
  19. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/.claude/settings.local.json +0 -0
  20. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/LICENSE +0 -0
  21. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/README.md +0 -0
  22. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/docs/configuration.md +0 -0
  23. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/docs/keyring.md +0 -0
  24. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/docs/proxmox-setup.md +0 -0
  25. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/all_settings.yaml +0 -0
  26. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/merge_anchors.yaml +0 -0
  27. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/minimal.yaml +0 -0
  28. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/typical.yaml +0 -0
  29. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_local_storage.png +0 -0
  30. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_permissions.png +0 -0
  31. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_user.png +0 -0
  32. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_user_api_token.png +0 -0
  33. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/__init__.py +0 -0
  34. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/__main__.py +0 -0
  35. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/arcane_mage.py +0 -0
  36. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/arcane_mage.tcss +0 -0
  37. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/fat_writer.py +0 -0
  38. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/helpers.py +0 -0
  39. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/images/__init__.py +0 -0
  40. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/images/arcane_config.raw.gz +0 -0
  41. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/images/arcane_efi.raw.gz +0 -0
  42. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/log.py +0 -0
  43. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/messages.py +0 -0
  44. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/__init__.py +0 -0
  45. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/config.py +0 -0
  46. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/delegate.py +0 -0
  47. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/hypervisor.py +0 -0
  48. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/identity.py +0 -0
  49. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/installer.py +0 -0
  50. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/notifications.py +0 -0
  51. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/system.py +0 -0
  52. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/password.py +0 -0
  53. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/provisioner.py +0 -0
  54. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/proxmox.py +0 -0
  55. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/py.typed +0 -0
  56. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/__init__.py +0 -0
  57. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/confirm_screen.py +0 -0
  58. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/hypervisor.py +0 -0
  59. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/provision_node.py +0 -0
  60. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/provisioning_info.py +0 -0
  61. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/welcome.py +0 -0
  62. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/welcome_proxmox.py +0 -0
  63. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/tpm_keyring.py +0 -0
  64. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/widgets/__init__.py +0 -0
  65. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/widgets/spinner.py +0 -0
  66. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/testing123.yaml +0 -0
  67. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/__init__.py +0 -0
  68. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/conftest.py +0 -0
  69. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_fat_writer.py +0 -0
  70. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_helpers.py +0 -0
  71. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/__init__.py +0 -0
  72. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_config_group.py +0 -0
  73. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_delegate.py +0 -0
  74. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_hypervisor.py +0 -0
  75. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_identity.py +0 -0
  76. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_installer.py +0 -0
  77. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_network.py +0 -0
  78. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_notifications.py +0 -0
  79. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_system.py +0 -0
  80. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_password.py +0 -0
  81. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_provisioner.py +0 -0
  82. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_proxmox.py +0 -0
  83. {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_public_api.py +0 -0
@@ -15,3 +15,4 @@ dev
15
15
  fluxnodes*.yaml
16
16
  .DS_Store
17
17
  .ruff_cache
18
+ *.log
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Added
6
+
7
+ - **`FluxnodeConfig.dev_token`**: optional dev-channel enrolment token, written to the autoinstall config next to the identity it is bound to; the installer verifies it
8
+
3
9
  ## 2.0.0
4
10
 
5
11
  ### Breaking Changes
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: arcane-mage
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: Automation helper for Fluxnodes
5
5
  Author-email: David White <david@runonflux.io>
6
6
  License-File: LICENSE
@@ -0,0 +1,108 @@
1
+ # Ansible Collection: runonflux.arcane_mage
2
+
3
+ Ansible collection for provisioning ArcaneOS FluxNodes on Proxmox hypervisors using the [arcane-mage](https://github.com/RunOnFlux/arcane-mage) library.
4
+
5
+ ## Requirements
6
+
7
+ - Python >= 3.13 on the Ansible controller
8
+ - `arcane-mage >= 2.0.0` (`pip install arcane-mage`)
9
+ - Proxmox VE >= 8.4.1
10
+ - Proxmox API token with appropriate permissions
11
+
12
+ ## Installation
13
+
14
+ ### From source (development)
15
+
16
+ ```bash
17
+ cd arcane-mage/ansible_collection/runonflux/arcane_mage
18
+ ansible-galaxy collection build
19
+ ansible-galaxy collection install runonflux-arcane_mage-*.tar.gz
20
+ ```
21
+
22
+ ### Using collections path
23
+
24
+ Add the path to your `ansible.cfg`:
25
+
26
+ ```ini
27
+ [defaults]
28
+ collections_path = /path/to/arcane-mage/ansible_collection
29
+ ```
30
+
31
+ ## Modules
32
+
33
+ | Module | Description |
34
+ |--------|-------------|
35
+ | `runonflux.arcane_mage.provision` | Provision an ArcaneOS FluxNode VM (UEFI, TPM 2.0, config disk) |
36
+ | `runonflux.arcane_mage.deprovision` | Stop and delete a FluxNode VM |
37
+ | `runonflux.arcane_mage.status` | Check VM status on a Proxmox node |
38
+ | `runonflux.arcane_mage.validate` | Pre-flight validation (API, storage, ISO, network) |
39
+ | `runonflux.arcane_mage.iso_info` | Get latest ArcaneOS ISO version, optionally download it |
40
+
41
+ All modules use `delegate_to: localhost` since they communicate with Proxmox via its REST API.
42
+
43
+ ## Quick Start
44
+
45
+ ```yaml
46
+ - name: Provision FluxNodes
47
+ hosts: proxmox_hosts
48
+ gather_facts: no
49
+ tasks:
50
+ - name: Get latest ISO
51
+ runonflux.arcane_mage.iso_info:
52
+ delegate_to: localhost
53
+ register: iso
54
+ run_once: true
55
+
56
+ - name: Download ISO to host
57
+ runonflux.arcane_mage.iso_info:
58
+ api_url: "https://{{ ansible_host }}:8006"
59
+ api_token: "{{ proxmox_token }}"
60
+ node: "{{ inventory_hostname | lower }}"
61
+ download: true
62
+ delegate_to: localhost
63
+
64
+ - name: Validate host is ready
65
+ runonflux.arcane_mage.validate:
66
+ api_url: "https://{{ ansible_host }}:8006"
67
+ api_token: "{{ proxmox_token }}"
68
+ node: "{{ inventory_hostname | lower }}"
69
+ network: vmbr4000
70
+ iso_name: "{{ iso.iso_name }}"
71
+ delegate_to: localhost
72
+
73
+ - name: Provision FluxNode
74
+ runonflux.arcane_mage.provision:
75
+ api_url: "https://{{ ansible_host }}:8006"
76
+ api_token: "{{ proxmox_token }}"
77
+ node: "{{ inventory_hostname | lower }}"
78
+ vm_name: "{{ inventory_hostname | lower }}-vs{{ idx + 1 }}"
79
+ node_tier: stratus
80
+ network: vmbr4000
81
+ iso_name: "{{ iso.iso_name }}"
82
+ hostname: "{{ inventory_hostname | lower }}-vs{{ idx + 1 }}"
83
+ console_password: "{{ console_password }}"
84
+ flux_id: "{{ flux_id }}"
85
+ identity_key: "{{ identity_key }}"
86
+ tx_id: "{{ tx_id }}"
87
+ output_id: "{{ output_id }}"
88
+ ip_allocation: static
89
+ ip_address: "{{ item }}/27"
90
+ gateway: "49.12.156.225"
91
+ start: true
92
+ delegate_to: localhost
93
+ loop: "{{ vswitch_vm_ips }}"
94
+ loop_control:
95
+ index_var: idx
96
+ ```
97
+
98
+ ## Node Tiers
99
+
100
+ | Tier | Cores | RAM | Disk |
101
+ |------|-------|-----|------|
102
+ | cumulus | 4 | 8 GB | 220 GB |
103
+ | nimbus | 8 | 32 GB | 440 GB |
104
+ | stratus | 16 | 64 GB | 880 GB |
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,21 @@
1
+ namespace: runonflux
2
+ name: arcane_mage
3
+ version: 1.0.0
4
+ readme: README.md
5
+ authors:
6
+ - RunOnFlux <info@runonflux.io>
7
+ description: Ansible collection for provisioning ArcaneOS FluxNodes on Proxmox via arcane-mage
8
+ license_file: ../../../LICENSE
9
+ tags:
10
+ - proxmox
11
+ - fluxnode
12
+ - arcaneos
13
+ - provisioning
14
+ - automation
15
+ repository: https://github.com/RunOnFlux/arcane-mage
16
+ documentation: https://github.com/RunOnFlux/arcane-mage
17
+ homepage: https://runonflux.com
18
+ issues: https://github.com/RunOnFlux/arcane-mage/issues
19
+ build_ignore:
20
+ - "*.pyc"
21
+ - __pycache__
@@ -0,0 +1 @@
1
+ requires_ansible: ">=2.14.0"
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Copyright: (c) 2025, RunOnFlux
5
+ # License: MIT
6
+
7
+ DOCUMENTATION = r"""
8
+ ---
9
+ module: deprovision
10
+ short_description: Remove a FluxNode VM from Proxmox
11
+ version_added: "1.0.0"
12
+ description:
13
+ - Stops a running VM and deletes it along with all its disks.
14
+ - Idempotent — succeeds if the VM does not exist.
15
+ - Target VM by name or ID.
16
+ options:
17
+ api_url:
18
+ description: Proxmox API URL.
19
+ required: true
20
+ type: str
21
+ api_token:
22
+ description: Proxmox API token in format C(user@realm!tokenname=tokenvalue).
23
+ required: true
24
+ type: str
25
+ no_log: true
26
+ verify_ssl:
27
+ description: Whether to verify SSL certificates.
28
+ type: bool
29
+ default: false
30
+ node:
31
+ description: Proxmox cluster node name.
32
+ required: true
33
+ type: str
34
+ vm_name:
35
+ description: Name of the VM to deprovision. Mutually exclusive with I(vm_id).
36
+ type: str
37
+ vm_id:
38
+ description: ID of the VM to deprovision. Mutually exclusive with I(vm_name).
39
+ type: int
40
+ requirements:
41
+ - "python >= 3.13"
42
+ - "arcane-mage >= 2.0.0"
43
+ author:
44
+ - RunOnFlux
45
+ """
46
+
47
+ EXAMPLES = r"""
48
+ - name: Remove a FluxNode VM by name
49
+ runonflux.arcane_mage.deprovision:
50
+ api_url: "https://65.109.86.37:8006"
51
+ api_token: "{{ proxmox_token }}"
52
+ node: tsa
53
+ vm_name: tsa-vs1
54
+ delegate_to: localhost
55
+
56
+ - name: Remove a FluxNode VM by ID
57
+ runonflux.arcane_mage.deprovision:
58
+ api_url: "https://65.109.86.37:8006"
59
+ api_token: "{{ proxmox_token }}"
60
+ node: tsa
61
+ vm_id: 200
62
+ delegate_to: localhost
63
+ """
64
+
65
+ RETURN = r"""
66
+ deprovision_log:
67
+ description: List of deprovision step messages.
68
+ type: list
69
+ elements: str
70
+ returned: always
71
+ """
72
+
73
+ import asyncio
74
+
75
+ from ansible.module_utils.basic import AnsibleModule
76
+
77
+
78
+ async def run_deprovision(params):
79
+ """Execute the deprovisioning workflow."""
80
+ from arcane_mage import Provisioner, ProxmoxApi
81
+
82
+ token = ProxmoxApi.parse_token(params["api_token"])
83
+ if not token:
84
+ return False, ["Invalid API token format. Expected: user@realm!tokenname=tokenvalue"]
85
+
86
+ api = ProxmoxApi.from_token(params["api_url"], token, verify_ssl=params["verify_ssl"])
87
+
88
+ log_messages = []
89
+
90
+ def callback(ok, msg):
91
+ log_messages.append(f"{'OK' if ok else 'FAIL'}: {msg}")
92
+
93
+ async with api:
94
+ provisioner = Provisioner(api)
95
+
96
+ # Check if VM exists first
97
+ vms_res = await api.get_vms(params["node"])
98
+ if not vms_res:
99
+ return False, ["Unable to list VMs on node"]
100
+
101
+ vm_name = params.get("vm_name")
102
+ vm_id = params.get("vm_id")
103
+
104
+ if vm_name:
105
+ existing = next((v for v in vms_res.payload if v.get("name") == vm_name), None)
106
+ else:
107
+ existing = next((v for v in vms_res.payload if v.get("vmid") == vm_id), None)
108
+
109
+ if not existing:
110
+ identifier = vm_name or str(vm_id)
111
+ return True, [f"VM '{identifier}' does not exist — nothing to do"]
112
+
113
+ success = await provisioner.deprovision_vm(
114
+ params["node"],
115
+ callback=callback,
116
+ vm_name=vm_name,
117
+ vm_id=vm_id,
118
+ )
119
+
120
+ return success, log_messages
121
+
122
+
123
+ def main():
124
+ module_args = dict(
125
+ api_url=dict(type="str", required=True),
126
+ api_token=dict(type="str", required=True, no_log=True),
127
+ verify_ssl=dict(type="bool", default=False),
128
+ node=dict(type="str", required=True),
129
+ vm_name=dict(type="str"),
130
+ vm_id=dict(type="int"),
131
+ )
132
+
133
+ module = AnsibleModule(
134
+ argument_spec=module_args,
135
+ mutually_exclusive=[("vm_name", "vm_id")],
136
+ required_one_of=[("vm_name", "vm_id")],
137
+ supports_check_mode=True,
138
+ )
139
+
140
+ if module.check_mode:
141
+ module.exit_json(changed=False, deprovision_log=["Check mode — no changes made"])
142
+
143
+ try:
144
+ success, log_messages = asyncio.run(run_deprovision(module.params))
145
+ except ImportError:
146
+ module.fail_json(msg="arcane-mage library is not installed. Install with: pip install arcane-mage")
147
+ except Exception as e:
148
+ module.fail_json(msg=f"Deprovision error: {e}")
149
+
150
+ if not success:
151
+ module.fail_json(
152
+ msg="Failed to deprovision VM",
153
+ deprovision_log=log_messages,
154
+ )
155
+
156
+ already_absent = any("does not exist" in m for m in log_messages)
157
+
158
+ module.exit_json(
159
+ changed=not already_absent,
160
+ deprovision_log=log_messages,
161
+ )
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # Copyright: (c) 2025, RunOnFlux
5
+ # License: MIT
6
+
7
+ DOCUMENTATION = r"""
8
+ ---
9
+ module: iso_info
10
+ short_description: Get the latest ArcaneOS ISO version
11
+ version_added: "1.0.0"
12
+ description:
13
+ - Queries the Flux release API for the latest ArcaneOS ISO filename.
14
+ - Optionally downloads the ISO to a Proxmox node via the Proxmox API.
15
+ - Useful for ensuring all hosts have the latest ISO before provisioning.
16
+ options:
17
+ api_url:
18
+ description: >
19
+ Proxmox API URL. Required only when I(download=true).
20
+ type: str
21
+ api_token:
22
+ description: >
23
+ Proxmox API token. Required only when I(download=true).
24
+ type: str
25
+ no_log: true
26
+ verify_ssl:
27
+ description: Whether to verify SSL certificates.
28
+ type: bool
29
+ default: false
30
+ node:
31
+ description: >
32
+ Proxmox cluster node name. Required when I(download=true).
33
+ type: str
34
+ storage_iso:
35
+ description: Storage backend for ISO files.
36
+ type: str
37
+ default: local
38
+ download:
39
+ description: >
40
+ Whether to download the latest ISO to the Proxmox node.
41
+ Uses the Proxmox API download-url endpoint.
42
+ type: bool
43
+ default: false
44
+ requirements:
45
+ - "python >= 3.13"
46
+ - "arcane-mage >= 2.0.0"
47
+ author:
48
+ - RunOnFlux
49
+ """
50
+
51
+ EXAMPLES = r"""
52
+ - name: Get latest ISO version
53
+ runonflux.arcane_mage.iso_info:
54
+ delegate_to: localhost
55
+ register: iso_result
56
+
57
+ - name: Download latest ISO to Proxmox node
58
+ runonflux.arcane_mage.iso_info:
59
+ api_url: "https://65.109.86.37:8006"
60
+ api_token: "{{ proxmox_token }}"
61
+ node: tsa
62
+ download: true
63
+ delegate_to: localhost
64
+
65
+ - name: Use ISO name in provisioning
66
+ runonflux.arcane_mage.provision:
67
+ iso_name: "{{ iso_result.iso_name }}"
68
+ # ... other params
69
+ delegate_to: localhost
70
+ """
71
+
72
+ RETURN = r"""
73
+ iso_name:
74
+ description: The latest ISO filename (e.g., FluxLive-1775071308.iso).
75
+ type: str
76
+ returned: success
77
+ downloaded:
78
+ description: Whether the ISO was downloaded to the node.
79
+ type: bool
80
+ returned: always
81
+ """
82
+
83
+ import asyncio
84
+
85
+ from ansible.module_utils.basic import AnsibleModule
86
+
87
+
88
+ async def run_iso_info(params):
89
+ """Fetch latest ISO info and optionally download."""
90
+ from arcane_mage import ProxmoxApi, get_latest_iso_version
91
+
92
+ iso_name = await get_latest_iso_version()
93
+
94
+ if not iso_name:
95
+ return False, None, False, "Unable to fetch latest ISO version from release API"
96
+
97
+ if not params.get("download"):
98
+ return True, iso_name, False, ""
99
+
100
+ # Download to Proxmox node
101
+ if not params.get("api_url") or not params.get("api_token") or not params.get("node"):
102
+ return False, iso_name, False, "api_url, api_token, and node are required for download"
103
+
104
+ token = ProxmoxApi.parse_token(params["api_token"])
105
+ if not token:
106
+ return False, iso_name, False, "Invalid API token format"
107
+
108
+ api = ProxmoxApi.from_token(params["api_url"], token, verify_ssl=params["verify_ssl"])
109
+
110
+ async with api:
111
+ # Check if ISO already exists
112
+ content_res = await api.get_storage_content(params["node"], params["storage_iso"])
113
+ if content_res:
114
+ existing = next(
115
+ (c for c in content_res.payload if c.get("volid", "").endswith(iso_name)),
116
+ None,
117
+ )
118
+ if existing:
119
+ return True, iso_name, False, ""
120
+
121
+ # Download via Proxmox API (direct POST without verify-certificates param)
122
+ # URL format: https://images.runonflux.io/arcane/releases/<build>/FluxLive-<build>.iso
123
+ build_id = iso_name.replace("FluxLive-", "").replace(".iso", "")
124
+ download_url = f"https://images.runonflux.io/arcane/releases/{build_id}/{iso_name}"
125
+
126
+ endpoint = f"nodes/{params['node']}/storage/{params['storage_iso']}/download-url"
127
+ data = {"content": "iso", "filename": iso_name, "url": download_url}
128
+
129
+ res = await api._do_post(endpoint, data=data)
130
+
131
+ if not res:
132
+ error_detail = f"status={res.status} error={res.error}" if res else "no response"
133
+ return False, iso_name, False, f"Failed to initiate ISO download: {error_detail}"
134
+
135
+ # Wait for download task
136
+ ok = await api.wait_for_task(res.payload, params["node"], max_wait_s=600)
137
+ if not ok:
138
+ return False, iso_name, False, "ISO download task did not complete in time"
139
+
140
+ return True, iso_name, True, ""
141
+
142
+
143
+ def main():
144
+ module_args = dict(
145
+ api_url=dict(type="str"),
146
+ api_token=dict(type="str", no_log=True),
147
+ verify_ssl=dict(type="bool", default=False),
148
+ node=dict(type="str"),
149
+ storage_iso=dict(type="str", default="local"),
150
+ download=dict(type="bool", default=False),
151
+ )
152
+
153
+ module = AnsibleModule(
154
+ argument_spec=module_args,
155
+ required_if=[
156
+ ("download", True, ["api_url", "api_token", "node"]),
157
+ ],
158
+ supports_check_mode=True,
159
+ )
160
+
161
+ if module.check_mode:
162
+ module.exit_json(changed=False, iso_name="", downloaded=False)
163
+
164
+ try:
165
+ success, iso_name, downloaded, error = asyncio.run(run_iso_info(module.params))
166
+ except ImportError:
167
+ module.fail_json(msg="arcane-mage library is not installed. Install with: pip install arcane-mage")
168
+ except Exception as e:
169
+ module.fail_json(msg=f"ISO info error: {e}")
170
+
171
+ if not success:
172
+ module.fail_json(msg=error, iso_name=iso_name or "")
173
+
174
+ module.exit_json(changed=downloaded, iso_name=iso_name, downloaded=downloaded)
175
+
176
+
177
+ if __name__ == "__main__":
178
+ main()