virtui-manager 1.1.5__py3-none-any.whl → 1.1.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtui-manager
3
- Version: 1.1.5
3
+ Version: 1.1.6
4
4
  Summary: Terminal-based interface to manage virtual machines using libvirt
5
5
  Home-page: https://aginies.github.io/virtui-manager/
6
6
  Author: Antoine Ginies
@@ -1,8 +1,8 @@
1
- virtui_manager-1.1.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
1
+ virtui_manager-1.1.6.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
2
  vmanager/__init__.py,sha256=-gTpOgkPAOtbLwxz-U9i2uVtFW1SB9JhBguViXnQPJ8,15
3
3
  vmanager/config.py,sha256=OBMWtXBTnmws1ITjQ7D_C0LVMxzvGaRH8nZOlW9cbqk,3102
4
4
  vmanager/connection_manager.py,sha256=osk0EHJ0ua2wnDOu-9faK8fMd7s9TGsI6OtgxLAMTCI,12984
5
- vmanager/constants.py,sha256=dtuqrTgud5YmkaVzW8JOvA7rkaSR-XeqovtbMf15lls,5907
5
+ vmanager/constants.py,sha256=q7oX6P0XKLOKEh6JxY1Jduhl5Q5aMFKk8DfKii7BvyM,5907
6
6
  vmanager/dialog.css,sha256=iWGRlTZ-D-wc2uQQTftPoqDpysrAD6qMMENE3HXXlQg,11300
7
7
  vmanager/events.py,sha256=yvjcYHMoDnnDKsMjfweZYnu0-U7lxQlB2uuIbtK5FeY,1556
8
8
  vmanager/firmware_manager.py,sha256=i64S8uPKIj-Qe-xpQYAvelOLYZc0yO31ome9qMT8ObQ,3360
@@ -11,7 +11,7 @@ vmanager/libvirt_utils.py,sha256=fj74jXP2fHrn5VRWf8kwSlf57VyvWIVZ8gF0POyPUCA,160
11
11
  vmanager/network_manager.py,sha256=yMU3VXBCDDBKT2H7OL5s9XAD5uknfLPLH72e4228_Do,8627
12
12
  vmanager/remote_viewer.py,sha256=evFvp7ilKPuTf-YA4jCGMgO6dzdhbL1iea1lzETwWeU,89495
13
13
  vmanager/remote_viewer_gtk4.py,sha256=KIyejYq_PgmSL6yAUOnnloqpaIWdkYIPdoP6CmMaTOo,45994
14
- vmanager/storage_manager.py,sha256=Sb5xI-5yopF93NhoS8hIJiSIk81kfPShqV610LSFZ_w,41349
14
+ vmanager/storage_manager.py,sha256=-Z-NqRklx9PjRhbipvHReSjTDkGqv5uO79vZRAZVv0g,42195
15
15
  vmanager/utils.py,sha256=V3jOjBVD9Hf29hC6xIRkfPkjqx-_VL7eozMomaMqW9U,17254
16
16
  vmanager/virtui_dev.py,sha256=M2pJEZRNApqezkj4RdEJR9neEdc_7HV3KInVGy9QgiQ,3118
17
17
  vmanager/vm_actions.py,sha256=HpK7O2TtpMj-RjBGfF2hdPElwh-aIXh3HbJwjbrEs3w,117279
@@ -36,11 +36,11 @@ vmanager/modals/config_modal.py,sha256=DMNGqjej-nAvN9t1OqK-uaR_r6d1m4Vi1X5eX7D3Z
36
36
  vmanager/modals/cpu_mem_pc_modals.py,sha256=puKOVeEqi36wzaXbKNyXjZQjCjXKntuIVTWKh5RsY38,9378
37
37
  vmanager/modals/custom_migration_modal.py,sha256=-kDRI8yeBJnh9jx5_ZJFWfXIfcmOZMO0dXUht3uowC8,2289
38
38
  vmanager/modals/disk_pool_modals.py,sha256=xme7vS52kkM4CBE3WduNfjqzJz5BAdVGxdD-HfMvw7o,28236
