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.
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/METADATA +1 -1
- virtui_manager-1.4.0.dist-info/RECORD +76 -0
- vmanager/constants.py +739 -108
- vmanager/dialog.css +24 -0
- vmanager/firmware_manager.py +4 -1
- vmanager/i18n.py +32 -0
- vmanager/libvirt_utils.py +132 -3
- vmanager/locales/de/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/locales/de/LC_MESSAGES/virtui-manager.po +3158 -0
- vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3155 -0
- vmanager/locales/it/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3132 -0
- vmanager/locales/virtui-manager.pot +3033 -0
- vmanager/modals/bulk_modals.py +13 -12
- vmanager/modals/cache_stats_modal.py +6 -5
- vmanager/modals/capabilities_modal.py +133 -0
- vmanager/modals/config_modal.py +25 -24
- vmanager/modals/cpu_mem_pc_modals.py +22 -21
- vmanager/modals/custom_migration_modal.py +10 -9
- vmanager/modals/disk_pool_modals.py +60 -59
- vmanager/modals/host_dashboard_modal.py +137 -0
- vmanager/modals/host_stats.py +199 -0
- vmanager/modals/howto_disk_modal.py +2 -1
- vmanager/modals/howto_network_modal.py +2 -1
- vmanager/modals/howto_overlay_modal.py +2 -1
- vmanager/modals/howto_ssh_modal.py +2 -1
- vmanager/modals/howto_virtiofs_modal.py +2 -1
- vmanager/modals/input_modals.py +11 -10
- vmanager/modals/log_modal.py +2 -1
- vmanager/modals/migration_modals.py +21 -19
- vmanager/modals/network_modals.py +45 -36
- vmanager/modals/provisioning_modals.py +56 -56
- vmanager/modals/select_server_modals.py +8 -7
- vmanager/modals/selection_modals.py +7 -6
- vmanager/modals/server_modals.py +24 -23
- vmanager/modals/server_prefs_modals.py +78 -71
- vmanager/modals/utils_modals.py +10 -9
- vmanager/modals/virsh_modals.py +3 -2
- vmanager/modals/virtiofs_modals.py +6 -5
- vmanager/modals/vm_type_info_modal.py +2 -1
- vmanager/modals/vmanager_modals.py +19 -19
- vmanager/modals/vmcard_dialog.py +57 -57
- vmanager/modals/vmdetails_modals.py +115 -123
- vmanager/modals/xml_modals.py +3 -2
- vmanager/network_manager.py +4 -1
- vmanager/storage_manager.py +157 -39
- vmanager/utils.py +54 -7
- vmanager/vm_actions.py +48 -24
- vmanager/vm_migration.py +4 -1
- vmanager/vm_queries.py +67 -25
- vmanager/vm_service.py +8 -5
- vmanager/vmanager.css +55 -1
- vmanager/vmanager.py +247 -120
- vmanager/vmcard.css +3 -1
- vmanager/vmcard.py +270 -205
- vmanager/webconsole_manager.py +22 -22
- virtui_manager-1.1.6.dist-info/RECORD +0 -65
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/WHEEL +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/entry_points.txt +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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=
|
|
162
|
-
Binding(key="f", action="filter_view", description=
|
|
163
|
-
Binding(key="k", action="compact_view", description=
|
|
164
|
-
#Binding(key="p", action="server_preferences", description=
|
|
165
|
-
Binding(key="c", action="config", description=
|
|
166
|
-
Binding(key="b", action="bulk_cmd", description=
|
|
167
|
-
Binding(key="s", action="select_server", description=
|
|
168
|
-
Binding(key="l", action="manage_server", description=
|
|
169
|
-
Binding(key="p", action="pattern_select", description=
|
|
170
|
-
Binding(key="ctrl+a", action="toggle_select_all", description=
|
|
171
|
-
Binding(key="ctrl+u", action="unselect_all", description=
|
|
172
|
-
Binding(key="left", action="previous_page", description=
|
|
173
|
-
Binding(key="right", action="next_page", description=
|
|
174
|
-
Binding(key="up", action="filter_running", description=
|
|
175
|
-
Binding(key="down", action="filter_all", description=
|
|
176
|
-
Binding(key="ctrl+v", action="virsh_shell", description=
|
|
177
|
-
Binding(key="
|
|
178
|
-
Binding(key="
|
|
179
|
-
Binding(key="
|
|
180
|
-
Binding(key="
|
|
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=
|
|
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=
|
|
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=
|
|
393
|
+
ButtonLabels.SELECT_SERVER, id="select_server_button", classes="Buttonpage"
|
|
360
394
|
)
|
|
361
|
-
yield Button(ButtonLabels.MANAGE_SERVERS, id=
|
|
395
|
+
yield Button(ButtonLabels.MANAGE_SERVERS, id="manage_servers_button", classes="Buttonpage")
|
|
362
396
|
yield Button(
|
|
363
|
-
ButtonLabels.SERVER_PREFERENCES, id=
|
|
397
|
+
ButtonLabels.SERVER_PREFERENCES, id="server_preferences_button", classes="Buttonpage"
|
|
364
398
|
)
|
|
365
|
-
yield Button(ButtonLabels.FILTER_VM, id=
|
|
366
|
-
#yield Button(ButtonLabels.VIEW_LOG, id=
|
|
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=
|
|
369
|
-
yield Button(ButtonLabels.PATTERN_SELECT, id=
|
|
370
|
-
#yield Button(ButtonLabels.CONFIG, id=
|
|
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=
|
|
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(
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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"#
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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=
|
|
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
|
-
#
|
|
1357
|
-
|
|
1358
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1420
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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."""
|