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.
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/.gitignore +1 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/CHANGELOG.md +6 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/PKG-INFO +2 -2
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/README.md +108 -0
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/galaxy.yml +21 -0
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/meta/runtime.yml +1 -0
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/deprovision.py +165 -0
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/iso_info.py +178 -0
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/provision.py +456 -0
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/status.py +153 -0
- arcane_mage-2.2.0/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py +186 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/pyproject.toml +1 -1
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/fluxnode.py +32 -4
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/network.py +3 -5
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_fluxnode.py +40 -3
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/uv.lock +689 -689
- arcane_mage-2.0.0/arcane_mage.log +0 -4
- arcane_mage-2.0.0/src/arcane_mage/images/arcane_mage.log +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/.claude/settings.local.json +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/LICENSE +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/README.md +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/docs/configuration.md +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/docs/keyring.md +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/docs/proxmox-setup.md +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/all_settings.yaml +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/merge_anchors.yaml +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/minimal.yaml +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/examples/typical.yaml +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_local_storage.png +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_permissions.png +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_user.png +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/proxmox_user_api_token.png +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/__init__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/__main__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/arcane_mage.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/arcane_mage.tcss +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/fat_writer.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/helpers.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/images/__init__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/images/arcane_config.raw.gz +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/images/arcane_efi.raw.gz +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/log.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/messages.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/__init__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/config.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/delegate.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/hypervisor.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/identity.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/installer.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/notifications.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/models/system.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/password.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/provisioner.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/proxmox.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/py.typed +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/__init__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/confirm_screen.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/hypervisor.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/provision_node.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/provisioning_info.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/welcome.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/screens/welcome_proxmox.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/tpm_keyring.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/widgets/__init__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/src/arcane_mage/widgets/spinner.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/testing123.yaml +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/__init__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/conftest.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_fat_writer.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_helpers.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/__init__.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_config_group.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_delegate.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_hypervisor.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_identity.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_installer.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_network.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_notifications.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_models/test_system.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_password.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_provisioner.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_proxmox.py +0 -0
- {arcane_mage-2.0.0 → arcane_mage-2.2.0}/tests/test_public_api.py +0 -0
|
@@ -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()
|