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
|
@@ -1,58 +1,32 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Modal to show how to configure networks.
|
|
3
3
|
"""
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from textual.app import ComposeResult
|
|
5
7
|
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
6
8
|
from textual.widgets import Button, Markdown
|
|
7
9
|
from textual import on
|
|
10
|
+
from ..constants import ButtonLabels
|
|
8
11
|
from .base_modals import BaseModal
|
|
9
12
|
|
|
10
|
-
HOW_TO_NETWORK_TEXT = """
|
|
11
|
-
# Understanding Network Configuration in libvirt
|
|
12
|
-
|
|
13
|
-
libvirt provides flexible networking capabilities for virtual machines, allowing them to communicate with each other, the host, and external networks.
|
|
14
|
-
|
|
15
|
-
### Types of Networks
|
|
16
|
-
|
|
17
|
-
1. **NAT (Network Address Translation) Network:**
|
|
18
|
-
* **Purpose:** Allows VMs to access the external network (internet) but prevents external machines from directly initiating connections to the VMs.
|
|
19
|
-
* **Mechanism:** libvirt creates a virtual bridge (e.g., `virbr0`) on the host, and VMs connect to this bridge. The host acts as a router, performing NAT for outgoing connections from VMs. VMs get IP addresses from a DHCP server managed by libvirt.
|
|
20
|
-
* **Use Cases:** Most common setup for general VM usage where VMs just need internet access.
|
|
21
|
-
|
|
22
|
-
2. **Routed Network:**
|
|
23
|
-
* **Purpose:** VMs can communicate with other machines on the host's physical network, and potentially external networks, with proper routing configured on the host and potentially external routers. VMs will have IP addresses on the same subnet as the host's physical network, or a dedicated routed subnet.
|
|
24
|
-
* **Mechanism:** Similar to NAT, a virtual bridge is used, but without NAT. The host needs to be configured to route traffic between the virtual bridge and the physical interface. VMs typically get IP addresses from a DHCP server on the physical network or static IPs.
|
|
25
|
-
* **Use Cases:** When VMs need to be directly accessible from the physical network, or participate as full members of an existing network.
|
|
26
|
-
|
|
27
|
-
3. **Isolated Network:**
|
|
28
|
-
* **Purpose:** VMs on this network can only communicate with each other and the host, but not with external networks.
|
|
29
|
-
* **Mechanism:** A virtual bridge is created, but no routing or NAT is configured to connect it to physical interfaces.
|
|
30
|
-
* **Use Cases:** Testing environments, isolated services, or when you need a private network segment for VMs.
|
|
31
|
-
|
|
32
|
-
### Key Concepts
|
|
33
|
-
|
|
34
|
-
* **Bridge:** A software-based network device that connects multiple network segments at the data link layer. VMs connect to a virtual bridge, which then connects to a physical interface (for NAT/routed) or remains isolated.
|
|
35
|
-
* **DHCP:** Dynamic Host Configuration Protocol. Automatically assigns IP addresses to VMs within a network.
|
|
36
|
-
* **Forward Device:** The physical network interface on the host through which the virtual network's traffic is forwarded (for NAT or routed modes).
|
|
37
|
-
* **MAC Address:** Media Access Control address. A unique identifier assigned to network interfaces. For KVM/QEMU, MAC addresses often start with `52:54:00:`.
|
|
38
|
-
|
|
39
|
-
### Common Tasks
|
|
40
|
-
|
|
41
|
-
* **Create/Edit Network:** Define network parameters like name, IP range, DHCP settings, and forward mode.
|
|
42
|
-
* **Activate/Deactivate Network:** Start or stop a virtual network.
|
|
43
|
-
* **Autostart Network:** Configure a network to start automatically when the libvirt daemon starts.
|
|
44
|
-
* **View XML:** Examine the underlying libvirt XML definition of a network, which provides full details of its configuration.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
13
|
class HowToNetworkModal(BaseModal[None]):
|
|
48
14
|
"""A modal to display instructions for network configuration."""
|
|
49
15
|
|
|
50
16
|
def compose(self) -> ComposeResult:
|
|
17
|
+
# Load markdown from external file
|
|
18
|
+
docs_path = Path(__file__).parent.parent / "appdocs" / "howto_network.md"
|
|
19
|
+
try:
|
|
20
|
+
with open(docs_path, "r") as f:
|
|
21
|
+
content = f.read()
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
content = "# Error: Documentation file not found."
|
|
24
|
+
|
|
51
25
|
with Vertical(id="howto-network-dialog"):
|
|
52
26
|
with ScrollableContainer(id="howto-network-content"):
|
|
53
|
-
yield Markdown(
|
|
27
|
+
yield Markdown(content, id="howto-network-markdown")
|
|
54
28
|
with Horizontal(id="dialog-buttons"):
|
|
55
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
56
30
|
|
|
57
31
|
@on(Button.Pressed)
|
|
58
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -1,71 +1,32 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Modal to show how overlay disks work.
|
|
3
3
|
"""
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from textual.app import ComposeResult
|
|
5
7
|
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
6
8
|
from textual.widgets import Button, Markdown
|
|
7
9
|
from textual import on
|
|
8
10
|
from .base_modals import BaseModal
|
|
9
|
-
|
|
10
|
-
HOW_TO_OVERLAY_TEXT = """
|
|
11
|
-
# Understanding Snapshots and Disk Overlays
|
|
12
|
-
|
|
13
|
-
Virtual machines in this manager support two ways to preserve states and manage changes: **Snapshots** and **Disk Overlays**. While they share similar goals, they work differently and are suited for different use cases.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
### 1. Snapshots (Internal)
|
|
18
|
-
Snapshots are managed directly by libvirt and are typically stored **inside** the disk image (if using QCOW2 format).
|
|
19
|
-
|
|
20
|
-
* **How they work:** When you take a snapshot, libvirt records the current state of the VM's disks and, if the VM is running, its memory (RAM). You can have many snapshots for a single VM, creating a timeline you can jump back and forth in.
|
|
21
|
-
* **Operations:**
|
|
22
|
-
* **Take Snapshot:** Creates a new restore point.
|
|
23
|
-
* **Restore Snapshot:** Reverts the VM to a previous state.
|
|
24
|
-
* **Delete Snapshot:** Removes the restore point from the timeline.
|
|
25
|
-
* **Best for:** Quick restore points before risky operations, and preserving the full "live" state of a running VM.
|
|
26
|
-
|
|
27
|
-
### 2. Disk Overlays (External)
|
|
28
|
-
Overlays are **new files** created on top of a base disk image. This is also known as "External Snapshots" or "Backing Files".
|
|
29
|
-
|
|
30
|
-
* **Key Concepts:**
|
|
31
|
-
* **Base Image (Backing File):** The original disk image that becomes read-only.
|
|
32
|
-
* **Overlay Image:** A new QCOW2 file that records only the changes made *after* its creation.
|
|
33
|
-
* **Backing Chain:** The relationship between layers (e.g., Base -> Overlay 1 -> Overlay 2).
|
|
34
|
-
* **Operations:**
|
|
35
|
-
* **New Overlay:** Freezes the current disk and starts a new layer.
|
|
36
|
-
* **Discard Overlay (Revert):** Deletes the overlay file and reverts to the base image.
|
|
37
|
-
* **Commit Disk (Merge):** Merges changes from the overlay into the base image, making them permanent.
|
|
38
|
-
* **Best for:** Maintaining "Golden Images", branching multiple VMs from a single base, and isolating large changes in separate files.
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
### Comparison: Snapshot vs. Overlay
|
|
43
|
-
|
|
44
|
-
| Feature | Snapshots (Internal) | Disk Overlays (External) |
|
|
45
|
-
| :--- | :--- | :--- |
|
|
46
|
-
| **Storage** | Inside the existing disk file | In a new, separate file |
|
|
47
|
-
| **VM State** | Can include RAM (Live state) | Disk only (requires VM stop) |
|
|
48
|
-
| **Management** | Timeline (multiple points) | Layered (Base + Changes) |
|
|
49
|
-
| **Primary Use** | Quick restore points | Permanent branching / Golden images |
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
### How Overlays Work (Technical)
|
|
54
|
-
|
|
55
|
-
1. **Creation:** When you create a "New Overlay", the current disk image is set as the backing file for a newly created QCOW2 file. The VM is then updated to point to this new overlay file instead of the original disk.
|
|
56
|
-
2. **Read Operations:** When the VM needs to read data, it first checks the overlay. If the data has been modified, it reads from the overlay. If not, it transparently reads from the backing file.
|
|
57
|
-
3. **Write Operations:** All writes are directed to the overlay file. The backing file remains untouched and pristine.
|
|
58
|
-
"""
|
|
11
|
+
from ..constants import ButtonLabels
|
|
59
12
|
|
|
60
13
|
class HowToOverlayModal(BaseModal[None]):
|
|
61
14
|
"""A modal to display instructions for disk overlays."""
|
|
62
15
|
|
|
63
16
|
def compose(self) -> ComposeResult:
|
|
17
|
+
# Load markdown from external file
|
|
18
|
+
docs_path = Path(__file__).parent.parent / "appdocs" / "howto_overlay.md"
|
|
19
|
+
try:
|
|
20
|
+
with open(docs_path, "r") as f:
|
|
21
|
+
content = f.read()
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
content = "# Error: Documentation file not found."
|
|
24
|
+
|
|
64
25
|
with Vertical(id="howto-overlay-dialog", classes="howto-dialog"):
|
|
65
26
|
with ScrollableContainer(id="howto-overlay-content"):
|
|
66
|
-
yield Markdown(
|
|
27
|
+
yield Markdown(content, id="howto-overlay-markdown")
|
|
67
28
|
with Horizontal(id="dialog-buttons"):
|
|
68
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
69
30
|
|
|
70
31
|
@on(Button.Pressed)
|
|
71
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -1,86 +1,31 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Modal to show how to use ssh-agent.
|
|
3
3
|
"""
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
from textual.app import ComposeResult
|
|
5
6
|
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
6
7
|
from textual.widgets import Button, Markdown
|
|
7
8
|
from textual import on
|
|
8
9
|
from .base_modals import BaseModal
|
|
9
|
-
|
|
10
|
-
HOW_TO_SSH_TEXT = """
|
|
11
|
-
# Using an SSH Agent for Passwordless Connections
|
|
12
|
-
|
|
13
|
-
Using an `ssh-agent` is the standard and most secure way to handle passphrase-protected SSH keys, allowing you to connect without repeatedly entering your passphrase.
|
|
14
|
-
|
|
15
|
-
### What is an SSH Agent?
|
|
16
|
-
|
|
17
|
-
An `ssh-agent` is a background program that securely stores your private SSH keys in memory. When you try to connect to a remote server, SSH can ask the agent for the key, and the agent provides it. You only need to "unlock" your key once.
|
|
18
|
-
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
### Step 1: Start the `ssh-agent`
|
|
22
|
-
|
|
23
|
-
On most modern desktop environments, an agent is often started automatically. If not, run this in your terminal:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
eval "$(ssh-agent -s)"
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
> To make this permanent, add the command to your shell's startup file (e.g., `~/.bashrc` or `~/.zshrc`).
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
### Step 2: Add Your SSH Key to the Agent
|
|
34
|
-
|
|
35
|
-
Use the `ssh-add` command. If your key is in a default location (`~/.ssh/id_rsa`, etc.), you can just run:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
ssh-add
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
If your key is elsewhere, specify the path to the **private key**:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
ssh-add /path/to/your/private_key
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
You will be prompted for your key's passphrase **one time**.
|
|
48
|
-
|
|
49
|
-
To verify the key was added, list the agent's keys:
|
|
50
|
-
```bash
|
|
51
|
-
ssh-add -l
|
|
52
|
-
```
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
### Step 3: Connect
|
|
56
|
-
|
|
57
|
-
That's it! `Virtui Manager` will now use the agent to authenticate for any `qemu+ssh://` connections without any more prompts.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
### SSH Compression for Performance
|
|
62
|
-
|
|
63
|
-
For connections over slower networks, enabling SSH compression can significantly improve performance. This is configured in your SSH client's configuration file.
|
|
64
|
-
|
|
65
|
-
To enable compression for a specific host, add the following to your `~/.ssh/config` file:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
Host your_remote_host_name
|
|
69
|
-
Compression yes
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Replace `your_remote_host_name` with the actual hostname or IP address you use in your `qemu+ssh://` URI. If you want to enable compression for all SSH connections, you can use `Host *`.
|
|
73
|
-
"""
|
|
10
|
+
from ..constants import ButtonLabels
|
|
74
11
|
|
|
75
12
|
class HowToSSHModal(BaseModal[None]):
|
|
76
13
|
"""A modal to display instructions for using an ssh-agent."""
|
|
77
14
|
|
|
78
15
|
def compose(self) -> ComposeResult:
|
|
16
|
+
# Load markdown from external file
|
|
17
|
+
docs_path = Path(__file__).parent.parent / "appdocs" / "howto_ssh.md"
|
|
18
|
+
try:
|
|
19
|
+
with open(docs_path, "r") as f:
|
|
20
|
+
content = f.read()
|
|
21
|
+
except FileNotFoundError:
|
|
22
|
+
content = "# Error: Documentation file not found."
|
|
23
|
+
|
|
79
24
|
with Vertical(id="howto-ssh-dialog"):
|
|
80
25
|
with ScrollableContainer(id="howto-ssh-content"):
|
|
81
|
-
yield Markdown(
|
|
26
|
+
yield Markdown(content, id="howto-ssh-markdown")
|
|
82
27
|
with Horizontal(id="dialog-buttons"):
|
|
83
|
-
yield Button(
|
|
28
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
84
29
|
|
|
85
30
|
@on(Button.Pressed)
|
|
86
31
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
@@ -1,83 +1,32 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Modal to show how to use VirtIO-FS.
|
|
3
3
|
"""
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from textual.app import ComposeResult
|
|
5
7
|
from textual.containers import Vertical, Horizontal, ScrollableContainer
|
|
6
8
|
from textual.widgets import Button, Markdown
|
|
7
9
|
from textual import on
|
|
8
10
|
from .base_modals import BaseModal
|
|
9
|
-
|
|
10
|
-
HOW_TO_VIRTIOFS_TEXT = """
|
|
11
|
-
# Using VirtIO-FS for Host-Guest File Sharing
|
|
12
|
-
|
|
13
|
-
VirtIO-FS is a high-performance shared filesystem that lets you share a directory from your host machine directly with a guest VM.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
### Host Prerequisites
|
|
18
|
-
|
|
19
|
-
1. **Shared Memory:** VirtIO-FS requires shared memory to be enabled for the VM. You can enable this in the **"Mem"** tab.
|
|
20
|
-
2. **Permissions:** The user running QEMU/libvirt on the host must have the necessary permissions to read (and write, if needed) the source directory you want to share.
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
### Adding a VirtIO-FS Mount
|
|
25
|
-
|
|
26
|
-
- **Source Path:** The absolute path to the directory on your **host machine** that you want to share.
|
|
27
|
-
- **Target Path:** This is a "mount tag" or a label that the guest VM will use to identify the shared directory. It is **not** a path inside the guest. For example, you could use `shared-data`.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
### Mounting in a Linux Guest
|
|
32
|
-
|
|
33
|
-
Most modern Linux distributions include the necessary VirtIO-FS drivers.
|
|
34
|
-
|
|
35
|
-
**1. Create a Mount Point:**
|
|
36
|
-
This is the directory inside your VM where the shared files will appear.
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
sudo mkdir /mnt/my_host_share
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**2. Mount the Share:**
|
|
43
|
-
Use the `mount` command with the filesystem type `virtiofs` and the **Target Path (mount tag)** you defined.
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
sudo mount -t virtiofs 'your-target-path' /mnt/my_host_share
|
|
47
|
-
```
|
|
48
|
-
*(Replace `'your-target-path'` with the actual tag you set)*
|
|
49
|
-
|
|
50
|
-
**3. Automount on Boot (Optional):**
|
|
51
|
-
To make the share available automatically every time the VM boots, add an entry to `/etc/fstab`:
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
your-target-path /mnt/my_host_share virtiofs defaults,nofail 0 0
|
|
55
|
-
```
|
|
56
|
-
> The `nofail` option is recommended to prevent boot issues if the share is not available.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
### Mounting in a Windows Guest
|
|
61
|
-
|
|
62
|
-
**1. Install Drivers:**
|
|
63
|
-
You must install the VirtIO-FS drivers in the Windows guest. These are included in the **"VirtIO-Win Guest Tools"** package, which you can typically download as an ISO file.
|
|
64
|
-
- Download the latest stable `virtio-win.iso` from the [Fedora VirtIO-Win project](https://github.com/virtio-win/virtio-win-pkg-scripts/blob/master/README.md).
|
|
65
|
-
- Attach the ISO to your VM as a CD-ROM.
|
|
66
|
-
- Open the CD-ROM in Windows and run the `virtio-win-guest-tools.exe` installer, ensuring the **"VirtIO-FS"** feature is selected.
|
|
67
|
-
|
|
68
|
-
**2. Access the Share:**
|
|
69
|
-
After installation and a reboot, the VirtIO-FS service will start. The shared folder will automatically appear as a network drive in **This PC** (or My Computer). The drive will be named after the **Target Path (mount tag)** you set.
|
|
70
|
-
"""
|
|
11
|
+
from ..constants import ButtonLabels
|
|
71
12
|
|
|
72
13
|
class HowToVirtIOFSModal(BaseModal[None]):
|
|
73
14
|
"""A modal to display instructions for using VirtIO-FS."""
|
|
74
15
|
|
|
75
16
|
def compose(self) -> ComposeResult:
|
|
17
|
+
# Load markdown from external file
|
|
18
|
+
docs_path = Path(__file__).parent.parent / "appdocs" / "howto_virtiofs.md"
|
|
19
|
+
try:
|
|
20
|
+
with open(docs_path, "r") as f:
|
|
21
|
+
content = f.read()
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
content = "# Error: Documentation file not found."
|
|
24
|
+
|
|
76
25
|
with Vertical(id="howto-virtiofs-dialog"):
|
|
77
26
|
with ScrollableContainer(id="howto-virtiofs-content"):
|
|
78
|
-
yield Markdown(
|
|
27
|
+
yield Markdown(content, id="howto-virtiofs-markdown")
|
|
79
28
|
with Horizontal(id="dialog-buttons"):
|
|
80
|
-
yield Button(
|
|
29
|
+
yield Button(ButtonLabels.CLOSE, id="close-btn", variant="primary")
|
|
81
30
|
|
|
82
31
|
@on(Button.Pressed)
|
|
83
32
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
vmanager/modals/input_modals.py
CHANGED
|
@@ -7,6 +7,7 @@ from textual.app import ComposeResult
|
|
|
7
7
|
from textual.containers import Vertical, Horizontal
|
|
8
8
|
from textual import on
|
|
9
9
|
from .base_modals import BaseModal
|
|
10
|
+
from ..constants import ButtonLabels, StaticText
|
|
10
11
|
|
|
11
12
|
class InputModal(BaseModal[str | None]):
|
|
12
13
|
"""A generic modal for getting text input from the user."""
|
|
@@ -21,8 +22,8 @@ class InputModal(BaseModal[str | None]):
|
|
|
21
22
|
yield Label(self.prompt)
|
|
22
23
|
yield Input(value=self.initial_value, id="text-input", restrict=self.restrict)
|
|
23
24
|
with Horizontal():
|
|
24
|
-
yield Button(
|
|
25
|
-
yield Button(
|
|
25
|
+
yield Button(ButtonLabels.OK, variant="primary", id="ok-btn")
|
|
26
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-btn")
|
|
26
27
|
|
|
27
28
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
28
29
|
if event.button.id == "ok-btn":
|
|
@@ -40,7 +41,7 @@ class AddInputDeviceModal(BaseModal[None]):
|
|
|
40
41
|
|
|
41
42
|
def compose(self) -> ComposeResult:
|
|
42
43
|
with Vertical(id="add-input-container"):
|
|
43
|
-
yield Label(
|
|
44
|
+
yield Label(StaticText.INPUT_DEVICE)
|
|
44
45
|
yield Select(
|
|
45
46
|
[(t, t) for t in self.available_types],
|
|
46
47
|
prompt="Input Type",
|
|
@@ -53,8 +54,8 @@ class AddInputDeviceModal(BaseModal[None]):
|
|
|
53
54
|
)
|
|
54
55
|
with Vertical():
|
|
55
56
|
with Horizontal():
|
|
56
|
-
yield Button(
|
|
57
|
-
yield Button(
|
|
57
|
+
yield Button(ButtonLabels.ADD, variant="primary", id="add-input", disabled=True)
|
|
58
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-input")
|
|
58
59
|
|
|
59
60
|
@on(Select.Changed)
|
|
60
61
|
def on_select_changed(self) -> None:
|
|
@@ -82,26 +83,26 @@ class AddChannelModal(BaseModal[dict | None]):
|
|
|
82
83
|
|
|
83
84
|
def compose(self) -> ComposeResult:
|
|
84
85
|
with Vertical(id="add-channel-container"):
|
|
85
|
-
yield Label(
|
|
86
|
+
yield Label(StaticText.ADD_CHANNEL_DEVICE)
|
|
86
87
|
yield Select(
|
|
87
88
|
[("unix", "unix"), ("virtio", "virtio"), ("spicevmc", "spicevmc")],
|
|
88
89
|
prompt="Channel Type",
|
|
89
90
|
id="channel-type-select",
|
|
90
91
|
value="unix"
|
|
91
92
|
)
|
|
92
|
-
yield Label(
|
|
93
|
+
yield Label(StaticText.STANDARD_TARGET_NAMES)
|
|
93
94
|
yield Select(
|
|
94
95
|
[],
|
|
95
96
|
id="target-preset-select",
|
|
96
97
|
prompt="Select a standard target or type below",
|
|
97
98
|
value=Select.BLANK
|
|
98
99
|
)
|
|
99
|
-
yield Label(
|
|
100
|
+
yield Label(StaticText.TARGET_NAME)
|
|
100
101
|
yield Input(placeholder="Target Name (e.g. org.qemu.guest_agent.0)", id="target-name-input")
|
|
101
102
|
|
|
102
103
|
with Horizontal():
|
|
103
|
-
yield Button(
|
|
104
|
-
yield Button(
|
|
104
|
+
yield Button(ButtonLabels.ADD, variant="primary", id="add-channel-btn")
|
|
105
|
+
yield Button(ButtonLabels.CANCEL, variant="default", id="cancel-channel-btn")
|
|
105
106
|
|
|
106
107
|
def on_mount(self) -> None:
|
|
107
108
|
# Initialize presets for default type (unix)
|
vmanager/modals/log_modal.py
CHANGED
|
@@ -6,6 +6,7 @@ from textual.widgets import Button, Label, TextArea
|
|
|
6
6
|
from textual.containers import Vertical, Horizontal
|
|
7
7
|
|
|
8
8
|
from .base_modals import BaseModal
|
|
9
|
+
from ..constants import ButtonLabels
|
|
9
10
|
|
|
10
11
|
class LogModal(BaseModal[None]):
|
|
11
12
|
""" Modal Screen to show Log"""
|
|
@@ -22,7 +23,7 @@ class LogModal(BaseModal[None]):
|
|
|
22
23
|
text_area.load_text(self.log_content)
|
|
23
24
|
yield text_area
|
|
24
25
|
with Horizontal():
|
|
25
|
-
yield Button(
|
|
26
|
+
yield Button(ButtonLabels.CLOSE, variant="default", id="cancel-btn", classes="Buttonpage")
|
|
26
27
|
|
|
27
28
|
def on_mount(self) -> None:
|
|
28
29
|
"""Called when the modal is mounted."""
|
|
@@ -11,6 +11,7 @@ from textual.screen import ModalScreen
|
|
|
11
11
|
from textual.widgets import Button, Static, Select, Checkbox, Label, ProgressBar
|
|
12
12
|
from textual import on, work
|
|
13
13
|
|
|
14
|
+
from ..constants import ErrorMessages, StaticText, ButtonLabels
|
|
14
15
|
from ..vm_actions import check_server_migration_compatibility, check_vm_migration_compatibility
|
|
15
16
|
from ..storage_manager import find_shared_storage_pools
|
|
16
17
|
from ..utils import extract_server_name_from_uri
|
|
@@ -74,38 +75,39 @@ class MigrationModal(ModalScreen):
|
|
|
74
75
|
|
|
75
76
|
with Vertical(id="migration-dialog",):
|
|
76
77
|
with Vertical(id="migration-content-wrapper"):
|
|
77
|
-
yield Label(
|
|
78
|
-
yield Static(
|
|
78
|
+
yield Label(StaticText.MIGRATE_VMS_TITLE.format(migration_type=migration_type, vm_names=vm_names))
|
|
79
|
+
yield Static(StaticText.SELECT_DESTINATION_SERVER)
|
|
79
80
|
yield Select(dest_servers, id="dest-server-select", prompt="Destination...", value=default_dest_uri, allow_blank=False)
|
|
80
81
|
|
|
81
|
-
yield Static(
|
|
82
|
+
yield Static(StaticText.MIGRATION_OPTIONS)
|
|
82
83
|
with Horizontal(classes="checkbox-container"):
|
|
83
|
-
yield Checkbox(
|
|
84
|
-
yield Checkbox(
|
|
85
|
-
yield Checkbox(
|
|
84
|
+
yield Checkbox(StaticText.COPY_STORAGE_ALL, id="copy-storage-all", tooltip="Copy all disk files during migration", value=False)
|
|
85
|
+
yield Checkbox(StaticText.UNSAFE_MIGRATION, id="unsafe", tooltip="Perform unsafe migration (may lose data)", disabled=not self.is_live)
|
|
86
|
+
yield Checkbox(StaticText.PERSISTENT_MIGRATION, id="persistent", tooltip="Keep VM persistent on destination", value=True)
|
|
86
87
|
with Horizontal(classes="checkbox-container"):
|
|
87
|
-
yield Checkbox(
|
|
88
|
-
yield Checkbox(
|
|
89
|
-
yield Checkbox(
|
|
90
|
-
yield Static(
|
|
88
|
+
yield Checkbox(StaticText.COMPRESS_DATA, id="compress", tooltip="Compress data during migration", disabled=not self.is_live)
|
|
89
|
+
yield Checkbox(StaticText.TUNNELLED_MIGRATION, id="tunnelled", tooltip="Tunnel migration data through libvirt daemon", disabled=not self.is_live)
|
|
90
|
+
yield Checkbox(StaticText.CUSTOM_MIGRATION, id="custom", tooltip="Use custom migration workflow", value=False)
|
|
91
|
+
yield Static(StaticText.COMPATIBILITY_CHECK_RESULTS)
|
|
91
92
|
yield ProgressBar(total=100, show_eta=False, id="migration-progress")
|
|
92
93
|
yield Static(id="results-log")
|
|
93
94
|
yield Grid(
|
|
94
95
|
ScrollableContainer(
|
|
95
|
-
Static(
|
|
96
|
+
Static(StaticText.VMS_READY_FOR_MIGRATION, classes="summary-title"),
|
|
96
97
|
Static(id="can-migrate-list"),
|
|
97
98
|
),
|
|
98
99
|
ScrollableContainer(
|
|
99
|
-
Static(
|
|
100
|
+
Static(StaticText.VMS_NOT_READY_FOR_MIGRATION, classes="summary-title"),
|
|
100
101
|
Static(id="cannot-migrate-list"),
|
|
101
102
|
),
|
|
102
103
|
id="migration-summary-grid"
|
|
103
104
|
)
|
|
104
105
|
|
|
105
106
|
with Horizontal(classes="modal-buttons"):
|
|
106
|
-
yield Button(
|
|
107
|
-
yield Button(
|
|
108
|
-
yield Button(
|
|
107
|
+
yield Button(ButtonLabels.CHECK_COMPATIBILITY, variant="primary", id="check", classes="Buttonpage")
|
|
108
|
+
yield Button(ButtonLabels.START_MIGRATION, variant="success", id="start", disabled=True, classes="Buttonpage")
|
|
109
|
+
yield Button(ButtonLabels.CLOSE, variant="default", id="close", disabled=False, classes="close-button")
|
|
110
|
+
|
|
109
111
|
|
|
110
112
|
def _lock_controls(self, lock: bool):
|
|
111
113
|
self.query_one("#check").disabled = lock
|
|
@@ -403,17 +405,17 @@ class MigrationModal(ModalScreen):
|
|
|
403
405
|
def on_button_pressed(self, event: Button.Pressed):
|
|
404
406
|
if event.button.id == "check":
|
|
405
407
|
if not self.dest_conn:
|
|
406
|
-
self.app.show_error_message(
|
|
408
|
+
self.app.show_error_message(ErrorMessages.SELECT_DESTINATION_SERVER)
|
|
407
409
|
return
|
|
408
410
|
self._clear_log()
|
|
409
411
|
self.run_compatibility_checks()
|
|
410
412
|
|
|
411
413
|
elif event.button.id == "start":
|
|
412
414
|
if not self.compatibility_checked:
|
|
413
|
-
self.app.show_error_message(
|
|
415
|
+
self.app.show_error_message(ErrorMessages.RUN_COMPATIBILITY_CHECK_FIRST)
|
|
414
416
|
return
|
|
415
417
|
if not self.checks_passed:
|
|
416
|
-
self.app.show_error_message(
|
|
418
|
+
self.app.show_error_message(ErrorMessages.MIGRATION_COMPATIBILITY_ERRORS)
|
|
417
419
|
return
|
|
418
420
|
|
|
419
421
|
self._clear_log()
|