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.
Files changed (83) hide show
  1. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/PKG-INFO +1 -1
  2. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/pyproject.toml +1 -1
  3. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/__main__.py +106 -60
  4. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/arcane_mage.py +12 -36
  5. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/fat_writer.py +2 -7
  6. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/helpers.py +2 -6
  7. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/log.py +24 -22
  8. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/config.py +4 -1
  9. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/delegate.py +1 -2
  10. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/hypervisor.py +3 -6
  11. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/network.py +31 -23
  12. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/notifications.py +0 -1
  13. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/password.py +1 -3
  14. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/provisioner.py +12 -7
  15. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/proxmox.py +8 -24
  16. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/hypervisor.py +5 -15
  17. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/provision_node.py +3 -13
  18. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/welcome.py +1 -3
  19. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/welcome_proxmox.py +1 -0
  20. arcane_mage-2.3.0/tests/test_models/test_chain_sources.py +110 -0
  21. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_fluxnode.py +9 -6
  22. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_identity.py +0 -1
  23. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_provisioner.py +3 -1
  24. arcane_mage-2.3.0/uv.lock +1579 -0
  25. arcane_mage-2.2.0/uv.lock +0 -1314
  26. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/.claude/settings.local.json +0 -0
  27. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/.gitignore +0 -0
  28. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/CHANGELOG.md +0 -0
  29. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/LICENSE +0 -0
  30. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/README.md +0 -0
  31. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/README.md +0 -0
  32. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/galaxy.yml +0 -0
  33. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/meta/runtime.yml +0 -0
  34. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/deprovision.py +0 -0
  35. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/iso_info.py +0 -0
  36. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/provision.py +0 -0
  37. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/status.py +0 -0
  38. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/ansible_collection/runonflux/arcane_mage/plugins/modules/validate.py +0 -0
  39. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/docs/configuration.md +0 -0
  40. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/docs/keyring.md +0 -0
  41. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/docs/proxmox-setup.md +0 -0
  42. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/all_settings.yaml +0 -0
  43. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/merge_anchors.yaml +0 -0
  44. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/minimal.yaml +0 -0
  45. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/examples/typical.yaml +0 -0
  46. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_local_storage.png +0 -0
  47. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_permissions.png +0 -0
  48. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_user.png +0 -0
  49. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/proxmox_user_api_token.png +0 -0
  50. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/__init__.py +2 -2
  51. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/arcane_mage.tcss +0 -0
  52. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/images/__init__.py +0 -0
  53. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/images/arcane_config.raw.gz +0 -0
  54. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/images/arcane_efi.raw.gz +0 -0
  55. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/messages.py +0 -0
  56. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/__init__.py +0 -0
  57. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/fluxnode.py +0 -0
  58. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/identity.py +0 -0
  59. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/installer.py +0 -0
  60. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/models/system.py +0 -0
  61. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/py.typed +0 -0
  62. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/__init__.py +0 -0
  63. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/confirm_screen.py +0 -0
  64. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/screens/provisioning_info.py +0 -0
  65. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/tpm_keyring.py +0 -0
  66. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/widgets/__init__.py +0 -0
  67. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/src/arcane_mage/widgets/spinner.py +0 -0
  68. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/testing123.yaml +0 -0
  69. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/__init__.py +0 -0
  70. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/conftest.py +0 -0
  71. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_fat_writer.py +0 -0
  72. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_helpers.py +0 -0
  73. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/__init__.py +0 -0
  74. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_config_group.py +0 -0
  75. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_delegate.py +0 -0
  76. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_hypervisor.py +0 -0
  77. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_installer.py +0 -0
  78. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_network.py +0 -0
  79. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_notifications.py +0 -0
  80. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_models/test_system.py +0 -0
  81. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_password.py +0 -0
  82. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/tests/test_proxmox.py +0 -0
  83. {arcane_mage-2.2.0 → arcane_mage-2.3.0}/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.3.0
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.3.0"
4
4
  description = "Automation helper for Fluxnodes"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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: Optional[str],
