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.
Files changed (57) hide show
  1. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/METADATA +1 -1
  2. virtui_manager-1.3.0.dist-info/RECORD +73 -0
  3. vmanager/constants.py +737 -108
  4. vmanager/dialog.css +24 -0
  5. vmanager/firmware_manager.py +4 -1
  6. vmanager/i18n.py +32 -0
  7. vmanager/libvirt_utils.py +132 -3
  8. vmanager/locales/de/LC_MESSAGES/virtui-manager.po +3012 -0
  9. vmanager/locales/fr/LC_MESSAGES/virtui-manager.mo +0 -0
  10. vmanager/locales/fr/LC_MESSAGES/virtui-manager.po +3124 -0
  11. vmanager/locales/it/LC_MESSAGES/virtui-manager.po +3012 -0
  12. vmanager/locales/virtui-manager.pot +3012 -0
  13. vmanager/modals/bulk_modals.py +13 -12
  14. vmanager/modals/cache_stats_modal.py +6 -5
  15. vmanager/modals/capabilities_modal.py +133 -0
  16. vmanager/modals/config_modal.py +25 -24
  17. vmanager/modals/cpu_mem_pc_modals.py +22 -21
  18. vmanager/modals/custom_migration_modal.py +10 -9
  19. vmanager/modals/disk_pool_modals.py +60 -59
  20. vmanager/modals/host_dashboard_modal.py +137 -0
  21. vmanager/modals/howto_disk_modal.py +13 -72
  22. vmanager/modals/howto_network_modal.py +13 -39
  23. vmanager/modals/howto_overlay_modal.py +13 -52
  24. vmanager/modals/howto_ssh_modal.py +12 -67
  25. vmanager/modals/howto_virtiofs_modal.py +13 -64
  26. vmanager/modals/input_modals.py +11 -10
  27. vmanager/modals/log_modal.py +2 -1
  28. vmanager/modals/migration_modals.py +20 -18
  29. vmanager/modals/network_modals.py +45 -36
  30. vmanager/modals/provisioning_modals.py +56 -56
  31. vmanager/modals/select_server_modals.py +8 -7
  32. vmanager/modals/selection_modals.py +7 -6
  33. vmanager/modals/server_modals.py +24 -23
  34. vmanager/modals/server_prefs_modals.py +103 -87
  35. vmanager/modals/utils_modals.py +10 -9
  36. vmanager/modals/virsh_modals.py +3 -2
  37. vmanager/modals/virtiofs_modals.py +6 -5
  38. vmanager/modals/vm_type_info_modal.py +2 -1
  39. vmanager/modals/vmanager_modals.py +19 -19
  40. vmanager/modals/vmcard_dialog.py +57 -57
  41. vmanager/modals/vmdetails_modals.py +115 -123
  42. vmanager/modals/xml_modals.py +3 -2
  43. vmanager/network_manager.py +4 -1
  44. vmanager/storage_manager.py +182 -42
  45. vmanager/utils.py +39 -6
  46. vmanager/vm_actions.py +28 -24
  47. vmanager/vm_queries.py +67 -25
  48. vmanager/vm_service.py +8 -5
  49. vmanager/vmanager.css +46 -0
  50. vmanager/vmanager.py +178 -112
  51. vmanager/vmcard.py +161 -159
  52. vmanager/webconsole_manager.py +21 -21
  53. virtui_manager-1.1.5.dist-info/RECORD +0 -65
  54. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/WHEEL +0 -0
  55. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/entry_points.txt +0 -0
  56. {virtui_manager-1.1.5.dist-info → virtui_manager-1.3.0.dist-info}/licenses/LICENSE +0 -0
  57. {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 AppCacheTimeout
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("Server Preferences", id="server-pref-title")
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("Not connected to any server.")
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("Multiple servers active but none selected for preferences.")
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(f"Failed to get connection for server preferences on {uri_to_connect}.")
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("De/Active", id="toggle-net-active-btn", classes="toggle-detail-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("Autostart", id="toggle-net-autostart-btn", classes="toggle-detail-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("Add", id="add-net-btn", variant="success", classes="toggle-detail-button",
116
+ Button(ButtonLabels.ADD, id="add-net-btn", variant="success", classes="toggle-detail-button",
113
117
  tooltip="Add a new network."),
114
- Button("Edit", id="edit-net-btn", variant="success", classes="toggle-detail-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("View", id="view-net-btn", variant="success", classes="toggle-detail-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("Delete", id="delete-net-btn", variant="error", classes="toggle-detail-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("Help", id="help-net-btn", variant="success", classes="toggle-detail-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(f"More than {AppCacheTimeout.DONT_DISPLAY_DISK_USAGE} VMs detected on this server.\nSkipping disk usage scan to prevent UI freeze.")
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(f"Runing disk usage scan, this can freeze the UI for larger numbers of VMs and disks.")
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("Add Pool", id="add-pool-btn", variant="success", classes="toggle-detail-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("Delete Pool", id="del-pool-btn", variant="error", classes="toggle-detail-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("New Volume", id="add-vol-btn", variant="success", classes="toggle-detail-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("Attach Vol", id="attach-vol-btn", variant="success", classes="toggle-detail-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("Move Volume", id="move-vol-btn", variant="success", classes="toggle-detail-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("Delete Volume", id="del-vol-btn", variant="error", classes="toggle-detail-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("View XML", id="view-storage-xml-btn", variant="primary", classes="toggle-detail-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("Edit XML", id="edit-pool-xml-btn", variant="primary", classes="toggle-detail-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
- pool_name = pool_data['name']
252
- status = pool_data['status']
253
- autostart = "autostart" if pool_data['autostart'] else "no autostart"
254
- label = f"{pool_name} [{status}, {autostart}]"
255
- pool_node = tree.root.add(label, data=pool_data)
256
- pool_node.data["type"] = "pool"
257
- # Add a dummy node to make the pool node expandable
258
- pool_node.add_leaf("Loading volumes...")
259
-
260
- if expand_pools and pool_name in expand_pools:
261
- self.app.call_later(pool_node.expand)
262
-
263
- if select_pool and pool_name == select_pool:
264
- node_to_select = pool_node
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(f"Runing network usage scan, this can freeze the UI for larger numbers of VMs.")
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
- if pool and pool.isActive():
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("Could not find object to display XML for.")
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(f"Error getting XML for {node_type}: {e}")
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("Could not find pool object to edit.")
417
+ self.app.show_error_message(ErrorMessages.COULD_NOT_FIND_POOL_EDIT)
405
418
  return
406
419
 
407
- if pool.isActive():
408
- self.app.show_error_message("Pool must be inactive to edit its XML definition.")
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(f"Error getting XML for pool: {e}")
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(f"Storage pool '{pool.name()}' updated successfully.")
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(f"Error updating pool XML: {e}")
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(f"An unexpected error occurred: {e}")
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
- self.app.show_success_message(f"Pool '{pool.name()}' is now {'inactive' if is_active else 'active'}.")
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(str(e))
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
- self.app.show_success_message(f"Autostart for pool '{pool.name()}' is now {'off' if has_autostart else 'on'}.")
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(str(e))
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(f"Volume [b]{result['name']} {result['size_gb']}Gb {result['format']}[/b] created successfully.")
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(f"Invalid or non-existent path: {volume_path}")
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
- f"A pool named '{existing_pool.name()}' already manages this directory.\n"
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(f"Error refreshing pool: {e}")
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
- f"Storage pool '{new_pool_name}' created for directory:\n{volume_dir}"
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(f"Error creating storage pool: {e}")
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
- f"No storage pool exists for the directory:\n'{volume_dir}'.\n\n"
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(f"Storage pool '{pool_name}' deleted successfully.")
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(str(e))
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(f"Are you sure you want to delete storage pool:\n' {pool_name}'\nThis will delete the pool definition but not the data on it."), on_confirm)
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(f"Volume '{vol_name}' deleted successfully.")
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(str(e))
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(f"Are you sure you want to delete volume:\n'{vol_name}'"),
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("Could not determine the source pool.")
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": f"Volume '{volume_name}' moved to pool '{dest_pool_name}'.",
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(f"Updated VM configurations for: {vm_list}")
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(f"Move operation failed: {event.worker.error}")
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
- self.app.show_success_message(f"Network '{net_name}' is now {'inactive' if net_info['active'] else 'active'}.")
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(str(e))
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
- self.app.show_success_message(f"Autostart for network '{net_name}' is now {'off' if net_info['autostart'] else 'on'}.")
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(str(e))
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("Not connected to libvirt.")
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(f"Error getting network XML: {e}")
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(f"An unexpected error occurred: {e}")
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(f"Could not retrieve info for network '{network_name}'.")
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 = f"Are you sure you want to delete network:\n'{network_name}'"
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 += f"\nThis network is currently in use by the following VMs:\n{vm_list}."
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(f"Network '{network_name}' deleted successfully.")
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(f"Error deleting network '{network_name}': {e}")
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
@@ -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("Select a Directory")
97
+ yield Label(StaticText.SELECT_A_DIRECTORY)
97
98
  yield SafeDirectoryTree(self.start_path, id="dir-tree")
98
99
  with Horizontal():
99
- yield Button("Select", variant="primary", id="select-btn", disabled=True)
100
- yield Button("Cancel", variant="default", id="cancel-btn")
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("Select a File")
130
+ yield Label(StaticText.SELECT_A_FILE)
130
131
  yield SafeDirectoryTree(self.start_path, id="file-tree")
131
132
  with Horizontal():
132
- yield Button("Select", variant="primary", id="select-btn", disabled=True)
133
- yield Button("Cancel", variant="default", id="cancel-btn")
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("Yes", variant="error", id="yes", classes="dialog-buttons"),
211
- Button("No", variant="primary", id="no", classes="dialog-buttons"),
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("OK", variant="primary", id="ok-btn")
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()
@@ -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("Virsh Interactive Shell (esc to quit)", id="virsh-shell-title")
30
- yield Label("Note: This shell starts a new `virsh` process, which may require re-authentication for remote connections. Using SSH keys is recommended.", classes="virsh-shell-note")
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("Edit VirtIO-FS Mount" if self.is_edit else "Add VirtIO-FS Mount")
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("Export filesystem as readonly mount", id="virtiofs-readonly-checkbox", value=self.readonly)
27
+ yield Checkbox(StaticText.EXPORT_READONLY_MOUNT, id="virtiofs-readonly-checkbox", value=self.readonly)
27
28
  with Horizontal():
28
- yield Button("Save" if self.is_edit else "Add", variant="primary", id="save-add-btn", classes="Buttonpage")
29
- yield Button("Cancel", variant="default", id="cancel-btn", classes="Buttonpage")
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("Source Path and Target Path cannot be empty.")
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("Close", id="close-btn", variant="primary")
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: