virtui-manager 1.1.6__py3-none-any.whl → 1.4.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 (62) hide show
  1. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/METADATA +1 -1
  2. virtui_manager-1.4.0.dist-info/RECORD +76 -0
  3. vmanager/constants.py +739 -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.mo +0 -0
  9. vmanager/locales/de/LC_MESSAGES/virtui-manager.po +3158 -0
  10. vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
  11. vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3155 -0
  12. vmanager/locales/it/LC_MESSAGES/virtui-manager.mo +0 -0
  13. vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3132 -0
  14. vmanager/locales/virtui-manager.pot +3033 -0
  15. vmanager/modals/bulk_modals.py +13 -12
  16. vmanager/modals/cache_stats_modal.py +6 -5
  17. vmanager/modals/capabilities_modal.py +133 -0
  18. vmanager/modals/config_modal.py +25 -24
  19. vmanager/modals/cpu_mem_pc_modals.py +22 -21
  20. vmanager/modals/custom_migration_modal.py +10 -9
  21. vmanager/modals/disk_pool_modals.py +60 -59
  22. vmanager/modals/host_dashboard_modal.py +137 -0
  23. vmanager/modals/host_stats.py +199 -0
  24. vmanager/modals/howto_disk_modal.py +2 -1
  25. vmanager/modals/howto_network_modal.py +2 -1
  26. vmanager/modals/howto_overlay_modal.py +2 -1
  27. vmanager/modals/howto_ssh_modal.py +2 -1
  28. vmanager/modals/howto_virtiofs_modal.py +2 -1
  29. vmanager/modals/input_modals.py +11 -10
  30. vmanager/modals/log_modal.py +2 -1
  31. vmanager/modals/migration_modals.py +21 -19
  32. vmanager/modals/network_modals.py +45 -36
  33. vmanager/modals/provisioning_modals.py +56 -56
  34. vmanager/modals/select_server_modals.py +8 -7
  35. vmanager/modals/selection_modals.py +7 -6
  36. vmanager/modals/server_modals.py +24 -23
  37. vmanager/modals/server_prefs_modals.py +78 -71
  38. vmanager/modals/utils_modals.py +10 -9
  39. vmanager/modals/virsh_modals.py +3 -2
  40. vmanager/modals/virtiofs_modals.py +6 -5
  41. vmanager/modals/vm_type_info_modal.py +2 -1
  42. vmanager/modals/vmanager_modals.py +19 -19
  43. vmanager/modals/vmcard_dialog.py +57 -57
  44. vmanager/modals/vmdetails_modals.py +115 -123
  45. vmanager/modals/xml_modals.py +3 -2
  46. vmanager/network_manager.py +4 -1
  47. vmanager/storage_manager.py +157 -39
  48. vmanager/utils.py +54 -7
  49. vmanager/vm_actions.py +48 -24
  50. vmanager/vm_migration.py +4 -1
  51. vmanager/vm_queries.py +67 -25
  52. vmanager/vm_service.py +8 -5
  53. vmanager/vmanager.css +55 -1
  54. vmanager/vmanager.py +247 -120
  55. vmanager/vmcard.css +3 -1
  56. vmanager/vmcard.py +270 -205
  57. vmanager/webconsole_manager.py +22 -22
  58. virtui_manager-1.1.6.dist-info/RECORD +0 -65
  59. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/WHEEL +0 -0
  60. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/entry_points.txt +0 -0
  61. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/licenses/LICENSE +0 -0
  62. {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/top_level.txt +0 -0
vmanager/vmanager.py CHANGED
@@ -4,7 +4,9 @@ Main interface
4
4
  import os
5
5
  import sys
6
6
  import re
7
+ import threading
7
8
  from threading import RLock
9
+ from concurrent.futures import ThreadPoolExecutor
8
10
  import logging
9
11
  import argparse
10
12
  from collections import deque
@@ -22,8 +24,10 @@ from textual.worker import Worker, WorkerState
22
24
 
23
25
  from .config import load_config, save_config, get_log_path
24
26
  from .constants import (
25
- VmAction, VmStatus, ButtonLabels, ButtonIds,
26
- ErrorMessages, AppInfo, StatusText, ServerPallette,
27
+ WarningMessages,
28
+ SuccessMessages,
29
+ VmAction, VmStatus, ButtonLabels, BindingDescriptions,
30
+ ErrorMessages, AppInfo, StatusText, ServerPallette, QuickMessages, ProgressMessages,
27
31
  )
28
32
  from .events import VmActionRequest, VMSelectionChanged, VmCardUpdateRequest #,VMNameClicked
29
33
  from .libvirt_error_handler import register_error_handler
@@ -34,6 +38,7 @@ from .modals.server_modals import ServerManagementModal
34
38
  from .modals.server_prefs_modals import ServerPrefModal
35
39
  from .modals.select_server_modals import SelectOneServerModal, SelectServerModal
36
40
  from .modals.selection_modals import PatternSelectModal
41
+ from .modals.capabilities_modal import CapabilitiesTreeModal
37
42
  from .modals.cache_stats_modal import CacheStatsModal
38
43
  from .modals.utils_modals import (
39
44
  show_error_message,
@@ -49,6 +54,7 @@ from .modals.vmanager_modals import (
49
54
  )
50
55
  from .modals.virsh_modals import VirshShellScreen
51
56
  from .modals.provisioning_modals import InstallVMModal
57
+ from .modals.host_dashboard_modal import HostDashboardModal
52
58
  from .utils import (
53
59
  check_novnc_path,
54
60
  check_r_viewer,
@@ -56,21 +62,22 @@ from .utils import (
56
62
  generate_webconsole_keys_if_needed,
57
63
  get_server_color_cached,
58
64
  setup_cache_monitoring,
65
+ setup_logging
59
66
  )
60
- from .libvirt_utils import get_internal_id
61
67
  from .vm_queries import (
62
68
  get_status,
63
69
  )
70
+ from .libvirt_utils import (
71
+ get_internal_id, get_host_resources,
72
+ get_active_vm_allocation
73
+ )
64
74
  from .vm_service import VMService
65
75
  from .vmcard import VMCard
66
76
  from .vmcard_pool import VMCardPool
67
77
  from .webconsole_manager import WebConsoleManager
78
+ from .modals.host_stats import HostStats, SingleHostStat
68
79
 
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)
80
+ setup_logging()
74
81
 
75
82
  class WorkerManager:
76
83
  """A class to manage and track Textual workers."""
@@ -158,26 +165,28 @@ class VMManagerTUI(App):
158
165
  """A Textual application to manage VMs."""
159
166
 
160
167
  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"),
168
+ Binding(key="v", action="view_log", description=BindingDescriptions.LOG),
169
+ Binding(key="f", action="filter_view", description=BindingDescriptions.FILTER, show=False),
170
+ Binding(key="k", action="compact_view", description=BindingDescriptions.COMPACT_VIEW, show=True),
171
+ #Binding(key="p", action="server_preferences", description=BindingDescriptions.SERVER_PREFS),
172
+ Binding(key="c", action="config", description=BindingDescriptions.CONFIG, show=True),
173
+ Binding(key="b", action="bulk_cmd", description=BindingDescriptions.BULK_CMD, show=False),
174
+ Binding(key="s", action="select_server", description=BindingDescriptions.SELECT_SERVERS, show=False),
175
+ Binding(key="l", action="manage_server", description=BindingDescriptions.MANAGE_SERVERS, show=False),
176
+ Binding(key="p", action="pattern_select", description=BindingDescriptions.PATTERN_SELECT, show=False),
177
+ Binding(key="ctrl+a", action="toggle_select_all", description=BindingDescriptions.SELECT_ALL),
178
+ Binding(key="ctrl+u", action="unselect_all", description=BindingDescriptions.UNSELECT_ALL),
179
+ Binding(key="left", action="previous_page", description=BindingDescriptions.PREVIOUS_PAGE, show=False),
180
+ Binding(key="right", action="next_page", description=BindingDescriptions.NEXT_PAGE, show=False),
181
+ Binding(key="up", action="filter_running", description=BindingDescriptions.RUNNING_VMS, show=False),
182
+ Binding(key="down", action="filter_all", description=BindingDescriptions.ALL_VMS, show=False),
183
+ Binding(key="ctrl+v", action="virsh_shell", description=BindingDescriptions.VIRSH_SHELL, show=False ),
184
+ Binding(key="h", action="host_capabilities", description=BindingDescriptions.HOST_CAPABILITIES, show=False),
185
+ Binding(key="H", action="host_dashboard", description=BindingDescriptions.HOST_DASHBOARD, show=False),
186
+ Binding(key="i", action="install_vm", description=BindingDescriptions.INSTALL_VM, show=True),
187
+ Binding(key="ctrl+l", action="toggle_stats_logging", description=BindingDescriptions.TOGGLE_STATS, show=False),
188
+ Binding(key="ctrl+s", action="show_cache_stats", description=BindingDescriptions.CACHE_STATS, show=False),
189
+ Binding(key="q", action="quit", description=BindingDescriptions.QUIT),
181
190
  ]
182
191
 
183
192
  config = load_config()
@@ -244,6 +253,8 @@ class VMManagerTUI(App):
244
253
  self.last_increase = {} # Dict {uri: last_how_many_more}
245
254
  self.last_method_increase = {} # Dict {(uri, method): last_increase}
246
255
  self.r_viewer = None
256
+ self.host_stats = HostStats(self.vm_service, self.get_server_color)
257
+ self._hide_stats_timer = None
247
258
 
248
259
  def on_unmount(self) -> None:
249
260
  """Called when the application is unmounted."""
@@ -273,7 +284,7 @@ class VMManagerTUI(App):
273
284
  target = self.show_warning_message
274
285
  elif level == "progress":
275
286
  target = self.show_in_progress_message
276
-
287
+
277
288
  try:
278
289
  self.call_from_thread(target, message)
279
290
  except RuntimeError:
@@ -296,12 +307,36 @@ class VMManagerTUI(App):
296
307
  except RuntimeError:
297
308
  self.worker_manager._cleanup_finished_workers()
298
309
 
310
+ def _trigger_host_stats_refresh(self):
311
+ """Triggers a refresh of host statistics, cancelling any existing refresh."""
312
+ # Show host stats
313
+ self.host_stats.styles.display = "block"
314
+
315
+ # Reset hide timer
316
+ if self._hide_stats_timer:
317
+ self._hide_stats_timer.stop()
318
+ self._hide_stats_timer = self.set_timer(10.0, self._hide_host_stats)
319
+
320
+ self.worker_manager.cancel("host_stats_refresh")
321
+ self.worker_manager.run(
322
+ self.host_stats.refresh_stats,
323
+ name="host_stats_refresh",
324
+ description="Refreshing host stats"
325
+ )
326
+
327
+ def _hide_host_stats(self):
328
+ """Hides the host stats widget."""
329
+ self.host_stats.styles.display = "none"
330
+ self._hide_stats_timer = None
331
+
299
332
  def on_vm_update(self, internal_id: str):
300
333
  """Callback from VMService for specific VM updates."""
301
334
  try:
302
335
  self.call_from_thread(self.post_message, VmCardUpdateRequest(internal_id))
336
+ self.call_from_thread(self._trigger_host_stats_refresh)
303
337
  except RuntimeError:
304
338
  self.post_message(VmCardUpdateRequest(internal_id))
339
+ self._trigger_host_stats_refresh()
305
340
 
306
341
  def watch_bulk_operation_in_progress(self, in_progress: bool) -> None:
307
342
  """
@@ -334,13 +369,12 @@ class VMManagerTUI(App):
334
369
  def compose(self) -> ComposeResult:
335
370
  """Create child widgets for the app."""
336
371
  self.ui["vms_container"] = Vertical(id="vms-container")
337
- self.ui["error_footer"] = Static(id="error-footer", classes="error-message")
338
372
  self.ui["page_info"] = Label("", id="page-info", classes="")
339
373
  self.ui["prev_button"] = Button(
340
- ButtonLabels.PREVIOUS_PAGE, id=ButtonIds.PREV_BUTTON, variant="primary", classes="ctrlpage"
374
+ ButtonLabels.PREVIOUS_PAGE, id="prev-button", variant="primary", classes="ctrlpage"
341
375
  )
342
376
  self.ui["next_button"] = Button(
343
- ButtonLabels.NEXT_PAGE, id=ButtonIds.NEXT_BUTTON, variant="primary", classes="ctrlpage"
377
+ ButtonLabels.NEXT_PAGE, id="next-button", variant="primary", classes="ctrlpage"
344
378
  )
345
379
  self.ui["pagination_controls"] = Horizontal(
346
380
  self.ui["prev_button"],
@@ -356,30 +390,28 @@ class VMManagerTUI(App):
356
390
  yield Header()
357
391
  with Horizontal(classes="top-controls"):
358
392
  yield Button(
359
- ButtonLabels.SELECT_SERVER, id=ButtonIds.SELECT_SERVER_BUTTON, classes="Buttonpage"
393
+ ButtonLabels.SELECT_SERVER, id="select_server_button", classes="Buttonpage"
360
394
  )
361
- yield Button(ButtonLabels.MANAGE_SERVERS, id=ButtonIds.MANAGE_SERVERS_BUTTON, classes="Buttonpage")
395
+ yield Button(ButtonLabels.MANAGE_SERVERS, id="manage_servers_button", classes="Buttonpage")
362
396
  yield Button(
363
- ButtonLabels.SERVER_PREFERENCES, id=ButtonIds.SERVER_PREFERENCES_BUTTON, classes="Buttonpage"
397
+ ButtonLabels.SERVER_PREFERENCES, id="server_preferences_button", classes="Buttonpage"
364
398
  )
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")
399
+ yield Button(ButtonLabels.FILTER_VM, id="filter_button", classes="Buttonpage")
400
+ #yield Button(ButtonLabels.VIEW_LOG, id="view-log-button", classes="Buttonpage")
367
401
  # 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")
402
+ yield Button(ButtonLabels.BULK_CMD, id="bulk_selected_vms", classes="Buttonpage")
403
+ yield Button(ButtonLabels.PATTERN_SELECT, id="pattern_select_button", classes="Buttonpage")
404
+ #yield Button(ButtonLabels.CONFIG, id="config-button", classes="Buttonpage")
371
405
  #yield Button(
372
- # ButtonLabels.COMPACT_VIEW, id=ButtonIds.COMPACT_VIEW_BUTTON, classes="Buttonpage"
406
+ # ButtonLabels.COMPACT_VIEW, id="compact-view-button", classes="Buttonpage"
373
407
  #)
374
408
  yield Link("About", url="https://aginies.github.io/virtui-manager/")
375
409
 
376
410
  yield self.ui["pagination_controls"]
411
+ yield self.host_stats
377
412
  yield self.ui["vms_container"]
378
- yield self.ui["error_footer"]
379
413
  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
- )
414
+ self.show_success_message(SuccessMessages.TERMINAL_COPY_HINT)
383
415
 
384
416
  def reload_servers(self, new_servers):
385
417
  self.servers = new_servers
@@ -398,7 +430,7 @@ class VMManagerTUI(App):
398
430
  )
399
431
  self.r_viewer_available = False
400
432
  else:
401
- self.show_quick_message(f"The remove viewer {self.r_viewer} has been selected.")
433
+ self.show_quick_message(QuickMessages.REMOTE_VIEWER_SELECTED.format(viewer=self.r_viewer))
402
434
 
403
435
  if not check_websockify():
404
436
  self.show_error_message(
@@ -426,9 +458,7 @@ class VMManagerTUI(App):
426
458
  vms_container.styles.grid_size_columns = 2
427
459
 
428
460
  if not self.servers:
429
- self.show_success_message(
430
- "No servers configured. Please add one via 'Servers List'."
431
- )
461
+ self.show_success_message(SuccessMessages.NO_SERVERS_CONFIGURED)
432
462
  else:
433
463
  # Launch initial connection and cache loading in background
434
464
  if self.active_uris:
@@ -441,10 +471,10 @@ class VMManagerTUI(App):
441
471
  """Connects to servers in background and then triggers cache loading."""
442
472
  if self.active_uris:
443
473
  for uri in self.active_uris:
444
- self.call_from_thread(self.show_in_progress_message, f"Connecting to [b]{uri}[/b]...")
474
+ self.call_from_thread(self.show_in_progress_message, ProgressMessages.CONNECTING_TO_SERVER.format(uri=uri))
445
475
  success = self.connect_libvirt(uri)
446
476
  if success:
447
- self.call_from_thread(self.show_success_message, f"Connected to [b]{uri}[/b]")
477
+ self.call_from_thread(self.show_success_message, SuccessMessages.CONNECTED_TO_SERVER.format(uri=uri))
448
478
  else:
449
479
  error_msg = self.vm_service.connection_manager.get_connection_error(uri)
450
480
  if error_msg:
@@ -461,7 +491,7 @@ class VMManagerTUI(App):
461
491
  # Log libvirt call statistics
462
492
  call_stats = self.vm_service.connection_manager.get_stats()
463
493
  if call_stats:
464
- logging.info("=== Libvirt Call Statistics ===")
494
+ logging.debug("=== Libvirt Call Statistics ===")
465
495
  for uri, methods in sorted(call_stats.items()):
466
496
  server_name = uri
467
497
  for s in self.servers:
@@ -479,7 +509,7 @@ class VMManagerTUI(App):
479
509
  if total_increase > 0:
480
510
  increase_pct = 100 - (previous_how_many_more*100 / total_increase)
481
511
 
482
- logging.info(f"{server_name} ({uri}): {total_calls} calls | +{total_increase} ({increase_pct:.1f}%)")
512
+ logging.debug(f"{server_name} ({uri}): {total_calls} calls | +{total_increase} ({increase_pct:.1f}%)")
483
513
  previous_how_many_more = how_many_more
484
514
 
485
515
  # Initialize previous method calls dict for this URI if needed
@@ -493,7 +523,7 @@ class VMManagerTUI(App):
493
523
 
494
524
  self.last_method_calls[uri][method] = count
495
525
  how_many_more_count = count - prev_method_count
496
- logging.info(f" - {method}: {count} calls (+{how_many_more_count})")
526
+ logging.debug(f" - {method}: {count} calls (+{how_many_more_count})")
497
527
 
498
528
  self.last_increase[uri] = how_many_more
499
529
  self.last_total_calls[uri] = total_calls
@@ -510,13 +540,13 @@ class VMManagerTUI(App):
510
540
  self._stats_interval_timer = None
511
541
  self._stats_logging_active = False
512
542
  setup_cache_monitoring(enable=False)
513
- self.show_success_message("Statistics logging and monitoring disabled.")
543
+ self.show_success_message(SuccessMessages.STATS_LOGGING_DISABLED)
514
544
  else:
515
545
  setup_cache_monitoring(enable=True)
516
546
  self._log_cache_statistics()
517
547
  self._stats_interval_timer = self.set_interval(10, self._log_cache_statistics)
518
548
  self._stats_logging_active = True
519
- self.show_success_message("Statistics logging and monitoring enabled (every 10s).")
549
+ self.show_success_message(SuccessMessages.STATS_LOGGING_ENABLED)
520
550
 
521
551
  def _initial_cache_worker(self):
522
552
  """Pre-loads VM cache before displaying the UI."""
@@ -545,10 +575,10 @@ class VMManagerTUI(App):
545
575
  if s['uri'] == uri:
546
576
  server_name = s['name']
547
577
  break
548
- self.call_from_thread(self.show_error_message, f"Server [b]{server_name}[/b]: {error_msg}")
578
+ self.call_from_thread(self.show_error_message, ErrorMessages.SERVER_CONNECTION_ERROR.format(server_name=server_name, error_msg=error_msg))
549
579
 
550
580
  if self.vm_service.connection_manager.is_max_retries_reached(uri):
551
- self.call_from_thread(self.remove_active_uri, uri)
581
+ self.call_from_thread(self.remove_active_uri, uri)
552
582
 
553
583
  # Pre-cache info and XML only for the first page of VMs
554
584
  # Full info will be loaded on-demand when cards are displayed
@@ -573,14 +603,14 @@ class VMManagerTUI(App):
573
603
 
574
604
  if active_vms_on_page:
575
605
  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}")
606
+ self.call_from_thread(self.show_quick_message, QuickMessages.CACHING_VM_STATE.format(vms_list=vms_list_str))
577
607
 
578
608
  self.call_from_thread(self._on_initial_cache_complete)
579
609
 
580
610
  except Exception as e:
581
611
  self.call_from_thread(
582
- self.show_error_message,
583
- f"Error during initial cache loading: {e}"
612
+ self.show_error_message,
613
+ ErrorMessages.ERROR_DURING_INITIAL_CACHE_LOADING.format(error=e)
584
614
  )
585
615
 
586
616
  def _on_initial_cache_complete(self):
@@ -588,7 +618,7 @@ class VMManagerTUI(App):
588
618
  self.initial_cache_loading = False
589
619
  self.initial_cache_complete = True
590
620
  if self.servers:
591
- self.show_quick_message("VM data loaded. Displaying VMs...")
621
+ self.show_quick_message(QuickMessages.VM_DATA_LOADED)
592
622
  self.refresh_vm_list()
593
623
 
594
624
  def _update_layout_for_size(self):
@@ -630,7 +660,7 @@ class VMManagerTUI(App):
630
660
  vms_container.styles.grid_size_columns = cols
631
661
 
632
662
  old_vms_per_page = self.VMS_PER_PAGE
633
-
663
+
634
664
  self.VMS_PER_PAGE = cols * rows
635
665
  if self.compact_view:
636
666
  self.VMS_PER_PAGE = cols * rows + cols
@@ -646,9 +676,7 @@ class VMManagerTUI(App):
646
676
  self.vm_card_pool.prefill_pool()
647
677
 
648
678
  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
- )
679
+ self.show_warning_message(WarningMessages.VMS_PER_PAGE_PERFORMANCE_WARNING.format(vms_per_page=self.VMS_PER_PAGE))
652
680
 
653
681
  self.refresh_vm_list(force=True)
654
682
 
@@ -676,7 +704,7 @@ class VMManagerTUI(App):
676
704
  if conn:
677
705
  yield conn
678
706
  else:
679
- self.show_error_message(f"Failed to open connection to [b]{uri}[/b]")
707
+ self.show_error_message(ErrorMessages.FAILED_TO_OPEN_CONNECTION.format(uri=uri))
680
708
 
681
709
  def connect_libvirt(self, uri: str) -> None:
682
710
  """Connects to libvirt."""
@@ -705,11 +733,11 @@ class VMManagerTUI(App):
705
733
  def show_warning_message(self, message: str):
706
734
  show_warning_message(self, message)
707
735
 
708
- @on(Button.Pressed, f"#{ButtonIds.COMPACT_VIEW_BUTTON}")
736
+ @on(Button.Pressed, f"#compact-view-button")
709
737
  def action_compact_view(self) -> None:
710
738
  """Toggle compact view."""
711
739
  if self.bulk_operation_in_progress:
712
- self.show_warning_message("Compact view is locked during bulk operations.")
740
+ self.show_warning_message(WarningMessages.COMPACT_VIEW_LOCKED)
713
741
  return
714
742
 
715
743
  if not self.compact_view:
@@ -754,8 +782,7 @@ class VMManagerTUI(App):
754
782
  uris_to_connect = [uri for uri in selected_uris if uri not in self.active_uris]
755
783
  # Show connecting message for each new server
756
784
  for uri in uris_to_connect:
757
- self.show_in_progress_message(f"Connecting to [b]{uri}[/b]...")
758
-
785
+ self.show_in_progress_message(ProgressMessages.CONNECTING_TO_SERVER.format(uri=uri))
759
786
  for uri in uris_to_disconnect:
760
787
  # Cleanup UI caches for VMs on this server
761
788
  uuids_to_release = [
@@ -795,11 +822,11 @@ class VMManagerTUI(App):
795
822
  server_name = s['name']
796
823
  break
797
824
  if success:
798
- self.call_from_thread(self.show_success_message, f"Connected to [b]{server_name}[/b]")
825
+ self.call_from_thread(self.show_success_message, SuccessMessages.SERVER_CONNECTED.format(name=server_name))
799
826
  else:
800
827
  error_msg = self.vm_service.connection_manager.get_connection_error(uri)
801
828
  if error_msg:
802
- self.call_from_thread(self.show_error_message, f"Failed to connect to [b]{server_name}[/b]: {error_msg}")
829
+ self.call_from_thread(self.show_error_message, ErrorMessages.SERVER_FAILED_TO_CONNECT.format(server_name=server_name, error_msg=error_msg))
803
830
 
804
831
  if uris_to_connect:
805
832
  self.worker_manager.run(show_connection_results, name="show_connection_results")
@@ -868,7 +895,7 @@ class VMManagerTUI(App):
868
895
  if self.sort_by != VmStatus.RUNNING:
869
896
  self.sort_by = VmStatus.RUNNING
870
897
  self.current_page = 0
871
- self.show_quick_message("Filter: Running VMs")
898
+ self.show_quick_message(QuickMessages.FILTER_RUNNING_VMS)
872
899
  self.refresh_vm_list()
873
900
 
874
901
  def action_filter_all(self) -> None:
@@ -876,7 +903,7 @@ class VMManagerTUI(App):
876
903
  if self.sort_by != VmStatus.DEFAULT:
877
904
  self.sort_by = VmStatus.DEFAULT
878
905
  self.current_page = 0
879
- self.show_quick_message("Filter: All VMs")
906
+ self.show_quick_message(QuickMessages.FILTER_ALL_VMS)
880
907
  self.refresh_vm_list()
881
908
 
882
909
  @on(FilterModal.FilterChanged)
@@ -899,7 +926,7 @@ class VMManagerTUI(App):
899
926
  self.search_text = new_search
900
927
  self.filtered_server_uris = new_selected_servers
901
928
  self.current_page = 0
902
- self.show_in_progress_message("Loading VM data from remote server(s)...")
929
+ self.show_in_progress_message(ProgressMessages.LOADING_VM_DATA_FROM_REMOTE_SERVERS)
903
930
  self.refresh_vm_list()
904
931
 
905
932
  def action_config(self) -> None:
@@ -919,7 +946,7 @@ class VMManagerTUI(App):
919
946
  logging.getLogger().setLevel(new_log_level)
920
947
  for handler in logging.getLogger().handlers:
921
948
  handler.setLevel(new_log_level)
922
- self.show_success_message(f"Log level changed to {new_log_level_str}")
949
+ self.show_success_message(SuccessMessages.LOG_LEVEL_CHANGED.format(level=new_log_level_str))
923
950
 
924
951
  # Update remote viewer if changed
925
952
  self.r_viewer = check_r_viewer(self.config.get("REMOTE_VIEWER"))
@@ -930,10 +957,10 @@ class VMManagerTUI(App):
930
957
  self.r_viewer_available = True
931
958
 
932
959
  if (self.config.get("STATS_INTERVAL") != old_stats_interval):
933
- self.show_in_progress_message("Configuration updated. Refreshing VM list...")
960
+ self.show_in_progress_message(ProgressMessages.CONFIG_UPDATED_REFRESHING_VM_LIST)
934
961
  self.refresh_vm_list(force=False, optimize_for_current_page=True)
935
962
  else:
936
- self.show_success_message("Configuration updated.")
963
+ self.show_success_message(SuccessMessages.CONFIG_UPDATED)
937
964
 
938
965
  @on(Button.Pressed, "#config_button")
939
966
  def on_config_button_pressed(self, event: Button.Pressed) -> None:
@@ -985,7 +1012,7 @@ class VMManagerTUI(App):
985
1012
  self.call_from_thread(self.push_screen, modal)
986
1013
  except Exception as e:
987
1014
  self.call_from_thread(loading.dismiss)
988
- self.call_from_thread(self.show_error_message, f"Error launching preferences: {e}")
1015
+ self.call_from_thread(self.show_error_message, ErrorMessages.PREFERENCES_LAUNCH_ERROR.format(error=e))
989
1016
 
990
1017
  self.worker_manager.run(show_prefs, name="launch_server_prefs")
991
1018
  else:
@@ -1004,7 +1031,7 @@ class VMManagerTUI(App):
1004
1031
  Handles 0, 1, or multiple active servers.
1005
1032
  """
1006
1033
  if len(self.active_uris) == 0:
1007
- self.show_error_message("Not connected to any server.")
1034
+ self.show_error_message(ErrorMessages.NOT_CONNECTED_TO_ANY_SERVER)
1008
1035
  return
1009
1036
 
1010
1037
  if len(self.active_uris) == 1:
@@ -1038,6 +1065,34 @@ class VMManagerTUI(App):
1038
1065
  """Callback for the virsh shell button."""
1039
1066
  self.action_virsh_shell()
1040
1067
 
1068
+ def action_host_dashboard(self) -> None:
1069
+ """Show Host Resource Dashboard."""
1070
+ def launch_dashboard_modal(uri: str):
1071
+ conn = self.vm_service.connect(uri)
1072
+ if conn:
1073
+ # Find server name
1074
+ server_name = uri
1075
+ for s in self.servers:
1076
+ if s['uri'] == uri:
1077
+ server_name = s['name']
1078
+ break
1079
+ self.push_screen(HostDashboardModal(conn, server_name))
1080
+ else:
1081
+ self.show_error_message(ErrorMessages.COULD_NOT_CONNECT_TO_SERVER.format(uri=uri))
1082
+
1083
+ self._select_server_and_run(launch_dashboard_modal, "Select a server for Dashboard", "View Dashboard")
1084
+
1085
+ def action_host_capabilities(self) -> None:
1086
+ """Show Host Capabilities."""
1087
+ def launch_caps_modal(uri: str):
1088
+ conn = self.vm_service.connect(uri)
1089
+ if conn:
1090
+ self.push_screen(CapabilitiesTreeModal(conn))
1091
+ else:
1092
+ self.show_error_message(ErrorMessages.COULD_NOT_CONNECT_TO_SERVER.format(uri=uri))
1093
+
1094
+ self._select_server_and_run(launch_caps_modal, "Select a server for Capabilities", "View")
1095
+
1041
1096
  def action_install_vm(self) -> None:
1042
1097
  """Launch the VM Installation Modal."""
1043
1098
  def launch_install_modal(uri: str):
@@ -1057,7 +1112,7 @@ class VMManagerTUI(App):
1057
1112
  def action_worker():
1058
1113
  domain = self.vm_service.find_domain_by_uuid(self.active_uris, message.internal_id)
1059
1114
  if not domain:
1060
- self.call_from_thread(self.show_error_message, f"Could not find VM with ID [b]{message.internal_id}[/b]")
1115
+ self.call_from_thread(self.show_error_message, ErrorMessages.VM_NOT_FOUND_BY_ID.format(vm_id=message.internal_id))
1061
1116
  return
1062
1117
 
1063
1118
  #vm_name = domain.name()
@@ -1069,6 +1124,41 @@ class VMManagerTUI(App):
1069
1124
  try:
1070
1125
  # Message are done by events
1071
1126
  if message.action == VmAction.START:
1127
+ # Check resources
1128
+ try:
1129
+ conn = domain.connect()
1130
+ host_res = get_host_resources(conn)
1131
+ current_alloc = get_active_vm_allocation(conn)
1132
+
1133
+ # domain.info() -> [state, maxMem(KB), memory(KB), nrVirtCpu, cpuTime]
1134
+ vm_info = domain.info()
1135
+ vm_mem_mb = vm_info[1] // 1024
1136
+ vm_vcpus = vm_info[3]
1137
+
1138
+ host_mem_mb = host_res.get('available_memory', 0)
1139
+ host_cpus = host_res.get('total_cpus', 0)
1140
+
1141
+ active_mem_mb = current_alloc.get('active_allocated_memory', 0)
1142
+ active_vcpus = current_alloc.get('active_allocated_vcpus', 0)
1143
+
1144
+ overcommit_mem = (active_mem_mb + vm_mem_mb) > host_mem_mb
1145
+ overcommit_cpu = (active_vcpus + vm_vcpus) > host_cpus
1146
+
1147
+ if overcommit_mem or overcommit_cpu:
1148
+ warnings = []
1149
+ if overcommit_mem:
1150
+ warnings.append(f"Memory: {active_mem_mb + vm_mem_mb} MB > {host_mem_mb} MB")
1151
+ if overcommit_cpu:
1152
+ warnings.append(f"vCPUs: {active_vcpus + vm_vcpus} > {host_cpus}")
1153
+
1154
+ warning_msg = (
1155
+ f"Starting VM '{vm_name}' will exceed host capacity (Active Allocation):\n"
1156
+ f"{chr(10).join(warnings)}"
1157
+ )
1158
+ self.show_warning_message(warning_msg)
1159
+ except Exception as e:
1160
+ logging.error(f"Error checking resources before start: {e}")
1161
+
1072
1162
  self.vm_service.start_vm(domain)
1073
1163
  elif message.action == VmAction.STOP:
1074
1164
  self.vm_service.stop_vm(domain)
@@ -1088,10 +1178,17 @@ class VMManagerTUI(App):
1088
1178
  except Exception as e:
1089
1179
  self.call_from_thread(
1090
1180
  self.show_error_message,
1091
- f"Error on VM [b]{vm_name}[/b] during '{message.action}': {e}",
1181
+ ErrorMessages.ERROR_ON_VM_DURING_ACTION.format(vm_name=vm_name, action=message.action, error=e),
1092
1182
  )
1093
1183
  finally:
1094
1184
  self.vm_service.unsuppress_vm_events(message.internal_id)
1185
+
1186
+ # Show stats update since event was suppressed
1187
+ try:
1188
+ self.call_from_thread(self._trigger_host_stats_refresh)
1189
+ except RuntimeError:
1190
+ pass
1191
+
1095
1192
  # Always try to unset the flag from the main thread
1096
1193
  def unset_flag():
1097
1194
  if self.bulk_operation_in_progress:
@@ -1129,7 +1226,7 @@ class VMManagerTUI(App):
1129
1226
  for card in self.query(VMCard):
1130
1227
  card.is_selected = False
1131
1228
 
1132
- self.show_quick_message("All VMs unselected.")
1229
+ self.show_quick_message(QuickMessages.ALL_VMS_UNSELECTED)
1133
1230
 
1134
1231
  @on(VMSelectionChanged)
1135
1232
  def on_vm_selection_changed(self, message: VMSelectionChanged) -> None:
@@ -1150,7 +1247,7 @@ class VMManagerTUI(App):
1150
1247
  delete_storage_flag = result.get('delete_storage', False)
1151
1248
 
1152
1249
  if not action_type:
1153
- self.show_error_message("No action type received from bulk action modal.")
1250
+ self.show_error_message(ErrorMessages.NO_ACTION_TYPE_BULK_MODAL)
1154
1251
  return
1155
1252
 
1156
1253
  selected_uuids_copy = list(self.selected_vm_uuids) # Take a copy for the worker
@@ -1162,7 +1259,7 @@ class VMManagerTUI(App):
1162
1259
  selected_domains = list(found_domains_map.values())
1163
1260
 
1164
1261
  if not selected_domains:
1165
- self.show_error_message("Could not find any of the selected VMs for editing.")
1262
+ self.show_error_message(ErrorMessages.VM_NOT_FOUND_FOR_EDITING)
1166
1263
  return
1167
1264
 
1168
1265
  # Check if all selected VMs are stopped
@@ -1172,7 +1269,7 @@ class VMManagerTUI(App):
1172
1269
  active_vms.append(domain.name())
1173
1270
 
1174
1271
  if active_vms:
1175
- self.show_error_message(f"All VMs must be stopped for bulk editing. Running VMs: {', '.join(active_vms)}")
1272
+ self.show_error_message(ErrorMessages.VMS_MUST_BE_STOPPED_FOR_BULK_EDITING.format(running_vms=', '.join(active_vms)))
1176
1273
  # Restore selection since we are aborting
1177
1274
  self.selected_vm_uuids = set(selected_uuids_copy)
1178
1275
  return
@@ -1209,9 +1306,9 @@ class VMManagerTUI(App):
1209
1306
  # Clear selection after launching modal
1210
1307
  self.selected_vm_uuids.clear()
1211
1308
  else:
1212
- self.show_error_message("Could not load details for reference VM.")
1309
+ self.show_error_message(ErrorMessages.COULD_NOT_LOAD_DETAILS_FOR_REFERENCE_VM)
1213
1310
  except Exception as e:
1214
- self.app.show_error_message(f"Error preparing bulk edit: {e}")
1311
+ self.app.show_error_message(ErrorMessages.BULK_EDIT_PREP_ERROR.format(error=e))
1215
1312
 
1216
1313
  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
1314
  self.app.push_screen(ConfirmationDialog(warning_message), on_confirm)
@@ -1257,16 +1354,16 @@ class VMManagerTUI(App):
1257
1354
  )
