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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Host Stats modals
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.widgets import Static, Label
|
|
8
|
+
from textual.reactive import reactive
|
|
9
|
+
from textual.events import Click, Message
|
|
10
|
+
from textual.worker import get_current_worker
|
|
11
|
+
|
|
12
|
+
from ..libvirt_utils import get_host_resources, get_active_vm_allocation
|
|
13
|
+
from ..utils import extract_server_name_from_uri
|
|
14
|
+
|
|
15
|
+
class SingleHostStat(Static):
|
|
16
|
+
"""
|
|
17
|
+
Displays stats for a single host.
|
|
18
|
+
"""
|
|
19
|
+
server_name = reactive("")
|
|
20
|
+
|
|
21
|
+
class ServerLabelClicked(Message):
|
|
22
|
+
"""Posted when the server label is clicked."""
|
|
23
|
+
def __init__(self, server_uri: str, server_name: str) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.server_uri = server_uri
|
|
26
|
+
self.server_name = server_name
|
|
27
|
+
|
|
28
|
+
DEFAULT_CSS = """
|
|
29
|
+
SingleHostStat {
|
|
30
|
+
layout: horizontal;
|
|
31
|
+
height: auto;
|
|
32
|
+
padding: 0 1;
|
|
33
|
+
background: $boost;
|
|
34
|
+
align-vertical: middle;
|
|
35
|
+
}
|
|
36
|
+
.stat-label {
|
|
37
|
+
color: $text;
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, uri: str, name: str, vm_service, server_color: str = "white"):
|
|
42
|
+
super().__init__()
|
|
43
|
+
self.uri = uri
|
|
44
|
+
self.server_name = name
|
|
45
|
+
self.vm_service = vm_service
|
|
46
|
+
self.server_color = server_color
|
|
47
|
+
self.server_label = Label("", id=f"single_host_stat_label_{self.server_name.replace(' ', '_').replace('.', '_')}")
|
|
48
|
+
self.cpu_label = Label("", classes="stat-label")
|
|
49
|
+
self.mem_label = Label("", classes="stat-label")
|
|
50
|
+
self.host_res = None
|
|
51
|
+
|
|
52
|
+
def compose(self) -> ComposeResult:
|
|
53
|
+
self.server_label.styles.color = self.server_color
|
|
54
|
+
self.server_label.styles.text_style = "bold"
|
|
55
|
+
self.server_label.update(f"{self.server_name} ")
|
|
56
|
+
yield self.server_label
|
|
57
|
+
yield self.cpu_label
|
|
58
|
+
yield Label(" ")
|
|
59
|
+
yield self.mem_label
|
|
60
|
+
|
|
61
|
+
def on_click(self, event: Click) -> None:
|
|
62
|
+
"""Called when the user clicks on a widget."""
|
|
63
|
+
if event.control.id == self.server_label.id:
|
|
64
|
+
self.post_message(self.ServerLabelClicked(self.uri, self.server_name))
|
|
65
|
+
|
|
66
|
+
def update_stats(self):
|
|
67
|
+
"""Fetches and updates stats for this host."""
|
|
68
|
+
def _fetch_and_update():
|
|
69
|
+
try:
|
|
70
|
+
# Check cancellation before potentially expensive op
|
|
71
|
+
try:
|
|
72
|
+
if get_current_worker().is_cancelled:
|
|
73
|
+
return
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
conn = self.vm_service.connect(self.uri)
|
|
78
|
+
if not conn:
|
|
79
|
+
if threading.current_thread() is threading.main_thread():
|
|
80
|
+
self.cpu_label.update("Offline")
|
|
81
|
+
self.mem_label.update("Offline")
|
|
82
|
+
else:
|
|
83
|
+
self.app.call_from_thread(self.cpu_label.update, "Offline")
|
|
84
|
+
self.app.call_from_thread(self.mem_label.update, "Offline")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if self.host_res is None:
|
|
88
|
+
self.host_res = get_host_resources(conn)
|
|
89
|
+
|
|
90
|
+
current_alloc = get_active_vm_allocation(conn)
|
|
91
|
+
|
|
92
|
+
# Check cancellation again after expensive op
|
|
93
|
+
try:
|
|
94
|
+
if get_current_worker().is_cancelled:
|
|
95
|
+
return
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
total_cpus = self.host_res.get('total_cpus', 1)
|
|
100
|
+
total_mem = self.host_res.get('available_memory', 1) # MB
|
|
101
|
+
|
|
102
|
+
used_cpus = current_alloc.get('active_allocated_vcpus', 0)
|
|
103
|
+
used_mem = current_alloc.get('active_allocated_memory', 0) # MB
|
|
104
|
+
|
|
105
|
+
cpu_pct = (used_cpus / total_cpus) * 100
|
|
106
|
+
mem_pct = (used_mem / total_mem) * 100
|
|
107
|
+
# Format memory string (GB if > 1024 MB)
|
|
108
|
+
def fmt_mem(mb):
|
|
109
|
+
if mb >= 1024:
|
|
110
|
+
return f"{mb/1024:.1f}G"
|
|
111
|
+
return f"{mb}M"
|
|
112
|
+
|
|
113
|
+
# UI Updates need to be on main thread
|
|
114
|
+
def _update_ui():
|
|
115
|
+
def get_status_bck(pct):
|
|
116
|
+
if pct >= 90:
|
|
117
|
+
return ("red")
|
|
118
|
+
if pct >= 75:
|
|
119
|
+
return ("orange")
|
|
120
|
+
if pct >= 55:
|
|
121
|
+
return ("yellow")
|
|
122
|
+
return ("green")
|
|
123
|
+
|
|
124
|
+
self.cpu_label.update(f"{used_cpus}/{total_cpus}CPU")
|
|
125
|
+
self.cpu_label.styles.background = get_status_bck(cpu_pct)
|
|
126
|
+
|
|
127
|
+
self.mem_label.update(f"{fmt_mem(used_mem)}/{fmt_mem(total_mem)}")
|
|
128
|
+
self.mem_label.styles.background = get_status_bck(mem_pct)
|
|
129
|
+
|
|
130
|
+
if threading.current_thread() is threading.main_thread():
|
|
131
|
+
_update_ui()
|
|
132
|
+
else:
|
|
133
|
+
self.app.call_from_thread(_update_ui)
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logging.error(f"Error updating host stats for {self.name}: {e}")
|
|
137
|
+
if threading.current_thread() is threading.main_thread():
|
|
138
|
+
self.cpu_label.update("Err")
|
|
139
|
+
self.mem_label.update("Err")
|
|
140
|
+
else:
|
|
141
|
+
self.app.call_from_thread(self.cpu_label.update, "Err")
|
|
142
|
+
self.app.call_from_thread(self.mem_label.update, "Err")
|
|
143
|
+
|
|
144
|
+
_fetch_and_update()
|
|
145
|
+
|
|
146
|
+
class HostStats(Static):
|
|
147
|
+
"""
|
|
148
|
+
Container for multiple SingleHostStat widgets.
|
|
149
|
+
"""
|
|
150
|
+
DEFAULT_CSS = """
|
|
151
|
+
HostStats {
|
|
152
|
+
layout: grid;
|
|
153
|
+
grid-size: 3;
|
|
154
|
+
overflow-y: auto;
|
|
155
|
+
margin-bottom: 0;
|
|
156
|
+
margin-top: 0;
|
|
157
|
+
display: none;
|
|
158
|
+
}
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, vm_service, get_server_color_callback):
|
|
162
|
+
super().__init__()
|
|
163
|
+
self.vm_service = vm_service
|
|
164
|
+
self.get_server_color = get_server_color_callback
|
|
165
|
+
self.active_hosts = {}
|
|
166
|
+
|
|
167
|
+
def update_hosts(self, active_uris, servers):
|
|
168
|
+
"""
|
|
169
|
+
Reconciles the list of active hosts.
|
|
170
|
+
"""
|
|
171
|
+
current_uris = set(active_uris)
|
|
172
|
+
existing_uris = set(self.active_hosts.keys())
|
|
173
|
+
|
|
174
|
+
# Remove stale
|
|
175
|
+
for uri in existing_uris - current_uris:
|
|
176
|
+
widget = self.active_hosts.pop(uri)
|
|
177
|
+
widget.remove()
|
|
178
|
+
|
|
179
|
+
# Add new hosts
|
|
180
|
+
for uri in current_uris - existing_uris:
|
|
181
|
+
name = self._get_server_name(uri, servers)
|
|
182
|
+
color = self.get_server_color(uri)
|
|
183
|
+
widget = SingleHostStat(uri, name, self.vm_service, color)
|
|
184
|
+
self.active_hosts[uri] = widget
|
|
185
|
+
self.mount(widget)
|
|
186
|
+
self.app.set_timer(0.5, widget.update_stats)
|
|
187
|
+
|
|
188
|
+
def _get_server_name(self, uri: str, servers) -> str:
|
|
189
|
+
"""Helper to get server name from URI."""
|
|
190
|
+
if servers:
|
|
191
|
+
for s in servers:
|
|
192
|
+
if s['uri'] == uri:
|
|
193
|
+
return s.get('name', extract_server_name_from_uri(uri))
|
|
194
|
+
return extract_server_name_from_uri(uri)
|
|
195
|
+
|
|
196
|
+
def refresh_stats(self):
|
|
197
|
+
"""Triggers update on all children."""
|
|
198
|
+
for widget in self.active_hosts.values():
|
|
199
|
+
widget.update_stats()
|
|
@@ -8,6 +8,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
|
8
8
|
from textual.widgets import Button, Markdown
|
|
9
9
|
from textual import on
|
|
10
10
|
from .base_modals import BaseModal
|
|
11
|
+
from ..constants import ButtonLabels
|
|
11
12
|
|
|
12
13
|
class HowToDiskModal(BaseModal[None]):
|
|
13
14
|
"""A modal to display instructions for managing VM disks."""
|
|
@@ -25,7 +26,7 @@ class HowToDiskModal(BaseModal[None]):
|
|
|
25
26
|
with ScrollableContainer(id="howto-disk-content"):
|
|
26
27
|
yield Markdown(content, id="howto-disk-markdown")
|
|
27
28
|
with Horizontal(id="dialog-buttons"):
|
|
28
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
29
30
|
|
|
30
31
|
@on(Button.Pressed)
|
|
31
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -7,6 +7,7 @@ from textual.app import ComposeResult
|
|
|
7
7
|
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
8
8
|
from textual.widgets import Button, Markdown
|
|
9
9
|
from textual import on
|
|
10
|
+
from ..constants import ButtonLabels
|
|
10
11
|
from .base_modals import BaseModal
|
|
11
12
|
|
|
12
13
|
class HowToNetworkModal(BaseModal[None]):
|
|
@@ -25,7 +26,7 @@ class HowToNetworkModal(BaseModal[None]):
|
|
|
25
26
|
with ScrollableContainer(id="howto-network-content"):
|
|
26
27
|
yield Markdown(content, id="howto-network-markdown")
|
|
27
28
|
with Horizontal(id="dialog-buttons"):
|
|
28
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
29
30
|
|
|
30
31
|
@on(Button.Pressed)
|
|
31
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -8,6 +8,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
|
8
8
|
from textual.widgets import Button, Markdown
|
|
9
9
|
from textual import on
|
|
10
10
|
from .base_modals import BaseModal
|
|
11
|
+
from ..constants import ButtonLabels
|
|
11
12
|
|
|
12
13
|
class HowToOverlayModal(BaseModal[None]):
|
|
13
14
|
"""A modal to display instructions for disk overlays."""
|
|
@@ -25,7 +26,7 @@ class HowToOverlayModal(BaseModal[None]):
|
|
|
25
26
|
with ScrollableContainer(id="howto-overlay-content"):
|
|
26
27
|
yield Markdown(content, id="howto-overlay-markdown")
|
|
27
28
|
with Horizontal(id="dialog-buttons"):
|
|
28
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
29
30
|
|
|
30
31
|
@on(Button.Pressed)
|
|
31
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -7,6 +7,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
|
7
7
|
from textual.widgets import Button, Markdown
|
|
8
8
|
from textual import on
|
|
9
9
|
from .base_modals import BaseModal
|
|
10
|
+
from ..constants import ButtonLabels
|
|
10
11
|
|
|
11
12
|
class HowToSSHModal(BaseModal[None]):
|
|
12
13
|
"""A modal to display instructions for using an ssh-agent."""
|
|
@@ -24,7 +25,7 @@ class HowToSSHModal(BaseModal[None]):
|
|
|
24
25
|
with ScrollableContainer(id="howto-ssh-content"):
|
|
25
26
|
yield Markdown(content, id="howto-ssh-markdown")
|
|
26
27
|
with Horizontal(id="dialog-buttons"):
|
|
27
|
-
yield Button(
|
|
28
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
28
29
|
|
|
29
30
|
@on(Button.Pressed)
|
|
30
31
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -8,6 +8,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
|
8
8
|
from textual.widgets import Button, Markdown
|
|
9
9
|
from textual import on
|
|
10
10
|
from .base_modals import BaseModal
|
|
11
|
+
from ..constants import ButtonLabels
|
|
11
12
|
|
|
12
13
|
class HowToVirtIOFSModal(BaseModal[None]):
|
|
13
14
|
"""A modal to display instructions for using VirtIO-FS."""
|
|
@@ -25,7 +26,7 @@ class HowToVirtIOFSModal(BaseModal[None]):
|
|
|
25
26
|
with ScrollableContainer(id="howto-virtiofs-content"):
|
|
26
27
|
yield Markdown(content, id="howto-virtiofs-markdown")
|
|
27
28
|
with Horizontal(id="dialog-buttons"):
|
|
28
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
29
30
|
|
|
30
31
|
@on(Button.Pressed)
|
|
31
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
vmanager/modals/input_modals.py
CHANGED
|
@@ -7,6 +7,7 @@ from textual.app import ComposeResult
|
|
|
7
7
|
from textual.containers import Vertical, Horizontal
|
|
8
8
|
from textual import on
|
|
9
9
|
from .base_modals import BaseModal
|
|
10
|
+
from ..constants import ButtonLabels, StaticText
|
|
10
11
|
|
|
11
12
|
class InputModal(BaseModal[str | None]):
|
|
12
13
|
"""A generic modal for getting text input from the user."""
|
|
@@ -21,8 +22,8 @@ class InputModal(BaseModal[str | None]):
|
|
|
21
22
|
yield Label(self.prompt)
|
|
22
23
|
yield Input(value=self.initial_value, id="text-input", restrict=self.restrict)
|
|
23
24
|
with Horizontal():
|
|
24
|
-
yield Button(
|
|
25
|
-
yield Button(
|
|
25
|
+
yield Button(ButtonLabels.OK, variant="primary", id="ok-btn")
|
|
26
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
26
27
|
|
|
27
28
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
28
29
|
if event.button.id == "ok-btn":
|
|
@@ -40,7 +41,7 @@ class AddInputDeviceModal(BaseModal[None]):
|
|
|
40
41
|
|
|
41
42
|
def compose(self) -> ComposeResult:
|
|
42
43
|
with Vertical(id="add-input-container"):
|
|
43
|
-
yield Label(
|
|
44
|
+
yield Label(StaticText.INPUT_DEVICE)
|
|
44
45
|
yield Select(
|
|
45
46
|
[(t, t) for t in self.available_types],
|
|
46
47
|
prompt="Input Type",
|
|
@@ -53,8 +54,8 @@ class AddInputDeviceModal(BaseModal[None]):
|
|
|
53
54
|
)
|
|
54
55
|
with Vertical():
|
|
55
56
|
with Horizontal():
|
|
56
|
-
yield Button(
|
|
57
|
-
yield Button(
|
|
57
|
+
yield Button(ButtonLabels.ADD, variant="primary", id="add-input", disabled=True)
|
|
58
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-input")
|
|
58
59
|
|
|
59
60
|
@on(Select.Changed)
|
|
60
61
|
def on_select_changed(self) -> None:
|
|
@@ -82,26 +83,26 @@ class AddChannelModal(BaseModal[dict | None]):
|
|
|
82
83
|
|
|
83
84
|
def compose(self) -> ComposeResult:
|
|
84
85
|
with Vertical(id="add-channel-container"):
|
|
85
|
-
yield Label(
|
|
86
|
+
yield Label(StaticText.ADD_CHANNEL_DEVICE)
|
|
86
87
|
yield Select(
|
|
87
88
|
[("unix", "unix"), ("virtio", "virtio"), ("spicevmc", "spicevmc")],
|
|
88
89
|
prompt="Channel Type",
|
|
89
90
|
id="channel-type-select",
|
|
90
91
|
value="unix"
|
|
91
92
|
)
|
|
92
|
-
yield Label(
|
|
93
|
+
yield Label(StaticText.STANDARD_TARGET_NAMES)
|
|
93
94
|
yield Select(
|
|
94
95
|
[],
|
|
95
96
|
id="target-preset-select",
|
|
96
97
|
prompt="Select a standard target or type below",
|
|
97
98
|
value=Select.BLANK
|
|
98
99
|
)
|
|
99
|
-
yield Label(
|
|
100
|
+
yield Label(StaticText.TARGET_NAME)
|
|
100
101
|
yield Input(placeholder="Target Name (e.g. org.qemu.guest_agent.0)", id="target-name-input")
|
|
101
102
|
|
|
102
103
|
with Horizontal():
|
|
103
|
-
yield Button(
|
|
104
|
-
yield Button(
|
|
104
|
+
yield Button(ButtonLabels.ADD, variant="primary", id="add-channel-btn")
|
|
105
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-channel-btn")
|
|
105
106
|
|
|
106
107
|
def on_mount(self) -> None:
|
|
107
108
|
# Initialize presets for default type (unix)
|
vmanager/modals/log_modal.py
CHANGED
|
@@ -6,6 +6,7 @@ from textual.widgets import Button, Label, TextArea
|
|
|
6
6
|
from textual.containers import Vertical, Horizontal
|
|
7
7
|
|
|
8
8
|
from .base_modals import BaseModal
|
|
9
|
+
from ..constants import ButtonLabels
|
|
9
10
|
|
|
10
11
|
class LogModal(BaseModal[None]):
|
|
11
12
|
""" Modal Screen to show Log"""
|
|
@@ -22,7 +23,7 @@ class LogModal(BaseModal[None]):
|
|
|
22
23
|
text_area.load_text(self.log_content)
|
|
23
24
|
yield text_area
|
|
24
25
|
with Horizontal():
|
|
25
|
-
yield Button(
|
|
26
|
+
yield Button(ButtonLabels.CLOSE, variant="default", id="cancel-btn", classes="Buttonpage")
|
|
26
27
|
|
|
27
28
|
def on_mount(self) -> None:
|
|
28
29
|
"""Called when the modal is mounted."""
|
|
@@ -11,6 +11,7 @@ from textual.screen import ModalScreen
|
|
|
11
11
|
from textual.widgets import Button, Static, Select, Checkbox, Label, ProgressBar
|
|
12
12
|
from textual import on, work
|
|
13
13
|
|
|
14
|
+
from ..constants import ErrorMessages, StaticText, ButtonLabels
|
|
14
15
|
from ..vm_actions import check_server_migration_compatibility, check_vm_migration_compatibility
|
|
15
16
|
from ..storage_manager import find_shared_storage_pools
|
|
16
17
|
from ..utils import extract_server_name_from_uri
|
|
@@ -74,38 +75,39 @@ class MigrationModal(ModalScreen):
|
|
|
74
75
|
|
|
75
76
|
with Vertical(id="migration-dialog",):
|
|
76
77
|
with Vertical(id="migration-content-wrapper"):
|
|
77
|
-
yield Label(
|
|
78
|
-
yield Static(
|
|
78
|
+
yield Label(StaticText.MIGRATE_VMS_TITLE.format(migration_type=migration_type, vm_names=vm_names))
|
|
79
|
+
yield Static(StaticText.SELECT_DESTINATION_SERVER)
|
|
79
80
|
yield Select(dest_servers, id="dest-server-select", prompt="Destination...", value=default_dest_uri, allow_blank=False)
|
|
80
81
|
|
|
81
|
-
yield Static(
|
|
82
|
+
yield Static(StaticText.MIGRATION_OPTIONS)
|
|
82
83
|
with Horizontal(classes="checkbox-container"):
|
|
83
|
-
yield Checkbox(
|
|
84
|
-
yield Checkbox(
|
|
85
|
-
yield Checkbox(
|
|
84
|
+
yield Checkbox(StaticText.COPY_STORAGE_ALL, id="copy-storage-all", tooltip="Copy all disk files during migration", value=False)
|
|
85
|
+
yield Checkbox(StaticText.UNSAFE_MIGRATION, id="unsafe", tooltip="Perform unsafe migration (may lose data)", disabled=not self.is_live)
|
|
86
|
+
yield Checkbox(StaticText.PERSISTENT_MIGRATION, id="persistent", tooltip="Keep VM persistent on destination", value=True)
|
|
86
87
|
with Horizontal(classes="checkbox-container"):
|
|
87
|
-
yield Checkbox(
|
|
88
|
-
yield Checkbox(
|
|
89
|
-
yield Checkbox(
|
|
90
|
-
yield Static(
|
|
88
|
+
yield Checkbox(StaticText.COMPRESS_DATA, id="compress", tooltip="Compress data during migration", disabled=not self.is_live)
|
|
89
|
+
yield Checkbox(StaticText.TUNNELLED_MIGRATION, id="tunnelled", tooltip="Tunnel migration data through libvirt daemon", disabled=not self.is_live)
|
|
90
|
+
yield Checkbox(StaticText.CUSTOM_MIGRATION, id="custom", tooltip="Use custom migration workflow", value=False)
|
|
91
|
+
yield Static(StaticText.COMPATIBILITY_CHECK_RESULTS)
|
|
91
92
|
yield ProgressBar(total=100, show_eta=False, id="migration-progress")
|
|
92
93
|
yield Static(id="results-log")
|
|
93
94
|
yield Grid(
|
|
94
95
|
ScrollableContainer(
|
|
95
|
-
Static(
|
|
96
|
+
Static(StaticText.VMS_READY_FOR_MIGRATION, classes="summary-title"),
|
|
96
97
|
Static(id="can-migrate-list"),
|
|
97
98
|
),
|
|
98
99
|
ScrollableContainer(
|
|
99
|
-
Static(
|
|
100
|
+
Static(StaticText.VMS_NOT_READY_FOR_MIGRATION, classes="summary-title"),
|
|
100
101
|
Static(id="cannot-migrate-list"),
|
|
101
102
|
),
|
|
102
103
|
id="migration-summary-grid"
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
with Horizontal(classes="modal-buttons"):
|
|
106
|
-
yield Button(
|
|
107
|
-
yield Button(
|
|
108
|
-
yield Button(
|
|
107
|
+
yield Button(ButtonLabels.CHECK_COMPATIBILITY, variant="primary", id="check", classes="Buttonpage")
|
|
108
|
+
yield Button(ButtonLabels.START_MIGRATION, variant="success", id="start", disabled=True, classes="Buttonpage")
|
|
109
|
+
yield Button(ButtonLabels.CLOSE, variant="default", id="close", disabled=False, classes="close-button")
|
|
110
|
+
|
|
109
111
|
|
|
110
112
|
def _lock_controls(self, lock: bool):
|
|
111
113
|
self.query_one("#check").disabled = lock
|
|
@@ -385,7 +387,7 @@ class MigrationModal(ModalScreen):
|
|
|
385
387
|
|
|
386
388
|
write_log("\n[bold]--- Migration process finished ---[/]")
|
|
387
389
|
self.app.call_from_thread(lambda: setattr(progress_bar.styles, "display", "none"))
|
|
388
|
-
self.app.call_from_thread(self.app.refresh_vm_list)
|
|
390
|
+
self.app.call_from_thread(self.app.refresh_vm_list, force=True)
|
|
389
391
|
self.app.call_from_thread(final_ui_state)
|
|
390
392
|
|
|
391
393
|
@on(Checkbox.Changed, "#custom")
|
|
@@ -403,17 +405,17 @@ class MigrationModal(ModalScreen):
|
|
|
403
405
|
def on_button_pressed(self, event: Button.Pressed):
|
|
404
406
|
if event.button.id == "check":
|
|
405
407
|
if not self.dest_conn:
|
|
406
|
-
self.app.show_error_message(
|
|
408
|
+
self.app.show_error_message(ErrorMessages.SELECT_DESTINATION_SERVER)
|
|
407
409
|
return
|
|
408
410
|
self._clear_log()
|
|
409
411
|
self.run_compatibility_checks()
|
|
410
412
|
|
|
411
413
|
elif event.button.id == "start":
|
|
412
414
|
if not self.compatibility_checked:
|
|
413
|
-
self.app.show_error_message(
|
|
415
|
+
self.app.show_error_message(ErrorMessages.RUN_COMPATIBILITY_CHECK_FIRST)
|
|
414
416
|
return
|
|
415
417
|
if not self.checks_passed:
|
|
416
|
-
self.app.show_error_message(
|
|
418
|
+
self.app.show_error_message(ErrorMessages.MIGRATION_COMPATIBILITY_ERRORS)
|
|
417
419
|
return
|
|
418
420
|
|
|
419
421
|
self._clear_log()
|