virtui-manager 1.1.6__py3-none-any.whl → 1.3.0__py3-none-any.whl

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 (57) hide show
  1. {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/METADATA +1 -1
  2. virtui_manager-1.3.0.dist-info/RECORD +73 -0
  3. vmanager/constants.py +737 -108
  4. vmanager/dialog.css +24 -0
  5. vmanager/firmware_manager.py +4 -1
  6. vmanager/i18n.py +32 -0
  7. vmanager/libvirt_utils.py +132 -3
  8. vmanager/locales/de/LC_MESSAGES/virtui-manager.po +3012 -0
  9. vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
  10. vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3124 -0
  11. vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3012 -0
  12. vmanager/locales/virtui-manager.pot +3012 -0
  13. vmanager/modals/bulk_modals.py +13 -12
  14. vmanager/modals/cache_stats_modal.py +6 -5
  15. vmanager/modals/capabilities_modal.py +133 -0
  16. vmanager/modals/config_modal.py +25 -24
  17. vmanager/modals/cpu_mem_pc_modals.py +22 -21
  18. vmanager/modals/custom_migration_modal.py +10 -9
  19. vmanager/modals/disk_pool_modals.py +60 -59
  20. vmanager/modals/host_dashboard_modal.py +137 -0
  21. vmanager/modals/howto_disk_modal.py +2 -1
  22. vmanager/modals/howto_network_modal.py +2 -1
  23. vmanager/modals/howto_overlay_modal.py +2 -1
  24. vmanager/modals/howto_ssh_modal.py +2 -1
  25. vmanager/modals/howto_virtiofs_modal.py +2 -1
  26. vmanager/modals/input_modals.py +11 -10
  27. vmanager/modals/log_modal.py +2 -1
  28. vmanager/modals/migration_modals.py +20 -18
  29. vmanager/modals/network_modals.py +45 -36
  30. vmanager/modals/provisioning_modals.py +56 -56
  31. vmanager/modals/select_server_modals.py +8 -7
  32. vmanager/modals/selection_modals.py +7 -6
  33. vmanager/modals/server_modals.py +24 -23
  34. vmanager/modals/server_prefs_modals.py +78 -71
  35. vmanager/modals/utils_modals.py +10 -9
  36. vmanager/modals/virsh_modals.py +3 -2
  37. vmanager/modals/virtiofs_modals.py +6 -5
  38. vmanager/modals/vm_type_info_modal.py +2 -1
  39. vmanager/modals/vmanager_modals.py +19 -19
  40. vmanager/modals/vmcard_dialog.py +57 -57
  41. vmanager/modals/vmdetails_modals.py +115 -123
  42. vmanager/modals/xml_modals.py +3 -2
  43. vmanager/network_manager.py +4 -1
  44. vmanager/storage_manager.py +157 -39
  45. vmanager/utils.py +39 -6
  46. vmanager/vm_actions.py +28 -24
  47. vmanager/vm_queries.py +67 -25
  48. vmanager/vm_service.py +8 -5
  49. vmanager/vmanager.css +46 -0
  50. vmanager/vmanager.py +178 -112
  51. vmanager/vmcard.py +161 -159
  52. vmanager/webconsole_manager.py +21 -21
  53. virtui_manager-1.1.6.dist-info/RECORD +0 -65
  54. {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/WHEEL +0 -0
  55. {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/entry_points.txt +0 -0
  56. {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/licenses/LICENSE +0 -0
  57. {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/top_level.txt +0 -0
vmanager/vmanager.py CHANGED
@@ -5,6 +5,7 @@ import os
5
5
  import sys
6
6
  import re
7
7
  from threading import RLock
8
+ from concurrent.futures import ThreadPoolExecutor
8
9
  import logging
9
10
  import argparse
10
11
  from collections import deque
@@ -22,8 +23,10 @@ from textual.worker import Worker, WorkerState
22
23
 
23
24
  from .config import load_config, save_config, get_log_path
24
25
  from .constants import (
25
- VmAction, VmStatus, ButtonLabels, ButtonIds,
26
- ErrorMessages, AppInfo, StatusText, ServerPallette,
26
+ WarningMessages,
27
+ SuccessMessages,
28
+ VmAction, VmStatus, ButtonLabels, BindingDescriptions,
29
+ ErrorMessages, AppInfo, StatusText, ServerPallette, QuickMessages, ProgressMessages,
27
30
  )
28
31
  from .events import VmActionRequest, VMSelectionChanged, VmCardUpdateRequest #,VMNameClicked
29
32
  from .libvirt_error_handler import register_error_handler
@@ -34,6 +37,7 @@ from .modals.server_modals import ServerManagementModal
34
37
  from .modals.server_prefs_modals import ServerPrefModal
35
38
  from .modals.select_server_modals import SelectOneServerModal, SelectServerModal
36
39
  from .modals.selection_modals import PatternSelectModal
40
+ from .modals.capabilities_modal import CapabilitiesTreeModal
37
41
  from .modals.cache_stats_modal import CacheStatsModal
38
42
  from .modals.utils_modals import (
39
43
  show_error_message,
@@ -49,6 +53,7 @@ from .modals.vmanager_modals import (
49
53
  )
50
54
  from .modals.virsh_modals import VirshShellScreen
51
55
  from .modals.provisioning_modals import InstallVMModal
56
+ from .modals.host_dashboard_modal import HostDashboardModal
52
57
  from .utils import (
53
58
  check_novnc_path,
54
59
  check_r_viewer,
@@ -56,21 +61,21 @@ from .utils import (
56
61
  generate_webconsole_keys_if_needed,
57
62
  get_server_color_cached,
58
63
  setup_cache_monitoring,
64
+ setup_logging
59
65
  )
60
- from .libvirt_utils import get_internal_id
61
66
  from .vm_queries import (
62
67
  get_status,
63
68
  )
69
+ from .libvirt_utils import (
70
+ get_internal_id, get_host_resources,
71
+ get_total_vm_allocation, get_active_vm_allocation
72
+ )
64
73
  from .vm_service import VMService
65
74
  from .vmcard import VMCard
66
75
  from .vmcard_pool import VMCardPool
67
76
  from .webconsole_manager import WebConsoleManager
68
77
 
69
- log_config = load_config()
70
- log_level_str = log_config.get("LOG_LEVEL", "INFO")
71
- log_level = getattr(logging, log_level_str, logging.INFO)
72
- file_handler = logging.FileHandler(get_log_path())
73
- file_handler.setLevel(log_level)
78
+ setup_logging()
74
79
 
75
80
  class WorkerManager:
76
81
  """A class to manage and track Textual workers."""
@@ -158,26 +163,28 @@ class VMManagerTUI(App):
158
163
  """A Textual application to manage VMs."""
159
164
 
160
165
  BINDINGS = [
161
- Binding(key="v", action="view_log", description="Log"),
162
- Binding(key="f", action="filter_view", description="Filter", show=False),
163
- Binding(key="k", action="compact_view", description="CompactView", show=True),
164
- #Binding(key="p", action="server_preferences", description="ServerPrefs"),
165
- Binding(key="c", action="config", description="Config", show=True),
166
- Binding(key="b", action="bulk_cmd", description="BulkCMD", show=False),
167
- Binding(key="s", action="select_server", description="SelServers", show=False),
168
- Binding(key="l", action="manage_server", description="ServList", show=False),
169
- Binding(key="p", action="pattern_select", description="PatternSel", show=False),
170
- Binding(key="ctrl+a", action="toggle_select_all", description="Sel/Des All"),
171
- Binding(key="ctrl+u", action="unselect_all", description="Unselect All"),
172
- Binding(key="left", action="previous_page", description="Previous Page", show=False),
173
- Binding(key="right", action="next_page", description="Next Page", show=False),
174
- Binding(key="up", action="filter_running", description="Running VMs", show=False),
175
- Binding(key="down", action="filter_all", description="All VMs", show=False),
176
- Binding(key="ctrl+v", action="virsh_shell", description="Virsh", show=False ),
177
- Binding(key="i", action="install_vm", description="InstallVM", show=True),
178
- Binding(key="ctrl+l", action="toggle_stats_logging", description="Log Stats", show=False),
179
- Binding(key="ctrl+s", action="show_cache_stats", description="Show cache Stats", show=False),
180
- Binding(key="q", action="quit", description="Quit"),
166
+ Binding(key="v", action="view_log", description=BindingDescriptions.LOG),
167
+ Binding(key="f", action="filter_view", description=BindingDescriptions.FILTER, show=False),
168
+ Binding(key="k", action="compact_view", description=BindingDescriptions.COMPACT_VIEW, show=True),
169
+ #Binding(key="p", action="server_preferences", description=BindingDescriptions.SERVER_PREFS),
170
+ Binding(key="c", action="config", description=BindingDescriptions.CONFIG, show=True),
171
+ Binding(key="b", action="bulk_cmd", description=BindingDescriptions.BULK_CMD, show=False),
172
+ Binding(key="s", action="select_server", description=BindingDescriptions.SELECT_SERVERS, show=False),
173
+ Binding(key="l", action="manage_server", description=BindingDescriptions.MANAGE_SERVERS, show=False),
174
+ Binding(key="p", action="pattern_select", description=BindingDescriptions.PATTERN_SELECT, show=False),
175
+ Binding(key="ctrl+a", action="toggle_select_all", description=BindingDescriptions.SELECT_ALL),
176
+ Binding(key="ctrl+u", action="unselect_all", description=BindingDescriptions.UNSELECT_ALL),
177
+ Binding(key="left", action="previous_page", description=BindingDescriptions.PREVIOUS_PAGE, show=False),
178
+ Binding(key="right", action="next_page", description=BindingDescriptions.NEXT_PAGE, show=False),
179
+ Binding(key="up", action="filter_running", description=BindingDescriptions.RUNNING_VMS, show=False),
180
+ Binding(key="down", action="filter_all", description=BindingDescriptions.ALL_VMS, show=False),
181
+ Binding(key="ctrl+v", action="virsh_shell", description=BindingDescriptions.VIRSH_SHELL, show=False ),
182
+ Binding(key="h", action="host_capabilities", description=BindingDescriptions.HOST_CAPABILITIES, show=False),
183
+ Binding(key="H", action="host_dashboard", description=BindingDescriptions.HOST_DASHBOARD, show=False),
184
+ Binding(key="i", action="install_vm", description=BindingDescriptions.INSTALL_VM, show=True),
185
+ Binding(key="ctrl+l", action="toggle_stats_logging", description=BindingDescriptions.TOGGLE_STATS, show=False),
186
+ Binding(key="ctrl+s", action="show_cache_stats", description=BindingDescriptions.CACHE_STATS, show=False),
187
+ Binding(key="q", action="quit", description=BindingDescriptions.QUIT),
181
188
  ]
182
189
 
183
190
  config = load_config()
@@ -337,10 +344,10 @@ class VMManagerTUI(App):
337
344
  self.ui["error_footer"] = Static(id="error-footer", classes="error-message")
338
345
  self.ui["page_info"] = Label("", id="page-info", classes="")
339
346
  self.ui["prev_button"] = Button(
340
- ButtonLabels.PREVIOUS_PAGE, id=ButtonIds.PREV_BUTTON, variant="primary", classes="ctrlpage"
347
+ ButtonLabels.PREVIOUS_PAGE, id="prev-button", variant="primary", classes="ctrlpage"
341
348
  )
342
349
  self.ui["next_button"] = Button(
343
- ButtonLabels.NEXT_PAGE, id=ButtonIds.NEXT_BUTTON, variant="primary", classes="ctrlpage"
350
+ ButtonLabels.NEXT_PAGE, id="next-button", variant="primary", classes="ctrlpage"
344
351
  )
345
352
  self.ui["pagination_controls"] = Horizontal(
346
353
  self.ui["prev_button"],
@@ -356,20 +363,20 @@ class VMManagerTUI(App):
356
363
  yield Header()
357
364
  with Horizontal(classes="top-controls"):
358
365
  yield Button(
359
- ButtonLabels.SELECT_SERVER, id=ButtonIds.SELECT_SERVER_BUTTON, classes="Buttonpage"
366
+ ButtonLabels.SELECT_SERVER, id="select_server_button", classes="Buttonpage"
360
367
  )
361
- yield Button(ButtonLabels.MANAGE_SERVERS, id=ButtonIds.MANAGE_SERVERS_BUTTON, classes="Buttonpage")
368
+ yield Button(ButtonLabels.MANAGE_SERVERS, id="manage_servers_button", classes="Buttonpage")
362
369
  yield Button(
363
- ButtonLabels.SERVER_PREFERENCES, id=ButtonIds.SERVER_PREFERENCES_BUTTON, classes="Buttonpage"
370
+ ButtonLabels.SERVER_PREFERENCES, id="server_preferences_button", classes="Buttonpage"
364
371
  )
365
- yield Button(ButtonLabels.FILTER_VM, id=ButtonIds.FILTER_BUTTON, classes="Buttonpage")
366
- #yield Button(ButtonLabels.VIEW_LOG, id=ButtonIds.VIEW_LOG_BUTTON, classes="Buttonpage")
372
+ yield Button(ButtonLabels.FILTER_VM, id="filter_button", classes="Buttonpage")
373
+ #yield Button(ButtonLabels.VIEW_LOG, id="view-log-button", classes="Buttonpage")
367
374
  # yield Button("Virsh Shell", id="virsh_shell_button", classes="Buttonpage")
368
- yield Button(ButtonLabels.BULK_CMD, id=ButtonIds.BULK_SELECTED_VMS, classes="Buttonpage")
369
- yield Button(ButtonLabels.PATTERN_SELECT, id=ButtonIds.PATTERN_SELECT_BUTTON, classes="Buttonpage")
370
- #yield Button(ButtonLabels.CONFIG, id=ButtonIds.CONFIG_BUTTON, classes="Buttonpage")
375
+ yield Button(ButtonLabels.BULK_CMD, id="bulk_selected_vms", classes="Buttonpage")
376
+ yield Button(ButtonLabels.PATTERN_SELECT, id="pattern_select_button", classes="Buttonpage")
377
+ #yield Button(ButtonLabels.CONFIG, id="config-button", classes="Buttonpage")
371
378
  #yield Button(
372
- # ButtonLabels.COMPACT_VIEW, id=ButtonIds.COMPACT_VIEW_BUTTON, classes="Buttonpage"
379
+ # ButtonLabels.COMPACT_VIEW, id="compact-view-button", classes="Buttonpage"
373
380
  #)
374
381
  yield Link("About", url="https://aginies.github.io/virtui-manager/")
375
382
 
@@ -377,9 +384,7 @@ class VMManagerTUI(App):
377
384
  yield self.ui["vms_container"]
378
385
  yield self.ui["error_footer"]
379
386
  yield Footer()
380
- self.show_success_message(
381
- "In some Terminal use [b]Shift[/b] key while selecting text with the mouse to copy it."
382
- )
387
+ self.show_success_message(SuccessMessages.TERMINAL_COPY_HINT)
383
388
 
384
389
  def reload_servers(self, new_servers):
385
390
  self.servers = new_servers
@@ -398,7 +403,7 @@ class VMManagerTUI(App):
398
403
  )
399
404
  self.r_viewer_available = False
400
405
  else:
401
- self.show_quick_message(f"The remove viewer {self.r_viewer} has been selected.")
406
+ self.show_quick_message(QuickMessages.REMOTE_VIEWER_SELECTED.format(viewer=self.r_viewer))
402
407
 
403
408
  if not check_websockify():
404
409
  self.show_error_message(
@@ -426,9 +431,7 @@ class VMManagerTUI(App):
426
431
  vms_container.styles.grid_size_columns = 2
427
432
 
428
433
  if not self.servers:
429
- self.show_success_message(
430
- "No servers configured. Please add one via 'Servers List'."
431
- )
434
+ self.show_success_message(SuccessMessages.NO_SERVERS_CONFIGURED)
432
435
  else:
433
436
  # Launch initial connection and cache loading in background
434
437
  if self.active_uris:
@@ -441,10 +444,10 @@ class VMManagerTUI(App):
441
444
  """Connects to servers in background and then triggers cache loading."""
442
445
  if self.active_uris:
443
446
  for uri in self.active_uris:
444
- self.call_from_thread(self.show_in_progress_message, f"Connecting to [b]{uri}[/b]...")
447
+ self.call_from_thread(self.show_in_progress_message, ProgressMessages.CONNECTING_TO_SERVER.format(uri=uri))
445
448
  success = self.connect_libvirt(uri)
446
449
  if success:
447
- self.call_from_thread(self.show_success_message, f"Connected to [b]{uri}[/b]")
450
+ self.call_from_thread(self.show_success_message, SuccessMessages.CONNECTED_TO_SERVER.format(uri=uri))
448
451
  else:
449
452
  error_msg = self.vm_service.connection_manager.get_connection_error(uri)
450
453
  if error_msg:
@@ -461,7 +464,7 @@ class VMManagerTUI(App):
461
464
  # Log libvirt call statistics
462
465
  call_stats = self.vm_service.connection_manager.get_stats()
463
466
  if call_stats:
464
- logging.info("=== Libvirt Call Statistics ===")
467
+ logging.debug("=== Libvirt Call Statistics ===")
465
468
  for uri, methods in sorted(call_stats.items()):
466
469
  server_name = uri
467
470
  for s in self.servers:
@@ -479,7 +482,7 @@ class VMManagerTUI(App):
479
482
  if total_increase > 0:
480
483
  increase_pct = 100 - (previous_how_many_more*100 / total_increase)
481
484
 
482
- logging.info(f"{server_name} ({uri}): {total_calls} calls | +{total_increase} ({increase_pct:.1f}%)")
485
+ logging.debug(f"{server_name} ({uri}): {total_calls} calls | +{total_increase} ({increase_pct:.1f}%)")
483
486
  previous_how_many_more = how_many_more
484
487
 
485
488
  # Initialize previous method calls dict for this URI if needed
@@ -493,7 +496,7 @@ class VMManagerTUI(App):
493
496
 
494
497
  self.last_method_calls[uri][method] = count
495
498
  how_many_more_count = count - prev_method_count
496
- logging.info(f" - {method}: {count} calls (+{how_many_more_count})")
499
+ logging.debug(f" - {method}: {count} calls (+{how_many_more_count})")
497
500
 
498
501
  self.last_increase[uri] = how_many_more
499
502
  self.last_total_calls[uri] = total_calls
@@ -510,13 +513,13 @@ class VMManagerTUI(App):
510
513
  self._stats_interval_timer = None
511
514
  self._stats_logging_active = False
512
515
  setup_cache_monitoring(enable=False)
513
- self.show_success_message("Statistics logging and monitoring disabled.")
516
+ self.show_success_message(SuccessMessages.STATS_LOGGING_DISABLED)
514
517
  else:
515
518
  setup_cache_monitoring(enable=True)
516
519
  self._log_cache_statistics()
517
520
  self._stats_interval_timer = self.set_interval(10, self._log_cache_statistics)
518
521
  self._stats_logging_active = True
519
- self.show_success_message("Statistics logging and monitoring enabled (every 10s).")
522
+ self.show_success_message(SuccessMessages.STATS_LOGGING_ENABLED)
520
523
 
521
524
  def _initial_cache_worker(self):
522
525
  """Pre-loads VM cache before displaying the UI."""
@@ -545,7 +548,7 @@ class VMManagerTUI(App):
545
548
  if s['uri'] == uri:
546
549
  server_name = s['name']
547
550
  break
548
- self.call_from_thread(self.show_error_message, f"Server [b]{server_name}[/b]: {error_msg}")
551
+ self.call_from_thread(self.show_error_message, ErrorMessages.SERVER_CONNECTION_ERROR.format(server_name=server_name, error_msg=error_msg))
549
552
 
550
553
  if self.vm_service.connection_manager.is_max_retries_reached(uri):
551
554
  self.call_from_thread(self.remove_active_uri, uri)
@@ -573,14 +576,14 @@ class VMManagerTUI(App):
573
576
 
574
577
  if active_vms_on_page:
575
578
  vms_list_str = ", ".join(active_vms_on_page)
576
- self.call_from_thread(self.show_quick_message, f"Caching VM state for: {vms_list_str}")
579
+ self.call_from_thread(self.show_quick_message, QuickMessages.CACHING_VM_STATE.format(vms_list=vms_list_str))
577
580
 
578
581
  self.call_from_thread(self._on_initial_cache_complete)
579
582
 
580
583
  except Exception as e:
581
584
  self.call_from_thread(
582
585
  self.show_error_message,
583
- f"Error during initial cache loading: {e}"
586
+ ErrorMessages.ERROR_DURING_INITIAL_CACHE_LOADING.format(error=e)
584
587
  )
585
588
 
586
589
  def _on_initial_cache_complete(self):
@@ -588,7 +591,7 @@ class VMManagerTUI(App):
588
591
  self.initial_cache_loading = False
589
592
  self.initial_cache_complete = True
590
593
  if self.servers:
591
- self.show_quick_message("VM data loaded. Displaying VMs...")
594
+ self.show_quick_message(QuickMessages.VM_DATA_LOADED)
592
595
  self.refresh_vm_list()
593
596
 
594
597
  def _update_layout_for_size(self):
@@ -646,9 +649,7 @@ class VMManagerTUI(App):
646
649
  self.vm_card_pool.prefill_pool()
647
650
 
648
651
  if self.VMS_PER_PAGE > 9 and old_vms_per_page <= 9 and not self.compact_view:
649
- self.show_warning_message(
650
- f"Displaying [b]{self.VMS_PER_PAGE}[/b] VMs per page. CPU usage may increase; 9 is recommended for optimal performance."
651
- )
652
+ self.show_warning_message(WarningMessages.VMS_PER_PAGE_PERFORMANCE_WARNING.format(vms_per_page=self.VMS_PER_PAGE))
652
653
 
653
654
  self.refresh_vm_list(force=True)
654
655
 
@@ -676,7 +677,7 @@ class VMManagerTUI(App):
676
677
  if conn:
677
678
  yield conn
678
679
  else:
679
- self.show_error_message(f"Failed to open connection to [b]{uri}[/b]")
680
+ self.show_error_message(ErrorMessages.FAILED_TO_OPEN_CONNECTION.format(uri=uri))
680
681
 
681
682
  def connect_libvirt(self, uri: str) -> None:
682
683
  """Connects to libvirt."""
@@ -705,11 +706,11 @@ class VMManagerTUI(App):
705
706
  def show_warning_message(self, message: str):
706
707
  show_warning_message(self, message)
707
708
 
708
- @on(Button.Pressed, f"#{ButtonIds.COMPACT_VIEW_BUTTON}")
709
+ @on(Button.Pressed, f"#compact-view-button")
709
710
  def action_compact_view(self) -> None:
710
711
  """Toggle compact view."""
711
712
  if self.bulk_operation_in_progress:
712
- self.show_warning_message("Compact view is locked during bulk operations.")
713
+ self.show_warning_message(WarningMessages.COMPACT_VIEW_LOCKED)
713
714
  return
714
715
 
715
716
  if not self.compact_view:
@@ -754,8 +755,7 @@ class VMManagerTUI(App):
754
755
  uris_to_connect = [uri for uri in selected_uris if uri not in self.active_uris]
755
756
  # Show connecting message for each new server
756
757
  for uri in uris_to_connect:
757
- self.show_in_progress_message(f"Connecting to [b]{uri}[/b]...")
758
-
758
+ self.show_in_progress_message(ProgressMessages.CONNECTING_TO_SERVER.format(uri=uri))
759
759
  for uri in uris_to_disconnect:
760
760
  # Cleanup UI caches for VMs on this server
761
761
  uuids_to_release = [
@@ -795,11 +795,11 @@ class VMManagerTUI(App):
795
795
  server_name = s['name']
796
796
  break
797
797
  if success:
798
- self.call_from_thread(self.show_success_message, f"Connected to [b]{server_name}[/b]")
798
+ self.call_from_thread(self.show_success_message, SuccessMessages.SERVER_CONNECTED.format(name=server_name))
799
799
  else:
800
800
  error_msg = self.vm_service.connection_manager.get_connection_error(uri)
801
801
  if error_msg:
802
- self.call_from_thread(self.show_error_message, f"Failed to connect to [b]{server_name}[/b]: {error_msg}")
802
+ self.call_from_thread(self.show_error_message, ErrorMessages.SERVER_FAILED_TO_CONNECT.format(server_name=server_name, error_msg=error_msg))
803
803
 
804
804
  if uris_to_connect:
805
805
  self.worker_manager.run(show_connection_results, name="show_connection_results")
@@ -868,7 +868,7 @@ class VMManagerTUI(App):
868
868
  if self.sort_by != VmStatus.RUNNING:
869
869
  self.sort_by = VmStatus.RUNNING
870
870
  self.current_page = 0
871
- self.show_quick_message("Filter: Running VMs")
871
+ self.show_quick_message(QuickMessages.FILTER_RUNNING_VMS)
872
872
  self.refresh_vm_list()
873
873
 
874
874
  def action_filter_all(self) -> None:
@@ -876,7 +876,7 @@ class VMManagerTUI(App):
876
876
  if self.sort_by != VmStatus.DEFAULT:
877
877
  self.sort_by = VmStatus.DEFAULT
878
878
  self.current_page = 0
879
- self.show_quick_message("Filter: All VMs")
879
+ self.show_quick_message(QuickMessages.FILTER_ALL_VMS)
880
880
  self.refresh_vm_list()
881
881
 
882
882
  @on(FilterModal.FilterChanged)
@@ -899,7 +899,7 @@ class VMManagerTUI(App):
899
899
  self.search_text = new_search
900
900
  self.filtered_server_uris = new_selected_servers
901
901
  self.current_page = 0
902
- self.show_in_progress_message("Loading VM data from remote server(s)...")
902
+ self.show_in_progress_message(ProgressMessages.LOADING_VM_DATA_FROM_REMOTE_SERVERS)
903
903
  self.refresh_vm_list()
904
904
 
905
905
  def action_config(self) -> None:
@@ -919,7 +919,7 @@ class VMManagerTUI(App):
919
919
  logging.getLogger().setLevel(new_log_level)
920
920
  for handler in logging.getLogger().handlers:
921
921
  handler.setLevel(new_log_level)
922
- self.show_success_message(f"Log level changed to {new_log_level_str}")
922
+ self.show_success_message(SuccessMessages.LOG_LEVEL_CHANGED.format(level=new_log_level_str))
923
923
 
924
924
  # Update remote viewer if changed
925
925
  self.r_viewer = check_r_viewer(self.config.get("REMOTE_VIEWER"))
@@ -930,10 +930,10 @@ class VMManagerTUI(App):
930
930
  self.r_viewer_available = True
931
931
 
932
932
  if (self.config.get("STATS_INTERVAL") != old_stats_interval):
933
- self.show_in_progress_message("Configuration updated. Refreshing VM list...")
933
+ self.show_in_progress_message(ProgressMessages.CONFIG_UPDATED_REFRESHING_VM_LIST)
934
934
  self.refresh_vm_list(force=False, optimize_for_current_page=True)
935
935
  else:
936
- self.show_success_message("Configuration updated.")
936
+ self.show_success_message(SuccessMessages.CONFIG_UPDATED)
937
937
 
938
938
  @on(Button.Pressed, "#config_button")
939
939
  def on_config_button_pressed(self, event: Button.Pressed) -> None:
@@ -985,7 +985,7 @@ class VMManagerTUI(App):
985
985
  self.call_from_thread(self.push_screen, modal)
986
986
  except Exception as e:
987
987
  self.call_from_thread(loading.dismiss)
988
- self.call_from_thread(self.show_error_message, f"Error launching preferences: {e}")
988
+ self.call_from_thread(self.show_error_message, ErrorMessages.PREFERENCES_LAUNCH_ERROR.format(error=e))
989
989
 
990
990
  self.worker_manager.run(show_prefs, name="launch_server_prefs")
991
991
  else:
@@ -1004,7 +1004,7 @@ class VMManagerTUI(App):
1004
1004
  Handles 0, 1, or multiple active servers.
1005
1005
  """
1006
1006
  if len(self.active_uris) == 0:
1007
- self.show_error_message("Not connected to any server.")
1007
+ self.show_error_message(ErrorMessages.NOT_CONNECTED_TO_ANY_SERVER)
1008
1008
  return
1009
1009
 
1010
1010
  if len(self.active_uris) == 1:
@@ -1038,6 +1038,34 @@ class VMManagerTUI(App):
1038
1038
  """Callback for the virsh shell button."""
1039
1039
  self.action_virsh_shell()
1040
1040
 
1041
+ def action_host_dashboard(self) -> None:
1042
+ """Show Host Resource Dashboard."""
1043
+ def launch_dashboard_modal(uri: str):
1044
+ conn = self.vm_service.connect(uri)
1045
+ if conn:
1046
+ # Find server name
1047
+ server_name = uri
1048
+ for s in self.servers:
1049
+ if s['uri'] == uri:
1050
+ server_name = s['name']
1051
+ break
1052
+ self.push_screen(HostDashboardModal(conn, server_name))
1053
+ else:
1054
+ self.show_error_message(ErrorMessages.COULD_NOT_CONNECT_TO_SERVER.format(uri=uri))
1055
+
1056
+ self._select_server_and_run(launch_dashboard_modal, "Select a server for Dashboard", "View Dashboard")
1057
+
1058
+ def action_host_capabilities(self) -> None:
1059
+ """Show Host Capabilities."""
1060
+ def launch_caps_modal(uri: str):
1061
+ conn = self.vm_service.connect(uri)
1062
+ if conn:
1063
+ self.push_screen(CapabilitiesTreeModal(conn))
1064
+ else:
1065
+ self.show_error_message(ErrorMessages.COULD_NOT_CONNECT_TO_SERVER.format(uri=uri))
1066
+
1067
+ self._select_server_and_run(launch_caps_modal, "Select a server for Capabilities", "View")
1068
+
1041
1069
  def action_install_vm(self) -> None:
1042
1070
  """Launch the VM Installation Modal."""
1043
1071
  def launch_install_modal(uri: str):
@@ -1057,7 +1085,7 @@ class VMManagerTUI(App):
1057
1085
  def action_worker():
1058
1086
  domain = self.vm_service.find_domain_by_uuid(self.active_uris, message.internal_id)
1059
1087
  if not domain:
1060
- self.call_from_thread(self.show_error_message, f"Could not find VM with ID [b]{message.internal_id}[/b]")
1088
+ self.call_from_thread(self.show_error_message, ErrorMessages.VM_NOT_FOUND_BY_ID.format(vm_id=message.internal_id))
1061
1089
  return
1062
1090
 
1063
1091
  #vm_name = domain.name()
@@ -1069,6 +1097,41 @@ class VMManagerTUI(App):
1069
1097
  try:
1070
1098
  # Message are done by events
1071
1099
  if message.action == VmAction.START:
1100
+ # Check resources
1101
+ try:
1102
+ conn = domain.connect()
1103
+ host_res = get_host_resources(conn)
1104
+ current_alloc = get_active_vm_allocation(conn)
1105
+
1106
+ # domain.info() -> [state, maxMem(KB), memory(KB), nrVirtCpu, cpuTime]
1107
+ vm_info = domain.info()
1108
+ vm_mem_mb = vm_info[1] // 1024
1109
+ vm_vcpus = vm_info[3]
1110
+
1111
+ host_mem_mb = host_res.get('available_memory', 0)
1112
+ host_cpus = host_res.get('total_cpus', 0)
1113
+
1114
+ active_mem_mb = current_alloc.get('active_allocated_memory', 0)
1115
+ active_vcpus = current_alloc.get('active_allocated_vcpus', 0)
1116
+
1117
+ overcommit_mem = (active_mem_mb + vm_mem_mb) > host_mem_mb
1118
+ overcommit_cpu = (active_vcpus + vm_vcpus) > host_cpus
1119
+
1120
+ if overcommit_mem or overcommit_cpu:
1121
+ warnings = []
1122
+ if overcommit_mem:
1123
+ warnings.append(f"Memory: {active_mem_mb + vm_mem_mb} MB > {host_mem_mb} MB")
1124
+ if overcommit_cpu:
1125
+ warnings.append(f"vCPUs: {active_vcpus + vm_vcpus} > {host_cpus}")
1126
+
1127
+ warning_msg = (
1128
+ f"Starting VM '{vm_name}' will exceed host capacity (Active Allocation):\n"
1129
+ f"{chr(10).join(warnings)}"
1130
+ )
1131
+ self.show_warning_message(warning_msg)
1132
+ except Exception as e:
1133
+ logging.error(f"Error checking resources before start: {e}")
1134
+
1072
1135
  self.vm_service.start_vm(domain)
1073
1136
  elif message.action == VmAction.STOP:
1074
1137
  self.vm_service.stop_vm(domain)
@@ -1088,7 +1151,7 @@ class VMManagerTUI(App):
1088
1151
  except Exception as e:
1089
1152
  self.call_from_thread(
1090
1153
  self.show_error_message,
1091
- f"Error on VM [b]{vm_name}[/b] during '{message.action}': {e}",
1154
+ ErrorMessages.ERROR_ON_VM_DURING_ACTION.format(vm_name=vm_name, action=message.action, error=e),
1092
1155
  )
1093
1156
  finally:
1094
1157
  self.vm_service.unsuppress_vm_events(message.internal_id)
@@ -1129,7 +1192,7 @@ class VMManagerTUI(App):
1129
1192
  for card in self.query(VMCard):
1130
1193
  card.is_selected = False
1131
1194
 
1132
- self.show_quick_message("All VMs unselected.")
1195
+ self.show_quick_message(QuickMessages.ALL_VMS_UNSELECTED)
1133
1196
 
1134
1197
  @on(VMSelectionChanged)
1135
1198
  def on_vm_selection_changed(self, message: VMSelectionChanged) -> None:
@@ -1150,7 +1213,7 @@ class VMManagerTUI(App):
1150
1213
  delete_storage_flag = result.get('delete_storage', False)
1151
1214
 
1152
1215
  if not action_type:
1153
- self.show_error_message("No action type received from bulk action modal.")
1216
+ self.show_error_message(ErrorMessages.NO_ACTION_TYPE_BULK_MODAL)
1154
1217
  return
1155
1218
 
1156
1219
  selected_uuids_copy = list(self.selected_vm_uuids) # Take a copy for the worker
@@ -1162,7 +1225,7 @@ class VMManagerTUI(App):
1162
1225
  selected_domains = list(found_domains_map.values())
1163
1226
 
1164
1227
  if not selected_domains:
1165
- self.show_error_message("Could not find any of the selected VMs for editing.")
1228
+ self.show_error_message(ErrorMessages.VM_NOT_FOUND_FOR_EDITING)
1166
1229
  return
1167
1230
 
1168
1231
  # Check if all selected VMs are stopped
@@ -1172,7 +1235,7 @@ class VMManagerTUI(App):
1172
1235
  active_vms.append(domain.name())
1173
1236
 
1174
1237
  if active_vms:
1175
- self.show_error_message(f"All VMs must be stopped for bulk editing. Running VMs: {', '.join(active_vms)}")
1238
+ self.show_error_message(ErrorMessages.VMS_MUST_BE_STOPPED_FOR_BULK_EDITING.format(running_vms=', '.join(active_vms)))
1176
1239
  # Restore selection since we are aborting
1177
1240
  self.selected_vm_uuids = set(selected_uuids_copy)
1178
1241
  return
@@ -1209,9 +1272,9 @@ class VMManagerTUI(App):
1209
1272
  # Clear selection after launching modal
1210
1273
  self.selected_vm_uuids.clear()
1211
1274
  else:
1212
- self.show_error_message("Could not load details for reference VM.")
1275
+ self.show_error_message(ErrorMessages.COULD_NOT_LOAD_DETAILS_FOR_REFERENCE_VM)
1213
1276
  except Exception as e:
1214
- self.app.show_error_message(f"Error preparing bulk edit: {e}")
1277
+ self.app.show_error_message(ErrorMessages.BULK_EDIT_PREP_ERROR.format(error=e))
1215
1278
 
1216
1279
  warning_message = "This will apply configuration changes to all selected VMs based on the settings you choose.\n\nSome changes modify the VM's XML directly. All change cannot be undone.\n\nAre you sure you want to proceed?"
1217
1280
  self.app.push_screen(ConfirmationDialog(warning_message), on_confirm)
@@ -1260,13 +1323,13 @@ class VMManagerTUI(App):
1260
1323
  logging.info(summary)
1261
1324
 
1262
1325
  if successful_vms:
1263
- self.call_from_thread(self.show_success_message, f"Bulk action [b]{action_type}[/b] successful for {len(successful_vms)} VMs.")
1326
+ self.call_from_thread(self.show_success_message, SuccessMessages.BULK_ACTION_SUCCESS_TEMPLATE.format(action_type=action_type, count=len(successful_vms)))
1264
1327
  if failed_vms:
1265
- self.call_from_thread(self.show_error_message, f"Bulk action [b]{action_type}[/b] failed for {len(failed_vms)} VMs.")
1328
+ self.call_from_thread(self.show_error_message, ErrorMessages.BULK_ACTION_FAILED_TEMPLATE.format(action_type=action_type, count=len(failed_vms)))
1266
1329
 
1267
1330
  except Exception as e:
1268
1331
  logging.error(f"An unexpected error occurred during bulk action service call: {e}", exc_info=True)
1269
- self.call_from_thread(self.show_error_message, f"A fatal error occurred during bulk action: {e}")
1332
+ self.call_from_thread(self.show_error_message, ErrorMessages.FATAL_ERROR_BULK_ACTION.format(error=e))
1270
1333
 
1271
1334
  finally:
1272
1335
  # Ensure these are called on the main thread
@@ -1310,7 +1373,7 @@ class VMManagerTUI(App):
1310
1373
  vms_per_page,
1311
1374
  uris_to_query,
1312
1375
  force=force,
1313
- optimize_for_current_page=current_page,
1376
+ optimize_for_current_page=optimize_for_current_page,
1314
1377
  on_complete=on_complete,
1315
1378
  ),
1316
1379
  name="list_vms"
@@ -1353,22 +1416,18 @@ class VMManagerTUI(App):
1353
1416
  end_index = start_index + vms_per_page
1354
1417
  paginated_domains = domains_to_display[start_index:end_index]
1355
1418
 
1356
- # Collect data in worker thread
1357
- vm_data_list = []
1358
- page_uuids = set()
1359
-
1360
- for domain, conn in paginated_domains:
1419
+ # Parallelize fetching of VM info
1420
+ def fetch_vm_data(item):
1421
+ domain, conn = item
1361
1422
  try:
1362
1423
  uri = self.vm_service.get_uri_for_connection(conn) or conn.getURI()
1363
1424
  uuid, vm_name = self.vm_service.get_vm_identity(domain, conn, known_uri=uri)
1364
- page_uuids.add(uuid)
1365
1425
 
1366
- # Get info from cache or fetch if not present. This is safe as we are in a worker.
1426
+ # Get info from cache or fetch if not present
1367
1427
  info = self.vm_service._get_domain_info(domain)
1368
1428
  cached_details = self.vm_service.get_cached_vm_details(uuid)
1369
1429
 
1370
- # Explicitly get state from cache/service which might be event-updated
1371
- # This avoids flickering if domain.info() (fetched by _get_domain_info) lags behind events
1430
+ # Explicitly get state from cache/service
1372
1431
  state_tuple = self.vm_service._get_domain_state(domain, internal_id=uuid)
1373
1432
 
1374
1433
  effective_state = None
@@ -1394,7 +1453,7 @@ class VMManagerTUI(App):
1394
1453
  cpu = 0
1395
1454
  memory = 0
1396
1455
 
1397
- vm_data = {
1456
+ return {
1398
1457
  'uuid': uuid,
1399
1458
  'name': vm_name,
1400
1459
  'status': status,
@@ -1405,19 +1464,28 @@ class VMManagerTUI(App):
1405
1464
  'conn': conn,
1406
1465
  'uri': uri
1407
1466
  }
1408
- vm_data_list.append(vm_data)
1409
-
1410
1467
  except libvirt.libvirtError as e:
1411
1468
  if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
1412
1469
  logging.warning(f"Skipping display of non-existent VM during refresh.")
1413
- continue
1470
+ return None
1414
1471
  else:
1415
1472
  try:
1416
1473
  name_for_error = vm_name if 'vm_name' in locals() else domain.name()
1417
1474
  except:
1418
1475
  name_for_error = "Unknown"
1419
- self.call_from_thread(self.show_error_message, f"Error getting info for VM '{name_for_error}': {e}")
1420
- continue
1476
+ self.call_from_thread(self.show_error_message, ErrorMessages.VM_INFO_ERROR.format(vm_name=name_for_error, error=e))
1477
+ return None
1478
+
1479
+ vm_data_list = []
1480
+ page_uuids = set()
1481
+
1482
+ with ThreadPoolExecutor(max_workers=20) as executor:
1483
+ results = list(executor.map(fetch_vm_data, paginated_domains))
1484
+
1485
+ for result in results:
1486
+ if result:
1487
+ vm_data_list.append(result)
1488
+ page_uuids.add(result['uuid'])
1421
1489
 
1422
1490
  # Cleanup cache: remove cards for VMs that no longer exist at all
1423
1491
  all_uuids_from_libvirt = set(all_active_uuids)
@@ -1550,7 +1618,7 @@ class VMManagerTUI(App):
1550
1618
  self.filtered_server_uris = [u for u in self.filtered_server_uris if u not in uris_to_remove]
1551
1619
 
1552
1620
  if removed_names:
1553
- self.show_error_message(f"Server(s) {', '.join(removed_names)} disconnected and autoconnect disabled due to connection failures.")
1621
+ self.show_error_message(ErrorMessages.SERVER_DISCONNECTED_AUTOCONNECT_DISABLED.format(names=', '.join(removed_names)))
1554
1622
 
1555
1623
  if config_changed:
1556
1624
  self.config['servers'] = self.servers
@@ -1563,7 +1631,7 @@ class VMManagerTUI(App):
1563
1631
  self.call_from_thread(update_ui_on_main_thread)
1564
1632
 
1565
1633
  except Exception as e:
1566
- self.call_from_thread(self.show_error_message, f"Error fetching VM data: {e}")
1634
+ self.call_from_thread(self.show_error_message, ErrorMessages.ERROR_FETCHING_VM_DATA.format(error=e))
1567
1635
  finally:
1568
1636
  if on_complete:
1569
1637
  self.call_from_thread(on_complete)
@@ -1613,7 +1681,7 @@ class VMManagerTUI(App):
1613
1681
  def action_pattern_select(self) -> None:
1614
1682
  """Handles the 'Pattern Sel' button press."""
1615
1683
  if not self.active_uris:
1616
- self.show_error_message("No active servers.")
1684
+ self.show_error_message(ErrorMessages.NO_ACTIVE_SERVERS)
1617
1685
  return
1618
1686
 
1619
1687
  # Gather all known VMs from cache
@@ -1638,7 +1706,7 @@ class VMManagerTUI(App):
1638
1706
  continue
1639
1707
 
1640
1708
  if not available_vms:
1641
- self.show_error_message("No VMs found in cache. Try refreshing first.")
1709
+ self.show_error_message(ErrorMessages.NO_VMS_IN_CACHE)
1642
1710
  return
1643
1711
 
1644
1712
  # Prepare server list for the modal, matching FilterModal logic
@@ -1661,7 +1729,7 @@ class VMManagerTUI(App):
1661
1729
  if selected_uuids:
1662
1730
  # Add found UUIDs to current selection
1663
1731
  self.selected_vm_uuids.update(selected_uuids)
1664
- self.show_success_message(f"Selected {len(selected_uuids)} VMs matching pattern.")
1732
+ self.show_success_message(SuccessMessages.VMS_SELECTED_BY_PATTERN.format(count=len(selected_uuids)))
1665
1733
  self.refresh_vm_list()
1666
1734
 
1667
1735
  self.push_screen(PatternSelectModal(available_vms, available_servers, selected_servers), handle_result)
@@ -1672,7 +1740,7 @@ class VMManagerTUI(App):
1672
1740
  """Handles the 'Bulk Selected' button press."""
1673
1741
  self._collapse_all_action_collapsibles()
1674
1742
  if not self.selected_vm_uuids:
1675
- self.show_error_message("No VMs selected.")
1743
+ self.show_error_message(ErrorMessages.NO_VMS_SELECTED)
1676
1744
  return
1677
1745
 
1678
1746
  uuids_snapshot = list(self.selected_vm_uuids)
@@ -1683,7 +1751,7 @@ class VMManagerTUI(App):
1683
1751
 
1684
1752
  # Use the service to find specific domains by their internal ID (UUID@URI)
1685
1753
  # This correctly handles cases where identical UUIDs exist on different servers
1686
- found_domains_map = self.vm_service.find_domains_by_uuids(self.active_uris, uuids)
1754
+ found_domains_map = self.vm_service.find_domains_by_uuids(self.active_uris, uuids, check_validity=False)
1687
1755
 
1688
1756
  all_names = set()
1689
1757
  for domain in found_domains_map.values():
@@ -1701,9 +1769,7 @@ class VMManagerTUI(App):
1701
1769
  self.push_screen, BulkActionModal(vm_names_list), self.handle_bulk_action_result
1702
1770
  )
1703
1771
  else:
1704
- self.call_from_thread(
1705
- self.show_error_message, "Could not retrieve names for selected VMs."
1706
- )
1772
+ self.call_from_thread(self.show_error_message, ErrorMessages.BULK_ACTION_VM_NAMES_RETRIEVAL_FAILED)
1707
1773
 
1708
1774
  self.worker_manager.run(
1709
1775
  get_names_and_show_modal,