1258
1355
 
1259
1356
  summary = f"Bulk action '{action_type}' complete. Successful: {len(successful_vms)}, Failed: {len(failed_vms)}"
1260
- logging.info(summary)
1357
+ logging.info(summary)
1261
1358
 
1262
1359
  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.")
1360
+ self.call_from_thread(self.show_success_message, SuccessMessages.BULK_ACTION_SUCCESS_TEMPLATE.format(action_type=action_type, count=len(successful_vms)))
1264
1361
  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.")
1362
+ self.call_from_thread(self.show_error_message, ErrorMessages.BULK_ACTION_FAILED_TEMPLATE.format(action_type=action_type, count=len(failed_vms)))
1266
1363
 
1267
1364
  except Exception as e:
1268
1365
  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}")
1366
+ self.call_from_thread(self.show_error_message, ErrorMessages.FATAL_ERROR_BULK_ACTION.format(error=e))
1270
1367
 
1271
1368
  finally:
1272
1369
  # Ensure these are called on the main thread
@@ -1310,7 +1407,7 @@ class VMManagerTUI(App):
1310
1407
  vms_per_page,
1311
1408
  uris_to_query,
1312
1409
  force=force,
1313
- optimize_for_current_page=current_page,
1410
+ optimize_for_current_page=optimize_for_current_page,
1314
1411
  on_complete=on_complete,
