arcane-mage 2.2.0__tar.gz → 2.3.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.2.0 → arcane_mage-2.3.0}/PKG-INFO +1 -1
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/pyproject.toml +1 -1
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/__main__.py +106 -60
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/arcane_mage.py +12 -36
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/fat_writer.py +2 -7
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/helpers.py +2 -6
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/log.py +24 -22
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/config.py +4 -1
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/delegate.py +1 -2
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/hypervisor.py +3 -6
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/network.py +31 -23
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/notifications.py +0 -1
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/password.py +1 -3
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/provisioner.py +12 -7
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/proxmox.py +8 -24
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/hypervisor.py +5 -15
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/provision_node.py +3 -13
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/welcome.py +1 -3
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/welcome_proxmox.py +1 -0
- arcane_mage-2.3.0/tests/test_models/test_chain_sources.py +110 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_fluxnode.py +9 -6
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_identity.py +0 -1
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_provisioner.py +3 -1
- arcane_mage-2.3.0/uv.lock +1579 -0
- arcane_mage-2.2.0/uv.lock +0 -1314
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/.claude/settings.local.json +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/.gitignore +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/CHANGELOG.md +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/LICENSE +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/README.md +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/README.md +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/galaxy.yml +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/meta/runtime.yml +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/deprovision.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/iso_info.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/provision.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/status.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/docs/configuration.md +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/docs/keyring.md +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/docs/proxmox-setup.md +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/all_settings.yaml +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/merge_anchors.yaml +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/minimal.yaml +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/typical.yaml +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_local_storage.png +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_permissions.png +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_user.png +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_user_api_token.png +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/__init__.py +2 -2
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/arcane_mage.tcss +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/images/__init__.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/images/arcane_config.raw.gz +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/images/arcane_efi.raw.gz +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/messages.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/__init__.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/fluxnode.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/identity.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/installer.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/system.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/py.typed +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/__init__.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/confirm_screen.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/provisioning_info.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/tpm_keyring.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/widgets/__init__.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/widgets/spinner.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/testing123.yaml +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/__init__.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/conftest.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_fat_writer.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_helpers.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/__init__.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_config_group.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_delegate.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_hypervisor.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_installer.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_network.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_notifications.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_system.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_password.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_proxmox.py +0 -0
- {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_public_api.py +0 -0
|
@@ -14,7 +14,6 @@ import os
|
|
|
14
14
|
import sys
|
|
15
15
|
from importlib.metadata import version as get_version
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Optional
|
|
18
17
|
from urllib.parse import urlparse
|
|
19
18
|
|
|
20
19
|
from rich.console import Console
|
|
@@ -145,9 +144,9 @@ def _resolve_hypervisor(hypervisor: str) -> ResolvedConnection:
|
|
|
145
144
|
|
|
146
145
|
|
|
147
146
|
def _resolve_connection(
|
|
148
|
-
url:
|
|
149
|
-
token:
|
|
150
|
-
hypervisor:
|
|
147
|
+
url: str | None,
|
|
148
|
+
token: str | None,
|
|
149
|
+
hypervisor: str | None,
|
|
151
150
|
) -> ResolvedConnection:
|
|
152
151
|
"""Resolve URL and parsed token from flags, env vars, stdin, or stored hypervisor."""
|
|
153
152
|
if hypervisor:
|
|
@@ -184,7 +183,9 @@ def _load_configs(config: str) -> ArcaneOsConfigGroup:
|
|
|
184
183
|
|
|
185
184
|
@app.callback()
|
|
186
185
|
def main(
|
|
187
|
-
version:
|
|
186
|
+
version: bool | None = typer.Option(
|
|
187
|
+
None, "-v", "--version", callback=version_callback, is_eager=True, help="Show the application version"
|
|
188
|
+
),
|
|
188
189
|
) -> None:
|
|
189
190
|
pass
|
|
190
191
|
|
|
@@ -197,10 +198,7 @@ def tui(
|
|
|
197
198
|
try:
|
|
198
199
|
from .arcane_mage import ArcaneMage
|
|
199
200
|
except ModuleNotFoundError:
|
|
200
|
-
raise SystemExit(
|
|
201
|
-
"arcane-mage TUI requires the 'tui' extra.\n"
|
|
202
|
-
"Install with: uv add arcane-mage[tui]"
|
|
203
|
-
) from None
|
|
201
|
+
raise SystemExit("arcane-mage TUI requires the 'tui' extra.\nInstall with: uv add arcane-mage[tui]") from None
|
|
204
202
|
|
|
205
203
|
tui_app = ArcaneMage(fluxnode_config=config)
|
|
206
204
|
tui_app.run()
|
|
@@ -208,12 +206,12 @@ def tui(
|
|
|
208
206
|
|
|
209
207
|
@app.command()
|
|
210
208
|
def provision(
|
|
211
|
-
url:
|
|
212
|
-
token:
|
|
213
|
-
hypervisor:
|
|
209
|
+
url: str | None = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
|
|
210
|
+
token: str | None = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
|
|
211
|
+
hypervisor: str | None = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
|
|
214
212
|
config: str = typer.Option("fluxnodes.yaml", "-c", "--config", help="The config file"),
|
|
215
|
-
node_filter:
|
|
216
|
-
start:
|
|
213
|
+
node_filter: str | None = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
|
|
214
|
+
start: bool | None = typer.Option(None, "--start/--no-start", help="Override start_on_creation setting"),
|
|
217
215
|
use_json: bool = typer.Option(False, "--json", help="Output JSON instead of text"),
|
|
218
216
|
) -> None:
|
|
219
217
|
"""Provision Fluxnodes on a Proxmox hypervisor via the API."""
|
|
@@ -250,7 +248,7 @@ def provision(
|
|
|
250
248
|
|
|
251
249
|
steps: list[dict[str, object]] = []
|
|
252
250
|
|
|
253
|
-
def callback(ok: bool, msg: str):
|
|
251
|
+
def callback(ok: bool, msg: str, steps: list[dict[str, object]] = steps):
|
|
254
252
|
steps.append({"ok": ok, "message": msg})
|
|
255
253
|
if not use_json:
|
|
256
254
|
mark = _CHECK_MARK if ok else _CROSS_MARK
|
|
@@ -264,15 +262,17 @@ def provision(
|
|
|
264
262
|
|
|
265
263
|
if not use_json:
|
|
266
264
|
if result:
|
|
267
|
-
console.print(
|
|
265
|
+
console.print(" [bold green]Provisioned successfully[/bold green]")
|
|
268
266
|
else:
|
|
269
|
-
console.print(
|
|
267
|
+
console.print(" [bold red]Provisioning failed[/bold red]")
|
|
270
268
|
|
|
271
269
|
if not result:
|
|
272
270
|
all_ok = False
|
|
273
271
|
|
|
274
272
|
if use_json:
|
|
275
|
-
print(
|
|
273
|
+
print(
|
|
274
|
+
_json_ok({"nodes": results}) if all_ok else _json_error("Provisioning failed", {"nodes": results})
|
|
275
|
+
)
|
|
276
276
|
|
|
277
277
|
return all_ok
|
|
278
278
|
|
|
@@ -287,13 +287,13 @@ def provision(
|
|
|
287
287
|
|
|
288
288
|
@app.command()
|
|
289
289
|
def deprovision(
|
|
290
|
-
url:
|
|
291
|
-
token:
|
|
292
|
-
hypervisor:
|
|
293
|
-
config:
|
|
294
|
-
vm_name:
|
|
295
|
-
vm_id:
|
|
296
|
-
node_filter:
|
|
290
|
+
url: str | None = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
|
|
291
|
+
token: str | None = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
|
|
292
|
+
hypervisor: str | None = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
|
|
293
|
+
config: str | None = typer.Option(None, "-c", "--config", help="The config file"),
|
|
294
|
+
vm_name: str | None = typer.Option(None, "--vm-name", help="Target a specific VM by name"),
|
|
295
|
+
vm_id: int | None = typer.Option(None, "--vm-id", help="Target a specific VM by ID"),
|
|
296
|
+
node_filter: str | None = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
|
|
297
297
|
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
|
|
298
298
|
use_json: bool = typer.Option(False, "--json", help="Output JSON instead of text"),
|
|
299
299
|
) -> None:
|
|
@@ -324,6 +324,7 @@ def deprovision(
|
|
|
324
324
|
_handle_error(e, use_json)
|
|
325
325
|
|
|
326
326
|
if direct_vm:
|
|
327
|
+
|
|
327
328
|
async def run_direct():
|
|
328
329
|
async with ProxmoxApi.from_token(conn.url, conn.token) as api:
|
|
329
330
|
provisioner = Provisioner(api)
|
|
@@ -362,7 +363,7 @@ def deprovision(
|
|
|
362
363
|
vm_status = found_vm.get("status", "unknown")
|
|
363
364
|
|
|
364
365
|
if not force and not use_json:
|
|
365
|
-
console.print(
|
|
366
|
+
console.print("[bold red]This will stop and delete:[/bold red]")
|
|
366
367
|
console.print(f" VM: {vm_display} (id={found_vm['vmid']})")
|
|
367
368
|
console.print(f" Node: {found_node}")
|
|
368
369
|
console.print(f" Status: {vm_status}")
|
|
@@ -371,7 +372,7 @@ def deprovision(
|
|
|
371
372
|
|
|
372
373
|
steps: list[dict[str, object]] = []
|
|
373
374
|
|
|
374
|
-
def callback(ok: bool, msg: str):
|
|
375
|
+
def callback(ok: bool, msg: str, steps: list[dict[str, object]] = steps):
|
|
375
376
|
steps.append({"ok": ok, "message": msg})
|
|
376
377
|
if not use_json:
|
|
377
378
|
mark = _CHECK_MARK if ok else _CROSS_MARK
|
|
@@ -381,7 +382,11 @@ def deprovision(
|
|
|
381
382
|
console.print(f"\n[bold]{vm_display}[/bold]")
|
|
382
383
|
|
|
383
384
|
result = await provisioner._stop_and_delete_vm(
|
|
384
|
-
found_vm["vmid"],
|
|
385
|
+
found_vm["vmid"],
|
|
386
|
+
vm_display,
|
|
387
|
+
vm_status,
|
|
388
|
+
found_node,
|
|
389
|
+
callback,
|
|
385
390
|
)
|
|
386
391
|
|
|
387
392
|
vm_info = {"vm_name": vm_display, "vm_id": found_vm["vmid"], "node": found_node, "steps": steps}
|
|
@@ -390,9 +395,9 @@ def deprovision(
|
|
|
390
395
|
print(_json_ok(vm_info) if result else _json_error("Deprovisioning failed", vm_info))
|
|
391
396
|
else:
|
|
392
397
|
if result:
|
|
393
|
-
console.print(
|
|
398
|
+
console.print(" [bold green]Deprovisioned successfully[/bold green]")
|
|
394
399
|
else:
|
|
395
|
-
console.print(
|
|
400
|
+
console.print(" [bold red]Deprovisioning failed[/bold red]")
|
|
396
401
|
|
|
397
402
|
return result
|
|
398
403
|
|
|
@@ -418,7 +423,7 @@ def deprovision(
|
|
|
418
423
|
_handle_error(CliError(f"No nodes match filter '{node_filter}'"), use_json)
|
|
419
424
|
|
|
420
425
|
if not force and not use_json:
|
|
421
|
-
console.print(
|
|
426
|
+
console.print("[bold red]This will stop and delete the following VMs:[/bold red]")
|
|
422
427
|
for node in nodes:
|
|
423
428
|
hyper_node = node.hypervisor.node if node.hypervisor else "?"
|
|
424
429
|
vm = node.hypervisor.vm_name if node.hypervisor else node.system.hostname
|
|
@@ -436,7 +441,7 @@ def deprovision(
|
|
|
436
441
|
hostname = node.system.hostname
|
|
437
442
|
steps: list[dict[str, object]] = []
|
|
438
443
|
|
|
439
|
-
def callback(ok: bool, msg: str):
|
|
444
|
+
def callback(ok: bool, msg: str, steps: list[dict[str, object]] = steps):
|
|
440
445
|
steps.append({"ok": ok, "message": msg})
|
|
441
446
|
if not use_json:
|
|
442
447
|
mark = _CHECK_MARK if ok else _CROSS_MARK
|
|
@@ -450,15 +455,17 @@ def deprovision(
|
|
|
450
455
|
|
|
451
456
|
if not use_json:
|
|
452
457
|
if result:
|
|
453
|
-
console.print(
|
|
458
|
+
console.print(" [bold green]Deprovisioned successfully[/bold green]")
|
|
454
459
|
else:
|
|
455
|
-
console.print(
|
|
460
|
+
console.print(" [bold red]Deprovisioning failed[/bold red]")
|
|
456
461
|
|
|
457
462
|
if not result:
|
|
458
463
|
all_ok = False
|
|
459
464
|
|
|
460
465
|
if use_json:
|
|
461
|
-
print(
|
|
466
|
+
print(
|
|
467
|
+
_json_ok({"nodes": results}) if all_ok else _json_error("Deprovisioning failed", {"nodes": results})
|
|
468
|
+
)
|
|
462
469
|
|
|
463
470
|
return all_ok
|
|
464
471
|
|
|
@@ -473,11 +480,11 @@ def deprovision(
|
|
|
473
480
|
|
|
474
481
|
@app.command()
|
|
475
482
|
def validate(
|
|
476
|
-
url:
|
|
477
|
-
token:
|
|
478
|
-
hypervisor:
|
|
483
|
+
url: str | None = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
|
|
484
|
+
token: str | None = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
|
|
485
|
+
hypervisor: str | None = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
|
|
479
486
|
config: str = typer.Option("fluxnodes.yaml", "-c", "--config", help="The config file"),
|
|
480
|
-
node_filter:
|
|
487
|
+
node_filter: str | None = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
|
|
481
488
|
use_json: bool = typer.Option(False, "--json", help="Output JSON instead of text"),
|
|
482
489
|
) -> None:
|
|
483
490
|
"""Check config, API connectivity, storage, ISO, and network without provisioning."""
|
|
@@ -511,27 +518,62 @@ def validate(
|
|
|
511
518
|
hyper = node.hypervisor
|
|
512
519
|
|
|
513
520
|
if not hyper:
|
|
514
|
-
results.append(
|
|
521
|
+
results.append(
|
|
522
|
+
{
|
|
523
|
+
"hostname": hostname,
|
|
524
|
+
"ok": False,
|
|
525
|
+
"checks": [{"check": "hypervisor", "ok": False, "message": "No hypervisor config"}],
|
|
526
|
+
}
|
|
527
|
+
)
|
|
515
528
|
if not use_json:
|
|
516
|
-
console.print(
|
|
529
|
+
console.print(
|
|
530
|
+
Panel(
|
|
531
|
+
"[red]No hypervisor config[/red]", title=f"[bold]{hostname}[/bold]", border_style="red"
|
|
532
|
+
)
|
|
533
|
+
)
|
|
517
534
|
all_ok = False
|
|
518
535
|
continue
|
|
519
536
|
|
|
520
537
|
checks = []
|
|
521
538
|
|
|
522
539
|
api_ok, api_msg = await provisioner.validate_api_version(hyper.node)
|
|
523
|
-
checks.append(
|
|
540
|
+
checks.append(
|
|
541
|
+
{"check": "api_version", "ok": api_ok, "message": api_msg if not api_ok else f"v{api_msg}"}
|
|
542
|
+
)
|
|
524
543
|
|
|
525
544
|
storage_ok, storage_msg = await provisioner.validate_storage(
|
|
526
|
-
hyper.node,
|
|
545
|
+
hyper.node,
|
|
546
|
+
hyper.storage_iso,
|
|
547
|
+
hyper.storage_images,
|
|
548
|
+
hyper.storage_import,
|
|
549
|
+
)
|
|
550
|
+
checks.append(
|
|
551
|
+
{
|
|
552
|
+
"check": "storage",
|
|
553
|
+
"ok": storage_ok,
|
|
554
|
+
"message": storage_msg
|
|
555
|
+
if storage_msg
|
|
556
|
+
else f"iso={hyper.storage_iso}, images={hyper.storage_images}, import={hyper.storage_import}",
|
|
557
|
+
}
|
|
527
558
|
)
|
|
528
|
-
checks.append({"check": "storage", "ok": storage_ok, "message": storage_msg if storage_msg else f"iso={hyper.storage_iso}, images={hyper.storage_images}, import={hyper.storage_import}"})
|
|
529
559
|
|
|
530
560
|
iso_ok = await provisioner.validate_iso_version(hyper.node, hyper.iso_name, hyper.storage_iso)
|
|
531
|
-
checks.append(
|
|
561
|
+
checks.append(
|
|
562
|
+
{
|
|
563
|
+
"check": "iso",
|
|
564
|
+
"ok": iso_ok,
|
|
565
|
+
"message": f"{hyper.iso_name} not found" if not iso_ok else hyper.iso_name,
|
|
566
|
+
}
|
|
567
|
+
)
|
|
532
568
|
|
|
533
569
|
net_ok = await provisioner.validate_network(hyper.node, hyper.network)
|
|
534
|
-
checks.append(
|
|
570
|
+
checks.append(
|
|
571
|
+
{
|
|
572
|
+
"check": "network",
|
|
573
|
+
"ok": net_ok,
|
|
574
|
+
"message": f"Bridge {hyper.network} not found" if not net_ok else hyper.network,
|
|
575
|
+
}
|
|
576
|
+
)
|
|
535
577
|
|
|
536
578
|
node_ok = all(c["ok"] for c in checks)
|
|
537
579
|
results.append({"hostname": hostname, "node": hyper.node, "ok": node_ok, "checks": checks})
|
|
@@ -550,7 +592,9 @@ def validate(
|
|
|
550
592
|
table.add_row(mark, c["check"].replace("_", " ").title(), detail)
|
|
551
593
|
|
|
552
594
|
border = "green" if node_ok else "red"
|
|
553
|
-
console.print(
|
|
595
|
+
console.print(
|
|
596
|
+
Panel(table, title=f"[bold]{hostname}[/bold] @ [bold]{hyper.node}[/bold]", border_style=border)
|
|
597
|
+
)
|
|
554
598
|
|
|
555
599
|
if use_json:
|
|
556
600
|
print(_json_ok({"nodes": results}) if all_ok else _json_error("Validation failed", {"nodes": results}))
|
|
@@ -568,9 +612,9 @@ def validate(
|
|
|
568
612
|
|
|
569
613
|
@app.command()
|
|
570
614
|
def ping(
|
|
571
|
-
url:
|
|
572
|
-
token:
|
|
573
|
-
hypervisor:
|
|
615
|
+
url: str | None = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
|
|
616
|
+
token: str | None = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
|
|
617
|
+
hypervisor: str | None = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
|
|
574
618
|
use_json: bool = typer.Option(False, "--json", help="Output JSON instead of text"),
|
|
575
619
|
) -> None:
|
|
576
620
|
"""Test connectivity and authentication to a Proxmox hypervisor."""
|
|
@@ -635,11 +679,11 @@ def ping(
|
|
|
635
679
|
|
|
636
680
|
@app.command()
|
|
637
681
|
def status(
|
|
638
|
-
url:
|
|
639
|
-
token:
|
|
640
|
-
hypervisor:
|
|
682
|
+
url: str | None = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
|
|
683
|
+
token: str | None = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
|
|
684
|
+
hypervisor: str | None = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
|
|
641
685
|
config: str = typer.Option("fluxnodes.yaml", "-c", "--config", help="The config file"),
|
|
642
|
-
node_filter:
|
|
686
|
+
node_filter: str | None = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
|
|
643
687
|
use_json: bool = typer.Option(False, "--json", help="Output JSON instead of text"),
|
|
644
688
|
) -> None:
|
|
645
689
|
"""Show which configured nodes are already provisioned on the hypervisor."""
|
|
@@ -693,12 +737,14 @@ def status(
|
|
|
693
737
|
provisioned = True
|
|
694
738
|
break
|
|
695
739
|
|
|
696
|
-
results.append(
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
740
|
+
results.append(
|
|
741
|
+
{
|
|
742
|
+
"hostname": hostname,
|
|
743
|
+
"vm_name": hyper.vm_name,
|
|
744
|
+
"node": hyper.node,
|
|
745
|
+
"provisioned": provisioned,
|
|
746
|
+
}
|
|
747
|
+
)
|
|
702
748
|
|
|
703
749
|
status_text = "[green]provisioned[/green]" if provisioned else "[yellow]not provisioned[/yellow]"
|
|
704
750
|
table.add_row(hostname, hyper.vm_name, hyper.node, status_text)
|
|
@@ -46,9 +46,7 @@ class ArcaneMage(App):
|
|
|
46
46
|
self.push_screen("welcome")
|
|
47
47
|
|
|
48
48
|
if self.config.default_page == "welcome-proxmox":
|
|
49
|
-
screen = WelcomeScreenProxmox(
|
|
50
|
-
self.config.hypervisors, self.fluxnodes, is_homepage=True
|
|
51
|
-
)
|
|
49
|
+
screen = WelcomeScreenProxmox(self.config.hypervisors, self.fluxnodes, is_homepage=True)
|
|
52
50
|
self.install_screen(screen, name="welcome-proxmox")
|
|
53
51
|
self.push_screen("welcome-proxmox")
|
|
54
52
|
|
|
@@ -66,9 +64,7 @@ class ArcaneMage(App):
|
|
|
66
64
|
screen = self.get_screen("welcome-proxmox", WelcomeScreenProxmox)
|
|
67
65
|
screen.validate_hypervisors(hypervisor)
|
|
68
66
|
|
|
69
|
-
def del_hypervisor_callback(
|
|
70
|
-
self, hypervisor: HypervisorConfig, delete: bool
|
|
71
|
-
) -> None:
|
|
67
|
+
def del_hypervisor_callback(self, hypervisor: HypervisorConfig, delete: bool) -> None:
|
|
72
68
|
if not delete:
|
|
73
69
|
return
|
|
74
70
|
|
|
@@ -76,17 +72,13 @@ class ArcaneMage(App):
|
|
|
76
72
|
screen = self.get_screen("welcome-proxmox", WelcomeScreenProxmox)
|
|
77
73
|
screen.validate_hypervisors()
|
|
78
74
|
|
|
79
|
-
async def provision_node_callback(
|
|
80
|
-
self, fluxnode: ArcaneOsConfig | None, delete_efi: bool = True
|
|
81
|
-
) -> Worker | None:
|
|
75
|
+
async def provision_node_callback(self, fluxnode: ArcaneOsConfig | None, delete_efi: bool = True) -> Worker | None:
|
|
82
76
|
if not fluxnode or not fluxnode.hypervisor:
|
|
83
77
|
return None
|
|
84
78
|
|
|
85
79
|
vm_name = fluxnode.hypervisor.vm_name
|
|
86
80
|
total_steps = 9 if fluxnode.hypervisor.start_on_creation else 8
|
|
87
|
-
info_screen = ProvisioningInfoScreen(
|
|
88
|
-
vm_name=vm_name, total_steps=total_steps
|
|
89
|
-
)
|
|
81
|
+
info_screen = ProvisioningInfoScreen(vm_name=vm_name, total_steps=total_steps)
|
|
90
82
|
|
|
91
83
|
# this is a bit rugged
|
|
92
84
|
mounted = self.push_screen(info_screen)
|
|
@@ -103,12 +95,8 @@ class ArcaneMage(App):
|
|
|
103
95
|
|
|
104
96
|
@on(ScreenRequested)
|
|
105
97
|
def on_screen_requested(self, event: ScreenRequested) -> None:
|
|
106
|
-
if event.screen == "welcome-proxmox" and not self.is_screen_installed(
|
|
107
|
-
|
|
108
|
-
):
|
|
109
|
-
screen = WelcomeScreenProxmox(
|
|
110
|
-
self.config.hypervisors, self.fluxnodes
|
|
111
|
-
)
|
|
98
|
+
if event.screen == "welcome-proxmox" and not self.is_screen_installed("welcome-proxmox"):
|
|
99
|
+
screen = WelcomeScreenProxmox(self.config.hypervisors, self.fluxnodes)
|
|
112
100
|
self.install_screen(screen, name="welcome-proxmox")
|
|
113
101
|
elif event.screen != "welcome-proxmox":
|
|
114
102
|
self.notify("Not implemented yet")
|
|
@@ -123,9 +111,7 @@ class ArcaneMage(App):
|
|
|
123
111
|
# this is blocking but meh
|
|
124
112
|
self.config.update_default_page(default)
|
|
125
113
|
|
|
126
|
-
def edit_hypervisor_callback(
|
|
127
|
-
self, old: HypervisorConfig, new: HypervisorConfig | None
|
|
128
|
-
) -> None:
|
|
114
|
+
def edit_hypervisor_callback(self, old: HypervisorConfig, new: HypervisorConfig | None) -> None:
|
|
129
115
|
if not new:
|
|
130
116
|
return
|
|
131
117
|
|
|
@@ -146,18 +132,14 @@ class ArcaneMage(App):
|
|
|
146
132
|
)
|
|
147
133
|
|
|
148
134
|
@on(WelcomeScreenProxmox.EditHypervisor)
|
|
149
|
-
def on_edit_hypervisor(
|
|
150
|
-
self, event: WelcomeScreenProxmox.EditHypervisor
|
|
151
|
-
) -> None:
|
|
135
|
+
def on_edit_hypervisor(self, event: WelcomeScreenProxmox.EditHypervisor) -> None:
|
|
152
136
|
self.push_screen(
|
|
153
137
|
AddHypervisorScreen(self.config.use_keyring, existing=event.hypervisor),
|
|
154
138
|
partial(self.edit_hypervisor_callback, event.hypervisor),
|
|
155
139
|
)
|
|
156
140
|
|
|
157
141
|
@on(WelcomeScreenProxmox.DelHypervisor)
|
|
158
|
-
def on_del_hypervisor(
|
|
159
|
-
self, event: WelcomeScreenProxmox.DelHypervisor
|
|
160
|
-
) -> None:
|
|
142
|
+
def on_del_hypervisor(self, event: WelcomeScreenProxmox.DelHypervisor) -> None:
|
|
161
143
|
dialog = f"Are you sure?\n\n This will delete hypervisor: {event.hypervisor.url}"
|
|
162
144
|
self.push_screen(
|
|
163
145
|
ConfirmScreen(dialog),
|
|
@@ -165,18 +147,14 @@ class ArcaneMage(App):
|
|
|
165
147
|
)
|
|
166
148
|
|
|
167
149
|
@on(WelcomeScreenProxmox.ProvisionNode)
|
|
168
|
-
def on_provision_node(
|
|
169
|
-
self, event: WelcomeScreenProxmox.ProvisionNode
|
|
170
|
-
) -> None:
|
|
150
|
+
def on_provision_node(self, event: WelcomeScreenProxmox.ProvisionNode) -> None:
|
|
171
151
|
self.push_screen(
|
|
172
152
|
ProvisionNodeScreen(event.fluxnode),
|
|
173
153
|
self.provision_node_callback,
|
|
174
154
|
)
|
|
175
155
|
|
|
176
156
|
@on(WelcomeScreenProxmox.ProvisionAllNodes)
|
|
177
|
-
def on_provision_all_nodes(
|
|
178
|
-
self, event: WelcomeScreenProxmox.ProvisionAllNodes
|
|
179
|
-
) -> None:
|
|
157
|
+
def on_provision_all_nodes(self, event: WelcomeScreenProxmox.ProvisionAllNodes) -> None:
|
|
180
158
|
self.provision_all_nodes(event.delay)
|
|
181
159
|
|
|
182
160
|
@work(name="provision_all_nodes")
|
|
@@ -230,9 +208,7 @@ class ArcaneMage(App):
|
|
|
230
208
|
|
|
231
209
|
last = fluxnode == provisionable_nodes.last
|
|
232
210
|
|
|
233
|
-
worker = await self.provision_node_callback(
|
|
234
|
-
fluxnode, delete_efi=last
|
|
235
|
-
)
|
|
211
|
+
worker = await self.provision_node_callback(fluxnode, delete_efi=last)
|
|
236
212
|
|
|
237
213
|
if not worker:
|
|
238
214
|
break
|
|
@@ -173,10 +173,7 @@ class FAT12Writer:
|
|
|
173
173
|
# Write data to clusters
|
|
174
174
|
bytes_written = 0
|
|
175
175
|
for cluster_num in free_clusters:
|
|
176
|
-
cluster_offset = (
|
|
177
|
-
(bs.first_data_sector + ((cluster_num - 2) * bs.sectors_per_cluster))
|
|
178
|
-
* bs.bytes_per_sector
|
|
179
|
-
)
|
|
176
|
+
cluster_offset = (bs.first_data_sector + ((cluster_num - 2) * bs.sectors_per_cluster)) * bs.bytes_per_sector
|
|
180
177
|
cluster_size = bs.bytes_per_sector * bs.sectors_per_cluster
|
|
181
178
|
chunk = data[bytes_written : bytes_written + cluster_size]
|
|
182
179
|
|
|
@@ -394,9 +391,7 @@ class FAT12Writer:
|
|
|
394
391
|
"""Get current time in DOS format."""
|
|
395
392
|
now = time.localtime()
|
|
396
393
|
|
|
397
|
-
dos_date = (
|
|
398
|
-
((now.tm_year - 1980) << 9) | (now.tm_mon << 5) | now.tm_mday
|
|
399
|
-
)
|
|
394
|
+
dos_date = ((now.tm_year - 1980) << 9) | (now.tm_mon << 5) | now.tm_mday
|
|
400
395
|
dos_time = (now.tm_hour << 11) | (now.tm_min << 5) | (now.tm_sec // 2)
|
|
401
396
|
|
|
402
397
|
return dos_date, dos_time
|
|
@@ -55,9 +55,7 @@ class ExecBinaryError(ChildProcessError):
|
|
|
55
55
|
return self.__stderr.decode(errors="ignore")
|
|
56
56
|
|
|
57
57
|
def __str__(self) -> str:
|
|
58
|
-
return "Failed to execute '{}', error: {}".format(
|
|
59
|
-
" ".join(self.__cmd), self.stderr()
|
|
60
|
-
)
|
|
58
|
+
return "Failed to execute '{}', error: {}".format(" ".join(self.__cmd), self.stderr())
|
|
61
59
|
|
|
62
60
|
|
|
63
61
|
def demote(user_uid: int, user_gid: int) -> Callable[[], None]:
|
|
@@ -260,9 +258,7 @@ async def do_http_iter(
|
|
|
260
258
|
resp: ClientResponse
|
|
261
259
|
|
|
262
260
|
if resp.status in [429, 500, 502, 503, 504]:
|
|
263
|
-
log.warning(
|
|
264
|
-
"URL: %s, Status: %s... retrying", url, resp.status
|
|
265
|
-
)
|
|
261
|
+
log.warning("URL: %s, Status: %s... retrying", url, resp.status)
|
|
266
262
|
if remaining or not max_tries:
|
|
267
263
|
await asyncio.sleep(retry_interval)
|
|
268
264
|
|
|
@@ -20,30 +20,32 @@ _BASE_HANDLERS: dict[str, dict[str, str | int]] = {
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _apply_config(handlers: dict[str, dict[str, str | int]], handler_names: list[str]) -> None:
|
|
23
|
-
dictConfig(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
dictConfig(
|
|
24
|
+
{
|
|
25
|
+
"version": 1,
|
|
26
|
+
"disable_existing_loggers": True,
|
|
27
|
+
"formatters": {
|
|
28
|
+
"standard": {
|
|
29
|
+
"format": _LOG_FORMAT,
|
|
30
|
+
"datefmt": _LOG_DATEFMT,
|
|
31
|
+
"style": "{",
|
|
32
|
+
},
|
|
31
33
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
"handlers": _BASE_HANDLERS | handlers,
|
|
35
|
+
"loggers": {
|
|
36
|
+
"": {
|
|
37
|
+
"handlers": [],
|
|
38
|
+
"level": "WARNING",
|
|
39
|
+
"propagate": False,
|
|
40
|
+
},
|
|
41
|
+
"arcane_mage": {
|
|
42
|
+
"handlers": handler_names,
|
|
43
|
+
"level": "INFO",
|
|
44
|
+
"propagate": False,
|
|
45
|
+
},
|
|
44
46
|
},
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
+
}
|
|
48
|
+
)
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def configure_tui_logging() -> None:
|
|
@@ -59,7 +59,10 @@ class ArcaneOsConfig:
|
|
|
59
59
|
]
|
|
60
60
|
|
|
61
61
|
async def write_installer_config(self, file_path: Path) -> bool:
|
|
62
|
-
|
|
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
|
|
|
@@ -60,8 +60,7 @@ class Delegate:
|
|
|
60
60
|
has_raw = self.delegate_private_key is not None
|
|
61
61
|
if has_encrypted and has_raw:
|
|
62
62
|
raise ValueError(
|
|
63
|
-
"Provide either delegate_private_key_encrypted or "
|
|
64
|
-
"delegate_private_key + delegate_passphrase, not both"
|
|
63
|
+
"Provide either delegate_private_key_encrypted or delegate_private_key + delegate_passphrase, not both"
|
|
65
64
|
)
|
|
66
65
|
|
|
67
66
|
return self
|
|
@@ -4,8 +4,6 @@ import contextlib
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import ClassVar, Literal
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
|
-
|
|
8
|
-
from ..proxmox import ProxmoxApi
|
|
9
7
|
from uuid import uuid4
|
|
10
8
|
|
|
11
9
|
import keyring
|
|
@@ -15,6 +13,8 @@ from pydantic import TypeAdapter
|
|
|
15
13
|
from pydantic.dataclasses import Field
|
|
16
14
|
from pydantic.dataclasses import dataclass as py_dataclass
|
|
17
15
|
|
|
16
|
+
from ..proxmox import ProxmoxApi
|
|
17
|
+
|
|
18
18
|
|
|
19
19
|
@py_dataclass
|
|
20
20
|
class HypervisorConfig:
|
|
@@ -56,10 +56,7 @@ class HypervisorConfig:
|
|
|
56
56
|
if parsed:
|
|
57
57
|
user = parsed.username
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
base = self.name
|
|
61
|
-
else:
|
|
62
|
-
base = urlparse(self.url).hostname or self.url
|
|
59
|
+
base = self.name or urlparse(self.url).hostname or self.url
|
|
63
60
|
|
|
64
61
|
return f"{base} ({user})" if user else base
|
|
65
62
|
|