π Lightweight Bash CLI for managing KVM virtual machines using
libvirt
,cloud-init
, andterraform
.
vmt
automates the lifecycle of local KVM virtual machines through a simple command-line interface. It uses:
- KVM/QEMU - fast, native Linux virtualization
- libvirt - VM orchestration (domains, pools, networks)
- Terraform - declarative configuration and lifecycle control
- cloud-init - inject SSH keys and configure VMs on first boot
- pure Bash - no Python, Go, or compiled binaries
Once the image is cached, everything runs offline and locally - ideal for air-gapped, reproducible, or automated setups.
Install vmt
using the official installer:
curl -fsSL https://raw.githubusercontent.com/opensecurity/vmt/main/install.sh | bash
This will:
- Download the latest version of the
vmt
script into~/.vmt
- Link it as
vmt
into~/.local/bin/
- Make it executable and ready to use
vmt
relies on standard tools available on most Linux distributions:
Tool | Role |
---|---|
bash |
Shell environment |
virsh |
libvirt CLI for domains, networks |
qemu-system |
Hypervisor engine for VM execution |
terraform |
Declarative infrastructure management |
cloud-localds |
Generate cloud-init seed disks |
ssh / scp |
Key injection and access |
curl |
Download base OS images |
A vmt doctor
command is planned to automatically detect and verify dependencies for RHEL and Debian-based systems.
Out-of-the-box support:
- Ubuntu 24.04 (Minimal Cloud Image)
- AlmaLinux 9 (Generic Cloud)
- RockyLinux 9 (Generic Cloud)
More distributions can be added by editing the DISTRO
case logic in the script.
vmt <action> <vm_name> [distro] [ram_mb] [cpus] [disk_gb]
Command | Description |
---|---|
bootstrap |
Generate shared SSH key pair |
create |
Prepare disk, seed image, and config |
apply |
Launch VM with Terraform |
start |
Start a defined VM |
stop |
Gracefully shutdown a running VM |
ssh |
Connect via SSH using generated key |
ls |
List all VMs, IPs, OS, and status |
destroy |
Run terraform destroy only |
delete |
Fully remove VM, storage, and network |
version |
Show CLI version |
help |
Show CLI usage summary |
Each VM is created under:
~/vms/<vm_name>/
βββ qemu/images/ # Disk image
βββ qemu/seed/ # cloud-init seed.img
βββ cloudinit/ # user-data and meta-data
βββ terraform/ # Terraform definition
Global:
~/vms/.images/
: Cached base OS images~/vms/.ssh/id_ed25519
: Shared SSH key pair~/vms/logs/*.log
: Logs for each VM run
All VMs are configured with:
devops
user- Public key injected via
cloud-init
NOPASSWD
sudo access- QEMU guest agent installed and enabled
Example:
vmt ssh myvm
Or directly:
ssh -i ~/vms/.ssh/id_ed25519 devops@<ip>
vmt bootstrap
π 2025-05-09 00:32:54 - Running: vmt bootstrap
π Generating SSH key for VM access...
Generating public/private ed25519 key pair.
Your identification has been saved in /home/dev/vms/.ssh/id_ed25519
Your public key has been saved in /home/dev/vms/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:QcA4JTKQWII0OVc***
The key's randomart image is:
****
β
SSH key generated at /home/dev/vms/.ssh/id_ed25519
vmt create securevm almalinux 4096 2 40
π 2025-05-09 00:34:57 - Running: vmt create securevm almalinux 4096 2 40
β
SSH key already exists: /home/dev/vms/.ssh/id_ed25519
Pool vmt_securevm defined
Pool vmt_securevm built
Pool vmt_securevm started
Pool vmt_securevm marked as autostarted
Network net_vmt_securevm defined from /home/dev/vms/securevm/qemu/network.xml
Network net_vmt_securevm started
Network net_vmt_securevm marked as autostarted
β
Created and started libvirt network: net_vmt_securevm
π¦ Checking for base image in cache...
β¬οΈ Downloading base image to /home/dev/vms/.images/alma-cloud.qcow2...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 468M 100 468M 0 0 93.5M 0 0:00:05 0:00:05 --:--:-- 94.6M
π Verifying downloaded image...
β
Image verified: /home/dev/vms/.images/alma-cloud.qcow2
β
Cached base image at /home/dev/vms/.images/alma-cloud.qcow2
π Copying base image to VM directory...
β
VM config created in /home/dev/vms/securevm
vmt apply securevm
π 2025-05-09 00:36:01 - Running: vmt apply securevm
π§Ή Checking for existing domain in libvirt...
Initializing the backend...
Initializing provider plugins...
- Finding dmacvicar/libvirt versions matching "~> 0.7.6"...
- Installing dmacvicar/libvirt v0.7.6...
- Installed dmacvicar/libvirt v0.7.6 (self-signed, key ID 0833E38C51E74D26)
Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# libvirt_domain.securevm will be created
+ resource "libvirt_domain" "securevm" {
+ arch = (known after apply)
+ autostart = (known after apply)
+ emulator = (known after apply)
+ fw_cfg_name = "opt/com.coreos/config"
+ id = (known after apply)
+ machine = (known after apply)
+ memory = 4096
+ name = "securevm"
+ qemu_agent = false
+ running = true
+ type = "kvm"
+ vcpu = 2
+ boot_device {
+ dev = [
+ "hd",
]
}
+ console {
+ source_host = "127.0.0.1"
+ source_service = "0"
+ target_port = "0"
+ target_type = "serial"
+ type = "pty"
}
+ cpu {
+ mode = "host-passthrough"
}
+ disk {
+ scsi = false
+ volume_id = (known after apply)
}
+ disk {
+ scsi = false
+ volume_id = (known after apply)
}
+ graphics {
+ autoport = true
+ listen_address = "127.0.0.1"
+ listen_type = "none"
+ type = "spice"
}
+ network_interface {
+ addresses = (known after apply)
+ hostname = (known after apply)
+ mac = (known after apply)
+ network_id = (known after apply)
+ network_name = "net_vmt_securevm"
}
}
# libvirt_volume.base will be created
+ resource "libvirt_volume" "base" {
+ format = "qcow2"
+ id = (known after apply)
+ name = "securevm-base"
+ pool = "vmt_securevm"
+ size = (known after apply)
+ source = "./../qemu/images/alma-cloud.qcow2"
}
# libvirt_volume.disk will be created
+ resource "libvirt_volume" "disk" {
+ base_volume_id = (known after apply)
+ format = (known after apply)
+ id = (known after apply)
+ name = "securevm.qcow2"
+ pool = "vmt_securevm"
+ size = 42949672960
}
# libvirt_volume.seed will be created
+ resource "libvirt_volume" "seed" {
+ format = "raw"
+ id = (known after apply)
+ name = "securevm-seed.img"
+ pool = "vmt_securevm"
+ size = (known after apply)
+ source = "./../qemu/seed/securevm-seed.img"
}
Plan: 4 to add, 0 to change, 0 to destroy.
libvirt_volume.base: Creating...
libvirt_volume.seed: Creating...
libvirt_volume.base: Creation complete after 1s [id=/home/dev/vms/securevm/qemu/securevm-base]
libvirt_volume.disk: Creating...
libvirt_volume.seed: Creation complete after 1s [id=/home/dev/vms/securevm/qemu/securevm-seed.img]
libvirt_volume.disk: Creation complete after 0s [id=/home/dev/vms/securevm/qemu/securevm.qcow2]
libvirt_domain.securevm: Creating...
libvirt_domain.securevm: Creation complete after 1s [id=cf38d944-0d11-4743-80be-73db495e190f]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
π VM 'securevm' is ready.
π Waiting for IP address...
π SSH: ssh -i /home/dev/vms/.ssh/id_ed25519 devops@192.168.185.230
vmt ls
π 2025-05-09 00:39:27 - Running: vmt ls
π All VMs in /home/o/vms:
β’ alma β’ 192.168.179.249 β’ AlmaLinux β’ running
β’ securevm β’ 192.168.185.230 β’ AlmaLinux β’ running
vmt ssh securevm
π 2025-05-09 00:40:12 - Running: vmt ssh securevm
Warning: Permanently added '192.168.185.230' (ED25519) to the list of known hosts.
[devops@securevm ~]$
vmt destroy securevm
vmt delete securevm
- Cloud-init handles provisioning, so no guest image modification is needed
- All disk images use QCOW2 with base + diff layers for efficiency
qemu-guest-agent
is installed by default for IP resolution and shutdown- Logs are captured per-VM in
~/vms/.logs/
trap
andset -euo pipefail
ensure reliable scripting behavior
MIT - see LICENSE