1315
1412
  ),
1316
1413
  name="list_vms"
@@ -1328,6 +1425,12 @@ class VMManagerTUI(App):
1328
1425
  ):
1329
1426
  """Worker to fetch, filter, and display VMs using a diffing strategy."""
1330
1427
  try:
1428
+ # Update Host Stats
1429
+ if threading.current_thread() is threading.main_thread():
1430
+ self.host_stats.update_hosts(uris_to_query, self.servers)
1431
+ else:
1432
+ self.call_from_thread(self.host_stats.update_hosts, uris_to_query, self.servers)
1433
+
1331
1434
  start_index = current_page * vms_per_page
1332
1435
  end_index = start_index + vms_per_page
1333
1436
  page_start = start_index if optimize_for_current_page else None
@@ -1353,22 +1456,18 @@ class VMManagerTUI(App):
1353
1456
  end_index = start_index + vms_per_page
1354
1457
  paginated_domains = domains_to_display[start_index:end_index]
1355
1458
 
1356
- # Collect data in worker thread
1357
- vm_data_list = []
1358
- page_uuids = set()
1359
-
1360
- for domain, conn in paginated_domains:
1459
+ # Parallelize fetching of VM info
1460
+ def fetch_vm_data(item):
1461
+ domain, conn = item
1361
1462
  try:
