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
|
@@ -25,7 +25,11 @@ from ..network_manager import (
|
|
|
25
25
|
from .. import storage_manager
|
|
26
26
|
from ..storage_manager import list_storage_volumes
|
|
27
27
|
|
|
28
|
-
from ..constants import
|
|
28
|
+
from ..constants import (
|
|
29
|
+
AppCacheTimeout, ErrorMessages, SuccessMessages,
|
|
30
|
+
ButtonLabels, WarningMessages,
|
|
31
|
+
StaticText
|
|
32
|
+
)
|
|
29
33
|
from .base_modals import BaseModal
|
|
30
34
|
from .network_modals import AddEditNetworkModal, NetworkXMLModal
|
|
31
35
|
from .disk_pool_modals import (
|
|
@@ -51,7 +55,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
51
55
|
|
|
52
56
|
def compose(self) -> ComposeResult:
|
|
53
57
|
with Vertical(id="server-pref-dialog",):
|
|
54
|
-
yield Label(
|
|
58
|
+
yield Label(StaticText.SERVER_PREFERENCES, id="server-pref-title")
|
|
55
59
|
yield Static(classes="button-separator")
|
|
56
60
|
with TabbedContent(id="server-pref-tabs"):
|
|
57
61
|
with TabPane("Network", id="tab-network"):
|
|
@@ -65,19 +69,19 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
65
69
|
uri_to_connect = self.uri
|
|
66
70
|
if uri_to_connect is None:
|
|
67
71
|
if len(self.app.active_uris) == 0:
|
|
68
|
-
self.app.show_error_message(
|
|
72
|
+
self.app.show_error_message(ErrorMessages.NOT_CONNECTED_TO_ANY_SERVER_PREFS)
|
|
69
73
|
self.dismiss()
|
|
70
74
|
return
|
|
71
75
|
if len(self.app.active_uris) > 1:
|
|
72
76
|
# This should not happen if the app logic uses the server selection modal
|
|
73
|
-
self.app.show_error_message(
|
|
77
|
+
self.app.show_error_message(ErrorMessages.MULTIPLE_SERVERS_ACTIVE_NONE_SELECTED)
|
|
74
78
|
self.dismiss()
|
|
75
79
|
return
|
|
76
80
|
uri_to_connect = self.app.active_uris[0]
|
|
77
81
|
|
|
78
82
|
self.conn = self.app.vm_service.connect(uri_to_connect)
|
|
79
83
|
if not self.conn:
|
|
80
|
-
self.app.show_error_message(
|
|
84
|
+
self.app.show_error_message(ErrorMessages.FAILED_TO_GET_CONNECTION_PREFS_TEMPLATE.format(uri=uri_to_connect))
|
|
81
85
|
self.dismiss()
|
|
82
86
|
return
|
|
83
87
|
|
|
@@ -103,21 +107,21 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
103
107
|
|
|
104
108
|
button_container = Vertical(
|
|
105
109
|
Horizontal(
|
|
106
|
-
Button(
|
|
110
|
+
Button(ButtonLabels.DE_ACTIVE, id="toggle-net-active-btn", classes="toggle-detail-button",
|
|
107
111
|
variant="primary", disabled=True, tooltip="Activate or Deactivate the selected network."),
|
|
108
|
-
Button(
|
|
112
|
+
Button(ButtonLabels.AUTOSTART, id="toggle-net-autostart-btn", classes="toggle-detail-button",
|
|
109
113
|
variant="primary", disabled=True, tooltip="Enable or Disable autostart for the selected network."),
|
|
110
114
|
),
|
|
111
115
|
Horizontal(
|
|
112
|
-
Button(
|
|
116
|
+
Button(ButtonLabels.ADD, id="add-net-btn", variant="success", classes="toggle-detail-button",
|
|
113
117
|
tooltip="Add a new network."),
|
|
114
|
-
Button(
|
|
118
|
+
Button(ButtonLabels.EDIT, id="edit-net-btn", variant="success", classes="toggle-detail-button",
|
|
115
119
|
disabled=True, tooltip="Edit the selected network."),
|
|
116
|
-
Button(
|
|
120
|
+
Button(ButtonLabels.VIEW, id="view-net-btn", variant="success", classes="toggle-detail-button",
|
|
117
121
|
disabled=True, tooltip="View XML details of the selected network."),
|
|
118
|
-
Button(
|
|
122
|
+
Button(ButtonLabels.DELETE, id="delete-net-btn", variant="error", classes="toggle-detail-button",
|
|
119
123
|
disabled=True, tooltip="Delete the selected network."),
|
|
120
|
-
Button(
|
|
124
|
+
Button(ButtonLabels.HELP, id="help-net-btn", variant="success", classes="toggle-detail-button",
|
|
121
125
|
tooltip="Show network configuration help."),
|
|
122
126
|
),
|
|
123
127
|
classes="server-pref-button"
|
|
@@ -145,9 +149,9 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
145
149
|
|
|
146
150
|
|
|
147
151
|
if num_vms > AppCacheTimeout.DONT_DISPLAY_DISK_USAGE:
|
|
148
|
-
self.app.show_warning_message(
|
|
152
|
+
self.app.show_warning_message(WarningMessages.TOO_MANY_VMS_DISK_USAGE_WARNING_TEMPLATE.format(count=AppCacheTimeout.DONT_DISPLAY_DISK_USAGE))
|
|
149
153
|
else:
|
|
150
|
-
self.app.show_warning_message(
|
|
154
|
+
#self.app.show_warning_message(WarningMessages.RUNNING_DISK_USAGE_SCAN_WARNING)
|
|
151
155
|
disk_map = get_all_vm_disk_usage(self.conn)
|
|
152
156
|
nvram_map = get_all_vm_nvram_usage(self.conn)
|
|
153
157
|
#overlay_map = get_all_vm_overlay_usage(self.conn)
|
|
@@ -177,25 +181,25 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
177
181
|
Button(id="toggle-autostart-pool-btn", variant="primary", classes="toggle-detail-button",
|
|
178
182
|
tooltip="Enable or Disable autostart for the selected storage pool."),
|
|
179
183
|
Vertical(
|
|
180
|
-
Button(
|
|
184
|
+
Button(ButtonLabels.ADD_POOL, id="add-pool-btn", variant="success", classes="toggle-detail-button",
|
|
181
185
|
tooltip="Add a new storage pool."),
|
|
182
|
-
Button(
|
|
186
|
+
Button(ButtonLabels.DELETE_POOL, id="del-pool-btn", variant="error", classes="toggle-detail-button",
|
|
183
187
|
tooltip="Delete the selected storage pool."),
|
|
184
188
|
),
|
|
185
189
|
Vertical(
|
|
186
|
-
Button(
|
|
190
|
+
Button(ButtonLabels.NEW_VOLUME, id="add-vol-btn", variant="success", classes="toggle-detail-button",
|
|
187
191
|
tooltip="Create a new volume in the selected pool."),
|
|
188
|
-
Button(
|
|
192
|
+
Button(ButtonLabels.ATTACH_VOL, id="attach-vol-btn", variant="success", classes="toggle-detail-button",
|
|
189
193
|
tooltip="Attach an existing disk file as a volume."),
|
|
190
|
-
Button(
|
|
194
|
+
Button(ButtonLabels.MOVE_VOLUME, id="move-vol-btn", variant="success", classes="toggle-detail-button",
|
|
191
195
|
tooltip="Move the selected volume to another pool."),
|
|
192
|
-
Button(
|
|
196
|
+
Button(ButtonLabels.DELETE_VOLUME, id="del-vol-btn", variant="error", classes="toggle-detail-button",
|
|
193
197
|
tooltip="Delete the selected volume."),
|
|
194
198
|
),
|
|
195
199
|
Vertical(
|
|
196
|
-
Button(
|
|
200
|
+
Button(ButtonLabels.VIEW_XML, id="view-storage-xml-btn", variant="primary", classes="toggle-detail-button",
|
|
197
201
|
tooltip="View XML details of the selected pool or volume."),
|
|
198
|
-
Button(
|
|
202
|
+
Button(ButtonLabels.EDIT_XML, id="edit-pool-xml-btn", variant="primary", classes="toggle-detail-button",
|
|
199
203
|
tooltip="Edit XML of the selected pool."),
|
|
200
204
|
),
|
|
201
205
|
),
|
|
@@ -248,20 +252,28 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
248
252
|
pools = storage_manager.list_storage_pools(self.conn)
|
|
249
253
|
node_to_select = None
|
|
250
254
|
for pool_data in pools:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
255
|
+
try:
|
|
256
|
+
pool_name = pool_data['name']
|
|
257
|
+
status = pool_data['status']
|
|
258
|
+
autostart = "autostart" if pool_data['autostart'] else "no autostart"
|
|
259
|
+
label = f"{pool_name} [{status}, {autostart}]"
|
|
260
|
+
pool_node = tree.root.add(label, data=pool_data)
|
|
261
|
+
pool_node.data["type"] = "pool"
|
|
262
|
+
# Add a dummy node to make the pool node expandable
|
|
263
|
+
pool_node.add_leaf("Loading volumes...")
|
|
264
|
+
|
|
265
|
+
if expand_pools and pool_name in expand_pools:
|
|
266
|
+
self.app.call_later(pool_node.expand)
|
|
267
|
+
|
|
268
|
+
if select_pool and pool_name == select_pool:
|
|
269
|
+
node_to_select = pool_node
|
|
270
|
+
except libvirt.libvirtError:
|
|
271
|
+
# Handle cases where pool might be listed but inaccessible (e.g. NFS down)
|
|
272
|
+
pool_name = pool_data.get('name', 'Unknown')
|
|
273
|
+
label = f"{pool_name} [unavailable]"
|
|
274
|
+
pool_node = tree.root.add(label, data=pool_data)
|
|
275
|
+
pool_node.data["type"] = "pool"
|
|
276
|
+
pool_node.add_leaf("Pool unavailable")
|
|
265
277
|
|
|
266
278
|
if node_to_select:
|
|
267
279
|
self.app.call_later(tree.select_node, node_to_select)
|
|
@@ -278,7 +290,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
278
290
|
|
|
279
291
|
table.clear()
|
|
280
292
|
|
|
281
|
-
self.app.show_warning_message(
|
|
293
|
+
#self.app.show_warning_message(WarningMessages.RUNNING_NETWORK_USAGE_SCAN_WARNING)
|
|
282
294
|
network_usage = get_all_network_usage(self.conn)
|
|
283
295
|
self.networks_list = list_networks(self.conn)
|
|
284
296
|
|
|
@@ -308,7 +320,8 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
308
320
|
if len(node.children) == 1 and node.children[0].data is None:
|
|
309
321
|
node.remove_children()
|
|
310
322
|
pool = node_data.get('pool')
|
|
311
|
-
|
|
323
|
+
# Check cached status instead of calling pool.isActive() to avoid blocking
|
|
324
|
+
if pool and node_data.get('status') == 'active':
|
|
312
325
|
volumes = storage_manager.list_storage_volumes(pool)
|
|
313
326
|
for vol_data in volumes:
|
|
314
327
|
vol_name = vol_data['name']
|
|
@@ -379,14 +392,14 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
379
392
|
return
|
|
380
393
|
|
|
381
394
|
if not target_obj:
|
|
382
|
-
self.app.show_error_message(
|
|
395
|
+
self.app.show_error_message(ErrorMessages.COULD_NOT_FIND_OBJECT_XML)
|
|
383
396
|
return
|
|
384
397
|
|
|
385
398
|
try:
|
|
386
399
|
xml_content = target_obj.XMLDesc(0)
|
|
387
400
|
self.app.push_screen(XMLDisplayModal(xml_content, read_only=True))
|
|
388
401
|
except libvirt.libvirtError as e:
|
|
389
|
-
self.app.show_error_message(
|
|
402
|
+
self.app.show_error_message(ErrorMessages.ERROR_GETTING_XML_FOR_TYPE_TEMPLATE.format(obj_type=node_type, error=e))
|
|
390
403
|
|
|
391
404
|
@on(Button.Pressed, "#edit-pool-xml-btn")
|
|
392
405
|
def on_edit_pool_xml_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -401,11 +414,11 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
401
414
|
|
|
402
415
|
pool = node_data.get('pool')
|
|
403
416
|
if not pool:
|
|
404
|
-
self.app.show_error_message(
|
|
417
|
+
self.app.show_error_message(ErrorMessages.COULD_NOT_FIND_POOL_EDIT)
|
|
405
418
|
return
|
|
406
419
|
|
|
407
|
-
if
|
|
408
|
-
self.app.show_error_message(
|
|
420
|
+
if node_data.get('status') == 'active':
|
|
421
|
+
self.app.show_error_message(ErrorMessages.POOL_MUST_BE_INACTIVE_FOR_XML_EDIT)
|
|
409
422
|
return
|
|
410
423
|
|
|
411
424
|
def on_edit_confirm(confirmed: bool) -> None:
|
|
@@ -415,7 +428,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
415
428
|
try:
|
|
416
429
|
xml_content = pool.XMLDesc(0)
|
|
417
430
|
except libvirt.libvirtError as e:
|
|
418
|
-
self.app.show_error_message(
|
|
431
|
+
self.app.show_error_message(ErrorMessages.ERROR_GETTING_XML_FOR_POOL_TEMPLATE.format(error=e))
|
|
419
432
|
return
|
|
420
433
|
|
|
421
434
|
def on_xml_save(new_xml: str | None) -> None:
|
|
@@ -425,21 +438,17 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
425
438
|
try:
|
|
426
439
|
# Redefine the pool with the new XML
|
|
427
440
|
self.conn.storagePoolDefineXML(new_xml, 0)
|
|
428
|
-
self.app.show_success_message(
|
|
441
|
+
self.app.show_success_message(SuccessMessages.STORAGE_POOL_UPDATED_SUCCESSFULLY_TEMPLATE.format(pool_name=pool.name()))
|
|
429
442
|
storage_manager.list_storage_pools.cache_clear()
|
|
430
443
|
self._load_storage_pools() # Refresh the tree
|
|
431
444
|
except libvirt.libvirtError as e:
|
|
432
|
-
self.app.show_error_message(
|
|
445
|
+
self.app.show_error_message(ErrorMessages.ERROR_UPDATING_POOL_XML_TEMPLATE.format(error=e))
|
|
433
446
|
except Exception as e:
|
|
434
|
-
self.app.show_error_message(
|
|
447
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE_XML.format(error=e))
|
|
435
448
|
|
|
436
449
|
self.app.push_screen(XMLDisplayModal(xml_content, read_only=False), on_xml_save)
|
|
437
450
|
|
|
438
|
-
warning_message =
|
|
439
|
-
"Editing a pool's XML definition is an advanced operation.\n"
|
|
440
|
-
"An invalid configuration may make its volumes inaccessible to VMs.\n\n"
|
|
441
|
-
"Are you sure you want to proceed?"
|
|
442
|
-
)
|
|
451
|
+
warning_message = ErrorMessages.EDIT_POOL_XML_WARNING
|
|
443
452
|
self.app.push_screen(ConfirmationDialog(warning_message), on_edit_confirm)
|
|
444
453
|
|
|
445
454
|
@on(Button.Pressed, "#toggle-active-pool-btn")
|
|
@@ -458,12 +467,13 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
458
467
|
is_active = node_data.get('status') == 'active'
|
|
459
468
|
try:
|
|
460
469
|
storage_manager.set_pool_active(pool, not is_active)
|
|
461
|
-
|
|
470
|
+
status = 'inactive' if is_active else 'active'
|
|
471
|
+
self.app.show_success_message(SuccessMessages.POOL_ACTIVATION_CHANGE_TEMPLATE.format(pool_name=pool.name(), status=status))
|
|
462
472
|
node_data['status'] = 'inactive' if is_active else 'active'
|
|
463
473
|
storage_manager.list_storage_pools.cache_clear()
|
|
464
474
|
self._load_storage_pools(select_pool=pool_name) # Refresh the tree
|
|
465
475
|
except Exception as e:
|
|
466
|
-
self.app.show_error_message(
|
|
476
|
+
self.app.show_error_message(ErrorMessages.ERROR_TOGGLE_POOL_ACTIVE_TEMPLATE.format(error=e))
|
|
467
477
|
|
|
468
478
|
@on(Button.Pressed, "#toggle-autostart-pool-btn")
|
|
469
479
|
def on_toggle_autostart_pool_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -481,12 +491,13 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
481
491
|
has_autostart = node_data.get('autostart', False)
|
|
482
492
|
try:
|
|
483
493
|
storage_manager.set_pool_autostart(pool, not has_autostart)
|
|
484
|
-
|
|
494
|
+
status = 'off' if has_autostart else 'on'
|
|
495
|
+
self.app.show_success_message(SuccessMessages.POOL_AUTOSTART_CHANGE_TEMPLATE.format(pool_name=pool.name(), status=status))
|
|
485
496
|
node_data['autostart'] = not has_autostart
|
|
486
497
|
storage_manager.list_storage_pools.cache_clear()
|
|
487
498
|
self._load_storage_pools(select_pool=pool_name) # Refresh the tree
|
|
488
499
|
except Exception as e:
|
|
489
|
-
self.app.show_error_message(
|
|
500
|
+
self.app.show_error_message(ErrorMessages.ERROR_TOGGLE_POOL_AUTOSTART_TEMPLATE.format(error=e))
|
|
490
501
|
|
|
491
502
|
@on(Button.Pressed, "#add-vol-btn")
|
|
492
503
|
def on_add_volume_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -510,7 +521,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
510
521
|
result['size_gb'],
|
|
511
522
|
result['format']
|
|
512
523
|
)
|
|
513
|
-
self.app.show_success_message(
|
|
524
|
+
self.app.show_success_message(SuccessMessages.VOLUME_CREATED_SUCCESSFULLY_TEMPLATE.format(name=result['name'], size_gb=result['size_gb'], format=result['format']))
|
|
514
525
|
storage_manager.list_storage_volumes.cache_clear()
|
|
515
526
|
pool_node.add_leaf(result['name'])
|
|
516
527
|
|
|
@@ -527,7 +538,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
527
538
|
|
|
528
539
|
volume_path = result.get('path')
|
|
529
540
|
if not volume_path or not os.path.exists(volume_path):
|
|
530
|
-
self.app.show_error_message(
|
|
541
|
+
self.app.show_error_message(ErrorMessages.INVALID_OR_NON_EXISTENT_PATH_TEMPLATE.format(path=volume_path))
|
|
531
542
|
return
|
|
532
543
|
|
|
533
544
|
volume_dir = os.path.dirname(volume_path)
|
|
@@ -538,11 +549,10 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
538
549
|
existing_pool.refresh(0)
|
|
539
550
|
storage_manager.list_storage_volumes.cache_clear()
|
|
540
551
|
self.app.show_success_message(
|
|
541
|
-
|
|
542
|
-
"Refreshed pool to include the new volume."
|
|
552
|
+
SuccessMessages.POOL_MANAGED_BY_EXISTING_POOL_TEMPLATE.format(pool_name=existing_pool.name())
|
|
543
553
|
)
|
|
544
554
|
except libvirt.libvirtError as e:
|
|
545
|
-
self.app.show_error_message(
|
|
555
|
+
self.app.show_error_message(ErrorMessages.ERROR_REFRESHING_POOL_TEMPLATE.format(error=e))
|
|
546
556
|
|
|
547
557
|
self._load_storage_pools(expand_pools=[existing_pool.name()])
|
|
548
558
|
return
|
|
@@ -554,17 +564,16 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
554
564
|
try:
|
|
555
565
|
storage_manager.create_storage_pool(self.conn, new_pool_name, 'dir', volume_dir)
|
|
556
566
|
self.app.show_success_message(
|
|
557
|
-
|
|
567
|
+
SuccessMessages.STORAGE_POOL_CREATED_TEMPLATE.format(name=new_pool_name)
|
|
558
568
|
)
|
|
559
569
|
storage_manager.list_storage_pools.cache_clear()
|
|
560
570
|
self._load_storage_pools(expand_pools=[new_pool_name])
|
|
561
571
|
except Exception as e:
|
|
562
|
-
self.app.show_error_message(
|
|
572
|
+
self.app.show_error_message(ErrorMessages.ERROR_CREATING_STORAGE_POOL_FOR_DIR_TEMPLATE.format(error=e))
|
|
563
573
|
|
|
564
574
|
self.app.push_screen(
|
|
565
575
|
ConfirmationDialog(
|
|
566
|
-
|
|
567
|
-
f"Create a new pool named '{new_pool_name}'?"
|
|
576
|
+
ErrorMessages.NO_POOL_FOR_DIRECTORY_CONFIRM_TEMPLATE.format(directory=volume_dir, pool_name=new_pool_name)
|
|
568
577
|
),
|
|
569
578
|
on_confirm_create
|
|
570
579
|
)
|
|
@@ -597,14 +606,14 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
597
606
|
if confirmed:
|
|
598
607
|
try:
|
|
599
608
|
storage_manager.delete_storage_pool(pool)
|
|
600
|
-
self.app.show_success_message(
|
|
609
|
+
self.app.show_success_message(SuccessMessages.STORAGE_POOL_DELETED_SUCCESSFULLY_TEMPLATE.format(pool_name=pool_name))
|
|
601
610
|
storage_manager.list_storage_pools.cache_clear()
|
|
602
611
|
self._load_storage_pools() # Refresh the tree
|
|
603
612
|
except Exception as e:
|
|
604
|
-
self.app.show_error_message(
|
|
613
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE_XML.format(error=e))
|
|
605
614
|
|
|
606
615
|
self.app.push_screen(
|
|
607
|
-
ConfirmationDialog(
|
|
616
|
+
ConfirmationDialog(ErrorMessages.DELETE_STORAGE_POOL_CONFIRMATION_TEMPLATE.format(pool_name=pool_name)), on_confirm)
|
|
608
617
|
|
|
609
618
|
@on(Button.Pressed, "#del-vol-btn")
|
|
610
619
|
def on_delete_volume_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -623,7 +632,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
623
632
|
if confirmed:
|
|
624
633
|
try:
|
|
625
634
|
storage_manager.delete_volume(vol)
|
|
626
|
-
self.app.show_success_message(
|
|
635
|
+
self.app.show_success_message(SuccessMessages.VOLUME_DELETED_SUCCESSFULLY_TEMPLATE.format(volume_name=vol_name))
|
|
627
636
|
# Clear the cache to force a refresh
|
|
628
637
|
list_storage_volumes.cache_clear()
|
|
629
638
|
# Refresh the parent node
|
|
@@ -633,10 +642,10 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
633
642
|
parent_node.add_leaf("No volumes")
|
|
634
643
|
|
|
635
644
|
except Exception as e:
|
|
636
|
-
self.app.show_error_message(
|
|
645
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE_XML.format(error=e))
|
|
637
646
|
|
|
638
647
|
self.app.push_screen(
|
|
639
|
-
ConfirmationDialog(
|
|
648
|
+
ConfirmationDialog(ErrorMessages.DELETE_VOLUME_CONFIRMATION_TEMPLATE.format(vol_name=vol_name)),
|
|
640
649
|
on_confirm
|
|
641
650
|
)
|
|
642
651
|
|
|
@@ -652,7 +661,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
652
661
|
|
|
653
662
|
volume_name = node_data.get('name')
|
|
654
663
|
if not tree.cursor_node.parent or not tree.cursor_node.parent.data:
|
|
655
|
-
self.app.show_error_message(
|
|
664
|
+
self.app.show_error_message(ErrorMessages.COULD_NOT_DETERMINE_SOURCE_POOL)
|
|
656
665
|
return
|
|
657
666
|
source_pool_name = tree.cursor_node.parent.data.get('name')
|
|
658
667
|
|
|
@@ -708,7 +717,7 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
708
717
|
self._load_storage_pools()
|
|
709
718
|
|
|
710
719
|
return {
|
|
711
|
-
"message":
|
|
720
|
+
"message": SuccessMessages.VOLUME_MOVED_SUCCESSFULLY_TEMPLATE.format(volume_name=volume_name, dest_pool_name=dest_pool_name),
|
|
712
721
|
"source_pool": source_pool_name,
|
|
713
722
|
"dest_pool": dest_pool_name,
|
|
714
723
|
"updated_vms": updated_vms
|
|
@@ -750,10 +759,10 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
750
759
|
updated_vms = result.get("updated_vms", [])
|
|
751
760
|
if updated_vms:
|
|
752
761
|
vm_list = ", ".join(updated_vms)
|
|
753
|
-
self.app.show_success_message(
|
|
762
|
+
self.app.show_success_message(SuccessMessages.UPDATED_VM_CONFIGURATIONS_TEMPLATE.format(vm_list=vm_list))
|
|
754
763
|
self._load_storage_pools(expand_pools=[result["source_pool"], result["dest_pool"]])
|
|
755
764
|
elif event.worker.state == "ERROR":
|
|
756
|
-
self.app.show_error_message(
|
|
765
|
+
self.app.show_error_message(ErrorMessages.MOVE_OPERATION_FAILED_TEMPLATE.format(error=event.worker.error))
|
|
757
766
|
self._load_storage_pools()
|
|
758
767
|
|
|
759
768
|
@on(DataTable.RowSelected, "#networks-table")
|
|
@@ -786,10 +795,12 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
786
795
|
if net_info:
|
|
787
796
|
try:
|
|
788
797
|
set_network_active(self.conn, net_name, not net_info['active'])
|
|
789
|
-
|
|
798
|
+
status = 'inactive' if net_info['active'] else 'active'
|
|
799
|
+
self.app.show_success_message(SuccessMessages.NETWORK_ACTIVATION_CHANGE_TEMPLATE.format(net_name=net_name, status=status))
|
|
800
|
+
list_networks.cache_clear()
|
|
790
801
|
self._load_networks()
|
|
791
802
|
except Exception as e:
|
|
792
|
-
self.app.show_error_message(
|
|
803
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE_XML.format(error=e))
|
|
793
804
|
|
|
794
805
|
@on(Button.Pressed, "#toggle-net-autostart-btn")
|
|
795
806
|
def on_toggle_net_autostart_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -804,10 +815,12 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
804
815
|
if net_info:
|
|
805
816
|
try:
|
|
806
817
|
set_network_autostart(self.conn, net_name, not net_info['autostart'])
|
|
807
|
-
|
|
818
|
+
status = 'off' if net_info['autostart'] else 'on'
|
|
819
|
+
self.app.show_success_message(SuccessMessages.NETWORK_AUTOSTART_CHANGE_TEMPLATE.format(net_name=net_name, status=status))
|
|
820
|
+
list_networks.cache_clear()
|
|
808
821
|
self._load_networks()
|
|
809
822
|
except Exception as e:
|
|
810
|
-
self.app.show_error_message(
|
|
823
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE_XML.format(error=e))
|
|
811
824
|
|
|
812
825
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
813
826
|
if event.button.id == "close-btn":
|
|
@@ -822,15 +835,15 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
822
835
|
try:
|
|
823
836
|
conn = self.conn
|
|
824
837
|
if conn is None:
|
|
825
|
-
self.app.show_error_message(
|
|
838
|
+
self.app.show_error_message(ErrorMessages.NOT_CONNECTED_TO_LIBVIRT)
|
|
826
839
|
return
|
|
827
840
|
net = conn.networkLookupByName(network_name)
|
|
828
841
|
network_xml = net.XMLDesc(0)
|
|
829
842
|
self.app.push_screen(NetworkXMLModal(network_name, network_xml))
|
|
830
843
|
except libvirt.libvirtError as e:
|
|
831
|
-
self.app.show_error_message(
|
|
844
|
+
self.app.show_error_message(ErrorMessages.ERROR_GETTING_NETWORK_XML_TEMPLATE.format(error=e))
|
|
832
845
|
except Exception as e:
|
|
833
|
-
self.app.show_error_message(
|
|
846
|
+
self.app.show_error_message(ErrorMessages.UNEXPECTED_ERROR_OCCURRED_TEMPLATE_XML.format(error=e))
|
|
834
847
|
|
|
835
848
|
elif event.button.id == "edit-net-btn":
|
|
836
849
|
table = self.query_one("#networks-table", DataTable)
|
|
@@ -842,17 +855,19 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
842
855
|
|
|
843
856
|
network_info = get_network_info(self.conn, network_name)
|
|
844
857
|
if not network_info:
|
|
845
|
-
self.app.show_error_message(
|
|
858
|
+
self.app.show_error_message(ErrorMessages.COULD_NOT_RETRIEVE_NETWORK_INFO_TEMPLATE.format(network_name=network_name))
|
|
846
859
|
return
|
|
847
860
|
|
|
848
861
|
def on_create(success: bool):
|
|
849
862
|
if success:
|
|
863
|
+
list_networks.cache_clear()
|
|
850
864
|
self._load_networks()
|
|
851
865
|
self.app.push_screen(AddEditNetworkModal(self.conn, network_info=network_info), on_create)
|
|
852
866
|
|
|
853
867
|
elif event.button.id == "add-net-btn":
|
|
854
868
|
def on_create(success: bool):
|
|
855
869
|
if success:
|
|
870
|
+
list_networks.cache_clear()
|
|
856
871
|
self._load_networks()
|
|
857
872
|
self.app.push_screen(AddEditNetworkModal(self.conn), on_create)
|
|
858
873
|
|
|
@@ -865,19 +880,20 @@ class ServerPrefModal(BaseModal[None]):
|
|
|
865
880
|
network_name = row_key.value
|
|
866
881
|
vms_using_network = get_vms_using_network(self.conn, network_name)
|
|
867
882
|
|
|
868
|
-
confirm_message =
|
|
883
|
+
confirm_message = ErrorMessages.DELETE_NETWORK_CONFIRM_TEMPLATE.format(network_name=network_name)
|
|
869
884
|
if vms_using_network:
|
|
870
885
|
vm_list = ", ".join(vms_using_network)
|
|
871
|
-
confirm_message +=
|
|
886
|
+
confirm_message += ErrorMessages.NETWORK_IN_USE_WARNING_TEMPLATE.format(vm_list=vm_list)
|
|
872
887
|
|
|
873
888
|
def on_confirm(confirmed: bool) -> None:
|
|
874
889
|
if confirmed:
|
|
875
890
|
try:
|
|
876
891
|
delete_network(self.conn, network_name)
|
|
877
|
-
self.app.show_success_message(
|
|
892
|
+
self.app.show_success_message(SuccessMessages.NETWORK_DELETED_SUCCESSFULLY_TEMPLATE.format(network_name=network_name))
|
|
893
|
+
list_networks.cache_clear()
|
|
878
894
|
self._load_networks()
|
|
879
895
|
except Exception as e:
|
|
880
|
-
self.app.show_error_message(
|
|
896
|
+
self.app.show_error_message(ErrorMessages.ERROR_DELETING_NETWORK_TEMPLATE.format(network_name=network_name, error=e))
|
|
881
897
|
|
|
882
898
|
self.app.push_screen(
|
|
883
899
|
ConfirmationDialog(confirm_message), on_confirm
|
vmanager/modals/utils_modals.py
CHANGED
|
@@ -20,6 +20,7 @@ from textual.widgets import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from .base_modals import BaseDialog, BaseModal
|
|
23
|
+
from ..constants import ButtonLabels
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
def _sanitize_message(message: str) -> str:
|
|
@@ -93,11 +94,11 @@ class DirectorySelectionModal(BaseModal[str | None]):
|
|
|
93
94
|
|
|
94
95
|
def compose(self) -> ComposeResult:
|
|
95
96
|
with Vertical(id="directory-selection-dialog"):
|
|
96
|
-
yield Label(
|
|
97
|
+
yield Label(StaticText.SELECT_A_DIRECTORY)
|
|
97
98
|
yield SafeDirectoryTree(self.start_path, id="dir-tree")
|
|
98
99
|
with Horizontal():
|
|
99
|
-
yield Button(
|
|
100
|
-
yield Button(
|
|
100
|
+
yield Button(ButtonLabels.SELECT, variant="primary", id="select-btn", disabled=True)
|
|
101
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
101
102
|
|
|
102
103
|
def on_mount(self) -> None:
|
|
103
104
|
self.query_one(SafeDirectoryTree).focus()
|
|
@@ -126,11 +127,11 @@ class FileSelectionModal(BaseModal[str | None]):
|
|
|
126
127
|
|
|
127
128
|
def compose(self) -> ComposeResult:
|
|
128
129
|
with Vertical(id="file-selection-dialog", classes="file-selection-dialog"):
|
|
129
|
-
yield Label(
|
|
130
|
+
yield Label(StaticText.SELECT_A_FILE)
|
|
130
131
|
yield SafeDirectoryTree(self.start_path, id="file-tree")
|
|
131
132
|
with Horizontal():
|
|
132
|
-
yield Button(
|
|
133
|
-
yield Button(
|
|
133
|
+
yield Button(ButtonLabels.SELECT, variant="primary", id="select-btn", disabled=True)
|
|
134
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
134
135
|
|
|
135
136
|
def on_mount(self) -> None:
|
|
136
137
|
self.query_one(SafeDirectoryTree).focus()
|
|
@@ -207,8 +208,8 @@ class ConfirmationDialog(BaseDialog[bool]):
|
|
|
207
208
|
yield Vertical(
|
|
208
209
|
Markdown(self.prompt, id="question"),
|
|
209
210
|
Horizontal(
|
|
210
|
-
Button(
|
|
211
|
-
Button(
|
|
211
|
+
Button(ButtonLabels.YES, variant="error", id="yes", classes="dialog-buttons"),
|
|
212
|
+
Button(ButtonLabels.NO, variant="primary", id="no", classes="dialog-buttons"),
|
|
212
213
|
id="dialog-buttons",
|
|
213
214
|
),
|
|
214
215
|
id="dialog",
|
|
@@ -237,7 +238,7 @@ class InfoModal(BaseModal[None]):
|
|
|
237
238
|
yield Label(self._title, classes="dialog-title")
|
|
238
239
|
yield Markdown(self._message, classes="dialog-message")
|
|
239
240
|
with Horizontal(classes="dialog-buttons"):
|
|
240
|
-
yield Button(
|
|
241
|
+
yield Button(ButtonLabels.OK, variant="primary", id="ok-btn")
|
|
241
242
|
|
|
242
243
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
243
244
|
self.dismiss()
|
vmanager/modals/virsh_modals.py
CHANGED
|
@@ -11,6 +11,7 @@ from textual.widgets import (
|
|
|
11
11
|
from textual.containers import Horizontal, Vertical
|
|
12
12
|
from textual.screen import ModalScreen
|
|
13
13
|
from textual import on
|
|
14
|
+
from ..constants import StaticText
|
|
14
15
|
|
|
15
16
|
class VirshShellScreen(ModalScreen):
|
|
16
17
|
"""Screen for an interactive virsh shell."""
|
|
@@ -26,8 +27,8 @@ class VirshShellScreen(ModalScreen):
|
|
|
26
27
|
def compose(self) -> ComposeResult:
|
|
27
28
|
with Vertical(id="virsh-shell-container"):
|
|
28
29
|
yield Header()
|
|
29
|
-
yield Label(
|
|
30
|
-
yield Label(
|
|
30
|
+
yield Label(StaticText.VIRSH_SHELL_TITLE, id="virsh-shell-title")
|
|
31
|
+
yield Label(StaticText.VIRSH_SHELL_NOTE, classes="virsh-shell-note")
|
|
31
32
|
yield TextArea(
|
|
32
33
|
id="virsh-output",
|
|
33
34
|
read_only=True,
|
|
@@ -6,6 +6,7 @@ from textual.containers import Horizontal, Vertical
|
|
|
6
6
|
from textual.widgets import (
|
|
7
7
|
Button, Input, Label, Checkbox,
|
|
8
8
|
)
|
|
9
|
+
from ..constants import ErrorMessages, ButtonLabels, StaticText
|
|
9
10
|
from .base_modals import BaseModal
|
|
10
11
|
|
|
11
12
|
class AddEditVirtIOFSModal(BaseModal[dict | None]):
|
|
@@ -20,13 +21,13 @@ class AddEditVirtIOFSModal(BaseModal[dict | None]):
|
|
|
20
21
|
|
|
21
22
|
def compose(self) -> ComposeResult:
|
|
22
23
|
with Vertical(id="add-edit-virtiofs-dialog"):
|
|
23
|
-
yield Label(
|
|
24
|
+
yield Label(StaticText.EDIT_VIRTIOFS_MOUNT if self.is_edit else StaticText.ADD_VIRTIOFS_MOUNT)
|
|
24
25
|
yield Input(placeholder="Source Path (e.g., /mnt/share)", id="virtiofs-source-input", value=self.source_path)
|
|
25
26
|
yield Input(placeholder="Target Path (e.g., /share)", id="virtiofs-target-input", value=self.target_path)
|
|
26
|
-
yield Checkbox(
|
|
27
|
+
yield Checkbox(StaticText.EXPORT_READONLY_MOUNT, id="virtiofs-readonly-checkbox", value=self.readonly)
|
|
27
28
|
with Horizontal():
|
|
28
|
-
yield Button(
|
|
29
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.SAVE if self.is_edit else ButtonLabels.ADD, variant="primary", id="save-add-btn", classes="Buttonpage")
|
|
30
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn", classes="Buttonpage")
|
|
30
31
|
|
|
31
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
32
33
|
if event.button.id == "save-add-btn":
|
|
@@ -35,7 +36,7 @@ class AddEditVirtIOFSModal(BaseModal[dict | None]):
|
|
|
35
36
|
readonly = self.query_one("#virtiofs-readonly-checkbox", Checkbox).value
|
|
36
37
|
|
|
37
38
|
if not source_path or not target_path:
|
|
38
|
-
self.app.show_error_message(
|
|
39
|
+
self.app.show_error_message(ErrorMessages.VIRTIOFS_PATH_EMPTY)
|
|
39
40
|
return
|
|
40
41
|
|
|
41
42
|
self.dismiss({'source_path': source_path, 'target_path': target_path, 'readonly': readonly})
|
|
@@ -6,6 +6,7 @@ from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
|
6
6
|
from textual.widgets import Button, Markdown
|
|
7
7
|
from textual import on
|
|
8
8
|
from .base_modals import BaseModal
|
|
9
|
+
from ..constants import ButtonLabels
|
|
9
10
|
|
|
10
11
|
VM_TYPE_INFO_TEXT = """
|
|
11
12
|
| [Storage Settings](https://www.qemu.org/docs/master/system/qemu-block-drivers.html) | Secure VM | Computation | Desktop (Linux) | Windows | Win Legacy | Server |
|
|
@@ -55,7 +56,7 @@ class VMTypeInfoModal(BaseModal[None]):
|
|
|
55
56
|
def compose(self) -> ComposeResult:
|
|
56
57
|
with ScrollableContainer(id="howto-vmtype-dialog", classes="howto-dialog"):
|
|
57
58
|
yield Markdown(VM_TYPE_INFO_TEXT, id="howto-vmtype-markdown")
|
|
58
|
-
yield Button(
|
|
59
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
59
60
|
|
|
60
61
|
@on(Button.Pressed)
|
|
61
62
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|