virtui-manager 1.1.6__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.6.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 +2 -1
- vmanager/modals/howto_network_modal.py +2 -1
- vmanager/modals/howto_overlay_modal.py +2 -1
- vmanager/modals/howto_ssh_modal.py +2 -1
- vmanager/modals/howto_virtiofs_modal.py +2 -1
- vmanager/modals/input_modals.py +11 -10
- vmanager/modals/log_modal.py +2 -1
- vmanager/modals/migration_modals.py +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 +78 -71
- vmanager/modals/utils_modals.py +10 -9
- vmanager/modals/virsh_modals.py +3 -2
- vmanager/modals/virtiofs_modals.py +6 -5
- vmanager/modals/vm_type_info_modal.py +2 -1
- vmanager/modals/vmanager_modals.py +19 -19
- vmanager/modals/vmcard_dialog.py +57 -57
- vmanager/modals/vmdetails_modals.py +115 -123
- vmanager/modals/xml_modals.py +3 -2
- vmanager/network_manager.py +4 -1
- vmanager/storage_manager.py +157 -39
- vmanager/utils.py +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.6.dist-info/RECORD +0 -65
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/WHEEL +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/entry_points.txt +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {virtui_manager-1.1.6.dist-info → virtui_manager-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -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:
|
|
@@ -8,6 +8,7 @@ from textual.containers import Horizontal, Vertical, VerticalScroll, Grid
|
|
|
8
8
|
from textual.widgets import Button, Input, Label, DataTable, Checkbox
|
|
9
9
|
from textual import on
|
|
10
10
|
from .base_modals import BaseModal
|
|
11
|
+
from ..constants import StaticText, ButtonLabels
|
|
11
12
|
|
|
12
13
|
class PatternSelectModal(BaseModal[set[str] | None]):
|
|
13
14
|
"""Modal for selecting VMs by pattern across servers."""
|
|
@@ -23,7 +24,7 @@ class PatternSelectModal(BaseModal[set[str] | None]):
|
|
|
23
24
|
|
|
24
25
|
def compose(self) -> ComposeResult:
|
|
25
26
|
with Vertical(id="pattern-select-container", classes="modal-container"):
|
|
26
|
-
yield Label(
|
|
27
|
+
yield Label(StaticText.SELECT_VMS_BY_PATTERN, id="pattern-select-title")
|
|
27
28
|
|
|
28
29
|
with Horizontal(classes="pattern-input-row"):
|
|
29
30
|
yield Input(
|
|
@@ -31,7 +32,7 @@ class PatternSelectModal(BaseModal[set[str] | None]):
|
|
|
31
32
|
id="pattern-input",
|
|
32
33
|
restrict=r"[a-zA-Z0-9_\-\*\?\.\^\|\$\( \[ \] \+\{\}\\]*"
|
|
33
34
|
)
|
|
34
|
-
yield Checkbox(
|
|
35
|
+
yield Checkbox(StaticText.REGEX, id="regex-checkbox")
|
|
35
36
|
|
|
36
37
|
if self.available_servers:
|
|
37
38
|
#yield Label("Search in Servers:")
|
|
@@ -48,15 +49,15 @@ class PatternSelectModal(BaseModal[set[str] | None]):
|
|
|
48
49
|
grid.styles.grid_gutter_horizontal = 0
|
|
49
50
|
yield grid
|
|
50
51
|
|
|
51
|
-
yield Button(
|
|
52
|
+
yield Button(ButtonLabels.SEARCH_VMS, variant="primary", id="search-vms-btn")
|
|
52
53
|
|
|
53
|
-
yield Label(
|
|
54
|
+
yield Label(StaticText.MATCHING_VMS, id="results-label")
|
|
54
55
|
with VerticalScroll(id="results-container"):
|
|
55
56
|
yield DataTable(id="results-table", cursor_type="row")
|
|
56
57
|
|
|
57
58
|
with Horizontal(id="pattern-action-buttons"):
|
|
58
|
-
yield Button(
|
|
59
|
-
yield Button(
|
|
59
|
+
yield Button(ButtonLabels.SELECT_MATCHING, variant="success", id="select-btn", disabled=True)
|
|
60
|
+
yield Button(ButtonLabels.CANCEL, variant="error", id="cancel-btn")
|
|
60
61
|
|
|
61
62
|
def on_mount(self) -> None:
|
|
62
63
|
table = self.query_one("#results-table", DataTable)
|
vmanager/modals/server_modals.py
CHANGED
|
@@ -11,19 +11,20 @@ from .howto_ssh_modal import HowToSSHModal
|
|
|
11
11
|
from .base_modals import BaseModal
|
|
12
12
|
|
|
13
13
|
from ..config import save_config
|
|
14
|
+
from ..constants import ErrorMessages, SuccessMessages, ButtonLabels, StaticText
|
|
14
15
|
|
|
15
16
|
class ConnectionModal(BaseModal[str | None]):
|
|
16
17
|
|
|
17
18
|
def compose(self) -> ComposeResult:
|
|
18
19
|
with Vertical(id="connection-dialog"):
|
|
19
|
-
yield Label(
|
|
20
|
+
yield Label(StaticText.ENTER_QEMU_CONNECTION_URI)
|
|
20
21
|
yield Input(
|
|
21
22
|
placeholder="qemu+ssh://user@host/system or qemu:///system",
|
|
22
23
|
id="uri-input",
|
|
23
24
|
)
|
|
24
25
|
with Horizontal():
|
|
25
|
-
yield Button(
|
|
26
|
-
yield Button(
|
|
26
|
+
yield Button(ButtonLabels.CONNECT, variant="primary", id="connect-btn", classes="Buttonpage")
|
|
27
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn", classes="Buttonpage")
|
|
27
28
|
|
|
28
29
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
29
30
|
if event.button.id == "connect-btn":
|
|
@@ -37,14 +38,14 @@ class AddServerModal(BaseModal[Tuple[str, str] | None]):
|
|
|
37
38
|
"""Modal for adding a new server with autoconnect option."""
|
|
38
39
|
def compose(self) -> ComposeResult:
|
|
39
40
|
with Vertical(id="add-server-dialog"):
|
|
40
|
-
yield Label(
|
|
41
|
+
yield Label(StaticText.ADD_NEW_SERVER)
|
|
41
42
|
yield Input(placeholder="Server Name", id="server-name-input")
|
|
42
43
|
yield Input(placeholder="qemu+ssh://user@host/system", id="server-uri-input")
|
|
43
|
-
yield Label(
|
|
44
|
-
yield Checkbox(
|
|
44
|
+
yield Label(StaticText.EMPTY_LABEL)
|
|
45
|
+
yield Checkbox(StaticText.AUTOCONNECT_AT_STARTUP, id="autoconnect-checkbox", value=False)
|
|
45
46
|
with Horizontal():
|
|
46
|
-
yield Button(
|
|
47
|
-
yield Button(
|
|
47
|
+
yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn", classes="Buttonpage")
|
|
48
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn", classes="Buttonpage")
|
|
48
49
|
|
|
49
50
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
50
51
|
if event.button.id == "save-btn":
|
|
@@ -69,14 +70,14 @@ class EditServerModal(BaseModal[Tuple[str, str, bool] | None]):
|
|
|
69
70
|
|
|
70
71
|
def compose(self) -> ComposeResult:
|
|
71
72
|
with Vertical(id="edit-server-dialog"):
|
|
72
|
-
yield Label(
|
|
73
|
+
yield Label(StaticText.EDIT_SERVER)
|
|
73
74
|
yield Input(value=self.server_name, id="server-name-input")
|
|
74
75
|
yield Input(value=self.server_uri, id="server-uri-input")
|
|
75
|
-
yield Label(
|
|
76
|
-
yield Checkbox(
|
|
76
|
+
yield Label(StaticText.EMPTY_LABEL)
|
|
77
|
+
yield Checkbox(StaticText.AUTOCONNECT_AT_STARTUP, id="autoconnect-checkbox", value=self.autoconnect)
|
|
77
78
|
with Horizontal():
|
|
78
|
-
yield Button(
|
|
79
|
-
yield Button(
|
|
79
|
+
yield Button(ButtonLabels.SAVE, variant="primary", id="save-btn", classes="Buttonpage")
|
|
80
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn", classes="Buttonpage")
|
|
80
81
|
|
|
81
82
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
82
83
|
if event.button.id == "save-btn":
|
|
@@ -102,18 +103,18 @@ class ServerManagementModal(BaseModal [str | None]):
|
|
|
102
103
|
|
|
103
104
|
def compose(self) -> ComposeResult:
|
|
104
105
|
with Vertical(id="server-management-dialog"): #, classes="info-details"):
|
|
105
|
-
yield Label(
|
|
106
|
+
yield Label(StaticText.SERVER_LIST_MANAGEMENT) #, id="server-list-title")
|
|
106
107
|
with ScrollableContainer(classes="info-details"):
|
|
107
108
|
yield DataTable(id="server-table", classes="server-list")
|
|
108
109
|
with Vertical(classes="server-list"):
|
|
109
110
|
with Horizontal():
|
|
110
|
-
yield Button(
|
|
111
|
-
yield Button(
|
|
112
|
-
yield Button(
|
|
111
|
+
yield Button(ButtonLabels.ADD, id="add-server-btn", classes="add-button", variant="success")
|
|
112
|
+
yield Button(ButtonLabels.EDIT, id="edit-server-btn", disabled=True, classes="edit-button")
|
|
113
|
+
yield Button(ButtonLabels.DELETE, id="delete-server-btn", disabled=True,)
|
|
113
114
|
with Horizontal():
|
|
114
|
-
yield Button(
|
|
115
|
-
yield Button(
|
|
116
|
-
yield Button(
|
|
115
|
+
yield Button(ButtonLabels.CONNECT, id="select-btn", variant="primary", disabled=True, classes="Buttonpage")
|
|
116
|
+
yield Button(ButtonLabels.CUSTOM_URL, id="custom-conn-btn", classes="Buttonpage")
|
|
117
|
+
yield Button(ButtonLabels.SSH_HELP, id="ssh-help-btn", classes="Buttonpage")
|
|
117
118
|
#yield Button("Close", id="close-btn", classes="close-button")
|
|
118
119
|
|
|
119
120
|
def on_mount(self) -> None:
|
|
@@ -193,13 +194,13 @@ class ServerManagementModal(BaseModal [str | None]):
|
|
|
193
194
|
self.query_one("#edit-server-btn").disabled = True
|
|
194
195
|
self.query_one("#delete-server-btn").disabled = True
|
|
195
196
|
self.query_one("#select-btn").disabled = True
|
|
196
|
-
self.app.show_success_message(
|
|
197
|
+
self.app.show_success_message(SuccessMessages.SERVER_DELETED_TEMPLATE.format(server_name=server_name_to_delete))
|
|
197
198
|
logging.info(f"Successfully deleted Server '{server_name_to_delete}'")
|
|
198
199
|
except Exception as e:
|
|
199
|
-
self.app.show_error_message(
|
|
200
|
+
self.app.show_error_message(ErrorMessages.ERROR_DELETING_SERVER_TEMPLATE.format(server_name=server_name_to_delete, error=e))
|
|
200
201
|
|
|
201
202
|
self.app.push_screen(
|
|
202
|
-
ConfirmationDialog(
|
|
203
|
+
ConfirmationDialog(ErrorMessages.DELETE_SERVER_CONFIRMATION_TEMPLATE.format(server_name=server_name_to_delete)), on_confirm)
|
|
203
204
|
|
|
204
205
|
elif event.button.id == "custom-conn-btn":
|
|
205
206
|
def connection_callback(uri: str | None):
|