1362
1463
  uri = self.vm_service.get_uri_for_connection(conn) or conn.getURI()
1363
1464
  uuid, vm_name = self.vm_service.get_vm_identity(domain, conn, known_uri=uri)
1364
- page_uuids.add(uuid)
1365
1465
 
1366
- # Get info from cache or fetch if not present. This is safe as we are in a worker.
1466
+ # Get info from cache or fetch if not present
1367
1467
  info = self.vm_service._get_domain_info(domain)
1368
1468
  cached_details = self.vm_service.get_cached_vm_details(uuid)
1369
1469
 
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
1470
+ # Explicitly get state from cache/service
1372
1471
  state_tuple = self.vm_service._get_domain_state(domain, internal_id=uuid)
1373
1472
 
1374
1473
  effective_state = None
@@ -1394,7 +1493,7 @@ class VMManagerTUI(App):
1394
1493
  cpu = 0
1395
1494
  memory = 0
1396
1495
 
1397
- vm_data = {
1496
+ return {
1398
1497
  'uuid': uuid,
1399
1498
  'name': vm_name,
1400
1499
  'status': status,
@@ -1405,19 +1504,28 @@ class VMManagerTUI(App):
1405
1504
  'conn': conn,
1406
1505
  'uri': uri
1407
1506
  }
1408
- vm_data_list.append(vm_data)
1409
-
1410
1507
  except libvirt.libvirtError as e:
1411
1508
  if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
1412
1509
  logging.warning(f"Skipping display of non-existent VM during refresh.")
1413
- continue
1510
+ return None
1414
1511
  else:
1415
1512
  try:
1416
1513
  name_for_error = vm_name if 'vm_name' in locals() else domain.name()
1417
1514
  except:
1418
1515
  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
1516
+ self.call_from_thread(self.show_error_message, ErrorMessages.VM_INFO_ERROR.format(vm_name=name_for_error, error=e))
1517
+ return None
1518
+
1519
+ vm_data_list = []
1520
+ page_uuids = set()
1521
+
1522
+ with ThreadPoolExecutor(max_workers=20) as executor:
1523
+ results = list(executor.map(fetch_vm_data, paginated_domains))
1524
+
1525
+ for result in results:
1526
+ if result:
1527
+ vm_data_list.append(result)
1528
+ page_uuids.add(result['uuid'])
1421
1529
 
1422
1530
  # Cleanup cache: remove cards for VMs that no longer exist at all
1423
1531
  all_uuids_from_libvirt = set(all_active_uuids)
@@ -1550,7 +1658,7 @@ class VMManagerTUI(App):
1550
1658
  self.filtered_server_uris = [u for u in self.filtered_server_uris if u not in uris_to_remove]
1551
1659
 
1552
1660
  if removed_names:
1553
- self.show_error_message(f"Server(s) {', '.join(removed_names)} disconnected and autoconnect disabled due to connection failures.")
1661
+ self.show_error_message(ErrorMessages.SERVER_DISCONNECTED_AUTOCONNECT_DISABLED.format(names=', '.join(removed_names)))
1554
1662
 
1555
1663
  if config_changed:
1556
1664
  self.config['servers'] = self.servers
@@ -1563,7 +1671,7 @@ class VMManagerTUI(App):
1563
1671
  self.call_from_thread(update_ui_on_main_thread)
1564
1672
 
1565
1673
  except Exception as e:
1566
- self.call_from_thread(self.show_error_message, f"Error fetching VM data: {e}")
1674
+ self.call_from_thread(self.show_error_message, ErrorMessages.ERROR_FETCHING_VM_DATA.format(error=e))
1567
1675
  finally:
1568
1676
  if on_complete:
1569
1677
  self.call_from_thread(on_complete)
@@ -1613,7 +1721,7 @@ class VMManagerTUI(App):
1613
1721
  def action_pattern_select(self) -> None:
1614
1722
  """Handles the 'Pattern Sel' button press."""
1615
1723
  if not self.active_uris:
1616
- self.show_error_message("No active servers.")
1724
+ self.show_error_message(ErrorMessages.NO_ACTIVE_SERVERS)
1617
1725
  return
1618
1726
 
1619
1727
  # Gather all known VMs from cache
@@ -1638,7 +1746,7 @@ class VMManagerTUI(App):
1638
1746
  continue
1639
1747
 
1640
1748
  if not available_vms:
1641
- self.show_error_message("No VMs found in cache. Try refreshing first.")
1749
+ self.show_error_message(ErrorMessages.NO_VMS_IN_CACHE)
1642
1750
  return
1643
1751
 
1644
1752
  # Prepare server list for the modal, matching FilterModal logic
@@ -1661,7 +1769,7 @@ class VMManagerTUI(App):
1661
1769
  if selected_uuids:
1662
1770
  # Add found UUIDs to current selection
1663
1771
  self.selected_vm_uuids.update(selected_uuids)
1664
- self.show_success_message(f"Selected {len(selected_uuids)} VMs matching pattern.")
1772
+ self.show_success_message(SuccessMessages.VMS_SELECTED_BY_PATTERN.format(count=len(selected_uuids)))
1665
1773
  self.refresh_vm_list()
1666
1774
 
1667
1775
  self.push_screen(PatternSelectModal(available_vms, available_servers, selected_servers), handle_result)
@@ -1672,7 +1780,7 @@ class VMManagerTUI(App):
1672
1780
  """Handles the 'Bulk Selected' button press."""
1673
1781
  self._collapse_all_action_collapsibles()
1674
1782
  if not self.selected_vm_uuids:
1675
- self.show_error_message("No VMs selected.")
1783
+ self.show_error_message(ErrorMessages.NO_VMS_SELECTED)
1676
1784
  return
1677
1785
 
1678
1786
  uuids_snapshot = list(self.selected_vm_uuids)
@@ -1683,7 +1791,7 @@ class VMManagerTUI(App):
1683
1791
 
1684
1792
  # Use the service to find specific domains by their internal ID (UUID@URI)
1685
1793
  # 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)
1794
+ found_domains_map = self.vm_service.find_domains_by_uuids(self.active_uris, uuids, check_validity=False)
1687
1795
 
1688
1796
  all_names = set()
1689
1797
  for domain in found_domains_map.values():
@@ -1701,9 +1809,7 @@ class VMManagerTUI(App):
1701
1809
  self.push_screen, BulkActionModal(vm_names_list), self.handle_bulk_action_result
1702
1810
  )