39
- vmanager/modals/howto_disk_modal.py,sha256=zvUb4dBFsObDdQK1J6bNczQ_sJfinvgUQiXOrGyczT4,3259
40
- vmanager/modals/howto_network_modal.py,sha256=kV4p5_fF1TudblOIJBJBGNb7SzX7rm54PR5TQwTUqs8,3940
41
- vmanager/modals/howto_overlay_modal.py,sha256=3L6-EaVVc5nv5sGIVazbYMTs67axaZXNu59IKaHwK0Q,3837
42
- vmanager/modals/howto_ssh_modal.py,sha256=F1E3c-aX2248CYjU5YmXlLJ1vSPQMxKwBuY50h5lTGw,2781
43
- vmanager/modals/howto_virtiofs_modal.py,sha256=9uNu3Ey6XglH1jCHDtGp2BQW5pi0Gk4NiZG8EtoAxSY,3392
39
+ vmanager/modals/howto_disk_modal.py,sha256=yHMy4GFB39YXV4PVFyJe2y_1WgoeBCIlwr20gZEjF5c,1193
40
+ vmanager/modals/howto_network_modal.py,sha256=arSiPHlq_wW7QLzwBkH_L5O-E7UmvwMXiuaEVlAGJdk,1215
41
+ vmanager/modals/howto_overlay_modal.py,sha256=EJYrrW9nJsD0qSXBKabFbAEP_R1MOodsyDxEDW-ONbc,1228
42
+ vmanager/modals/howto_ssh_modal.py,sha256=e0I-ntbk8-DZSl9t2T3eKW_EBcQvVqiwdqQKa9HIylM,1177
43
+ vmanager/modals/howto_virtiofs_modal.py,sha256=GSnKjSBAutOXa16EToyuOWHycLRf94Qdlnq1XcJGHd0,1209
44
44
  vmanager/modals/input_modals.py,sha256=PEfM2IHc8QFR4BZf5uSuJz-7uiclRq6z65cZES_GJpk,7719
45
45
  vmanager/modals/log_modal.py,sha256=WXzha9mm5dO9jOmTSO52gJnOmgO7ZkBRrjS2_MiYMyQ,1095
46
46
  vmanager/modals/migration_modals.py,sha256=MkuGeDBe1v-BxP7BjrpGB8KxQh4x-SZHHKD1c9mV5wE,20422
@@ -49,7 +49,7 @@ vmanager/modals/provisioning_modals.py,sha256=Yx7zO8Zvo0qVpW9ctRZg0RmzIwik-tevXA
49
49
  vmanager/modals/select_server_modals.py,sha256=eAA9hmBjeK_2_AF7jRKyWcocj_LvmgP7ZFRp6H0NGi4,4994
50
50
  vmanager/modals/selection_modals.py,sha256=Pd8ZhQ-ezp2MUNT7SV0pdk2OpAYpI5YuuizaDJQTtek,6096
51
51
  vmanager/modals/server_modals.py,sha256=fQP8Sj7y7-i2UijS27yFvOywFAWiXdQcj27scYa2IoE,10191
52
- vmanager/modals/server_prefs_modals.py,sha256=-UNwWmBW6wbksd_Iet7x0Gy88uw7Xqj6bn276q_93m0,38669
52
+ vmanager/modals/server_prefs_modals.py,sha256=AxRYfJm8Q-IFkKx1u3To_7yZkGhaPanabI-COdULD-k,39275
53
53
  vmanager/modals/utils_modals.py,sha256=ijJQZF68icCWNwmKFnoeS7avufjafc7Z4egaaB_79Xg,8675
54
54
  vmanager/modals/virsh_modals.py,sha256=t5TT8sNyb3vUJ0a7j6KpH8LdXAvjJ0U9L6NtulSB5x0,4782
55
55
  vmanager/modals/virtiofs_modals.py,sha256=R2R6Rcaeg3QLsUgC5v9BjJfvyXtXWVO_eL3iqchanuk,2145
@@ -58,8 +58,8 @@ vmanager/modals/vmanager_modals.py,sha256=Ix4SxO_e8SfBg0B2iou6M5xk1DigO814e6iwD5
58
58
  vmanager/modals/vmcard_dialog.py,sha256=EYS19Ooy6KviTPLVzBjGEkALvKRYLJG9rksMHXRO0VU,18674
59
59
  vmanager/modals/vmdetails_modals.py,sha256=YE0u71-K-GV5n_38oIzCD2EvqTCIJmJcv3B1Ofej5Rg,149636
60
60
  vmanager/modals/xml_modals.py,sha256=iY981cqzVnDYOUo-kw1DHpW9I7IVAU1K7rFqqvxltOk,1653
