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/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,10 +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
|
-
ButtonLabels,
|
|
57
|
-
SparklineLabels, ErrorMessages, DialogMessages, VmAction
|
|
57
|
+
ButtonLabels, TabTitles, StatusText,
|
|
58
|
+
SparklineLabels, ErrorMessages, DialogMessages, VmAction,
|
|
59
|
+
WarningMessages, SuccessMessages
|
|
58
60
|
)
|
|
59
61
|
|
|
60
62
|
class VMCardActions(Static):
|
|
@@ -63,29 +65,31 @@ class VMCardActions(Static):
|
|
|
63
65
|
super().__init__()
|
|
64
66
|
|
|
65
67
|
def compose(self):
|
|
66
|
-
self.card.ui[
|
|
67
|
-
self.card.ui[
|
|
68
|
-
self.card.ui[
|
|
69
|
-
self.card.ui[
|
|
70
|
-
self.card.ui[
|
|
71
|
-
self.card.ui[
|
|
72
|
-
self.card.ui[
|
|
73
|
-
self.card.ui[
|
|
68
|
+
self.card.ui["start"] = Button(ButtonLabels.START, id="start", variant="success")
|
|
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")
|
|
71
|
+
self.card.ui["stop"] = Button(ButtonLabels.FORCE_OFF, id="stop", variant="error")
|
|
72
|
+
self.card.ui["pause"] = Button(ButtonLabels.PAUSE, id="pause", variant="primary")
|
|
73
|
+
self.card.ui["resume"] = Button(ButtonLabels.RESUME, id="resume", variant="success")
|
|
74
|
+
self.card.ui["configure-button"] = Button(ButtonLabels.CONFIGURE, id="configure-button", variant="primary")
|
|
75
|
+
self.card.ui["web_console"] = Button(ButtonLabels.WEB_CONSOLE, id="web_console", variant="default")
|
|
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")
|
|
74
78
|
|
|
75
|
-
self.card.ui[
|
|
76
|
-
self.card.ui[
|
|
77
|
-
self.card.ui[
|
|
79
|
+
self.card.ui["snapshot_take"] = Button(ButtonLabels.SNAPSHOT, id="snapshot_take", variant="primary")
|
|
80
|
+
self.card.ui["snapshot_restore"] = Button(ButtonLabels.RESTORE_SNAPSHOT, id="snapshot_restore", variant="primary")
|
|
81
|
+
self.card.ui["snapshot_delete"] = Button(ButtonLabels.DELETE_SNAPSHOT, id="snapshot_delete", variant="error")
|
|
78
82
|
|
|
79
|
-
self.card.ui[
|
|
80
|
-
self.card.ui[
|
|
81
|
-
self.card.ui[
|
|
82
|
-
self.card.ui[
|
|
83
|
-
self.card.ui[
|
|
83
|
+
self.card.ui["delete"] = Button(ButtonLabels.DELETE, id="delete", variant="error", classes="delete-button")
|
|
84
|
+
self.card.ui["clone"] = Button(ButtonLabels.CLONE, id="clone", classes="clone-button")
|
|
85
|
+
self.card.ui["migration"] = Button(ButtonLabels.MIGRATION, id="migration", variant="primary", classes="migration-button")
|
|
86
|
+
self.card.ui["xml"] = Button(ButtonLabels.VIEW_XML, id="xml")
|
|
87
|
+
self.card.ui["rename-button"] = Button(ButtonLabels.RENAME, id="rename-button", variant="primary", classes="rename-button")
|
|
84
88
|
|
|
85
|
-
self.card.ui[
|
|
86
|
-
self.card.ui[
|
|
87
|
-
self.card.ui[
|
|
88
|
-
self.card.ui[
|
|
89
|
+
self.card.ui["create_overlay"] = Button(ButtonLabels.CREATE_OVERLAY, id="create_overlay", variant="primary")
|
|
90
|
+
self.card.ui["commit_disk"] = Button(ButtonLabels.COMMIT_DISK, id="commit_disk", variant="error")
|
|
91
|
+
self.card.ui["discard_overlay"] = Button(ButtonLabels.DISCARD_OVERLAY, id="discard_overlay", variant="error")
|
|
92
|
+
self.card.ui["snap_overlay_help"] = Button(ButtonLabels.SNAP_OVERLAY_HELP, id="snap_overlay_help", variant="default")
|
|
89
93
|
|
|
90
94
|
self.card.ui["tabbed_content"] = TabbedContent(id="button-container")
|
|
91
95
|
|
|
@@ -93,37 +97,40 @@ class VMCardActions(Static):
|
|
|
93
97
|
with TabPane(TabTitles.MANAGE, id="manage-tab"):
|
|
94
98
|
with Horizontal():
|
|
95
99
|
with Vertical():
|
|
96
|
-
yield self.card.ui[
|
|
97
|
-
yield self.card.ui[
|
|
98
|
-
yield self.card.ui[
|
|
99
|
-
yield self.card.ui[
|
|
100
|
-
yield self.card.ui[
|
|
100
|
+
yield self.card.ui["start"]
|
|
101
|
+
yield self.card.ui["shutdown"]
|
|
102
|
+
yield self.card.ui["stop"]
|
|
103
|
+
yield self.card.ui["pause"]
|
|
104
|
+
yield self.card.ui["resume"]
|
|
101
105
|
with Vertical():
|
|
102
|
-
yield self.card.ui[
|
|
103
|
-
yield self.card.ui[
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
yield self.card.ui["web_console"]
|
|
107
|
+
yield self.card.ui["connect"]
|
|
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"):
|
|
106
111
|
with Horizontal():
|
|
107
112
|
with Vertical():
|
|
108
|
-
yield self.card.ui[
|
|
109
|
-
yield self.card.ui[
|
|
110
|
-
yield self.card.ui[
|
|
113
|
+
yield self.card.ui["snapshot_take"]
|
|
114
|
+
yield self.card.ui["snapshot_restore"]
|
|
115
|
+
yield self.card.ui["snapshot_delete"]
|
|
111
116
|
with Vertical():
|
|
112
|
-
yield self.card.ui[
|
|
113
|
-
yield self.card.ui[
|
|
114
|
-
yield self.card.ui[
|
|
115
|
-
yield self.card.ui[
|
|
117
|
+
yield self.card.ui["hibernate"]
|
|
118
|
+
yield self.card.ui["create_overlay"]
|
|
119
|
+
yield self.card.ui["commit_disk"]
|
|
120
|
+
yield self.card.ui["discard_overlay"]
|
|
121
|
+
yield self.card.ui["snap_overlay_help"]
|
|
116
122
|
with TabPane(TabTitles.OTHER, id="special-tab"):
|
|
117
123
|
with Horizontal():
|
|
118
124
|
with Vertical():
|
|
119
|
-
yield self.card.ui[
|
|
125
|
+
yield self.card.ui["delete"]
|
|
120
126
|
yield Static(classes="button-separator")
|
|
121
|
-
yield self.card.ui[
|
|
122
|
-
yield self.card.ui[
|
|
127
|
+
yield self.card.ui["clone"]
|
|
128
|
+
yield self.card.ui["migration"]
|
|
123
129
|
with Vertical():
|
|
124
|
-
yield self.card.ui[
|
|
130
|
+
yield self.card.ui["configure-button"]
|
|
131
|
+
yield self.card.ui["xml"]
|
|
125
132
|
yield Static(classes="button-separator")
|
|
126
|
-
yield self.card.ui[
|
|
133
|
+
yield self.card.ui["rename-button"]
|
|
127
134
|
|
|
128
135
|
|
|
129
136
|
class VMCard(Static):
|
|
@@ -183,21 +190,19 @@ class VMCard(Static):
|
|
|
183
190
|
def _get_snapshot_tab_title(self, num_snapshots: int = -1) -> str:
|
|
184
191
|
"""Get snapshot tab title. Pass num_snapshots to avoid blocking libvirt call."""
|
|
185
192
|
if num_snapshots == -1:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
189
196
|
|
|
190
197
|
if self.vm:
|
|
191
198
|
try:
|
|
192
|
-
if num_snapshots
|
|
193
|
-
return TabTitles.
|
|
194
|
-
|
|
195
|
-
return TabTitles.
|
|
196
|
-
elif num_snapshots >= 2:
|
|
197
|
-
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})"
|
|
198
203
|
except libvirt.libvirtError:
|
|
199
204
|
pass # Domain might be transient or invalid
|
|
200
|
-
return TabTitles.
|
|
205
|
+
return TabTitles.STATE_MANAGEMENT
|
|
201
206
|
|
|
202
207
|
def update_snapshot_tab_title(self, num_snapshots: int = -1) -> None:
|
|
203
208
|
"""Updates the snapshot tab title."""
|
|
@@ -228,7 +233,7 @@ class VMCard(Static):
|
|
|
228
233
|
self.webc_status_indicator = new_indicator
|
|
229
234
|
|
|
230
235
|
# Update button label and style
|
|
231
|
-
web_console_button = self.ui.get(
|
|
236
|
+
web_console_button = self.ui.get("web_console")
|
|
232
237
|
if web_console_button:
|
|
233
238
|
if webc_is_running:
|
|
234
239
|
web_console_button.label = "Show Console"
|
|
@@ -249,7 +254,7 @@ class VMCard(Static):
|
|
|
249
254
|
def compose(self):
|
|
250
255
|
self.ui["checkbox"] = Checkbox("", id="vm-select-checkbox", classes="vm-select-checkbox", value=self.is_selected, tooltip="Select VM")
|
|
251
256
|
self.ui["vmname"] = Static(self._get_vm_display_name(), id="vmname", classes="vmname")
|
|
252
|
-
self.ui["status"] = Static(f"{self.status}{self.webc_status_indicator}", id="status"
|
|
257
|
+
self.ui["status"] = Static(f"{self.status}{self.webc_status_indicator}", id="status")
|
|
253
258
|
|
|
254
259
|
# Create all sparkline components
|
|
255
260
|
self.ui["cpu_label"] = Static("", classes="sparkline-label")
|
|
@@ -341,7 +346,7 @@ class VMCard(Static):
|
|
|
341
346
|
"""Updates the tooltip for the VM name using Markdown."""
|
|
342
347
|
if not self.display or not self.ui or "vmname" not in self.ui:
|
|
343
348
|
return
|
|
344
|
-
|
|
349
|
+
|
|
345
350
|
uuid = self.internal_id
|
|
346
351
|
if not uuid:
|
|
347
352
|
return
|
|
@@ -387,7 +392,7 @@ class VMCard(Static):
|
|
|
387
392
|
if ips:
|
|
388
393
|
ip_display = ", ".join(ips)
|
|
389
394
|
|
|
390
|
-
cpu_model_display = f"
|
|
395
|
+
cpu_model_display = f" {self.cpu_model}" if self.cpu_model else ""
|
|
391
396
|
|
|
392
397
|
tooltip_md = generate_tooltip_markdown(
|
|
393
398
|
uuid=uuid_display,
|
|
@@ -396,7 +401,7 @@ class VMCard(Static):
|
|
|
396
401
|
ip=ip_display,
|
|
397
402
|
boot=self.boot_device or "N/A",
|
|
398
403
|
cpu=self.cpu,
|
|
399
|
-
cpu_model=
|
|
404
|
+
cpu_model=cpu_model_display or "",
|
|
400
405
|
memory=self.memory
|
|
401
406
|
)
|
|
402
407
|
|
|
@@ -575,8 +580,7 @@ class VMCard(Static):
|
|
|
575
580
|
logging.warning(f"Could not find #info-container on VMCard {self.name} when switching to detailed view.")
|
|
576
581
|
except Exception as e:
|
|
577
582
|
# Catch-all for potential mounting errors (e.g. already mounted elsewhere?)
|
|
578
|
-
|
|
579
|
-
|
|
583
|
+
logging.warning(f"Error restoring collapsible in detailed view: {e}")
|
|
580
584
|
|
|
581
585
|
# Ensure sparklines visibility is correct
|
|
582
586
|
self.watch_stats_view_mode(self.stats_view_mode, self.stats_view_mode)
|
|
@@ -864,8 +868,7 @@ class VMCard(Static):
|
|
|
864
868
|
self._update_fast_buttons()
|
|
865
869
|
self._update_webc_status()
|
|
866
870
|
|
|
867
|
-
|
|
868
|
-
if self.ui.get(ButtonIds.RENAME_BUTTON):
|
|
871
|
+
if self.ui.get("rename-button"):
|
|
869
872
|
# Check if collapsible is expanded before fetching heavy data
|
|
870
873
|
collapsible = self.ui.get("collapsible")
|
|
871
874
|
if collapsible and not collapsible.collapsed:
|
|
@@ -885,31 +888,32 @@ class VMCard(Static):
|
|
|
885
888
|
is_pmsuspended = self.status == StatusText.PMSUSPENDED
|
|
886
889
|
is_blocked = self.status == StatusText.BLOCKED
|
|
887
890
|
|
|
888
|
-
if not self.ui.get(
|
|
891
|
+
if not self.ui.get("rename-button"):
|
|
889
892
|
return
|
|
890
893
|
|
|
891
|
-
self.ui[
|
|
892
|
-
self.ui[
|
|
893
|
-
self.ui[
|
|
894
|
-
self.ui[
|
|
895
|
-
self.ui[
|
|
896
|
-
self.ui[
|
|
897
|
-
self.ui[
|
|
898
|
-
self.ui[
|
|
899
|
-
self.ui[
|
|
900
|
-
self.ui[
|
|
901
|
-
self.ui[
|
|
902
|
-
self.ui[
|
|
903
|
-
self.ui[
|
|
904
|
-
self.ui[
|
|
905
|
-
self.ui[
|
|
906
|
-
|
|
907
|
-
|
|
894
|
+
self.ui["start"].display = is_stopped
|
|
895
|
+
self.ui["shutdown"].display = is_running or is_blocked
|
|
896
|
+
self.ui["hibernate"].display = is_running or is_blocked
|
|
897
|
+
self.ui["stop"].display = is_running or is_paused or is_pmsuspended or is_blocked
|
|
898
|
+
self.ui["delete"].display = is_running or is_paused or is_stopped or is_pmsuspended or is_blocked
|
|
899
|
+
self.ui["clone"].display = is_stopped
|
|
900
|
+
self.ui["migration"].display = not is_loading
|
|
901
|
+
self.ui["rename-button"].display = is_stopped
|
|
902
|
+
self.ui["pause"].display = is_running
|
|
903
|
+
self.ui["resume"].display = is_paused or is_pmsuspended
|
|
904
|
+
self.ui["connect"].display = self.app.r_viewer_available
|
|
905
|
+
self.ui["web_console"].display = (is_running or is_paused or is_blocked)
|
|
906
|
+
self.ui["configure-button"].display = not is_loading
|
|
907
|
+
self.ui["snap_overlay_help"].display = not is_loading
|
|
908
|
+
self.ui["snapshot_take"].display = not is_loading #is_running or is_paused
|
|
909
|
+
self.ui["snapshot_restore"].display = not is_running and not is_loading and not is_blocked
|
|
910
|
+
|
|
911
|
+
xml_button = self.ui["xml"]
|
|
908
912
|
if is_stopped:
|
|
909
|
-
xml_button.label =
|
|
913
|
+
xml_button.label = ButtonLabels.EDIT_XML
|
|
910
914
|
self.stats_view_mode = "resources"
|
|
911
915
|
else:
|
|
912
|
-
xml_button.label =
|
|
916
|
+
xml_button.label = ButtonLabels.VIEW_XML
|
|
913
917
|
xml_button.display = not is_loading
|
|
914
918
|
|
|
915
919
|
def _fetch_actions_state_worker(self):
|
|
@@ -993,7 +997,7 @@ class VMCard(Static):
|
|
|
993
997
|
|
|
994
998
|
def update_ui():
|
|
995
999
|
self._update_slow_buttons(snapshot_summary, has_overlay)
|
|
996
|
-
|
|
1000
|
+
|
|
997
1001
|
try:
|
|
998
1002
|
self.app.call_from_thread(update_ui)
|
|
999
1003
|
except RuntimeError:
|
|
@@ -1004,7 +1008,7 @@ class VMCard(Static):
|
|
|
1004
1008
|
|
|
1005
1009
|
def _update_slow_buttons(self, snapshot_summary: dict, has_overlay: bool):
|
|
1006
1010
|
"""Updates buttons that rely on heavy state."""
|
|
1007
|
-
if not self.ui.get(
|
|
1011
|
+
if not self.ui.get("rename-button"):
|
|
1008
1012
|
return
|
|
1009
1013
|
|
|
1010
1014
|
snapshot_count = snapshot_summary.get('count', 0)
|
|
@@ -1031,12 +1035,12 @@ class VMCard(Static):
|
|
|
1031
1035
|
|
|
1032
1036
|
has_snapshots = snapshot_count > 0
|
|
1033
1037
|
|
|
1034
|
-
self.ui[
|
|
1035
|
-
self.ui[
|
|
1038
|
+
self.ui["snapshot_restore"].display = has_snapshots and not is_running and not is_loading and not is_blocked
|
|
1039
|
+
self.ui["snapshot_delete"].display = has_snapshots
|
|
1036
1040
|
|
|
1037
|
-
self.ui[
|
|
1038
|
-
self.ui[
|
|
1039
|
-
self.ui[
|
|
1041
|
+
self.ui["commit_disk"].display = (is_running or is_blocked) and has_overlay
|
|
1042
|
+
self.ui["discard_overlay"].display = is_stopped and has_overlay
|
|
1043
|
+
self.ui["create_overlay"].display = is_stopped and not has_overlay
|
|
1040
1044
|
|
|
1041
1045
|
self.update_snapshot_tab_title(snapshot_count)
|
|
1042
1046
|
|
|
@@ -1046,39 +1050,45 @@ class VMCard(Static):
|
|
|
1046
1050
|
status_widget.remove_class("stopped", "running", "paused", "loading", "pmsuspended", "blocked")
|
|
1047
1051
|
if self.status == StatusText.LOADING:
|
|
1048
1052
|
status_widget.add_class("loading")
|
|
1053
|
+
elif self.status == StatusText.RUNNING:
|
|
1054
|
+
status_widget.add_class("running")
|
|
1055
|
+
elif self.status == StatusText.STOPPED:
|
|
1056
|
+
status_widget.add_class("stopped")
|
|
1057
|
+
elif self.status == StatusText.PAUSED:
|
|
1058
|
+
status_widget.add_class("paused")
|
|
1049
1059
|
elif self.status == StatusText.PMSUSPENDED:
|
|
1050
1060
|
status_widget.add_class("pmsuspended")
|
|
1051
1061
|
elif self.status == StatusText.BLOCKED:
|
|
1052
1062
|
status_widget.add_class("blocked")
|
|
1053
|
-
else:
|
|
1054
|
-
status_widget.add_class(self.status.lower())
|
|
1055
1063
|
|
|
1056
1064
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
1057
1065
|
"""Handle button presses."""
|
|
1058
|
-
if event.button.id ==
|
|
1066
|
+
if event.button.id == "start":
|
|
1059
1067
|
self.post_message(VmActionRequest(self.internal_id, VmAction.START))
|
|
1060
1068
|
return
|
|
1061
1069
|
|
|
1062
1070
|
button_handlers = {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1071
|
+
"shutdown": self._handle_shutdown_button,
|
|
1072
|
+
"hibernate": self._handle_hibernate_button,
|
|
1073
|
+
"stop": self._handle_stop_button,
|
|
1074
|
+
"pause": self._handle_pause_button,
|
|
1075
|
+
"resume": self._handle_resume_button,
|
|
1076
|
+
"xml": self._handle_xml_button,
|
|
1077
|
+
"connect": self._handle_connect_button,
|
|
1078
|
+
"web_console": self._handle_web_console_button,
|
|
1079
|
+
"tmux_console": self._handle_tmux_console_button,
|
|
1080
|
+
"snapshot_take": self._handle_snapshot_take_button,
|
|
1081
|
+
"snapshot_restore": self._handle_snapshot_restore_button,
|
|
1082
|
+
"snapshot_delete": self._handle_snapshot_delete_button,
|
|
1083
|
+
"delete": self._handle_delete_button,
|
|
1084
|
+
"clone": self._handle_clone_button,
|
|
1085
|
+
"migration": self._handle_migration_button,
|
|
1086
|
+
"rename-button": self._handle_rename_button,
|
|
1087
|
+
"configure-button": self._handle_configure_button,
|
|
1088
|
+
"create_overlay": self._handle_create_overlay,
|
|
1089
|
+
"commit_disk": self._handle_commit_disk,
|
|
1090
|
+
"discard_overlay": self._handle_discard_overlay,
|
|
1091
|
+
"snap_overlay_help": self._handle_overlay_help,
|
|
1082
1092
|
}
|
|
1083
1093
|
handler = button_handlers.get(event.button.id)
|
|
1084
1094
|
if handler:
|
|
@@ -1100,7 +1110,7 @@ class VMCard(Static):
|
|
|
1100
1110
|
valid_disks = [d['path'] for d in disks if d.get('device_type') == 'disk']
|
|
1101
1111
|
|
|
1102
1112
|
if not valid_disks:
|
|
1103
|
-
self.app.show_error_message(
|
|
1113
|
+
self.app.show_error_message(ErrorMessages.NO_SUITABLE_DISKS_FOR_OVERLAY)
|
|
1104
1114
|
return
|
|
1105
1115
|
|
|
1106
1116
|
target_disk = valid_disks[0]
|
|
@@ -1120,29 +1130,29 @@ class VMCard(Static):
|
|
|
1120
1130
|
return
|
|
1121
1131
|
|
|
1122
1132
|
if was_modified:
|
|
1123
|
-
self.app.show_success_message(
|
|
1133
|
+
self.app.show_success_message(SuccessMessages.INPUT_SANITIZED.format(original_input=overlay_name_raw, sanitized_input=overlay_name))
|
|
1124
1134
|
|
|
1125
1135
|
if not overlay_name:
|
|
1126
|
-
self.app.show_error_message(
|
|
1136
|
+
self.app.show_error_message(ErrorMessages.OVERLAY_NAME_EMPTY_AFTER_SANITIZATION)
|
|
1127
1137
|
return
|
|
1128
1138
|
|
|
1129
1139
|
self.app.vm_service.suppress_vm_events(self.internal_id)
|
|
1130
1140
|
try:
|
|
1131
1141
|
create_external_overlay(self.vm, target_disk, overlay_name)
|
|
1132
|
-
self.app.show_success_message(
|
|
1142
|
+
self.app.show_success_message(SuccessMessages.OVERLAY_CREATED.format(overlay_name=overlay_name))
|
|
1133
1143
|
self.app.vm_service.invalidate_vm_state_cache(self.internal_id)
|
|
1134
1144
|
self._boot_device_checked = False
|
|
1135
1145
|
self.post_message(VmCardUpdateRequest(self.internal_id))
|
|
1136
1146
|
self.update_button_layout()
|
|
1137
1147
|
except Exception as e:
|
|
1138
|
-
self.app.show_error_message(
|
|
1148
|
+
self.app.show_error_message(ErrorMessages.ERROR_CREATING_OVERLAY_TEMPLATE.format(error=e))
|
|
1139
1149
|
finally:
|
|
1140
1150
|
self.app.vm_service.unsuppress_vm_events(self.internal_id)
|
|
1141
1151
|
|
|
1142
1152
|
self.app.push_screen(InputModal("Enter name for new overlay volume:", default_name, restrict=r"[a-zA-Z0-9_-]*"), on_name_input)
|
|
1143
1153
|
|
|
1144
1154
|
except Exception as e:
|
|
1145
|
-
self.app.show_error_message(
|
|
1155
|
+
self.app.show_error_message(ErrorMessages.ERROR_PREPARING_OVERLAY_CREATION_TEMPLATE.format(error=e))
|
|
1146
1156
|
|
|
1147
1157
|
def _handle_discard_overlay(self, event: Button.Pressed) -> None:
|
|
1148
1158
|
"""Handles the discard overlay button press."""
|
|
@@ -1150,7 +1160,7 @@ class VMCard(Static):
|
|
|
1150
1160
|
overlay_disks = get_overlay_disks(self.vm)
|
|
1151
1161
|
|
|
1152
1162
|
if not overlay_disks:
|
|
1153
|
-
self.app.show_error_message(
|
|
1163
|
+
self.app.show_error_message(ErrorMessages.NO_OVERLAY_DISKS_FOUND)
|
|
1154
1164
|
return
|
|
1155
1165
|
|
|
1156
1166
|
def proceed_with_discard(target_disk: str | None):
|
|
@@ -1162,13 +1172,13 @@ class VMCard(Static):
|
|
|
1162
1172
|
self.app.vm_service.suppress_vm_events(self.internal_id)
|
|
1163
1173
|
try:
|
|
1164
1174
|
discard_overlay(self.vm, target_disk)
|
|
1165
|
-
self.app.show_success_message(
|
|
1175
|
+
self.app.show_success_message(SuccessMessages.OVERLAY_DISCARDED.format(target_disk=target_disk))
|
|
1166
1176
|
self.app.vm_service.invalidate_vm_state_cache(self.internal_id)
|
|
1167
1177
|
self._boot_device_checked = False
|
|
1168
1178
|
self.post_message(VmCardUpdateRequest(self.internal_id))
|
|
1169
1179
|
self.update_button_layout()
|
|
1170
1180
|
except Exception as e:
|
|
1171
|
-
self.app.show_error_message(
|
|
1181
|
+
self.app.show_error_message(ErrorMessages.ERROR_DISCARDING_OVERLAY_TEMPLATE.format(error=e))
|
|
1172
1182
|
finally:
|
|
1173
1183
|
self.app.vm_service.unsuppress_vm_events(self.internal_id)
|
|
1174
1184
|
|
|
@@ -1186,7 +1196,7 @@ class VMCard(Static):
|
|
|
1186
1196
|
)
|
|
1187
1197
|
|
|
1188
1198
|
except Exception as e:
|
|
1189
|
-
self.app.show_error_message(
|
|
1199
|
+
self.app.show_error_message(ErrorMessages.ERROR_PREPARING_DISCARD_OVERLAY_TEMPLATE.format(error=e))
|
|
1190
1200
|
|
|
1191
1201
|
|
|
1192
1202
|
def _handle_commit_disk(self, event: Button.Pressed) -> None:
|
|
@@ -1202,7 +1212,7 @@ class VMCard(Static):
|
|
|
1202
1212
|
break
|
|
1203
1213
|
|
|
1204
1214
|
if not target_disk:
|
|
1205
|
-
self.app.show_error_message(
|
|
1215
|
+
self.app.show_error_message(ErrorMessages.NO_DISKS_FOUND_TO_COMMIT)
|
|
1206
1216
|
return
|
|
1207
1217
|
|
|
1208
1218
|
def on_confirm(confirmed: bool):
|
|
@@ -1214,11 +1224,11 @@ class VMCard(Static):
|
|
|
1214
1224
|
self.app.vm_service.suppress_vm_events(self.internal_id)
|
|
1215
1225
|
try:
|
|
1216
1226
|
commit_disk_changes(self.vm, target_disk)
|
|
1217
|
-
self.app.call_from_thread(self.app.show_success_message,
|
|
1227
|
+
self.app.call_from_thread(self.app.show_success_message, SuccessMessages.DISK_COMMITTED)
|
|
1218
1228
|
self.app.call_from_thread(self.app.refresh_vm_list)
|
|
1219
1229
|
self.app.call_from_thread(self.update_button_layout)
|
|
1220
1230
|
except Exception as e:
|
|
1221
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
1231
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.ERROR_COMMITTING_DISK_TEMPLATE.format(error=e))
|
|
1222
1232
|
finally:
|
|
1223
1233
|
self.app.vm_service.unsuppress_vm_events(self.internal_id)
|
|
1224
1234
|
self.app.call_from_thread(progress_modal.dismiss)
|
|
@@ -1231,7 +1241,7 @@ class VMCard(Static):
|
|
|
1231
1241
|
)
|
|
1232
1242
|
|
|
1233
1243
|
except Exception as e:
|
|
1234
|
-
self.app.show_error_message(
|
|
1244
|
+
self.app.show_error_message(ErrorMessages.ERROR_PREPARING_COMMIT_TEMPLATE.format(error=e))
|
|
1235
1245
|
|
|
1236
1246
|
def _handle_shutdown_button(self, event: Button.Pressed) -> None:
|
|
1237
1247
|
"""Handles the shutdown button press."""
|
|
@@ -1239,6 +1249,27 @@ class VMCard(Static):
|
|
|
1239
1249
|
if self.status in (StatusText.RUNNING, StatusText.PAUSED):
|
|
1240
1250
|
self.post_message(VmActionRequest(self.internal_id, VmAction.STOP))
|
|
1241
1251
|
|
|
1252
|
+
def _handle_hibernate_button(self, event: Button.Pressed) -> None:
|
|
1253
|
+
"""Handles the save button press."""
|
|
1254
|
+
logging.info(f"Attempting to save (hibernate) VM: {self.name}")
|
|
1255
|
+
|
|
1256
|
+
def do_save():
|
|
1257
|
+
self.stop_background_activities()
|
|
1258
|
+
self.app.vm_service.suppress_vm_events(self.internal_id)
|
|
1259
|
+
try:
|
|
1260
|
+
hibernate_vm(self.vm)
|
|
1261
|
+
self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_SAVED_TEMPLATE.format(vm_name=self.name))
|
|
1262
|
+
self.app.vm_service.invalidate_vm_state_cache(self.internal_id)
|
|
1263
|
+
self.app.call_from_thread(setattr, self, 'status', StatusText.STOPPED)
|
|
1264
|
+
self.app.call_from_thread(self.update_button_layout)
|
|
1265
|
+
except Exception as e:
|
|
1266
|
+
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))
|
|
1267
|
+
finally:
|
|
1268
|
+
self.app.vm_service.unsuppress_vm_events(self.internal_id)
|
|
1269
|
+
|
|
1270
|
+
if self.status in (StatusText.RUNNING, StatusText.PAUSED):
|
|
1271
|
+
self.app.worker_manager.run(do_save, name=f"save_{self.internal_id}")
|
|
1272
|
+
|
|
1242
1273
|
def stop_background_activities(self):
|
|
1243
1274
|
""" Stop background activities before action """
|
|
1244
1275
|
with self._timer_lock:
|
|
@@ -1273,7 +1304,7 @@ class VMCard(Static):
|
|
|
1273
1304
|
self.stop_background_activities()
|
|
1274
1305
|
self.post_message(VmActionRequest(self.internal_id, VmAction.PAUSE))
|
|
1275
1306
|
else:
|
|
1276
|
-
self.app.show_warning_message(
|
|
1307
|
+
self.app.show_warning_message(WarningMessages.LIBVIRT_XML_NO_EFFECTIVE_CHANGE.format(vm_name=self.name))
|
|
1277
1308
|
|
|
1278
1309
|
def _handle_resume_button(self, event: Button.Pressed) -> None:
|
|
1279
1310
|
"""Handles the resume button press."""
|
|
@@ -1304,42 +1335,41 @@ class VMCard(Static):
|
|
|
1304
1335
|
try:
|
|
1305
1336
|
conn = self.vm.connect()
|
|
1306
1337
|
new_domain = conn.defineXML(modified_xml)
|
|
1307
|
-
|
|
1338
|
+
|
|
1308
1339
|
# Verify if changes were effectively applied
|
|
1309
1340
|
new_xml = new_domain.XMLDesc(xml_flags)
|
|
1310
|
-
|
|
1341
|
+
|
|
1311
1342
|
if original_xml == new_xml:
|
|
1312
|
-
self.app.show_warning_message(
|
|
1343
|
+
self.app.show_warning_message(WarningMessages.LIBVIRT_XML_NO_EFFECTIVE_CHANGE.format(vm_name=self.name))
|
|
1313
1344
|
logging.warning(f"XML update for {self.name} resulted in no effective changes.")
|
|
1314
1345
|
else:
|
|
1315
|
-
self.app.show_success_message(
|
|
1346
|
+
self.app.show_success_message(SuccessMessages.VM_CONFIG_UPDATED.format(vm_name=self.name))
|
|
1316
1347
|
logging.info(f"Successfully updated XML for VM: {self.name}")
|
|
1317
|
-
|
|
1348
|
+
|
|
1318
1349
|
self.app.vm_service.invalidate_vm_state_cache(self.internal_id)
|
|
1319
1350
|
self._boot_device_checked = False
|
|
1320
1351
|
self.app.refresh_vm_list()
|
|
1321
1352
|
except libvirt.libvirtError as e:
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
logging.error(error_msg)
|
|
1353
|
+
self.app.show_error_message(ErrorMessages.INVALID_XML_TEMPLATE.format(vm_name=self.name, error=e))
|
|
1354
|
+
logging.error(e)
|
|
1325
1355
|
else:
|
|
1326
|
-
self.app.show_success_message(
|
|
1356
|
+
self.app.show_success_message(SuccessMessages.NO_XML_CHANGES)
|
|
1327
1357
|
|
|
1328
1358
|
self.app.push_screen(
|
|
1329
1359
|
XMLDisplayModal(original_xml, read_only=not is_stopped),
|
|
1330
1360
|
handle_xml_modal_result
|
|
1331
1361
|
)
|
|
1332
1362
|
except libvirt.libvirtError as e:
|
|
1333
|
-
self.app.show_error_message(
|
|
1363
|
+
self.app.show_error_message(ErrorMessages.ERROR_GETTING_XML_TEMPLATE.format(vm_name=self.name, error=e))
|
|
1334
1364
|
except Exception as e:
|
|
1335
|
-
self.app.show_error_message(
|
|
1365
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE.format(error=e))
|
|
1336
1366
|
logging.error(f"Unexpected error handling XML button: {traceback.format_exc()}")
|
|
1337
1367
|
|
|
1338
1368
|
def _handle_connect_button(self, event: Button.Pressed) -> None:
|
|
1339
1369
|
"""Handles the connect button press by running the remove virt viewer in a worker."""
|
|
1340
1370
|
logging.info(f"Attempting to connect to VM: {self.name}")
|
|
1341
1371
|
if not hasattr(self, 'conn') or not self.conn:
|
|
1342
|
-
self.app.show_error_message(
|
|
1372
|
+
self.app.show_error_message(ErrorMessages.CONNECTION_INFO_NOT_AVAILABLE)
|
|
1343
1373
|
return
|
|
1344
1374
|
|
|
1345
1375
|
def do_connect() -> None:
|
|
@@ -1368,7 +1398,7 @@ class VMCard(Static):
|
|
|
1368
1398
|
logging.error(f"Failed to spawn {self.app.r_viewer} for {domain_name}: {e}")
|
|
1369
1399
|
self.app.call_from_thread(
|
|
1370
1400
|
self.app.show_error_message,
|
|
1371
|
-
|
|
1401
|
+
ErrorMessages.REMOTE_VIEWER_FAILED_TO_START_TEMPLATE.format(viewer=self.app.r_viewer, domain_name=domain_name, error=e)
|
|
1372
1402
|
)
|
|
1373
1403
|
return
|
|
1374
1404
|
|
|
@@ -1380,13 +1410,13 @@ class VMCard(Static):
|
|
|
1380
1410
|
except libvirt.libvirtError as e:
|
|
1381
1411
|
self.app.call_from_thread(
|
|
1382
1412
|
self.app.show_error_message,
|
|
1383
|
-
|
|
1413
|
+
ErrorMessages.ERROR_GETTING_VM_DETAILS_TEMPLATE.format(vm_name=self.name, error=e)
|
|
1384
1414
|
)
|
|
1385
1415
|
except Exception as e:
|
|
1386
1416
|
logging.error(f"An unexpected error occurred during connect: {e}", exc_info=True)
|
|
1387
1417
|
self.app.call_from_thread(
|
|
1388
1418
|
self.app.show_error_message,
|
|
1389
|
-
|
|
1419
|
+
ErrorMessages.UNEXPECTED_ERROR_CONNECTING
|
|
1390
1420
|
)
|
|
1391
1421
|
|
|
1392
1422
|
self.app.worker_manager.run(do_connect, name=f"r_viewer_{self.name}")
|
|
@@ -1403,7 +1433,7 @@ class VMCard(Static):
|
|
|
1403
1433
|
)
|
|
1404
1434
|
return
|
|
1405
1435
|
except Exception as e:
|
|
1406
|
-
self.app.show_error_message(
|
|
1436
|
+
self.app.show_error_message(ErrorMessages.ERROR_CHECKING_WEB_CONSOLE_STATUS_TEMPLATE.format(vm_name=self.name, error=e))
|
|
1407
1437
|
return
|
|
1408
1438
|
|
|
1409
1439
|
#is_remote = self.app.webconsole_manager.is_remote_connection(self.conn.getURI())
|
|
@@ -1427,6 +1457,50 @@ class VMCard(Static):
|
|
|
1427
1457
|
else:
|
|
1428
1458
|
self.app.worker_manager.run(worker, name=f"start_console_{self.vm.name()}")
|
|
1429
1459
|
|
|
1460
|
+
def _handle_tmux_console_button(self, event: Button.Pressed) -> None:
|
|
1461
|
+
"""Handles the text console button press by opening a new tmux window."""
|
|
1462
|
+
logging.info(f"Attempting to open text console for VM: {self.name}")
|
|
1463
|
+
|
|
1464
|
+
# Check if running in tmux
|
|
1465
|
+
if not os.environ.get("TMUX"):
|
|
1466
|
+
self.app.show_error_message("This feature requires running inside tmux.")
|
|
1467
|
+
return
|
|
1468
|
+
|
|
1469
|
+
try:
|
|
1470
|
+
# Use cached values to avoid libvirt calls where possible
|
|
1471
|
+
uri = self.app.vm_service.get_uri_for_connection(self.conn)
|
|
1472
|
+
if not uri:
|
|
1473
|
+
uri = self.conn.getURI()
|
|
1474
|
+
|
|
1475
|
+
# Get proper domain name
|
|
1476
|
+
_, domain_name = self.app.vm_service.get_vm_identity(self.vm, self.conn)
|
|
1477
|
+
|
|
1478
|
+
# Construct command
|
|
1479
|
+
# tmux new-window -n "Console: <vm_name>" "virsh -c <uri> console <vm_name>; read"
|
|
1480
|
+
help_msg = (
|
|
1481
|
+
"echo '---------------------------------------------------------'; "
|
|
1482
|
+
"echo 'Tmux Navigation Help:'; "
|
|
1483
|
+
"echo ' Ctrl+B N or P - Move to the next or previous window.'; "
|
|
1484
|
+
"echo ' Ctrl+B W - Open a panel to navigate across windows in multiple sessions.'; "
|
|
1485
|
+
"echo ' Ctrl+] - Close the current view.'; "
|
|
1486
|
+
"echo ' Ctrl+B ? - View all keybindings. Press Q to exit.';"
|
|
1487
|
+
"echo '---------------------------------------------------------'; "
|
|
1488
|
+
"echo 'Starting console...'; sleep 1;"
|
|
1489
|
+
)
|
|
1490
|
+
cmd = [
|
|
1491
|
+
"tmux", "new-window",
|
|
1492
|
+
"-n", f"{domain_name}",
|
|
1493
|
+
f"{help_msg} virsh -c {uri} console {domain_name}; echo '\nConsole session ended. Press Enter to close window.'; read"
|
|
1494
|
+
]
|
|
1495
|
+
|
|
1496
|
+
logging.info(f"Launching tmux console: {' '.join(cmd)}")
|
|
1497
|
+
subprocess.Popen(cmd)
|
|
1498
|
+
self.app.show_quick_message(f"Opened console for {domain_name}")
|
|
1499
|
+
|
|
1500
|
+
except Exception as e:
|
|
1501
|
+
logging.error(f"Failed to open tmux console: {e}")
|
|
1502
|
+
self.app.show_error_message(f"Failed to open console: {e}")
|
|
1503
|
+
|
|
1430
1504
|
def _handle_snapshot_take_button(self, event: Button.Pressed) -> None:
|
|
1431
1505
|
"""Handles the snapshot take button press."""
|
|
1432
1506
|
logging.info(f"Attempting to take snapshot for VM: {self.name}")
|
|
@@ -1468,9 +1542,9 @@ class VMCard(Static):
|
|
|
1468
1542
|
def finalize_snapshot():
|
|
1469
1543
|
loading_modal.dismiss()
|
|
1470
1544
|
if error:
|
|
1471
|
-
self.app.show_error_message(
|
|
1545
|
+
self.app.show_error_message(ErrorMessages.SNAPSHOT_ERROR_TEMPLATE.format(vm_name=vm_name, error=error))
|
|
1472
1546
|
else:
|
|
1473
|
-
self.app.show_success_message(
|
|
1547
|
+
self.app.show_success_message(SuccessMessages.SNAPSHOT_CREATED.format(snapshot_name=name))
|
|
1474
1548
|
# Defer refresh and restart stats to avoid racing
|
|
1475
1549
|
self.app.set_timer(0.5, self._refresh_snapshot_tab_async)
|
|
1476
1550
|
# Restart stats timer after a delay
|
|
@@ -1498,7 +1572,7 @@ class VMCard(Static):
|
|
|
1498
1572
|
self.app.call_from_thread(loading_modal.dismiss)
|
|
1499
1573
|
|
|
1500
1574
|
if not snapshots_info:
|
|
1501
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
1575
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.NO_SNAPSHOTS_TO_RESTORE)
|
|
1502
1576
|
return
|
|
1503
1577
|
|
|
1504
1578
|
def restore_snapshot_callback(snapshot_name: str | None) -> None:
|
|
@@ -1537,10 +1611,10 @@ class VMCard(Static):
|
|
|
1537
1611
|
def finalize_ui():
|
|
1538
1612
|
restore_loading_modal.dismiss()
|
|
1539
1613
|
if error:
|
|
1540
|
-
self.app.show_error_message(
|
|
1614
|
+
self.app.show_error_message(ErrorMessages.ERROR_ON_VM_DURING_ACTION.format(vm_name=vm_name, action='snapshot restore', error=error))
|
|
1541
1615
|
else:
|
|
1542
1616
|
self._boot_device_checked = False
|
|
1543
|
-
self.app.show_success_message(
|
|
1617
|
+
self.app.show_success_message(SuccessMessages.SNAPSHOT_RESTORED.format(snapshot_name=snapshot_name))
|
|
1544
1618
|
logging.info(f"Successfully restored snapshot [b]{snapshot_name}[/b] for VM: {vm_name}")
|
|
1545
1619
|
self.app.refresh_vm_list(force=True)
|
|
1546
1620
|
self.app.call_from_thread(finalize_ui)
|
|
@@ -1550,11 +1624,11 @@ class VMCard(Static):
|
|
|
1550
1624
|
self.app.push_screen,
|
|
1551
1625
|
SelectSnapshotDialog(snapshots_info, "Select snapshot to restore"),
|
|
1552
1626
|
restore_snapshot_callback
|
|
1553
|
-
|
|
1627
|
+
)
|
|
1554
1628
|
|
|
1555
1629
|
except Exception as e:
|
|
1556
1630
|
self.app.call_from_thread(loading_modal.dismiss)
|
|
1557
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
1631
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.ERROR_FETCHING_SNAPSHOTS_TEMPLATE.format(error=e))
|
|
1558
1632
|
|
|
1559
1633
|
self.app.worker_manager.run(fetch_and_show_worker, name=f"snapshot_restore_fetch_{self.internal_id}")
|
|
1560
1634
|
|
|
@@ -1575,7 +1649,7 @@ class VMCard(Static):
|
|
|
1575
1649
|
self.app.call_from_thread(loading_modal.dismiss)
|
|
1576
1650
|
|
|
1577
1651
|
if not snapshots_info:
|
|
1578
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
1652
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.NO_SNAPSHOTS_TO_DELETE)
|
|
1579
1653
|
return
|
|
1580
1654
|
|
|
1581
1655
|
def delete_snapshot_callback(snapshot_name: str | None) -> None:
|
|
@@ -1607,9 +1681,9 @@ class VMCard(Static):
|
|
|
1607
1681
|
def finalize_ui():
|
|
1608
1682
|
loading_modal.dismiss()
|
|
1609
1683
|
if error:
|
|
1610
|
-
self.app.show_error_message(
|
|
1684
|
+
self.app.show_error_message(ErrorMessages.ERROR_ON_VM_DURING_ACTION.format(vm_name=vm_name, action='snapshot delete', error=error))
|
|
1611
1685
|
else:
|
|
1612
|
-
self.app.show_success_message(
|
|
1686
|
+
self.app.show_success_message(SuccessMessages.SNAPSHOT_DELETED.format(snapshot_name=snapshot_name))
|
|
1613
1687
|
logging.info(f"Successfully deleted snapshot '{snapshot_name}' for VM: {vm_name}")
|
|
1614
1688
|
self.app.set_timer(0.1, self._refresh_snapshot_tab_async)
|
|
1615
1689
|
# Restart stats timer
|
|
@@ -1631,7 +1705,7 @@ class VMCard(Static):
|
|
|
1631
1705
|
|
|
1632
1706
|
except Exception as e:
|
|
1633
1707
|
self.app.call_from_thread(loading_modal.dismiss)
|
|
1634
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
1708
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.ERROR_FETCHING_SNAPSHOTS_TEMPLATE.format(error=e))
|
|
1635
1709
|
|
|
1636
1710
|
self.app.worker_manager.run(fetch_and_show_worker, name=f"snapshot_delete_fetch_{self.internal_id}")
|
|
1637
1711
|
|
|
@@ -1685,12 +1759,12 @@ class VMCard(Static):
|
|
|
1685
1759
|
pass
|
|
1686
1760
|
|
|
1687
1761
|
# Also update button visibility
|
|
1688
|
-
if self.ui.get(
|
|
1762
|
+
if self.ui.get("rename-button"):
|
|
1689
1763
|
has_snapshots = snapshot_count > 0
|
|
1690
1764
|
is_running = self.status == StatusText.RUNNING
|
|
1691
1765
|
is_loading = self.status == StatusText.LOADING
|
|
1692
|
-
self.ui[
|
|
1693
|
-
self.ui[
|
|
1766
|
+
self.ui["snapshot_restore"].display = has_snapshots and not is_running and not is_loading
|
|
1767
|
+
self.ui["snapshot_delete"].display = has_snapshots
|
|
1694
1768
|
|
|
1695
1769
|
self.app.call_from_thread(update_ui)
|
|
1696
1770
|
|
|
@@ -1744,7 +1818,7 @@ class VMCard(Static):
|
|
|
1744
1818
|
# delete_vm handles opening its own connection
|
|
1745
1819
|
delete_vm(self.vm, delete_storage=delete_storage, delete_nvram=True, log_callback=log_callback)
|
|
1746
1820
|
|
|
1747
|
-
self.app.call_from_thread(self.app.show_success_message,
|
|
1821
|
+
self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_DELETED.format(vm_name=vm_name))
|
|
1748
1822
|
|
|
1749
1823
|
# Invalidate cache
|
|
1750
1824
|
self.app.vm_service.invalidate_vm_cache(internal_id)
|
|
@@ -1757,7 +1831,7 @@ class VMCard(Static):
|
|
|
1757
1831
|
|
|
1758
1832
|
except Exception as e:
|
|
1759
1833
|
self.app.vm_service.unsuppress_vm_events(internal_id)
|
|
1760
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
1834
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.ERROR_DELETING_VM_TEMPLATE.format(vm_name=vm_name, error=e))
|
|
1761
1835
|
finally:
|
|
1762
1836
|
self.app.call_from_thread(progress_modal.dismiss)
|
|
1763
1837
|
|
|
@@ -1812,8 +1886,8 @@ class VMCard(Static):
|
|
|
1812
1886
|
existing_vm_names.add(name)
|
|
1813
1887
|
|
|
1814
1888
|
except libvirt.libvirtError as e:
|
|
1815
|
-
log_callback(f"ERROR:
|
|
1816
|
-
app.call_from_thread(app.show_error_message,
|
|
1889
|
+
log_callback(f"ERROR: {ErrorMessages.ERROR_GETTING_EXISTING_VM_NAMES_TEMPLATE.format(error=e)}")
|
|
1890
|
+
app.call_from_thread(app.show_error_message, ErrorMessages.ERROR_GETTING_EXISTING_VM_NAMES_TEMPLATE.format(error=e))
|
|
1817
1891
|
app.call_from_thread(progress_modal.dismiss)
|
|
1818
1892
|
return
|
|
1819
1893
|
|
|
@@ -1852,11 +1926,11 @@ class VMCard(Static):
|
|
|
1852
1926
|
app.call_from_thread(lambda: progress_modal.query_one("#progress-bar").advance(1))
|
|
1853
1927
|
|
|
1854
1928
|
if success_clones:
|
|
1855
|
-
msg =
|
|
1929
|
+
msg = SuccessMessages.VM_CLONED.format(vm_names=', '.join(success_clones))
|
|
1856
1930
|
app.call_from_thread(app.show_success_message, msg)
|
|
1857
1931
|
log_callback(msg)
|
|
1858
1932
|
if failed_clones:
|
|
1859
|
-
msg =
|
|
1933
|
+
msg = ErrorMessages.VM_CLONE_FAILED_TEMPLATE.format(vm_names=', '.join(failed_clones))
|
|
1860
1934
|
app.call_from_thread(app.show_error_message, msg)
|
|
1861
1935
|
log_callback(f"ERROR: {msg}")
|
|
1862
1936
|
|
|
@@ -1895,13 +1969,13 @@ class VMCard(Static):
|
|
|
1895
1969
|
return
|
|
1896
1970
|
|
|
1897
1971
|
if was_modified:
|
|
1898
|
-
self.app.show_success_message(
|
|
1972
|
+
self.app.show_success_message(SuccessMessages.INPUT_SANITIZED.format(original_input=new_name_raw, sanitized_input=new_name))
|
|
1899
1973
|
|
|
1900
1974
|
if not new_name:
|
|
1901
|
-
self.app.show_error_message(
|
|
1975
|
+
self.app.show_error_message(ErrorMessages.VM_NAME_EMPTY_AFTER_SANITIZATION)
|
|
1902
1976
|
return
|
|
1903
1977
|
if new_name == self.name:
|
|
1904
|
-
self.app.show_success_message(
|
|
1978
|
+
self.app.show_success_message(SuccessMessages.VM_RENAME_NO_CHANGE)
|
|
1905
1979
|
return
|
|
1906
1980
|
|
|
1907
1981
|
def do_rename():
|
|
@@ -1909,26 +1983,23 @@ class VMCard(Static):
|
|
|
1909
1983
|
self.app.vm_service.suppress_vm_events(internal_id)
|
|
1910
1984
|
try:
|
|
1911
1985
|
rename_vm(self.vm, new_name)
|
|
1912
|
-
|
|
1913
|
-
self.app.show_success_message(msg)
|
|
1986
|
+
self.app.show_success_message(SuccessMessages.VM_RENAMED.format(old_name=self.name, new_name=new_name))
|
|
1914
1987
|
self.app.vm_service.invalidate_domain_cache()
|
|
1915
1988
|
self._boot_device_checked = False
|
|
1916
1989
|
self.app.refresh_vm_list()
|
|
1917
1990
|
logging.info(f"Successfully renamed VM '{self.name}' to '{new_name}'")
|
|
1918
1991
|
except Exception as e:
|
|
1919
|
-
self.app.show_error_message(
|
|
1992
|
+
self.app.show_error_message(ErrorMessages.ERROR_RENAMING_VM_TEMPLATE.format(vm_name=self.name, error=e))
|
|
1920
1993
|
finally:
|
|
1921
1994
|
self.app.vm_service.unsuppress_vm_events(internal_id)
|
|
1922
1995
|
|
|
1923
|
-
num_snapshots = self.vm.snapshotNum(0)
|
|
1924
|
-
|
|
1925
1996
|
def on_confirm_rename(confirmed: bool, delete_snapshots=False) -> None:
|
|
1926
1997
|
if confirmed:
|
|
1927
1998
|
do_rename()
|
|
1928
1999
|
if delete_snapshots:
|
|
1929
2000
|
self.app.set_timer(0.1, self._refresh_snapshot_tab_async)
|
|
1930
2001
|
else:
|
|
1931
|
-
self.app.show_success_message(
|
|
2002
|
+
self.app.show_success_message(SuccessMessages.VM_RENAME_CANCELLED)
|
|
1932
2003
|
|
|
1933
2004
|
msg = f"Are you sure you want to rename VM {self.name} to {new_name}?\n\nWarning: This operation involves undefining and redefining the VM."
|
|
1934
2005
|
self.app.push_screen(
|
|
@@ -1958,70 +2029,64 @@ class VMCard(Static):
|
|
|
1958
2029
|
def get_details_worker():
|
|
1959
2030
|
try:
|
|
1960
2031
|
result = self.app.vm_service.get_vm_details(
|
|
1961
|
-
active_uris,
|
|
1962
|
-
uuid,
|
|
1963
|
-
domain=vm_obj,
|
|
1964
|
-
conn=conn_obj,
|
|
2032
|
+
active_uris,
|
|
2033
|
+
uuid,
|
|
2034
|
+
domain=vm_obj,
|
|
2035
|
+
conn=conn_obj,
|
|
1965
2036
|
cached_ips=cached_ips
|
|
1966
2037
|
)
|
|
1967
2038
|
|
|
1968
2039
|
def show_details():
|
|
1969
2040
|
loading_modal.dismiss()
|
|
1970
2041
|
if not result:
|
|
1971
|
-
self.app.show_error_message(
|
|
2042
|
+
self.app.show_error_message(ErrorMessages.VM_NOT_FOUND_ON_ACTIVE_SERVER_TEMPLATE.format(vm_name=vm_name, uuid=uuid))
|
|
1972
2043
|
return
|
|
1973
2044
|
|
|
1974
2045
|
vm_info, domain, conn_for_domain = result
|
|
1975
2046
|
|
|
1976
|
-
def on_detail_modal_dismissed(
|
|
2047
|
+
def on_detail_modal_dismissed(_=None):
|
|
1977
2048
|
self.post_message(VmCardUpdateRequest(self.internal_id))
|
|
1978
2049
|
self._perform_tooltip_update()
|
|
1979
2050
|
|
|
1980
2051
|
self.app.push_screen(
|
|
1981
|
-
VMDetailModal(
|
|
2052
|
+
VMDetailModal(
|
|
2053
|
+
vm_name,
|
|
2054
|
+
vm_info,
|
|
2055
|
+
domain,
|
|
2056
|
+
conn_for_domain,
|
|
2057
|
+
self.app.vm_service.invalidate_vm_state_cache),
|
|
1982
2058
|
on_detail_modal_dismissed
|
|
1983
|
-
|
|
2059
|
+
)
|
|
1984
2060
|
|
|
1985
2061
|
self.app.call_from_thread(show_details)
|
|
1986
2062
|
|
|
1987
2063
|
except Exception as e:
|
|
1988
|
-
def show_error():
|
|
2064
|
+
def show_error(error_instance):
|
|
1989
2065
|
loading_modal.dismiss()
|
|
1990
|
-
self.app.show_error_message(
|
|
1991
|
-
self.app.call_from_thread(show_error)
|
|
2066
|
+
self.app.show_error_message(ErrorMessages.ERROR_GETTING_VM_DETAILS_TEMPLATE.format(vm_name=vm_name, error=error_instance))
|
|
2067
|
+
self.app.call_from_thread(show_error, e)
|
|
1992
2068
|
|
|
1993
2069
|
self.app.worker_manager.run(get_details_worker, name=f"get_details_{uuid}")
|
|
1994
2070
|
|
|
1995
2071
|
except Exception as e:
|
|
1996
|
-
self.app.show_error_message(
|
|
2072
|
+
self.app.show_error_message(ErrorMessages.ERROR_GETTING_ID_TEMPLATE.format(vm_name=self.name, error=e))
|
|
1997
2073
|
|
|
1998
2074
|
def _handle_migration_button(self, event: Button.Pressed) -> None:
|
|
1999
2075
|
"""Handles the migration button press."""
|
|
2000
2076
|
if len(self.app.active_uris) < 2:
|
|
2001
|
-
self.app.show_error_message(
|
|
2077
|
+
self.app.show_error_message(ErrorMessages.SELECT_AT_LEAST_TWO_SERVERS_FOR_MIGRATION)
|
|
2002
2078
|
return
|
|
2003
2079
|
|
|
2004
|
-
selected_vm_uuids = self.app.selected_vm_uuids
|
|
2080
|
+
selected_vm_uuids = list(self.app.selected_vm_uuids)
|
|
2005
2081
|
selected_vms = []
|
|
2006
2082
|
if selected_vm_uuids:
|
|
2083
|
+
found_domains_dict = self.app.vm_service.find_domains_by_uuids(self.app.active_uris, selected_vm_uuids)
|
|
2007
2084
|
for uuid in selected_vm_uuids:
|
|
2008
|
-
|
|
2009
|
-
with self.app.vm_service._cache_lock:
|
|
2010
|
-
domain = self.app.vm_service._domain_cache.get(uuid)
|
|
2011
|
-
|
|
2085
|
+
domain = found_domains_dict.get(uuid)
|
|
2012
2086
|
if domain:
|
|
2013
|
-
|
|
2014
|
-
# Verify domain is still valid
|
|
2015
|
-
domain.info()
|
|
2016
|
-
selected_vms.append(domain)
|
|
2017
|
-
found_domain = True
|
|
2018
|
-
except libvirt.libvirtError:
|
|
2019
|
-
found_domain = False
|
|
2087
|
+
selected_vms.append(domain)
|
|
2020
2088
|
else:
|
|
2021
|
-
|
|
2022
|
-
if not found_domain:
|
|
2023
|
-
self.app.show_error_message(f"Selected VM with ID [b]{uuid}[/b] not found on any active server.")
|
|
2024
|
-
|
|
2089
|
+
self.app.show_error_message(ErrorMessages.SELECTED_VM_NOT_FOUND_ON_ACTIVE_SERVER_TEMPLATE.format(uuid=uuid))
|
|
2025
2090
|
if not selected_vms:
|
|
2026
2091
|
selected_vms = [self.vm]
|
|
2027
2092
|
|
|
@@ -2037,7 +2102,7 @@ class VMCard(Static):
|
|
|
2037
2102
|
source_conns.add(uri)
|
|
2038
2103
|
|
|
2039
2104
|
if len(source_conns) > 1:
|
|
2040
|
-
self.app.show_error_message(
|
|
2105
|
+
self.app.show_error_message(ErrorMessages.DIFFERENT_SOURCE_HOSTS)
|
|
2041
2106
|
return
|
|
2042
2107
|
|
|
2043
2108
|
#active_vms = [vm for vm in selected_vms if vm.isActive()]
|
|
@@ -2051,14 +2116,14 @@ class VMCard(Static):
|
|
|
2051
2116
|
state, _ = state_tuple
|
|
2052
2117
|
if state in [libvirt.VIR_DOMAIN_RUNNING, libvirt.VIR_DOMAIN_PAUSED]:
|
|
2053
2118
|
active_vms.append(vm)
|
|
2054
|
-
except:
|
|
2119
|
+
except Exception:
|
|
2055
2120
|
# Fallback to isActive() if cache lookup fails
|
|
2056
2121
|
if vm.isActive():
|
|
2057
2122
|
active_vms.append(vm)
|
|
2058
2123
|
|
|
2059
2124
|
is_live = len(active_vms) > 0
|
|
2060
2125
|
if is_live and len(active_vms) < len(selected_vms):
|
|
2061
|
-
self.app.show_error_message(
|
|
2126
|
+
self.app.show_error_message(ErrorMessages.MIXED_VM_STATES)
|
|
2062
2127
|
return
|
|
2063
2128
|
|
|
2064
2129
|
active_uris = self.app.vm_service.get_all_uris()
|
|
@@ -2116,7 +2181,7 @@ class VMCard(Static):
|
|
|
2116
2181
|
try:
|
|
2117
2182
|
# Use vm_service to get XML (handles caching)
|
|
2118
2183
|
self.app.vm_service._get_domain_xml(self.vm, internal_id=self.internal_id)
|
|
2119
|
-
|
|
2184
|
+
|
|
2120
2185
|
# Update tooltip on main thread
|
|
2121
2186
|
self.app.call_from_thread(self._perform_tooltip_update)
|
|
2122
2187
|
self.app.call_from_thread(self.app.show_quick_message, f"Info refreshed for {self.name}")
|