149
- token: Optional[str],
150
- hypervisor: Optional[str],
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: Optional[bool] = typer.Option(None, "-v", "--version", callback=version_callback, is_eager=True, help="Show the application 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: Optional[str] = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
212
- token: Optional[str] = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
213
- hypervisor: Optional[str] = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
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: Optional[str] = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
216
- start: Optional[bool] = typer.Option(None, "--start/--no-start", help="Override start_on_creation setting"),
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(f" [bold green]Provisioned successfully[/bold green]")
265
+ console.print(" [bold green]Provisioned successfully[/bold green]")
268
266
  else:
269
- console.print(f" [bold red]Provisioning failed[/bold red]")
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(_json_ok({"nodes": results}) if all_ok else _json_error("Provisioning failed", {"nodes": results}))
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: Optional[str] = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
291
- token: Optional[str] = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
292
- hypervisor: Optional[str] = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
293
- config: Optional[str] = typer.Option(None, "-c", "--config", help="The config file"),
294
- vm_name: Optional[str] = typer.Option(None, "--vm-name", help="Target a specific VM by name"),
295
- vm_id: Optional[int] = typer.Option(None, "--vm-id", help="Target a specific VM by ID"),
296
- node_filter: Optional[str] = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
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(f"[bold red]This will stop and delete:[/bold red]")
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"], vm_display, vm_status, found_node, callback,
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(f" [bold green]Deprovisioned successfully[/bold green]")
398
+ console.print(" [bold green]Deprovisioned successfully[/bold green]")
394
399
  else:
395
- console.print(f" [bold red]Deprovisioning failed[/bold red]")
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(f"[bold red]This will stop and delete the following VMs:[/bold red]")
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(f" [bold green]Deprovisioned successfully[/bold green]")
458
+ console.print(" [bold green]Deprovisioned successfully[/bold green]")
454
459
  else:
455
- console.print(f" [bold red]Deprovisioning failed[/bold red]")
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(_json_ok({"nodes": results}) if all_ok else _json_error("Deprovisioning failed", {"nodes": results}))
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: Optional[str] = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
477
- token: Optional[str] = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
478
- hypervisor: Optional[str] = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
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: Optional[str] = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
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({"hostname": hostname, "ok": False, "checks": [{"check": "hypervisor", "ok": False, "message": "No hypervisor config"}]})
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(Panel(f"[red]No hypervisor config[/red]", title=f"[bold]{hostname}[/bold]", border_style="red"))
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({"check": "api_version", "ok": api_ok, "message": api_msg if not api_ok else f"v{api_msg}"})
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, hyper.storage_iso, hyper.storage_images, hyper.storage_import,
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({"check": "iso", "ok": iso_ok, "message": f"{hyper.iso_name} not found" if not iso_ok else hyper.iso_name})
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({"check": "network", "ok": net_ok, "message": f"Bridge {hyper.network} not found" if not net_ok else hyper.network})
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(Panel(table, title=f"[bold]{hostname}[/bold] @ [bold]{hyper.node}[/bold]", border_style=border))
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: Optional[str] = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
572
- token: Optional[str] = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
573
- hypervisor: Optional[str] = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
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: Optional[str] = typer.Option(None, help="Proxmox API URL (also accepts ARCANE_MAGE_URL)"),
639
- token: Optional[str] = typer.Option(None, help="API token (also accepts stdin or ARCANE_MAGE_TOKEN)"),
640
- hypervisor: Optional[str] = typer.Option(None, "--hypervisor", "-H", help="Use stored hypervisor by name"),
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: Optional[str] = typer.Option(None, "--node", help="Cluster node to target (e.g. pve1)"),
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
- "hostname": hostname,
698
- "vm_name": hyper.vm_name,
699
- "node": hyper.node,
700
- "provisioned": provisioned,
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
- "welcome-proxmox"
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
- "version": 1,
25
- "disable_existing_loggers": True,
26
- "formatters": {
27
- "standard": {
28
- "format": _LOG_FORMAT,
29
- "datefmt": _LOG_DATEFMT,
30
- "style": "{",
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
- "handlers": _BASE_HANDLERS | handlers,
34
- "loggers": {
35
- "": {
36
- "handlers": [],
37
- "level": "WARNING",
38
- "propagate": False,
39
- },
40
- "arcane_mage": {
41
- "handlers": handler_names,
42
- "level": "INFO",
43
- "propagate": False,
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
- 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
 
@@ -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
- if self.name:
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