61
- virtui_manager-1.1.5.dist-info/METADATA,sha256=NkuWhmBKj2I_mR27vU01T4nhV-q2fLwztbEvCxH26mk,5329
62
- virtui_manager-1.1.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
63
- virtui_manager-1.1.5.dist-info/entry_points.txt,sha256=hnJHxJyDFPSNdOaxmMYikXeoJMXG2llKG-l_Ygv7ges,163
64
- virtui_manager-1.1.5.dist-info/top_level.txt,sha256=fQ1nwH-4wZrf6Rc9HEHbudVahpy3TqeuMxgaaNHevxM,9
65
- virtui_manager-1.1.5.dist-info/RECORD,,
61
+ virtui_manager-1.1.6.dist-info/METADATA,sha256=rVmqF190noCHtV6z3ZsXhWjC-OoAfUmnn6pKC0-8v9c,5329
62
+ virtui_manager-1.1.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
63
+ virtui_manager-1.1.6.dist-info/entry_points.txt,sha256=hnJHxJyDFPSNdOaxmMYikXeoJMXG2llKG-l_Ygv7ges,163
64
+ virtui_manager-1.1.6.dist-info/top_level.txt,sha256=fQ1nwH-4wZrf6Rc9HEHbudVahpy3TqeuMxgaaNHevxM,9
65
+ virtui_manager-1.1.6.dist-info/RECORD,,
vmanager/constants.py CHANGED
@@ -37,7 +37,7 @@ class AppInfo:
37
37
  """Define app data"""
38
38
  name = "virtui-manager"
39
39
  namecase = "VirtUI Manager"
40
- version = "1.1.5"
40
+ version = "1.1.6"
41
41
 
42
42
  class VmAction:
43
43
  """Defines constants for VM action types."""
@@ -1,89 +1,29 @@
1
1
  """
2
2
  Modal to show how to manage VM disks.
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
11
 
10
- HOW_TO_DISK_TEXT = """
11
- # Managing VM Disks
12
-
13
- This guide explains the functions of the buttons available in the "Disks" tab.
14
-
15
- ---
16
-
17
- ### Add Disk
18
-
19
- This button allows you to create a **brand new virtual disk** and attach it to the VM.
20
-
21
- - You will be prompted to select a **Storage Pool** where the new disk image file will be created.
22
- - You must provide a **Volume Name** for the new disk (e.g., `new-data-disk.qcow2`).
23
- - You must specify its **Size** and **Format** (`qcow2` or `raw`).
24
- - The VM does **not** need to be stopped to add a new disk.
25
-
26
- ---
27
-
28
- ### Attach Existing Disk
29
-
30
- This attaches a **pre-existing disk image** to your VM. This is useful if you have an existing `.qcow2`, `.raw`, or `.iso` file you want to use.
31
-
32
- - The system will first ask you to select a **Storage Pool** that contains the disk volume you want to attach.
33
- - If the disk file is not part of a libvirt storage pool yet, you should first use the "Attach" button in the **Server Preferences -> Storage** tab to make it known to libvirt. This might involve creating a new storage pool for the directory containing your disk file.
34
-
35
- ---
36
-
37
- ### Edit Disk
38
-
39
- *(This button is enabled only when a disk is selected in the table)*
40
-
41
- Allows you to modify properties of an attached disk, such as:
42
- - **Bus Type:** `virtio`, `sata`, `scsi`, etc.
43
- - **Cache Mode:** `none`, `writeback`, etc.
44
- - **Discard Mode:** `unmap`, `ignore`.
45
-
46
- > **Important:** The VM must be **stopped** to edit disk properties.
47
-
48
- ---
49
-
50
- ### Remove Disk
51
-
52
- *(This button is enabled only when a disk is selected)*
53
-
54
- This **detaches** the selected disk from the virtual machine.
55
-
56
- - **This action does NOT delete the disk image file.** The file remains in its storage pool or on your filesystem.
57
- - It only removes the disk from this specific VM's configuration.
58
- - The VM must be **stopped** to remove most types of disks.
59
-
60
- ---
61
-
62
- ### Disable Disk
63
-
64
- *(This button is enabled for active disks)*
65
-
66
- This temporarily "unplugs" the disk from the VM **without removing its configuration**.
67
-
68
- - The disk will become invisible to the guest operating system but remains in the disk list with a `(disabled)` status.
69
- - This is useful for troubleshooting or temporarily preventing access to a disk without fully removing it.
70
-
71
- ---
72
-
73
- ### Enable Disk
74
-
75
- *(This button is enabled for disabled disks)*
76
-
77
- This "re-plugs" a disabled disk back into the VM, making it available to the guest operating system again.
78
- """
79
-
80
12
  class HowToDiskModal(BaseModal[None]):
81
13
  """A modal to display instructions for managing VM disks."""
82
14
 
83
15
  def compose(self) -> ComposeResult:
16
+ # Load markdown from external file
17
+ docs_path = Path(__file__).parent.parent / "appdocs" / "howto_disk.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
+
84
24
  with Vertical(id="howto-disk-dialog"):
85
25
  with ScrollableContainer(id="howto-disk-content"):
86
- yield Markdown(HOW_TO_DISK_TEXT, id="howto-disk-markdown")
26
+ yield Markdown(content, id="howto-disk-markdown")
87
27
  with Horizontal(id="dialog-buttons"):
88
28
  yield Button("Close", id="close-btn", variant="primary")
89
29
 
@@ -1,56 +1,29 @@
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
8
10
  from .base_modals import BaseModal
9
11
 
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
12
  class HowToNetworkModal(BaseModal[None]):
48
13
  """A modal to display instructions for network configuration."""
49
14
 
50
15
  def compose(self) -> ComposeResult:
16
+ # Load markdown from external file
17
+ docs_path = Path(__file__).parent.parent / "appdocs" / "howto_network.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
+
51
24
  with Vertical(id="howto-network-dialog"):
52
25
  with ScrollableContainer(id="howto-network-content"):
53
- yield Markdown(HOW_TO_NETWORK_TEXT, id="howto-network-markdown")
26
+ yield Markdown(content, id="howto-network-markdown")
54
27
  with Horizontal(id="dialog-buttons"):
55
28
  yield Button("Close", id="close-btn", variant="primary")
56
29
 
@@ -1,69 +1,29 @@
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
11
 
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
- """
59
-
60
12
  class HowToOverlayModal(BaseModal[None]):