1703
1811
  else:
1704
- self.call_from_thread(
1705
- self.show_error_message, "Could not retrieve names for selected VMs."
1706
- )
1812
+ self.call_from_thread(self.show_error_message, ErrorMessages.BULK_ACTION_VM_NAMES_RETRIEVAL_FAILED)
1707
1813
 
1708
1814
  self.worker_manager.run(
1709
1815
  get_names_and_show_modal,
@@ -1760,7 +1866,7 @@ class VMManagerTUI(App):
1760
1866
  else:
1761
1867
  cpu = 0
1762
1868
  memory = 0
1763
-
1869
+
1764
1870
  logging.debug(f"Updating card {vm_internal_id} with status {status}")
1765
1871
  # Update card on main thread
1766
1872
  def update_ui():
@@ -1780,6 +1886,27 @@ class VMManagerTUI(App):
1780
1886
  update_single_card, name=f"update_card_{vm_internal_id}"
1781
1887
  )
1782
1888
 
1889
+ @on(SingleHostStat.ServerLabelClicked)
1890
+ def on_single_host_stat_server_label_clicked(self, message: SingleHostStat.ServerLabelClicked) -> None:
1891
+ """Called when a server label is clicked in the host stats."""
1892
+ available_servers = []
1893
+ for uri in self.active_uris:
1894
+ name = uri
1895
+ for s in self.servers:
1896
+ if s['uri'] == uri:
1897
+ name = s['name']
1898
+ break
1899
+ available_servers.append({'name': name, 'uri': uri, 'color': self.get_server_color(uri)})
1900
+
1901
+ self.push_screen(
1902
+ FilterModal(
1903
+ current_search=self.search_text,
1904
+ current_status=self.sort_by,
1905
+ available_servers=available_servers,
1906
+ selected_servers=[message.server_uri] # Pre-select the clicked server
1907
+ )
1908
+ )
1909
+
1783
1910
 
1784
1911
  def main():
1785
1912
  """Entry point for vmanager TUI application."""