virtui-manager 1.1.5__py3-none-any.whl → 1.3.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.5.dist-info → virtui_manager-1.3.0.dist-info}/METADATA +1 -1
- virtui_manager-1.3.0.dist-info/RECORD +73 -0
- vmanager/constants.py +737 -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.po +3012 -0
- vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
- vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3124 -0
- vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3012 -0
- vmanager/locales/virtui-manager.pot +3012 -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/howto_disk_modal.py +13 -72
- vmanager/modals/howto_network_modal.py +13 -39
- vmanager/modals/howto_overlay_modal.py +13 -52
- vmanager/modals/howto_ssh_modal.py +12 -67
- vmanager/modals/howto_virtiofs_modal.py +13 -64
- vmanager/modals/input_modals.py +11 -10
- vmanager/modals/log_modal.py +2 -1
- vmanager/modals/migration_modals.py +20 -18
- 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 +103 -87
- 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 +182 -42
- vmanager/utils.py +39 -6
- vmanager/vm_actions.py +28 -24
- vmanager/vm_queries.py +67 -25
- vmanager/vm_service.py +8 -5
- vmanager/vmanager.css +46 -0
- vmanager/vmanager.py +178 -112
- vmanager/vmcard.py +161 -159
- vmanager/webconsole_manager.py +21 -21
- virtui_manager-1.1.5.dist-info/RECORD +0 -65
- {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/WHEEL +0 -0
- {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/entry_points.txt +0 -0
- {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -8,6 +8,7 @@ from textual.widgets.text_area import LanguageDoesNotExist
|
|
|
8
8
|
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
9
9
|
from textual import on
|
|
10
10
|
|
|
11
|
+
from ..constants import ErrorMessages, SuccessMessages, ButtonLabels, StaticText
|
|
11
12
|
from .base_modals import BaseModal, BaseDialog
|
|
12
13
|
from ..network_manager import (
|
|
13
14
|
create_network, get_host_network_interfaces, get_existing_subnets
|
|
@@ -35,7 +36,7 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
|
|
|
35
36
|
if self.is_edit and self.interface_info:
|
|
36
37
|
network_value = self.interface_info.get("network")
|
|
37
38
|
if network_value not in self.networks:
|
|
38
|
-
self.app.show_error_message(
|
|
39
|
+
self.app.show_error_message(ErrorMessages.NETWORK_NOT_FOUND_TEMPLATE.format(network=network_value))
|
|
39
40
|
network_value = self.networks[0] if self.networks else None # Set to first available network if any, otherwise None
|
|
40
41
|
model_value = self.interface_info.get("model", "virtio")
|
|
41
42
|
mac_value = self.interface_info.get("mac", "")
|
|
@@ -43,7 +44,7 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
|
|
|
43
44
|
network_value = self.networks[0]
|
|
44
45
|
|
|
45
46
|
with Vertical(id="add-edit-network-dialog"):
|
|
46
|
-
yield Label(
|
|
47
|
+
yield Label(StaticText.SELECT_NETWORK_AND_MODEL)
|
|
47
48
|
|
|
48
49
|
if self.networks:
|
|
49
50
|
yield Select(network_options, id="network-select", prompt="Select a network", value=network_value)
|
|
@@ -60,8 +61,8 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
|
|
|
60
61
|
)
|
|
61
62
|
|
|
62
63
|
with Horizontal(id="dialog-buttons"):
|
|
63
|
-
yield Button(
|
|
64
|
-
yield Button(
|
|
64
|
+
yield Button(ButtonLabels.SAVE if self.is_edit else ButtonLabels.ADD, variant="success", id="save")
|
|
65
|
+
yield Button(ButtonLabels.CANCEL, variant="error", id="cancel")
|
|
65
66
|
|
|
66
67
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
67
68
|
if event.button.id == "save":
|
|
@@ -72,7 +73,7 @@ class AddEditNetworkInterfaceModal(BaseDialog[dict | None]):
|
|
|
72
73
|
new_model = model_select.value
|
|
73
74
|
|
|
74
75
|
if new_network is Select.BLANK:
|
|
75
|
-
self.app.show_error_message(
|
|
76
|
+
self.app.show_error_message(ErrorMessages.SELECT_NETWORK)
|
|
76
77
|
return
|
|
77
78
|
|
|
78
79
|
result = {"network": new_network, "model": new_model}
|
|
@@ -94,7 +95,7 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
94
95
|
|
|
95
96
|
def compose(self) -> ComposeResult:
|
|
96
97
|
title = "Edit Network" if self.is_edit else "Create New Network"
|
|
97
|
-
button_label =
|
|
98
|
+
button_label = ButtonLabels.SAVE_CHANGES if self.is_edit else ButtonLabels.CREATE_NETWORK
|
|
98
99
|
|
|
99
100
|
name_val = ""
|
|
100
101
|
forward_mode = "nat"
|
|
@@ -137,7 +138,6 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
137
138
|
dhcp_end_val = "192.168.11.30"
|
|
138
139
|
|
|
139
140
|
|
|
140
|
-
|
|
141
141
|
with Vertical(id="create-network-dialog"):
|
|
142
142
|
yield Label(title, id="create-network-title")
|
|
143
143
|
|
|
@@ -150,8 +150,8 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
150
150
|
disabled=self.is_edit
|
|
151
151
|
)
|
|
152
152
|
with RadioSet(id="type-network", classes="type-network-radioset"):
|
|
153
|
-
yield RadioButton(
|
|
154
|
-
yield RadioButton(
|
|
153
|
+
yield RadioButton(StaticText.NAT_NETWORK, id="type-network-nat", value=(forward_mode == "nat"))
|
|
154
|
+
yield RadioButton(StaticText.ROUTED_NETWORK, id="type-network-routed", value=(forward_mode == "route"))
|
|
155
155
|
yield Select(
|
|
156
156
|
[("Loading...", "")],
|
|
157
157
|
prompt="Select Forward Interface",
|
|
@@ -160,33 +160,33 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
160
160
|
disabled=True
|
|
161
161
|
)
|
|
162
162
|
yield Input(
|
|
163
|
-
placeholder=
|
|
163
|
+
placeholder=StaticText.IPV4_NETWORK_EXAMPLE, id="net-ip-input", value=ip_val
|
|
164
164
|
)
|
|
165
|
-
yield Checkbox(
|
|
165
|
+
yield Checkbox(StaticText.ENABLE_DHCPV4, id="dhcp-checkbox", value=dhcp_val)
|
|
166
166
|
with Vertical(id="dhcp-inputs-horizontal"):
|
|
167
167
|
dhcp_options_classes = "" if dhcp_val else "hidden"
|
|
168
168
|
with Horizontal(id="dhcp-options", classes=dhcp_options_classes):
|
|
169
169
|
yield Input(
|
|
170
|
-
placeholder=
|
|
170
|
+
placeholder=StaticText.DHCP_START_EXAMPLE,
|
|
171
171
|
id="dhcp-start-input",
|
|
172
172
|
classes="dhcp-input",
|
|
173
173
|
value=dhcp_start_val
|
|
174
174
|
)
|
|
175
175
|
yield Input(
|
|
176
|
-
placeholder=
|
|
176
|
+
placeholder=StaticText.DHCP_END_EXAMPLE,
|
|
177
177
|
id="dhcp-end-input",
|
|
178
178
|
classes="dhcp-input",
|
|
179
179
|
value=dhcp_end_val
|
|
180
180
|
)
|
|
181
181
|
with RadioSet(id="dns-domain-radioset", classes="dns-domain-radioset"):
|
|
182
182
|
yield RadioButton(
|
|
183
|
-
|
|
183
|
+
StaticText.USE_NETWORK_NAME_FOR_DNS, id="dns-use-net-name", value=not use_custom_domain
|
|
184
184
|
)
|
|
185
|
-
yield RadioButton(
|
|
185
|
+
yield RadioButton(StaticText.USE_CUSTOM_DNS_DOMAIN, id="dns-use-custom", value=use_custom_domain)
|
|
186
186
|
|
|
187
187
|
custom_domain_classes = "hidden" if not use_custom_domain else ""
|
|
188
188
|
yield Input(
|
|
189
|
-
placeholder=
|
|
189
|
+
placeholder=StaticText.CUSTOM_DNS_DOMAIN,
|
|
190
190
|
id="dns-custom-domain-input",
|
|
191
191
|
value=domain_name,
|
|
192
192
|
classes=custom_domain_classes
|
|
@@ -195,9 +195,9 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
195
195
|
with Horizontal(classes="action-buttons"):
|
|
196
196
|
yield Button(
|
|
197
197
|
button_label, variant="primary", id="create-net-btn", classes="create-net-btn"
|
|
198
|
-
|
|
199
|
-
yield Button(
|
|
200
|
-
|
|
198
|
+
)
|
|
199
|
+
yield Button(ButtonLabels.CLOSE, variant="default", id="close-btn", classes="close-button")
|
|
200
|
+
|
|
201
201
|
def on_mount(self) -> None:
|
|
202
202
|
"""Called when the modal is mounted to populate network interfaces."""
|
|
203
203
|
self.run_worker(self.populate_interfaces, thread=True)
|
|
@@ -225,7 +225,7 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
225
225
|
else:
|
|
226
226
|
select.clear()
|
|
227
227
|
self.app.show_error_message(
|
|
228
|
-
|
|
228
|
+
ErrorMessages.FORWARD_DEVICE_NOT_FOUND_TEMPLATE.format(device=forward_dev)
|
|
229
229
|
)
|
|
230
230
|
else:
|
|
231
231
|
select.clear()
|
|
@@ -234,7 +234,7 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
234
234
|
except Exception as e:
|
|
235
235
|
self.app.call_from_thread(
|
|
236
236
|
self.app.show_error_message,
|
|
237
|
-
|
|
237
|
+
ErrorMessages.ERROR_GETTING_HOST_INTERFACES_TEMPLATE.format(error=e)
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
@on(Checkbox.Changed, "#dhcp-checkbox")
|
|
@@ -270,13 +270,13 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
270
270
|
dhcp_end = self.query_one("#dhcp-end-input", Input).value
|
|
271
271
|
|
|
272
272
|
domain_radio = self.query_one("#dns-domain-radioset", RadioSet).pressed_button.id
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
try:
|
|
275
275
|
name, name_modified = _sanitize_input(name_raw)
|
|
276
276
|
if name_modified:
|
|
277
|
-
self.app.show_success_message(
|
|
277
|
+
self.app.show_success_message(SuccessMessages.INPUT_SANITIZED.format(original_input=name_raw, sanitized_input=name))
|
|
278
278
|
except ValueError as e:
|
|
279
|
-
self.app.show_error_message(
|
|
279
|
+
self.app.show_error_message(ErrorMessages.INVALID_NETWORK_NAME_TEMPLATE.format(error=e))
|
|
280
280
|
return
|
|
281
281
|
|
|
282
282
|
domain_name_raw = self.query_one("#dns-custom-domain-input", Input).value
|
|
@@ -285,13 +285,13 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
285
285
|
try:
|
|
286
286
|
domain_name, domain_name_modified = _sanitize_domain_name(domain_name_raw)
|
|
287
287
|
if domain_name_modified:
|
|
288
|
-
self.app.show_success_message(
|
|
288
|
+
self.app.show_success_message(SuccessMessages.INPUT_SANITIZED.format(original_input=domain_name_raw, sanitized_input=domain_name))
|
|
289
289
|
except ValueError as e:
|
|
290
|
-
self.app.show_error_message(
|
|
290
|
+
self.app.show_error_message(ErrorMessages.INVALID_CUSTOM_DNS_DOMAIN_TEMPLATE.format(error=e))
|
|
291
291
|
return
|
|
292
292
|
|
|
293
293
|
if not name:
|
|
294
|
-
self.app.show_error_message(
|
|
294
|
+
self.app.show_error_message(ErrorMessages.NETWORK_NAME_REQUIRED)
|
|
295
295
|
return
|
|
296
296
|
|
|
297
297
|
if ip:
|
|
@@ -302,16 +302,16 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
302
302
|
dhcp_start_ip = ipaddress.ip_address(dhcp_start)
|
|
303
303
|
dhcp_end_ip = ipaddress.ip_address(dhcp_end)
|
|
304
304
|
if dhcp_start_ip not in ip_network or dhcp_end_ip not in ip_network:
|
|
305
|
-
self.app.show_error_message(
|
|
305
|
+
self.app.show_error_message(ErrorMessages.DHCP_IPS_NOT_IN_NETWORK_TEMPLATE.format(network=ip_network))
|
|
306
306
|
return
|
|
307
307
|
if dhcp_start_ip >= dhcp_end_ip:
|
|
308
|
-
self.app.show_error_message(
|
|
308
|
+
self.app.show_error_message(ErrorMessages.DHCP_START_BEFORE_END)
|
|
309
309
|
return
|
|
310
310
|
except ValueError as e:
|
|
311
|
-
self.app.show_error_message(
|
|
311
|
+
self.app.show_error_message(ErrorMessages.INVALID_IP_OR_NETWORK_TEMPLATE.format(error=e))
|
|
312
312
|
return
|
|
313
313
|
elif dhcp:
|
|
314
|
-
self.app.show_error_message(
|
|
314
|
+
self.app.show_error_message(ErrorMessages.DHCP_REQUIRES_IP)
|
|
315
315
|
return
|
|
316
316
|
|
|
317
317
|
def do_create_or_update_network():
|
|
@@ -344,20 +344,29 @@ class AddEditNetworkModal(BaseModal[None]):
|
|
|
344
344
|
if ip_network.overlaps(existing_subnet):
|
|
345
345
|
self.app.call_from_thread(
|
|
346
346
|
self.app.show_error_message,
|
|
347
|
-
|
|
347
|
+
ErrorMessages.SUBNET_OVERLAPS_TEMPLATE.format(subnet=ip_network)
|
|
348
348
|
)
|
|
349
349
|
return
|
|
350
350
|
|
|
351
351
|
uuid = self.network_info.get('uuid') if self.is_edit and self.network_info else None
|
|
352
352
|
create_network(self.conn, name, typenet, forward, ip, dhcp, dhcp_start, dhcp_end, domain_name, uuid=uuid)
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
if self.is_edit:
|
|
355
|
+
message = SuccessMessages.NETWORK_UPDATED_TEMPLATE.format(name=name)
|
|
356
|
+
else:
|
|
357
|
+
message = SuccessMessages.NETWORK_CREATED_TEMPLATE.format(name=name)
|
|
358
|
+
|
|
355
359
|
self.app.call_from_thread(self.app.show_success_message, message)
|
|
356
360
|
self.app.call_from_thread(self.dismiss, True)
|
|
357
361
|
except Exception as e:
|
|
362
|
+
if self.is_edit:
|
|
363
|
+
err_msg = ErrorMessages.ERROR_UPDATING_NETWORK_TEMPLATE.format(error=e)
|
|
364
|
+
else:
|
|
365
|
+
err_msg = ErrorMessages.ERROR_CREATING_NETWORK_TEMPLATE.format(error=e)
|
|
366
|
+
|
|
358
367
|
self.app.call_from_thread(
|
|
359
368
|
self.app.show_error_message,
|
|
360
|
-
|
|
369
|
+
err_msg
|
|
361
370
|
)
|
|
362
371
|
|
|
363
372
|
self.app.worker_manager.run(
|
|
@@ -374,7 +383,7 @@ class NetworkXMLModal(BaseModal[None]):
|
|
|
374
383
|
|
|
375
384
|
def compose(self) -> ComposeResult:
|
|
376
385
|
with Vertical(id="network-detail-dialog"):
|
|
377
|
-
yield Label(
|
|
386
|
+
yield Label(StaticText.NETWORK_DETAILS.format(network_name=self.network_name), id="title")
|
|
378
387
|
with ScrollableContainer():
|
|
379
388
|
text_area = TextArea(self.network_xml, read_only=True)
|
|
380
389
|
try:
|
|
@@ -384,7 +393,7 @@ class NetworkXMLModal(BaseModal[None]):
|
|
|
384
393
|
text_area.styles.height = "auto"
|
|
385
394
|
yield text_area
|
|
386
395
|
with Horizontal():
|
|
387
|
-
yield Button(
|
|
396
|
+
yield Button(ButtonLabels.CLOSE, variant="default", id="close-btn", classes="close-btn")
|
|
388
397
|
|
|
389
398
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
390
399
|
if event.button.id == "close-btn":
|
|
@@ -12,7 +12,7 @@ from textual import on, work
|
|
|
12
12
|
|
|
13
13
|
import libvirt
|
|
14
14
|
from ..config import load_config
|
|
15
|
-
from ..constants import AppInfo
|
|
15
|
+
from ..constants import AppInfo, ErrorMessages, SuccessMessages, ButtonLabels, StaticText
|
|
16
16
|
from ..vm_provisioner import VMProvisioner, VMType, OpenSUSEDistro
|
|
17
17
|
from ..storage_manager import list_storage_pools
|
|
18
18
|
from ..vm_service import VMService
|
|
@@ -43,16 +43,16 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
43
43
|
default_pool = 'default' if any(p[0] == 'default' for p in active_pools) else (active_pools[0][1] if active_pools else None)
|
|
44
44
|
|
|
45
45
|
with ScrollableContainer(id="install-dialog"):
|
|
46
|
-
yield Label(
|
|
47
|
-
yield Label(
|
|
46
|
+
yield Label(StaticText.INSTALL_OPENSUSE_VM.format(uri=self.uri), classes="title")
|
|
47
|
+
yield Label(StaticText.VM_NAME, classes="label")
|
|
48
48
|
yield Input(placeholder="my-new-vm", id="vm-name")
|
|
49
49
|
|
|
50
|
-
yield Label(
|
|
50
|
+
yield Label(StaticText.VM_TYPE, classes="label")
|
|
51
51
|
with Horizontal(classes="label-row"):
|
|
52
52
|
yield Select([(t.value, t) for t in VMType], value=VMType.DESKTOP, id="vm-type", allow_blank=False)
|
|
53
|
-
yield Button(
|
|
53
|
+
yield Button(ButtonLabels.INFO, id="vm-type-info-btn", variant="primary")
|
|
54
54
|
|
|
55
|
-
yield Label(
|
|
55
|
+
yield Label(StaticText.DISTRIBUTION, classes="label")
|
|
56
56
|
distro_options = [(d.value, d) for d in OpenSUSEDistro]
|
|
57
57
|
distro_options.insert(0, ("Cached ISOs", "cached"))
|
|
58
58
|
custom_repos = self.provisioner.get_custom_repos()
|
|
@@ -70,58 +70,58 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
70
70
|
|
|
71
71
|
# Container for ISO selection (Repo)
|
|
72
72
|
with Vertical(id="repo-iso-container"):
|
|
73
|
-
yield Label(
|
|
73
|
+
yield Label(StaticText.ISO_IMAGE_REPO, classes="label")
|
|
74
74
|
config = load_config()
|
|
75
75
|
iso_path = Path(config.get('ISO_DOWNLOAD_PATH', str(Path.home() / ".cache" / AppInfo.name / "isos")))
|
|
76
|
-
yield Label(
|
|
76
|
+
yield Label(StaticText.ISOS_DOWNLOAD_PATH.format(iso_path=iso_path), classes="info-text", id="iso-path-label")
|
|
77
77
|
yield Select([], prompt="Select ISO...", id="iso-select", disabled=True)
|
|
78
78
|
|
|
79
79
|
# Container for Custom ISO
|
|
80
80
|
with Vertical(id="custom-iso-container"):
|
|
81
|
-
yield Label(
|
|
81
|
+
yield Label(StaticText.CUSTOM_ISO_LOCAL_PATH, classes="label")
|
|
82
82
|
with Horizontal(classes="input-row"):
|
|
83
83
|
yield Input(placeholder="/path/to/local.iso", id="custom-iso-path", classes="path-input")
|
|
84
|
-
yield Button(
|
|
84
|
+
yield Button(ButtonLabels.BROWSE, id="browse-iso-btn")
|
|
85
85
|
|
|
86
86
|
with Vertical(id="checksum-container"):
|
|
87
|
-
yield Checkbox(
|
|
87
|
+
yield Checkbox(StaticText.VALIDATE_CHECKSUM, id="validate-checksum", value=False)
|
|
88
88
|
yield Input(placeholder="SHA256 Checksum (Optional)", id="checksum-input", disabled=True)
|
|
89
|
-
yield Label(
|
|
90
|
-
|
|
89
|
+
yield Label(StaticText.EMPTY_LABEL, id="checksum-status", classes="status-text")
|
|
90
|
+
|
|
91
91
|
# Container for ISO selection from Storage Pools
|
|
92
92
|
with Vertical(id="pool-iso-container"):
|
|
93
|
-
yield Label(
|
|
93
|
+
yield Label(StaticText.SELECT_STORAGE_POOL, classes="label")
|
|
94
94
|
yield Select(active_pools, prompt="Select Pool...", id="storage-pool-select", allow_blank=False)
|
|
95
|
-
yield Label(
|
|
95
|
+
yield Label(StaticText.SELECT_ISO_VOLUME, classes="label")
|
|
96
96
|
yield Select([], prompt="Select ISO Volume...", id="iso-volume-select", disabled=True)
|
|
97
97
|
|
|
98
|
-
yield Label(
|
|
98
|
+
yield Label(StaticText.STORAGE_POOL, id="vminstall-storage-label")
|
|
99
99
|
yield Select(active_pools, value=default_pool, id="pool", allow_blank=False)
|
|
100
100
|
with Collapsible(title="Expert Mode", id="expert-mode-collapsible"):
|
|
101
101
|
with Horizontal(id="expert-mode"):
|
|
102
102
|
with Vertical(id="expert-mem"):
|
|
103
|
-
yield Label(
|
|
103
|
+
yield Label(StaticText.MEMORY_GB_LABEL, classes="label")
|
|
104
104
|
yield Input("4", id="memory-input", type="integer")
|
|
105
105
|
with Vertical(id="expert-cpu"):
|
|
106
|
-
yield Label(
|
|
106
|
+
yield Label(StaticText.CPUS_LABEL, classes="label")
|
|
107
107
|
yield Input("2", id="cpu-input", type="integer")
|
|
108
108
|
with Vertical(id="expert-disk-size"):
|
|
109
|
-
yield Label(
|
|
109
|
+
yield Label(StaticText.DISK_SIZE_GB_LABEL, classes="label")
|
|
110
110
|
yield Input("8", id="disk-size-input", type="integer")
|
|
111
111
|
with Vertical(id="expert-disk-format"):
|
|
112
|
-
yield Label(
|
|
112
|
+
yield Label(StaticText.DISK_FORMAT_LABEL, classes="label")
|
|
113
113
|
yield Select([("Qcow2", "qcow2"), ("Raw", "raw")], value="qcow2", id="disk-format")
|
|
114
114
|
with Vertical(id="expert-firmware"):
|
|
115
|
-
yield Label(
|
|
116
|
-
yield Checkbox(
|
|
115
|
+
yield Label(StaticText.FIRMWARE_LABEL, classes="label")
|
|
116
|
+
yield Checkbox(StaticText.UEFI, id="boot-uefi-checkbox", value=True, tooltip="Unchecked means legacy boot")
|
|
117
117
|
|
|
118
|
-
yield Checkbox(
|
|
118
|
+
yield Checkbox(StaticText.CONFIGURE_BEFORE_INSTALL, id="configure-before-install-checkbox", value=False, tooltip="Show VM configuration before starting")
|
|
119
119
|
yield ProgressBar(total=100, show_eta=False, id="progress-bar")
|
|
120
|
-
yield Label(
|
|
120
|
+
yield Label(StaticText.EMPTY_LABEL, id="status-label")
|
|
121
121
|
|
|
122
122
|
with Horizontal(classes="buttons"):
|
|
123
|
-
yield Button(
|
|
124
|
-
yield Button(
|
|
123
|
+
yield Button(ButtonLabels.INSTALL, variant="primary", id="install-btn", disabled=True)
|
|
124
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
125
125
|
|
|
126
126
|
def on_mount(self):
|
|
127
127
|
"""Called when modal is mounted."""
|
|
@@ -270,7 +270,7 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
270
270
|
self.app.call_from_thread(update_iso_volume_select)
|
|
271
271
|
|
|
272
272
|
except Exception as e:
|
|
273
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
273
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.FAILED_TO_FETCH_ISO_VOLUMES_TEMPLATE.format(pool_name=pool_name, error=e))
|
|
274
274
|
self.app.call_from_thread(self._update_iso_status, "Error fetching volumes", False)
|
|
275
275
|
self.app.call_from_thread(iso_volume_select.clear)
|
|
276
276
|
self.app.call_from_thread(lambda: setattr(iso_volume_select, 'disabled', True))
|
|
@@ -311,7 +311,7 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
311
311
|
self.app.call_from_thread(update_select)
|
|
312
312
|
|
|
313
313
|
except Exception as e:
|
|
314
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
314
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.FAILED_TO_FETCH_ISOS_TEMPLATE.format(error=e))
|
|
315
315
|
self.app.call_from_thread(self._update_iso_status, "Error fetching ISOs", False)
|
|
316
316
|
|
|
317
317
|
def _update_iso_status(self, message, loading):
|
|
@@ -385,24 +385,24 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
385
385
|
return
|
|
386
386
|
|
|
387
387
|
if was_modified:
|
|
388
|
-
self.app.show_quick_message(
|
|
388
|
+
self.app.show_quick_message(SuccessMessages.VM_NAME_SANITIZED_TEMPLATE.format(original=vm_name_raw, sanitized=vm_name))
|
|
389
389
|
self.query_one("#vm-name", Input).value = vm_name
|
|
390
390
|
|
|
391
391
|
if not vm_name:
|
|
392
|
-
self.app.show_error_message(
|
|
392
|
+
self.app.show_error_message(ErrorMessages.VM_NAME_CANNOT_BE_EMPTY)
|
|
393
393
|
return
|
|
394
394
|
|
|
395
395
|
# 2. Check if VM exists
|
|
396
396
|
try:
|
|
397
397
|
self.conn.lookupByName(vm_name)
|
|
398
|
-
self.app.show_error_message(
|
|
398
|
+
self.app.show_error_message(ErrorMessages.VM_NAME_ALREADY_EXISTS_TEMPLATE.format(vm_name=vm_name))
|
|
399
399
|
return
|
|
400
400
|
except libvirt.libvirtError as e:
|
|
401
401
|
if e.get_error_code() != libvirt.VIR_ERR_NO_DOMAIN:
|
|
402
|
-
self.app.show_error_message(
|
|
402
|
+
self.app.show_error_message(ErrorMessages.ERROR_CHECKING_VM_NAME_TEMPLATE.format(error=e))
|
|
403
403
|
return
|
|
404
404
|
except Exception as e:
|
|
405
|
-
self.app.show_error_message(
|
|
405
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE.format(error=e))
|
|
406
406
|
return
|
|
407
407
|
|
|
408
408
|
vm_type = self.query_one("#vm-type", Select).value
|
|
@@ -412,7 +412,7 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
412
412
|
|
|
413
413
|
# Validate storage pool
|
|
414
414
|
if not pool_name or pool_name == Select.BLANK:
|
|
415
|
-
self.app.show_error_message(
|
|
415
|
+
self.app.show_error_message(ErrorMessages.PLEASE_SELECT_VALID_STORAGE_POOL)
|
|
416
416
|
return
|
|
417
417
|
|
|
418
418
|
iso_url = None
|
|
@@ -428,11 +428,11 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
428
428
|
elif distro == "pool_volumes":
|
|
429
429
|
iso_url = self.query_one("#iso-volume-select", Select).value
|
|
430
430
|
if not iso_url or iso_url == Select.BLANK:
|
|
431
|
-
self.app.show_error_message(
|
|
431
|
+
self.app.show_error_message(ErrorMessages.SELECT_VALID_ISO_VOLUME)
|
|
432
432
|
return
|
|
433
433
|
# Validate that the volume path exists and is accessible
|
|
434
434
|
if not os.path.exists(iso_url):
|
|
435
|
-
self.app.show_error_message(
|
|
435
|
+
self.app.show_error_message(ErrorMessages.ISO_VOLUME_NOT_FOUND_TEMPLATE.format(iso_url=iso_url))
|
|
436
436
|
return
|
|
437
437
|
else:
|
|
438
438
|
iso_url = self.query_one("#iso-select", Select).value
|
|
@@ -446,27 +446,27 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
446
446
|
disk_format = self.query_one("#disk-format", Select).value
|
|
447
447
|
boot_uefi = self.query_one("#boot-uefi-checkbox", Checkbox).value
|
|
448
448
|
except ValueError:
|
|
449
|
-
self.app.show_error_message(
|
|
449
|
+
self.app.show_error_message(ErrorMessages.INVALID_EXPERT_SETTINGS)
|
|
450
450
|
return
|
|
451
451
|
|
|
452
452
|
# Validate expert mode inputs
|
|
453
453
|
if memory_gb < 1 or memory_gb > 8192:
|
|
454
|
-
self.app.show_error_message(
|
|
454
|
+
self.app.show_error_message(ErrorMessages.MEMORY_RANGE_ERROR)
|
|
455
455
|
return
|
|
456
456
|
if vcpu < 1 or vcpu > 768:
|
|
457
|
-
self.app.show_error_message(
|
|
457
|
+
self.app.show_error_message(ErrorMessages.CPU_RANGE_ERROR)
|
|
458
458
|
return
|
|
459
459
|
if disk_size < 1 or disk_size > 10000:
|
|
460
|
-
self.app.show_error_message(
|
|
460
|
+
self.app.show_error_message(ErrorMessages.DISK_SIZE_RANGE_ERROR)
|
|
461
461
|
return
|
|
462
462
|
|
|
463
463
|
try:
|
|
464
464
|
pool = self.conn.storagePoolLookupByName(pool_name)
|
|
465
465
|
if not pool.isActive():
|
|
466
|
-
self.app.show_error_message(
|
|
466
|
+
self.app.show_error_message(ErrorMessages.STORAGE_POOL_NOT_ACTIVE_TEMPLATE.format(pool_name=pool_name))
|
|
467
467
|
return
|
|
468
468
|
except Exception as e:
|
|
469
|
-
self.app.show_error_message(
|
|
469
|
+
self.app.show_error_message(ErrorMessages.ERROR_ACCESSING_STORAGE_POOL_TEMPLATE.format(pool_name=pool_name, error=e))
|
|
470
470
|
return
|
|
471
471
|
|
|
472
472
|
# Disable inputs
|
|
@@ -494,17 +494,17 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
494
494
|
if custom_path:
|
|
495
495
|
# Validate custom path exists
|
|
496
496
|
if not os.path.exists(custom_path):
|
|
497
|
-
raise Exception(
|
|
497
|
+
raise Exception(ErrorMessages.CUSTOM_ISO_PATH_NOT_EXIST_TEMPLATE.format(path=custom_path))
|
|
498
498
|
if not os.path.isfile(custom_path):
|
|
499
|
-
raise Exception(
|
|
499
|
+
raise Exception(ErrorMessages.CUSTOM_ISO_NOT_FILE_TEMPLATE.format(path=custom_path))
|
|
500
500
|
|
|
501
501
|
# 1. Validate Checksum
|
|
502
502
|
if validate:
|
|
503
503
|
if not checksum:
|
|
504
|
-
raise Exception(
|
|
504
|
+
raise Exception(ErrorMessages.CHECKSUM_MISSING)
|
|
505
505
|
progress_cb("Validating Checksum...", 0)
|
|
506
506
|
if not self.provisioner.validate_iso(custom_path, checksum):
|
|
507
|
-
raise Exception(
|
|
507
|
+
raise Exception(ErrorMessages.CHECKSUM_VALIDATION_FAILED)
|
|
508
508
|
progress_cb("Checksum Validated", 10)
|
|
509
509
|
|
|
510
510
|
# 2. Upload
|
|
@@ -514,7 +514,7 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
514
514
|
|
|
515
515
|
final_iso_url = self.provisioner.upload_iso(custom_path, pool_name, upload_progress)
|
|
516
516
|
if not final_iso_url:
|
|
517
|
-
raise Exception(
|
|
517
|
+
raise Exception(ErrorMessages.NO_ISO_URL_SPECIFIED)
|
|
518
518
|
|
|
519
519
|
# 3. Provision
|
|
520
520
|
# Suspend global updates to prevent UI freeze during heavy provisioning ops
|
|
@@ -544,7 +544,7 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
544
544
|
try:
|
|
545
545
|
if not domain_obj.isActive():
|
|
546
546
|
domain_obj.create()
|
|
547
|
-
app.call_from_thread(app.show_success_message,
|
|
547
|
+
app.call_from_thread(app.show_success_message, SuccessMessages.VM_STARTED_TEMPLATE.format(vm_name=vm_name))
|
|
548
548
|
# Launch viewer
|
|
549
549
|
domain_name = domain_obj.name()
|
|
550
550
|
cmd = remote_viewer_cmd(self.uri, domain_name, app.r_viewer)
|
|
@@ -555,10 +555,10 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
555
555
|
preexec_fn=os.setsid,
|
|
556
556
|
)
|
|
557
557
|
logging.info(f"{app.r_viewer} started with PID {proc.pid} for {domain_name}")
|
|
558
|
-
app.call_from_thread(app.show_quick_message,
|
|
558
|
+
app.call_from_thread(app.show_quick_message, SuccessMessages.REMOTE_VIEWER_STARTED_TEMPLATE.format(viewer=app.r_viewer, vm_name=domain_name))
|
|
559
559
|
except Exception as e:
|
|
560
560
|
logging.error(f"Failed to start VM or viewer: {e}")
|
|
561
|
-
app.call_from_thread(app.show_error_message,
|
|
561
|
+
app.call_from_thread(app.show_error_message, ErrorMessages.FAILED_TO_START_VM_OR_VIEWER_TEMPLATE.format(error=e))
|
|
562
562
|
|
|
563
563
|
app.worker_manager.run(start_and_view, name=f"start_view_{vm_name}")
|
|
564
564
|
|
|
@@ -567,7 +567,7 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
567
567
|
on_details_closed
|
|
568
568
|
)
|
|
569
569
|
else:
|
|
570
|
-
app.show_error_message(
|
|
570
|
+
app.show_error_message(ErrorMessages.COULD_NOT_GET_VM_DETAILS_TEMPLATE.format(vm_name=vm_name))
|
|
571
571
|
self.app.call_from_thread(push_details)
|
|
572
572
|
|
|
573
573
|
dom = self.provisioner.provision_vm(
|
|
@@ -591,10 +591,10 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
591
591
|
self.app.call_from_thread(self.app.on_vm_data_update)
|
|
592
592
|
|
|
593
593
|
if configure_before_install:
|
|
594
|
-
self.app.call_from_thread(self.app.show_success_message,
|
|
594
|
+
self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_DEFINED_CONFIGURE_TEMPLATE.format(vm_name=name))
|
|
595
595
|
return
|
|
596
596
|
|
|
597
|
-
self.app.call_from_thread(self.app.show_success_message,
|
|
597
|
+
self.app.call_from_thread(self.app.show_success_message, SuccessMessages.VM_CREATED_SUCCESSFULLY_TEMPLATE.format(vm_name=name))
|
|
598
598
|
|
|
599
599
|
# 4. Auto-connect Remote Viewer
|
|
600
600
|
def launch_viewer():
|
|
@@ -608,12 +608,12 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
608
608
|
preexec_fn=os.setsid,
|
|
609
609
|
)
|
|
610
610
|
logging.info(f"{self.app.r_viewer} started with PID {proc.pid} for {domain_name}")
|
|
611
|
-
self.app.show_quick_message(
|
|
611
|
+
self.app.show_quick_message(SuccessMessages.REMOTE_VIEWER_STARTED_TEMPLATE.format(viewer=self.app.r_viewer, vm_name=domain_name))
|
|
612
612
|
except Exception as e:
|
|
613
613
|
logging.error(f"Failed to spawn {self.app.r_viewer} for {domain_name}: {e}")
|
|
614
614
|
self.app.call_from_thread(
|
|
615
615
|
self.app.show_error_message,
|
|
616
|
-
|
|
616
|
+
ErrorMessages.REMOTE_VIEWER_FAILED_TO_START_TEMPLATE.format(viewer=self.app.r_viewer, domain_name=domain_name, error=e)
|
|
617
617
|
)
|
|
618
618
|
return
|
|
619
619
|
|
|
@@ -621,5 +621,5 @@ class InstallVMModal(BaseModal[str | None]):
|
|
|
621
621
|
self.app.call_from_thread(self.dismiss, True)
|
|
622
622
|
|
|
623
623
|
except Exception as e:
|
|
624
|
-
self.app.call_from_thread(self.app.show_error_message,
|
|
624
|
+
self.app.call_from_thread(self.app.show_error_message, ErrorMessages.PROVISIONING_FAILED_TEMPLATE.format(error=e))
|
|
625
625
|
self.app.call_from_thread(self.dismiss)
|
|
@@ -12,6 +12,7 @@ from textual.screen import ModalScreen
|
|
|
12
12
|
from .base_modals import BaseModal
|
|
13
13
|
from .utils_modals import LoadingModal
|
|
14
14
|
from ..connection_manager import ConnectionManager
|
|
15
|
+
from ..constants import ErrorMessages, StaticText, ButtonLabels
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class SelectServerModal(BaseModal[None]):
|
|
@@ -26,7 +27,7 @@ class SelectServerModal(BaseModal[None]):
|
|
|
26
27
|
|
|
27
28
|
def compose(self) -> ComposeResult:
|
|
28
29
|
with Vertical(id="select-server-container", classes="info-details"):
|
|
29
|
-
yield Label(
|
|
30
|
+
yield Label(StaticText.SELECT_SERVERS_TO_DISPLAY)
|
|
30
31
|
|
|
31
32
|
checkboxes = []
|
|
32
33
|
for i, server in enumerate(self.servers):
|
|
@@ -44,8 +45,8 @@ class SelectServerModal(BaseModal[None]):
|
|
|
44
45
|
yield grid
|
|
45
46
|
|
|
46
47
|
with Horizontal(classes="button-details"):
|
|
47
|
-
yield Button(
|
|
48
|
-
yield Button(
|
|
48
|
+
yield Button(ButtonLabels.DONE, id="done-servers", variant="primary", classes="done-button")
|
|
49
|
+
yield Button(ButtonLabels.CANCEL, id="cancel-servers", classes="cancel-button")
|
|
49
50
|
|
|
50
51
|
@on(Checkbox.Changed)
|
|
51
52
|
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
|
@@ -57,7 +58,7 @@ class SelectServerModal(BaseModal[None]):
|
|
|
57
58
|
return
|
|
58
59
|
|
|
59
60
|
if event.value: # If checkbox is checked
|
|
60
|
-
loading_modal = LoadingModal(
|
|
61
|
+
loading_modal = LoadingModal(ErrorMessages.CONNECTING_TO_SERVER_TEMPLATE.format(uri=uri))
|
|
61
62
|
self.app.push_screen(loading_modal)
|
|
62
63
|
|
|
63
64
|
def connect_and_update():
|
|
@@ -66,7 +67,7 @@ class SelectServerModal(BaseModal[None]):
|
|
|
66
67
|
if conn is None:
|
|
67
68
|
self.app.call_from_thread(
|
|
68
69
|
self.app.show_error_message,
|
|
69
|
-
|
|
70
|
+
ErrorMessages.FAILED_TO_CONNECT_TO_SERVER_TEMPLATE.format(uri=uri)
|
|
70
71
|
)
|
|
71
72
|
# Revert checkbox state on failure
|
|
72
73
|
checkbox = self.query(f"#{checkbox_id}").first()
|
|
@@ -104,10 +105,10 @@ class SelectOneServerModal(BaseModal[str]):
|
|
|
104
105
|
with Vertical(id="select-one-server-container"):
|
|
105
106
|
yield Label(self.title_text)
|
|
106
107
|
yield Select(self.server_options, prompt="Select server...", id="server-select")
|
|
107
|
-
yield Label(
|
|
108
|
+
yield Label(StaticText.EMPTY_LABEL)
|
|
108
109
|
with Horizontal():
|
|
109
110
|
yield Button(self.button_label, id="launch-btn", variant="primary", disabled=True)
|
|
110
|
-
yield Button(
|
|
111
|
+
yield Button(ButtonLabels.CANCEL, id="cancel-btn")
|
|
111
112
|
|
|
112
113
|
@on(Select.Changed, "#server-select")
|
|
113
114
|
def on_server_select_changed(self, event: Select.Changed) -> None:
|