61
13
  """A modal to display instructions for disk overlays."""
62
14
 
63
15
  def compose(self) -> ComposeResult:
16
+ # Load markdown from external file
17
+ docs_path = Path(__file__).parent.parent / "appdocs" / "howto_overlay.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
+
64
24
  with Vertical(id="howto-overlay-dialog", classes="howto-dialog"):
65
25
  with ScrollableContainer(id="howto-overlay-content"):
66
- yield Markdown(HOW_TO_OVERLAY_TEXT, id="howto-overlay-markdown")
26
+ yield Markdown(content, id="howto-overlay-markdown")
67
27
  with Horizontal(id="dialog-buttons"):
68
28
  yield Button("Close", id="close-btn", variant="primary")
69
29
 
@@ -1,84 +1,28 @@
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
 
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
- """
74
-
75
11
  class HowToSSHModal(BaseModal[None]):
76
12
  """A modal to display instructions for using an ssh-agent."""
77
13
 
78
14
  def compose(self) -> ComposeResult:
15
+ # Load markdown from external file
16
+ docs_path = Path(__file__).parent.parent / "appdocs" / "howto_ssh.md"
17
+ try:
18
+ with open(docs_path, "r") as f:
19
+ content = f.read()
20
+ except FileNotFoundError:
21
+ content = "# Error: Documentation file not found."
22
+
79
23
  with Vertical(id="howto-ssh-dialog"):
80
24
  with ScrollableContainer(id="howto-ssh-content"):
81
- yield Markdown(HOW_TO_SSH_TEXT, id="howto-ssh-markdown")
25
+ yield Markdown(content, id="howto-ssh-markdown")
82
26
  with Horizontal(id="dialog-buttons"):
83
27
  yield Button("Close", id="close-btn", variant="primary")
84
28
 
@@ -1,81 +1,29 @@
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
11
 
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
- """
71
-
72
12
  class HowToVirtIOFSModal(BaseModal[None]):
73
13
  """A modal to display instructions for using VirtIO-FS."""
74
14
 
75
15
  def compose(self) -> ComposeResult:
16
+ # Load markdown from external file
17
+ docs_path = Path(__file__).parent.parent / "appdocs" / "howto_virtiofs.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
+
76
24
  with Vertical(id="howto-virtiofs-dialog"):
