virtui-manager 1.3.0__py3-none-any.whl → 1.5.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.3.0.dist-info → virtui_manager-1.5.0.dist-info}/METADATA +1 -1
- {virtui_manager-1.3.0.dist-info → virtui_manager-1.5.0.dist-info}/RECORD +27 -23
- {virtui_manager-1.3.0.dist-info → virtui_manager-1.5.0.dist-info}/entry_points.txt +1 -0
- vmanager/constants.py +15 -7
- vmanager/gui_wrapper.py +89 -0
- vmanager/i18n.py +2 -2
- vmanager/locale/de/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/{locales → locale}/de/LC_MESSAGES/virtui-manager.po +1217 -1037
- vmanager/locale/fr/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/{locales → locale}/fr/LC_MESSAGES/virtui-manager.po +564 -503
- vmanager/locale/it/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/{locales → locale}/it/LC_MESSAGES/virtui-manager.po +1205 -1050
- vmanager/{locales → locale}/virtui-manager.pot +555 -498
- vmanager/modals/host_stats.py +199 -0
- vmanager/modals/migration_modals.py +1 -1
- vmanager/modals/utils_modals.py +1 -1
- vmanager/utils.py +18 -1
- vmanager/vm_actions.py +20 -0
- vmanager/vm_migration.py +4 -1
- vmanager/vmanager.css +9 -1
- vmanager/vmanager.py +101 -16
- vmanager/vmcard.css +19 -1
- vmanager/vmcard.py +176 -72
- vmanager/webconsole_manager.py +1 -1
- vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
- {virtui_manager-1.3.0.dist-info → virtui_manager-1.5.0.dist-info}/WHEEL +0 -0
- {virtui_manager-1.3.0.dist-info → virtui_manager-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {virtui_manager-1.3.0.dist-info → virtui_manager-1.5.0.dist-info}/top_level.txt +0 -0
vmanager/vmcard.py
CHANGED
|
@@ -28,7 +28,7 @@ from .vm_actions import (
|
|
|
28
28
|
clone_vm, rename_vm, create_vm_snapshot,
|
|
29
29
|
restore_vm_snapshot, delete_vm_snapshot,
|
|
30
30
|
create_external_overlay, commit_disk_changes,
|
|
31
|
-
discard_overlay, delete_vm
|
|
31
|
+
discard_overlay, delete_vm, hibernate_vm
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
from .vm_queries import (
|
|
@@ -51,11 +51,12 @@ from .utils import (
|
|
|
51
51
|
extract_server_name_from_uri,
|
|
52
52
|
generate_tooltip_markdown,
|
|
53
53
|
remote_viewer_cmd,
|
|
54
|
+
check_tmux,
|
|
54
55
|
)
|
|
55
56
|
from .constants import (
|
|
56
57
|
ButtonLabels, TabTitles, StatusText,
|
|
57
58
|
SparklineLabels, ErrorMessages, DialogMessages, VmAction,
|
|
58
|
-
WarningMessages, SuccessMessages
|
|
59
|
+
WarningMessages, SuccessMessages, StaticText, ProgressMessages
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
class VMCardActions(Static):
|
|
@@ -66,12 +67,14 @@ class VMCardActions(Static):
|
|
|
66
67
|
def compose(self):
|
|
67
68
|
self.card.ui["start"] = Button(ButtonLabels.START, id="start", variant="success")
|
|
68
69
|
self.card.ui["shutdown"] = Button(ButtonLabels.SHUTDOWN, id="shutdown", variant="primary")
|
|
70
|
+
self.card.ui["hibernate"] = Button(ButtonLabels.HIBERNATE_VM, id="hibernate", variant="primary")
|
|
69
71
|
self.card.ui["stop"] = Button(ButtonLabels.FORCE_OFF, id="stop", variant="error")
|
|
70
72
|
self.card.ui["pause"] = Button(ButtonLabels.PAUSE, id="pause", variant="primary")
|
|
71
73
|
self.card.ui["resume"] = Button(ButtonLabels.RESUME, id="resume", variant="success")
|
|
72
74
|
self.card.ui["configure-button"] = Button(ButtonLabels.CONFIGURE, id="configure-button", variant="primary")
|
|
73
75
|
self.card.ui["web_console"] = Button(ButtonLabels.WEB_CONSOLE, id="web_console", variant="default")
|
|
74
76
|
self.card.ui["connect"] = Button(ButtonLabels.CONNECT, id="connect", variant="default")
|
|
77
|
+
self.card.ui["tmux_console"] = Button(ButtonLabels.TEXT_CONSOLE, id="tmux_console", variant="default")
|
|
75
78
|
|
|
76
79
|
self.card.ui["snapshot_take"] = Button(ButtonLabels.SNAPSHOT, id="snapshot_take", variant="primary")
|
|
77
80
|
self.card.ui["snapshot_restore"] = Button(ButtonLabels.RESTORE_SNAPSHOT, id="snapshot_restore", variant="primary")
|
|
@@ -100,16 +103,18 @@ class VMCardActions(Static):
|
|
|
100
103
|
yield self.card.ui["pause"]
|
|
101
104
|
yield self.card.ui["resume"]
|
|
102
105
|
with Vertical():
|
|
103
|
-
yield self.card.ui["configure-button"]
|
|
104
106
|
yield self.card.ui["web_console"]
|
|
105
107
|
yield self.card.ui["connect"]
|
|
106
|
-
|
|
108
|
+
if os.environ.get("TMUX") and check_tmux():
|
|
109
|
+
yield self.card.ui["tmux_console"]
|
|
110
|
+
with TabPane(TabTitles.STATE_MANAGEMENT, id="snapshot-tab"):
|
|
107
111
|
with Horizontal():
|
|
108
112
|
with Vertical():
|
|
109
113
|
yield self.card.ui["snapshot_take"]
|
|
110
114
|
yield self.card.ui["snapshot_restore"]
|
|
111
115
|
yield self.card.ui["snapshot_delete"]
|
|
112
116
|
with Vertical():
|
|
117
|
+
yield self.card.ui["hibernate"]
|
|
113
118
|
yield self.card.ui["create_overlay"]
|
|
114
119
|
yield self.card.ui["commit_disk"]
|
|
115
120
|
yield self.card.ui["discard_overlay"]
|
|
@@ -122,6 +127,7 @@ class VMCardActions(Static):
|
|
|
122
127
|
yield self.card.ui["clone"]
|
|
123
128
|
yield self.card.ui["migration"]
|
|
124
129
|
with Vertical():
|
|
130
|
+
yield self.card.ui["configure-button"]
|
|
125
131
|
yield self.card.ui["xml"]
|
|
126
132
|
yield Static(classes="button-separator")
|
|
127
133
|
yield self.card.ui["rename-button"]
|
|
@@ -184,21 +190,19 @@ class VMCard(Static):
|
|
|
184
190
|
def _get_snapshot_tab_title(self, num_snapshots: int = -1) -> str:
|
|
185
191
|
"""Get snapshot tab title. Pass num_snapshots to avoid blocking libvirt call."""
|
|
186
192
|
if num_snapshots == -1:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
193
|
+
# If no count provided, don't fetch it here to avoid blocking.
|
|
194
|
+
# For now, return default if we can't get it cheaply.
|
|
195
|
+
return TabTitles.SNAP_OVER_UPDATE
|
|
190
196
|
|
|
191
197
|
if self.vm:
|
|
192
198
|
try:
|
|
193
|
-
if num_snapshots
|
|
194
|
-
return TabTitles.
|
|
195
|
-
|
|
196
|
-
return TabTitles.
|
|
197
|
-
elif num_snapshots >= 2:
|
|
198
|
-
return TabTitles.SNAPSHOTS + "(" + str(num_snapshots) + ")" "/" + TabTitles.OVERLAY
|
|
199
|
+
if num_snapshots <= 0:
|
|
200
|
+
return TabTitles.STATE_MANAGEMENT
|
|
201
|
+
else:
|
|
202
|
+
return f"{TabTitles.STATE_MANAGEMENT}({num_snapshots})"
|
|
199
203
|
except libvirt.libvirtError:
|
|
200
204
|
pass # Domain might be transient or invalid
|
|
201
|
-
return TabTitles.
|
|
205
|
+
return TabTitles.STATE_MANAGEMENT
|
|
202
206
|
|
|
203
207
|
def update_snapshot_tab_title(self, num_snapshots: int = -1) -> None:
|
|
204
208
|
"""Updates the snapshot tab title."""
|
|
@@ -248,6 +252,15 @@ class VMCard(Static):
|
|
|
248
252
|
status_widget.update(status_text)
|
|
249
253
|
|
|
250
254
|
def compose(self):
|
|
255
|
+
self.ui["btn_quick_start"] = Button("▶", id="start", variant="success", classes="btn-small")
|
|
256
|
+
self.ui["btn_quick_start"].tooltip = StaticText.START_VMS
|
|
257
|
+
self.ui["btn_quick_view"] = Button("👁", id="connect", classes="btn-small")
|
|
258
|
+
self.ui["btn_quick_view"].tooltip = StaticText.REMOTE_VIEWER
|
|
259
|
+
self.ui["btn_quick_resume"] = Button("⏯", id="resume", variant="success", classes="btn-small")
|
|
260
|
+
self.ui["btn_quick_resume"].tooltip = ButtonLabels.RESUME
|
|
261
|
+
self.ui["btn_quick_stop"] = Button("■", id="shutdown", variant="primary", classes="btn-small")
|
|
262
|
+
self.ui["btn_quick_stop"].tooltip = ButtonLabels.SHUTDOWN
|
|
263
|
+
|
|
251
264
|
self.ui["checkbox"] = Checkbox("", id="vm-select-checkbox", classes="vm-select-checkbox", value=self.is_selected, tooltip="Select VM")
|
|
252
265
|
self.ui["vmname"] = Static(self._get_vm_display_name(), id="vmname", classes="vmname")
|
|
253
266
|
self.ui["status"] = Static(f"{self.status}{self.webc_status_indicator}", id="status")
|
|
@@ -286,6 +299,12 @@ class VMCard(Static):
|
|
|
286
299
|
with Vertical():
|
|
287
300
|
yield self.ui["vmname"]
|
|
288
301
|
yield self.ui["status"]
|
|
302
|
+
with Horizontal(classes="quick-actions"):
|
|
303
|
+
with Vertical():
|
|
304
|
+
yield self.ui["btn_quick_resume"]
|
|
305
|
+
yield self.ui["btn_quick_stop"]
|
|
306
|
+
yield self.ui["btn_quick_view"]
|
|
307
|
+
yield self.ui["btn_quick_start"]
|
|
289
308
|
|
|
290
309
|
yield self.ui["sparklines_container"]
|
|
291
310
|
yield self.ui["collapsible"]
|
|
@@ -311,6 +330,8 @@ class VMCard(Static):
|
|
|
311
330
|
# Clean up dynamic UI references to avoid memory leaks and stale state
|
|
312
331
|
keys_to_keep = {
|
|
313
332
|
"checkbox", "vmname", "status", "collapsible",
|
|
333
|
+
"btn_quick_start", "btn_quick_stop", "btn_quick_view",
|
|
334
|
+
"btn_quick_resume",
|
|
314
335
|
"sparklines_container",
|
|
315
336
|
# Resource Sparklines
|
|
316
337
|
"cpu_label", "cpu_sparkline", "cpu_sparkline_container",
|
|
@@ -342,7 +363,7 @@ class VMCard(Static):
|
|
|
342
363
|
"""Updates the tooltip for the VM name using Markdown."""
|
|
343
364
|
if not self.display or not self.ui or "vmname" not in self.ui:
|
|
344
365
|
return
|
|
345
|
-
|
|
366
|
+
|
|
346
367
|
uuid = self.internal_id
|
|
347
368
|
if not uuid:
|
|
348
369
|
return
|
|
@@ -388,7 +409,7 @@ class VMCard(Static):
|
|
|
388
409
|
if ips:
|
|
389
410
|
ip_display = ", ".join(ips)
|
|
390
411
|
|
|
391
|
-
cpu_model_display = f"
|
|
412
|
+
cpu_model_display = f" {self.cpu_model}" if self.cpu_model else ""
|
|
392
413
|
|
|
393
414
|
tooltip_md = generate_tooltip_markdown(
|
|
394
415
|
uuid=uuid_display,
|
|
@@ -397,7 +418,7 @@ class VMCard(Static):
|
|
|
397
418
|
ip=ip_display,
|
|
398
419
|
boot=self.boot_device or "N/A",
|
|
399
420
|
cpu=self.cpu,
|
|
400
|
-
cpu_model=
|
|
421
|
+
cpu_model=cpu_model_display or "",
|
|
401
422
|
memory=self.memory
|
|
402
423
|
)
|
|
403
424
|
|
|
@@ -551,19 +572,20 @@ class VMCard(Static):
|
|
|
551
572
|
if not self.ui:
|
|
552
573
|
return
|
|
553
574
|
|
|
554
|
-
|
|
575
|
+
sparklines = self.ui.get("sparklines_container")
|
|
555
576
|
collapsible = self.ui.get("collapsible")
|
|
556
577
|
vmname = self.ui.get("vmname")
|
|
557
578
|
vmstatus = self.ui.get("status")
|
|
558
579
|
checkbox = self.ui.get("checkbox")
|
|
559
580
|
|
|
560
|
-
if value:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
# sparklines.remove()
|
|
581
|
+
if value:
|
|
582
|
+
if sparklines and sparklines.is_mounted:
|
|
583
|
+
sparklines.display = False
|
|
564
584
|
if collapsible and collapsible.is_mounted:
|
|
565
585
|
collapsible.collapsed = True
|
|
566
586
|
collapsible.remove()
|
|
587
|
+
#if checkbox and checkbox.is_mounted:
|
|
588
|
+
# checkbox.display = False
|
|
567
589
|
else:
|
|
568
590
|
try:
|
|
569
591
|
info_container = self.query_one("#info-container")
|
|
@@ -571,13 +593,17 @@ class VMCard(Static):
|
|
|
571
593
|
# Check if collapsible is already a child of info_container to avoid double mounting
|
|
572
594
|
if collapsible not in info_container.children:
|
|
573
595
|
info_container.mount(collapsible)
|
|
596
|
+
if sparklines:
|
|
597
|
+
sparklines.display = True
|
|
598
|
+
#if checkbox:
|
|
599
|
+
# checkbox.display = True
|
|
600
|
+
|
|
574
601
|
except NoMatches:
|
|
575
602
|
# This can happen if the card is not fully initialized or structures changed
|
|
576
603
|
logging.warning(f"Could not find #info-container on VMCard {self.name} when switching to detailed view.")
|
|
577
604
|
except Exception as e:
|
|
578
605
|
# Catch-all for potential mounting errors (e.g. already mounted elsewhere?)
|
|
579
|
-
|
|
580
|
-
|
|
606
|
+
logging.warning(f"Error restoring collapsible in detailed view: {e}")
|
|
581
607
|
|
|
582
608
|
# Ensure sparklines visibility is correct
|
|
583
609
|
self.watch_stats_view_mode(self.stats_view_mode, self.stats_view_mode)
|
|
@@ -586,10 +612,11 @@ class VMCard(Static):
|
|
|
586
612
|
if value: # Compact view
|
|
587
613
|
self.styles.height = 4
|
|
588
614
|
self.styles.width = 20
|
|
589
|
-
if vmname:
|
|
615
|
+
if vmname:
|
|
616
|
+
vmname.styles.content_align = ("left", "middle")
|
|
590
617
|
if vmstatus: vmstatus.styles.content_align = ("left", "middle")
|
|
591
618
|
if checkbox: checkbox.styles.width = "2"
|
|
592
|
-
else: # Detailed view
|
|
619
|
+
else: # Detailed view
|
|
593
620
|
self.styles.height = 14
|
|
594
621
|
self.styles.width = 41
|
|
595
622
|
if vmname: vmname.styles.content_align = ("center", "middle")
|
|
@@ -626,7 +653,17 @@ class VMCard(Static):
|
|
|
626
653
|
|
|
627
654
|
def watch_server_border_color(self, old_color: str, new_color: str) -> None:
|
|
628
655
|
"""Called when server_border_color changes."""
|
|
629
|
-
self.
|
|
656
|
+
if self.is_selected:
|
|
657
|
+
self.styles.border = ("panel", "white")
|
|
658
|
+
else:
|
|
659
|
+
self.styles.border = ("solid", new_color)
|
|
660
|
+
|
|
661
|
+
def on_click(self, event: Click) -> None:
|
|
662
|
+
"""Handle click events on the card."""
|
|
663
|
+
if event.button == 3:
|
|
664
|
+
self.is_selected = not self.is_selected
|
|
665
|
+
self.post_message(VMSelectionChanged(vm_uuid=self.raw_uuid, is_selected=self.is_selected))
|
|
666
|
+
event.stop()
|
|
630
667
|
|
|
631
668
|
def on_unmount(self) -> None:
|
|
632
669
|
"""Stop the timer and cancel any running stat workers when the widget is removed."""
|
|
@@ -670,11 +707,10 @@ class VMCard(Static):
|
|
|
670
707
|
|
|
671
708
|
def watch_is_selected(self, old_value: bool, new_value: bool) -> None:
|
|
672
709
|
"""Called when is_selected changes to update the checkbox."""
|
|
673
|
-
if
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
checkbox.value = new_value
|
|
710
|
+
if self.ui:
|
|
711
|
+
checkbox = self.ui.get("checkbox")
|
|
712
|
+
if checkbox:
|
|
713
|
+
checkbox.value = new_value
|
|
678
714
|
|
|
679
715
|
if new_value:
|
|
680
716
|
self.styles.border = ("panel", "white")
|
|
@@ -885,11 +921,20 @@ class VMCard(Static):
|
|
|
885
921
|
is_pmsuspended = self.status == StatusText.PMSUSPENDED
|
|
886
922
|
is_blocked = self.status == StatusText.BLOCKED
|
|
887
923
|
|
|
924
|
+
if not self.ui.get("btn_quick_start"):
|
|
925
|
+
return
|
|
926
|
+
|
|
927
|
+
self.ui["btn_quick_start"].display = is_stopped
|
|
928
|
+
self.ui["btn_quick_stop"].display = is_running or is_blocked
|
|
929
|
+
self.ui["btn_quick_view"].display = (is_running or is_paused or is_blocked)
|
|
930
|
+
self.ui["btn_quick_resume"].display = is_paused or is_pmsuspended
|
|
931
|
+
|
|
888
932
|
if not self.ui.get("rename-button"):
|
|
889
933
|
return
|
|
890
934
|
|
|
891
935
|
self.ui["start"].display = is_stopped
|
|
892
936
|
self.ui["shutdown"].display = is_running or is_blocked
|
|
937
|
+
self.ui["hibernate"].display = is_running or is_blocked
|
|
893
938
|
self.ui["stop"].display = is_running or is_paused or is_pmsuspended or is_blocked
|
|
894
939
|
self.ui["delete"].display = is_running or is_paused or is_stopped or is_pmsuspended or is_blocked
|
|
895
940
|
self.ui["clone"].display = is_stopped
|
|
@@ -993,7 +1038,7 @@ class VMCard(Static):
|
|
|
993
1038
|
|
|
994
1039
|
def update_ui():
|
|
995
1040
|
self._update_slow_buttons(snapshot_summary, has_overlay)
|
|
996
|
-
|
|
1041
|
+
|
|
997
1042
|
try:
|
|
998
1043
|
self.app.call_from_thread(update_ui)
|
|
999
1044
|
except RuntimeError:
|
|
@@ -1016,10 +1061,10 @@ class VMCard(Static):
|
|
|
1016
1061
|
pane = tabbed_content.get_tab("snapshot-tab")
|
|
1017
1062
|
if snapshot_count > 0:
|
|
1018
1063
|
latest = snapshot_summary.get('latest')
|
|
1019
|
-
info = f"
|
|
1064
|
+
info = f"{StaticText.LATEST_SNAPSHOT} {latest['name']} ({latest['time']})" if latest else "Unknown"
|
|
1020
1065
|
pane.tooltip = f"{info}\nTotal: {snapshot_count}"
|
|
1021
1066
|
else:
|
|
1022
|
-
|
|
1067
|
+
pane.tooltip = StaticText.NO_SNAPSHOTS_CREATED
|
|
1023
1068
|
except Exception:
|
|
1024
1069
|
pass
|
|
1025
1070
|
|
|
@@ -1065,12 +1110,14 @@ class VMCard(Static):
|
|
|
1065
1110
|
|
|
1066
1111
|
button_handlers = {
|
|
1067
1112
|
"shutdown": self._handle_shutdown_button,
|
|
1113
|
+
"hibernate": self._handle_hibernate_button,
|
|
1068
1114
|
"stop": self._handle_stop_button,
|
|
1069
1115
|
"pause": self._handle_pause_button,
|
|
1070
1116
|
"resume": self._handle_resume_button,
|
|
1071
1117
|
"xml": self._handle_xml_button,
|
|
1072
1118
|
"connect": self._handle_connect_button,
|
|
1073
1119
|
"web_console": self._handle_web_console_button,
|
|
1120
|
+
"tmux_console": self._handle_tmux_console_button,
|
|
1074
1121
|
"snapshot_take": self._handle_snapshot_take_button,
|
|
1075
1122
|
"snapshot_restore": self._handle_snapshot_restore_button,
|
|
1076
1123
|
"snapshot_delete": self._handle_snapshot_delete_button,
|
|
@@ -1177,7 +1224,7 @@ class VMCard(Static):
|
|
|
1177
1224
|
self.app.vm_service.unsuppress_vm_events(self.internal_id)
|
|
1178
1225
|
|
|
1179
1226
|
self.app.push_screen(
|
|
1180
|
-
ConfirmationDialog(
|
|
1227
|
+
ConfirmationDialog(DialogMessages.CONFIRM_DISCARD_CHANGES.format(target_disk=target_disk)),
|
|
1181
1228
|
on_confirm
|
|
1182
1229
|
)
|
|
1183
1230
|
|
|
@@ -1185,7 +1232,7 @@ class VMCard(Static):
|
|
|
1185
1232
|
proceed_with_discard(overlay_disks[0])
|
|
1186
1233
|
else:
|
|
1187
1234
|
self.app.push_screen(
|
|
1188
|
-
SelectDiskModal(overlay_disks,
|
|
1235
|
+
SelectDiskModal(overlay_disks, StaticText.SELECT_OVERLAY_DISCARD),
|
|
1189
1236
|
proceed_with_discard
|
|
1190
1237
|
)
|
|
1191
1238
|
|
|
@@ -1211,7 +1258,7 @@ class VMCard(Static):
|
|
|
1211
1258
|
|
|
1212
1259
|
def on_confirm(confirmed: bool):
|
|
1213
1260
|
if confirmed:
|
|
1214
|
-
progress_modal = ProgressModal(title=
|
|
1261
|
+
progress_modal = ProgressModal(title=ProgressMessages.COMMITTING_CHANGES_FOR.format(name=self.name))
|
|
1215
1262
|
self.app.push_screen(progress_modal)
|
|
1216
1263
|
|
|
1217
1264
|
def do_commit():
|
|
@@ -1230,7 +1277,7 @@ class VMCard(Static):
|
|
|
1230
1277
|
self.app.worker_manager.run(do_commit, name=f"commit_{self.name}")
|
|
1231
1278
|
|
|
1232
1279
|
self.app.push_screen(
|
|
1233
|
-
ConfirmationDialog(
|
|
1280
|
+
ConfirmationDialog(DialogMessages.CONFIRM_MERGE_CHANGES.format(target_disk=target_disk)),
|
|
1234
1281
|
on_confirm
|
|
1235
1282
|
)
|
|
1236
1283
|
|
|
@@ -1243,6 +1290,27 @@ class VMCard(Static):
|
|
|
1243
1290
|
if self.status in (StatusText.RUNNING, StatusText.PAUSED):
|
|
1244
1291
|
self.post_message(VmActionRequest(self.internal_id, VmAction.STOP))
|
|
1245
1292
|
|
|
1293
|
+
def _handle_hibernate_button(self, event: Button.Pressed) -> None:
|
|
1294
|
+
"""Handles the save button press."""
|
|
1295
|
+
logging.info(f"Attempting to save (hibernate) VM: {self.name}")
|
|
1296
|
+
|
|
1297
|
+
def do_save():
|
|
1298
|
+
self.stop_background_activities()
|
|
1299
|
+
self.app.vm_service.suppress_vm_events(self.internal_id)
|
|
1300
|
+
try:
|
|
1301
|
+
hibernate_vm(self.vm)
|
|
1302
|
+
self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_SAVED_TEMPLATE.format(vm_name=self.name))
|
|
1303
|
+
self.app.vm_service.invalidate_vm_state_cache(self.internal_id)
|
|
1304
|
+
self.app.call_from_thread(setattr, self, 'status', StatusText.STOPPED)
|
|
1305
|
+
self.app.call_from_thread(self.update_button_layout)
|
|
1306
|
+
except Exception as e:
|
|
1307
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.ERROR_ON_VM_DURING_ACTION.format(vm_name=self.name, action='save', error=e))
|
|
1308
|
+
finally:
|
|
1309
|
+
self.app.vm_service.unsuppress_vm_events(self.internal_id)
|
|
1310
|
+
|
|
1311
|
+
if self.status in (StatusText.RUNNING, StatusText.PAUSED):
|
|
1312
|
+
self.app.worker_manager.run(do_save, name=f"save_{self.internal_id}")
|
|
1313
|
+
|
|
1246
1314
|
def stop_background_activities(self):
|
|
1247
1315
|
""" Stop background activities before action """
|
|
1248
1316
|
with self._timer_lock:
|
|
@@ -1324,7 +1392,7 @@ class VMCard(Static):
|
|
|
1324
1392
|
self.app.refresh_vm_list()
|
|
1325
1393
|
except libvirt.libvirtError as e:
|
|
1326
1394
|
self.app.show_error_message(ErrorMessages.INVALID_XML_TEMPLATE.format(vm_name=self.name, error=e))
|
|
1327
|
-
logging.error(
|
|
1395
|
+
logging.error(e)
|
|
1328
1396
|
else:
|
|
1329
1397
|
self.app.show_success_message(SuccessMessages.NO_XML_CHANGES)
|
|
1330
1398
|
|
|
@@ -1430,6 +1498,50 @@ class VMCard(Static):
|
|
|
1430
1498
|
else:
|
|
1431
1499
|
self.app.worker_manager.run(worker, name=f"start_console_{self.vm.name()}")
|
|
1432
1500
|
|
|
1501
|
+
def _handle_tmux_console_button(self, event: Button.Pressed) -> None:
|
|
1502
|
+
"""Handles the text console button press by opening a new tmux window."""
|
|
1503
|
+
logging.info(f"Attempting to open text console for VM: {self.name}")
|
|
1504
|
+
|
|
1505
|
+
# Check if running in tmux
|
|
1506
|
+
if not os.environ.get("TMUX"):
|
|
1507
|
+
self.app.show_error_message("This feature requires running inside tmux.")
|
|
1508
|
+
return
|
|
1509
|
+
|
|
1510
|
+
try:
|
|
1511
|
+
# Use cached values to avoid libvirt calls where possible
|
|
1512
|
+
uri = self.app.vm_service.get_uri_for_connection(self.conn)
|
|
1513
|
+
if not uri:
|
|
1514
|
+
uri = self.conn.getURI()
|
|
1515
|
+
|
|
1516
|
+
# Get proper domain name
|
|
1517
|
+
_, domain_name = self.app.vm_service.get_vm_identity(self.vm, self.conn)
|
|
1518
|
+
|
|
1519
|
+
# Construct command
|
|
1520
|
+
# tmux new-window -n "Console: <vm_name>" "virsh -c <uri> console <vm_name>; read"
|
|
1521
|
+
help_msg = (
|
|
1522
|
+
"echo '---------------------------------------------------------'; "
|
|
1523
|
+
"echo 'Tmux Navigation Help:'; "
|
|
1524
|
+
"echo ' Ctrl+B N or P - Move to the next or previous window.'; "
|
|
1525
|
+
"echo ' Ctrl+B W - Open a panel to navigate across windows in multiple sessions.'; "
|
|
1526
|
+
"echo ' Ctrl+] - Close the current view.'; "
|
|
1527
|
+
"echo ' Ctrl+B ? - View all keybindings. Press Q to exit.';"
|
|
1528
|
+
"echo '---------------------------------------------------------'; "
|
|
1529
|
+
"echo 'Starting console...'; sleep 1;"
|
|
1530
|
+
)
|
|
1531
|
+
cmd = [
|
|
1532
|
+
"tmux", "new-window",
|
|
1533
|
+
"-n", f"{domain_name}",
|
|
1534
|
+
f"{help_msg} virsh -c {uri} console {domain_name}; echo '\nConsole session ended. Press Enter to close window.'; read"
|
|
1535
|
+
]
|
|
1536
|
+
|
|
1537
|
+
logging.info(f"Launching tmux console: {' '.join(cmd)}")
|
|
1538
|
+
subprocess.Popen(cmd)
|
|
1539
|
+
self.app.show_quick_message(f"Opened console for {domain_name}")
|
|
1540
|
+
|
|
1541
|
+
except Exception as e:
|
|
1542
|
+
logging.error(f"Failed to open tmux console: {e}")
|
|
1543
|
+
self.app.show_error_message(f"Failed to open console: {e}")
|
|
1544
|
+
|
|
1433
1545
|
def _handle_snapshot_take_button(self, event: Button.Pressed) -> None:
|
|
1434
1546
|
"""Handles the snapshot take button press."""
|
|
1435
1547
|
logging.info(f"Attempting to take snapshot for VM: {self.name}")
|
|
@@ -1680,10 +1792,10 @@ class VMCard(Static):
|
|
|
1680
1792
|
pane = tabbed_content.get_tab("snapshot-tab")
|
|
1681
1793
|
if snapshot_count > 0:
|
|
1682
1794
|
latest = snapshot_summary.get('latest')
|
|
1683
|
-
info = f"
|
|
1795
|
+
info = f"{StaticText.LATEST_SNAPSHOT} {latest['name']} ({latest['time']})" if latest else "Unknown"
|
|
1684
1796
|
pane.tooltip = f"{info}\nTotal: {snapshot_count}"
|
|
1685
1797
|
else:
|
|
1686
|
-
pane.tooltip =
|
|
1798
|
+
pane.tooltip = StaticText.NO_SNAPSHOTS_CREATED
|
|
1687
1799
|
except Exception:
|
|
1688
1800
|
pass
|
|
1689
1801
|
|
|
@@ -1922,8 +2034,6 @@ class VMCard(Static):
|
|
|
1922
2034
|
finally:
|
|
1923
2035
|
self.app.vm_service.unsuppress_vm_events(internal_id)
|
|
1924
2036
|
|
|
1925
|
-
num_snapshots = self.vm.snapshotNum(0)
|
|
1926
|
-
|
|
1927
2037
|
def on_confirm_rename(confirmed: bool, delete_snapshots=False) -> None:
|
|
1928
2038
|
if confirmed:
|
|
1929
2039
|
do_rename()
|
|
@@ -1960,10 +2070,10 @@ class VMCard(Static):
|
|
|
1960
2070
|
def get_details_worker():
|
|
1961
2071
|
try:
|
|
1962
2072
|
result = self.app.vm_service.get_vm_details(
|
|
1963
|
-
active_uris,
|
|
1964
|
-
uuid,
|
|
1965
|
-
domain=vm_obj,
|
|
1966
|
-
conn=conn_obj,
|
|
2073
|
+
active_uris,
|
|
2074
|
+
uuid,
|
|
2075
|
+
domain=vm_obj,
|
|
2076
|
+
conn=conn_obj,
|
|
1967
2077
|
cached_ips=cached_ips
|
|
1968
2078
|
)
|
|
1969
2079
|
|
|
@@ -1975,22 +2085,27 @@ class VMCard(Static):
|
|
|
1975
2085
|
|
|
1976
2086
|
vm_info, domain, conn_for_domain = result
|
|
1977
2087
|
|
|
1978
|
-
def on_detail_modal_dismissed(
|
|
2088
|
+
def on_detail_modal_dismissed(_=None):
|
|
1979
2089
|
self.post_message(VmCardUpdateRequest(self.internal_id))
|
|
1980
2090
|
self._perform_tooltip_update()
|
|
1981
2091
|
|
|
1982
2092
|
self.app.push_screen(
|
|
1983
|
-
VMDetailModal(
|
|
2093
|
+
VMDetailModal(
|
|
2094
|
+
vm_name,
|
|
2095
|
+
vm_info,
|
|
2096
|
+
domain,
|
|
2097
|
+
conn_for_domain,
|
|
2098
|
+
self.app.vm_service.invalidate_vm_state_cache),
|
|
1984
2099
|
on_detail_modal_dismissed
|
|
1985
|
-
|
|
2100
|
+
)
|
|
1986
2101
|
|
|
1987
2102
|
self.app.call_from_thread(show_details)
|
|
1988
2103
|
|
|
1989
2104
|
except Exception as e:
|
|
1990
|
-
def show_error():
|
|
2105
|
+
def show_error(error_instance):
|
|
1991
2106
|
loading_modal.dismiss()
|
|
1992
|
-
self.app.show_error_message(ErrorMessages.ERROR_GETTING_VM_DETAILS_TEMPLATE.format(vm_name=vm_name, error=
|
|
1993
|
-
self.app.call_from_thread(show_error)
|
|
2107
|
+
self.app.show_error_message(ErrorMessages.ERROR_GETTING_VM_DETAILS_TEMPLATE.format(vm_name=vm_name, error=error_instance))
|
|
2108
|
+
self.app.call_from_thread(show_error, e)
|
|
1994
2109
|
|
|
1995
2110
|
self.app.worker_manager.run(get_details_worker, name=f"get_details_{uuid}")
|
|
1996
2111
|
|
|
@@ -2003,27 +2118,16 @@ class VMCard(Static):
|
|
|
2003
2118
|
self.app.show_error_message(ErrorMessages.SELECT_AT_LEAST_TWO_SERVERS_FOR_MIGRATION)
|
|
2004
2119
|
return
|
|
2005
2120
|
|
|
2006
|
-
selected_vm_uuids = self.app.selected_vm_uuids
|
|
2121
|
+
selected_vm_uuids = list(self.app.selected_vm_uuids)
|
|
2007
2122
|
selected_vms = []
|
|
2008
2123
|
if selected_vm_uuids:
|
|
2124
|
+
found_domains_dict = self.app.vm_service.find_domains_by_uuids(self.app.active_uris, selected_vm_uuids)
|
|
2009
2125
|
for uuid in selected_vm_uuids:
|
|
2010
|
-
|
|
2011
|
-
with self.app.vm_service._cache_lock:
|
|
2012
|
-
domain = self.app.vm_service._domain_cache.get(uuid)
|
|
2013
|
-
|
|
2126
|
+
domain = found_domains_dict.get(uuid)
|
|
2014
2127
|
if domain:
|
|
2015
|
-
|
|
2016
|
-
# Verify domain is still valid
|
|
2017
|
-
domain.info()
|
|
2018
|
-
selected_vms.append(domain)
|
|
2019
|
-
found_domain = True
|
|
2020
|
-
except libvirt.libvirtError:
|
|
2021
|
-
found_domain = False
|
|
2128
|
+
selected_vms.append(domain)
|
|
2022
2129
|
else:
|
|
2023
|
-
|
|
2024
|
-
if not vm_info:
|
|
2025
|
-
self.app.show_error_message(ErrorMessages.SELECTED_VM_NOT_FOUND_ON_ACTIVE_SERVER_TEMPLATE.format(uuid=uuid))
|
|
2026
|
-
|
|
2130
|
+
self.app.show_error_message(ErrorMessages.SELECTED_VM_NOT_FOUND_ON_ACTIVE_SERVER_TEMPLATE.format(uuid=uuid))
|
|
2027
2131
|
if not selected_vms:
|
|
2028
2132
|
selected_vms = [self.vm]
|
|
2029
2133
|
|
|
@@ -2053,7 +2157,7 @@ class VMCard(Static):
|
|
|
2053
2157
|
state, _ = state_tuple
|
|
2054
2158
|
if state in [libvirt.VIR_DOMAIN_RUNNING, libvirt.VIR_DOMAIN_PAUSED]:
|
|
2055
2159
|
active_vms.append(vm)
|
|
2056
|
-
except:
|
|
2160
|
+
except Exception:
|
|
2057
2161
|
# Fallback to isActive() if cache lookup fails
|
|
2058
2162
|
if vm.isActive():
|
|
2059
2163
|
active_vms.append(vm)
|
|
@@ -2118,7 +2222,7 @@ class VMCard(Static):
|
|
|
2118
2222
|
try:
|
|
2119
2223
|
# Use vm_service to get XML (handles caching)
|
|
2120
2224
|
self.app.vm_service._get_domain_xml(self.vm, internal_id=self.internal_id)
|
|
2121
|
-
|
|
2225
|
+
|
|
2122
2226
|
# Update tooltip on main thread
|
|
2123
2227
|
self.app.call_from_thread(self._perform_tooltip_update)
|
|
2124
2228
|
self.app.call_from_thread(self.app.show_quick_message, f"Info refreshed for {self.name}")
|
vmanager/webconsole_manager.py
CHANGED
|
@@ -17,7 +17,7 @@ from urllib.parse import urlparse
|
|
|
17
17
|
|
|
18
18
|
import libvirt
|
|
19
19
|
|
|
20
|
-
from .constants import AppInfo
|
|
20
|
+
from .constants import AppInfo, ErrorMessages, SuccessMessages
|
|
21
21
|
from .events import VmCardUpdateRequest
|
|
22
22
|
from .config import load_config, get_log_path
|
|
23
23
|
from .vm_queries import get_vm_graphics_info
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|