- Go 40.8%
- Python 37%
- Shell 19.5%
- Makefile 2.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Introduced a new UI section to allow users to select an additional ISO file, seamlessly integrating with the backend `--extra-cdrom` flag. Key changes: * Added an "Extra CD-ROM" row (QLineEdit + Browse button) below the "Boot ISO" section. * Implemented `_browse_extra_cdrom()` method to handle the ISO file picker dialog. * Added file existence validation in `_build_args()` to alert the user if the selected ISO is missing. * Updated `_build_args()` to append the `--extra-cdrom` argument when passing commands to `main.go`. * Maintained backward compatibility: leaving the field blank safely bypasses the logic, ensuring no impact on standard Linux VMs. |
||
| assets | ||
| build | ||
| gui | ||
| installer | ||
| .gitignore | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
Virt-Forge
A QEMU virtual machine manager for Linux — CLI launcher, controller, disk manager, and PyQt6 GUI, written in Go and Python.
Features
- VM Launcher — launch QEMU VMs with full control over profile, RAM, CPU, display, network, and audio
- Snapshot management — create, list, apply, and delete internal QCOW2 snapshots
- Live migration — transfer a running VM to another machine without shutting it down
- Disk Manager — create, inspect, resize, and convert QCOW2 disk images
- Control panel — monitor running VMs with PID, uptime, disk name, and port info
- Multi-arch — supports
x86_64andaarch64 - KVM + TCG — hardware acceleration where available, TCG fallback otherwise
Project Structure
virt-forge/
├── assets/
│ ├── go.mod
│ ├── go.sum
│ ├── main.go # qemu-run — VM launcher
│ ├── qemu-ctl.go # qemu-ctl — list / stop / status
│ └── qemu-disk.go # qemu-disk — disk image manager
│
├── bin/ # Build output: binaries + disk images
│ ├── qemu-run
│ ├── qemu-ctl
│ ├── qemu-disk
│ └── *.qcow2 # disk images live here
│
├── build/
│ ├── make # Main build script (Go + GUI)
│ └── make-gui # GUI-only build script (PyInstaller)
│
├── gui/
│ └── virt-forge.py # PyQt6 GUI source
│
├── installer/
│ ├── install-deps.sh # System dependency installer
│ ├── install-desktop.sh # Desktop menu entry installer
│ └── virt-forge.png # App icon (512×512)
│
├── Makefile # Convenience targets (wraps build/ and installer/)
├── _internal/ # GUI runtime libs — must stay beside the binary
├── virt-forge # GUI executable
└── README.md
Note:
_internal/is only present in one-dir builds. It must always remain in the same directory as thevirt-forgebinary. In one-file builds it is not needed — everything is bundled into the single binary.
Quick Start
# 1. Install QEMU + Python build tools
make install-deps
# 2. Build everything
make
# 3. Run
./virt-forge
Step 1 — Install Dependencies
install-deps.sh handles two separate dependency groups:
| Target | What it installs |
|---|---|
engine-deps |
qemu-system-x86_64, qemu-system-aarch64, qemu-utils |
build-deps |
python3, python3-venv, go |
# Via Makefile
make install-deps # install both (default)
make install-engine-deps # QEMU packages only
make install-build-deps # python3 + python3-venv + go only
# Or directly
sudo ./installer/install-deps.sh # both
sudo ./installer/install-deps.sh engine-deps # QEMU only
sudo ./installer/install-deps.sh build-deps # Python + Go only
engine-deps automatically adds the current user to the kvm group if
/dev/kvm is present (re-login required).
Supported package managers:
| Manager | Distributions |
|---|---|
apt |
Debian, Ubuntu, Mint, Pop!_OS, Kali, Raspbian |
dnf |
Fedora, RHEL 8+, AlmaLinux, Rocky |
yum |
CentOS 7 |
pacman |
Arch, Manjaro, EndeavourOS |
zypper |
openSUSE Leap / Tumbleweed |
apk |
Alpine Linux |
xbps |
Void Linux |
emerge |
Gentoo |
Step 2 — Build
Requirements:
| Tool | Purpose |
|---|---|
go 1.21+ |
Build Go binaries |
python3 + python3-venv |
Build the GUI (auto-venv handles PyInstaller + PyQt6) |
The GUI build script automatically creates a temporary .venv in the project
root, installs PyInstaller and PyQt6 inside it, builds the binary, then
removes the .venv when done. No manual pip install needed.
Using Make (recommended)
make # Build all — Go binaries + GUI (one-dir, default)
make onefile # Build all — Go binaries + GUI (single portable binary)
make gui # GUI only (one-dir)
make gui-onefile # GUI only (single portable binary)
make qemu-run # VM launcher binary only
make qemu-ctl # Control binary only
make qemu-disk # Disk manager binary only
make clean # Remove all build output
make help # Show all available targets
Using the build script directly
./build/make # same as make
./build/make gui --onefile
./build/make clean
One-dir vs One-file
| Mode | Output | Notes |
|---|---|---|
onedir (default) |
virt-forge + _internal/ |
Faster startup; _internal/ must stay beside the binary |
onefile |
virt-forge only |
Single portable binary; slightly slower first launch |
After a successful one-dir build:
virt-forge ← run this
_internal/ ← keep this alongside the binary
After a successful one-file build:
virt-forge ← single self-contained binary, copy anywhere
Step 3 — Desktop Entry (Optional)
Add Virt-Forge to your application menu:
# Via Makefile
make install-desktop # user-level (no sudo)
make install-desktop-sys # system-wide (sudo)
make remove-desktop # remove user-level entry
make remove-desktop-sys # remove system-wide entry
# Or directly
./installer/install-desktop.sh # install (user-level)
sudo ./installer/install-desktop.sh # install (system-wide)
./installer/install-desktop.sh remove # remove
GUI Usage
./virt-forge
Or launch Virt-Forge from your application menu if the desktop entry is installed. Click ❓ Help in the VM Launcher tab for a full in-app guide.
VM Launcher Tab
| Field | Description |
|---|---|
| Profile | normal (4 GB RAM, 2 CPU) · lowram (2 GB RAM, 1 CPU) · saved profiles from ~/.vm_profiles/ |
| Architecture | x86_64 or aarch64 |
| Disk | Select from bin/ or enter/browse a full path |
| Boot ISO | Optional — for OS installation only |
| RAM / CPU | Override profile defaults |
| SSH port | Host port forwarded to guest port 22 (default: 4444) |
| Extra forwards | Additional port mappings e.g. 8080:8080,5432:5432 |
| VNC | Remote display; port in 5900–5999 range |
| SPICE | Better-performance remote display; requires a password |
| Audio | PulseAudio passthrough (off by default) |
| Mode | Daemon (background) or foreground |
| Snapshot | Boot into a saved snapshot by name |
| Live Migration | Send, Receive, or Monitor-only mode (see below) |
Connecting to a Running VM
ssh user@localhost -p 4444
vncviewer 127.0.0.1:5909
remote-viewer spice://localhost:5910
Control Tab
Displays all running VMs with PID, uptime, disk name, and port info. Auto-refreshes every 5 seconds.
| Button | Action |
|---|---|
| ⟳ Refresh | Force-refresh the VM list |
| 📊 Status | Show detailed status for the selected VM |
| 🛑 Stop Selected | Stop the selected VM |
| 💀 Stop All | Stop all running VMs |
Disk Manager Tab
| Button | Action |
|---|---|
| ➕ Create Image | Create a new QCOW2 disk image |
| ℹ Image Info | Show virtual/actual size and format |
| 📏 Resize Image | Grow an existing image (shrinking not supported) |
| 🔄 Convert Image | Convert between qcow2 / raw / vmdk / vdi / vpc / vhdx |
| 📋 List Snapshots | List all snapshots inside a disk image |
| 📸 Create Snapshot | Save the current VM state as a named snapshot |
| ⏪ Apply Snapshot | Restore the disk to a saved snapshot |
| 🗑 Delete Snapshot | Remove a snapshot from the image |
Shut down the VM before creating a snapshot to ensure a consistent disk state.
Live Migration
Live migration transfers a running VM from one machine to another without shutting it down. RAM and CPU state are streamed over the network. The disk image must already exist on the destination (copy it manually or use shared storage such as NFS).
Automatic (recommended)
System B — destination (run first):
# GUI: Live Migration → Receive, Listen port: 5555 → Launch VM
qemu-run --disk vm.qcow2 --incoming tcp:0:5555
System A — source:
# GUI: Live Migration → Send, Destination: 192.168.1.x:5555 → Launch VM
qemu-run --disk vm.qcow2 --migrate 192.168.1.x:5555
qemu-run opens an internal monitor, issues the migrate command, polls
status every second, and shuts down the source QEMU automatically on
completion.
Manual (advanced)
# System A — expose the QEMU monitor
qemu-run --disk vm.qcow2 --monitor 4445
# From another terminal on System A
telnet localhost 4445
(qemu) migrate tcp:192.168.1.x:5555
Notes
- Both machines should run the same QEMU version and the same acceleration mode (KVM or TCG).
- Open the migration port (e.g.
5555) in the firewall on System B. - After migration completes, reconnect your VNC/SPICE client to System B.
Snapshots
Snapshots are stored inside the QCOW2 file — no extra files needed.
./bin/qemu-disk snapshot list --name bin/debian.qcow2
./bin/qemu-disk snapshot create --name bin/debian.qcow2 --snap before-upgrade
./bin/qemu-disk snapshot apply --name bin/debian.qcow2 --snap before-upgrade
./bin/qemu-disk snapshot delete --name bin/debian.qcow2 --snap before-upgrade
# Boot into a snapshot
./bin/qemu-run --disk bin/debian.qcow2 --snapshot before-upgrade
CLI Reference
All binaries use explicit flags — no interactive prompts.
qemu-run
qemu-run --disk <path> [options]
Required:
--disk <path>
Profile:
--profile normal|lowram|<saved-name> default: normal
VM config:
--arch x86_64|aarch64 default: x86_64
--ram <MB>
--cpu <n>
--iso <path>
Network:
--ssh <port> default: 4444
--extra-fwds hostport:guestport,...
Display:
--vnc <port> default: 5909
--no-vnc
--spice <port>
--spice-pass <password>
--no-spice
Audio:
--audio
--no-audio
Mode:
--fg foreground (default: daemon)
Snapshot:
--snapshot <n> boot into saved snapshot
Live Migration:
--migrate <ip:port> send this VM to destination
--incoming tcp:0:<port> receive an incoming VM
--monitor <port> expose QEMU monitor via TCP
Examples:
qemu-run --disk bin/alpine.qcow2
qemu-run --disk bin/debian.qcow2 --ram 8192 --cpu 4 --vnc 5901 --fg
qemu-run --disk bin/alpine.qcow2 --spice 5910 --spice-pass hunter2 \
--extra-fwds 8080:8080
qemu-run --disk bin/debian.qcow2 --snapshot before-upgrade
qemu-run --disk bin/debian.qcow2 --migrate 192.168.1.50:5555
qemu-run --disk bin/debian.qcow2 --incoming tcp:0:5555
qemu-ctl
qemu-ctl list # list running VMs with PID, ports, and uptime
qemu-ctl status # detailed status for all VMs
qemu-ctl stop # stop a VM
qemu-ctl debug # raw parsed fields and lock directory contents
qemu-disk
qemu-disk create --name <file> --size <size>
qemu-disk info --name <file>
qemu-disk resize --name <file> --size <size>
qemu-disk convert --src <file> --dst <file> --fmt <format>
qemu-disk snapshot list --name <file>
qemu-disk snapshot create --name <file> --snap <n>
qemu-disk snapshot apply --name <file> --snap <n>
qemu-disk snapshot delete --name <file> --snap <n>
Size format: integer followed by K, M, G, or T — e.g. 20G, 512M, 2T.
Supported convert formats: qcow2 raw vmdk vdi vpc vhdx qed parallels
Lock File System
Running VMs create lock files in ~/.virt-forge-locks/:
~/.virt-forge-locks/
├── qemu_4444.pid # QEMU PID (keyed by SSH port)
├── ssh_4444.lock
├── vnc_5909.lock # keyed by VNC port
└── spice_5910.lock
- Locks are removed automatically when the VM stops
qemu-ctl statusreports stale locks from crashed sessions- Port conflicts are caught before launch
KVM Hardware Acceleration
ls /dev/kvm # check availability
groups $USER # check group membership
sudo usermod -aG kvm $USER # add yourself (re-login required)
Without KVM, QEMU falls back to TCG (software emulation) — functional but slower.
Environment Variables
# Override project root (default: auto-detected from binary location)
VIRT_FORGE_ROOT=/opt/virt-forge ./virt-forge
# Override bin directory
VIRT_FORGE_BIN=/custom/bin ./virt-forge
Dependencies
| Component | Requirements |
|---|---|
| VM Launcher / CTL / Disk | qemu-system-x86_64 qemu-system-aarch64 qemu-utils |
| Build — Go | go 1.21+ |
| Build — GUI | python3 · python3-venv (PyInstaller + PyQt6 auto-installed in temp venv) |
| GUI Runtime (one-dir) | _internal/ (bundled — no separate install needed) |
| GUI Runtime (one-file) | none — fully self-contained |
License
MIT — see LICENSE for details.