77
25
  with ScrollableContainer(id="howto-virtiofs-content"):
78
- yield Markdown(HOW_TO_VIRTIOFS_TEXT, id="howto-virtiofs-markdown")
26
+ yield Markdown(content, id="howto-virtiofs-markdown")
79
27
  with Horizontal(id="dialog-buttons"):
80
28
  yield Button("Close", id="close-btn", variant="primary")
81
29
 
@@ -248,20 +248,28 @@ class ServerPrefModal(BaseModal[None]):
248
248
  pools = storage_manager.list_storage_pools(self.conn)
249
249
  node_to_select = None
250
250
  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
251
+ try:
252
+ pool_name = pool_data['name']
253
+ status = pool_data['status']
254
+ autostart = "autostart" if pool_data['autostart'] else "no autostart"
255
+ label = f"{pool_name} [{status}, {autostart}]"
256
+ pool_node = tree.root.add(label, data=pool_data)
257
+ pool_node.data["type"] = "pool"
258
+ # Add a dummy node to make the pool node expandable
259
+ pool_node.add_leaf("Loading volumes...")
260
+
261
+ if expand_pools and pool_name in expand_pools:
262
+ self.app.call_later(pool_node.expand)
263
+
264
+ if select_pool and pool_name == select_pool:
265
+ node_to_select = pool_node
266
+ except libvirt.libvirtError:
267
+ # Handle cases where pool might be listed but inaccessible (e.g. NFS down)
268
+ pool_name = pool_data.get('name', 'Unknown')
269
+ label = f"{pool_name} [unavailable]"
270
+ pool_node = tree.root.add(label, data=pool_data)
271
+ pool_node.data["type"] = "pool"
272
+ pool_node.add_leaf("Pool unavailable")
265
273
 
266
274
  if node_to_select:
267
275
  self.app.call_later(tree.select_node, node_to_select)
@@ -308,7 +316,8 @@ class ServerPrefModal(BaseModal[None]):
308
316
  if len(node.children) == 1 and node.children[0].data is None:
309
317
  node.remove_children()
310
318
  pool = node_data.get('pool')
311
- if pool and pool.isActive():
319
+ # Check cached status instead of calling pool.isActive() to avoid blocking
320
+ if pool and node_data.get('status') == 'active':
312
321
  volumes = storage_manager.list_storage_volumes(pool)
313
322
  for vol_data in volumes:
314
323
  vol_name = vol_data['name']
@@ -404,7 +413,7 @@ class ServerPrefModal(BaseModal[None]):
404
413
  self.app.show_error_message("Could not find pool object to edit.")
405
414
  return
406
415
 
407
- if pool.isActive():
416
+ if node_data.get('status') == 'active':
408
417
  self.app.show_error_message("Pool must be inactive to edit its XML definition.")
409
418
  return
410
419
 
@@ -29,18 +29,40 @@ def list_storage_pools(conn: libvirt.virConnect) -> List[Dict[str, Any]]:
29
29
  pools = conn.listAllStoragePools(0)
30
30
  for pool in pools:
31
31
  try:
32
+ # Try to get basic info
33
+ try:
34
+ name = pool.name()
35
+ except libvirt.libvirtError:
36
+ name = "Unknown Pool"
37
+
32
38
  is_active = pool.isActive()
33
39
  info = pool.info()
34
40
  pools_info.append({
35
- 'name': pool.name(),
41
+ 'name': name,
36
42
  'pool': pool,
37
43
  'status': 'active' if is_active else 'inactive',
38
44
  'autostart': pool.autostart() == 1,
39
45
  'capacity': info[1],
40
46
  'allocation': info[2],
41
47
  })
42
- except libvirt.libvirtError:
43
- continue
48
+ except libvirt.libvirtError as e:
49
+ # If we fail to get details (e.g. NFS down), still list the pool but as unavailable
50
+ if 'name' not in locals():
51
+ try:
52
+ name = pool.name()
53
+ except:
54
+ name = "Unknown Pool"
55
+
56
+ logging.warning(f"Failed to get details for pool '{name}': {e}")
57
+ pools_info.append({
58
+ 'name': name,
59
+ 'pool': pool,
60
+ 'status': 'unavailable',
61
+ 'autostart': False,
62
+ 'capacity': 0,
63
+ 'allocation': 0,
64
+ 'error': str(e)
65
+ })
44
66
  except libvirt.libvirtError:
45
67
  return []
46
68