Files
easyqemu/easyqemu
2026-01-07 02:01:17 +01:00

959 líneas
25 KiB
Bash
Archivo Ejecutable

#!/bin/bash
##############################################################################
# EasyQEMU - Intuitive QEMU Virtual Machine Management Tool
#
# A comprehensive tool for managing QEMU virtual machines with features like:
# - VM creation, modification, deletion
# - Volume management (create, import, export, convert)
# - Snapshot management
# - Repository system for VMs and volumes (Docker-like registry)
# - Support for multiple disk formats (raw, qcow2, vmdk, vdi, vhdx)
#
# Author: EasyQEMU Project
# License: MIT
##############################################################################
set -e
# Version
VERSION="1.0.0"
# Default paths
EASYQEMU_HOME="${EASYQEMU_HOME:-$HOME/.easyqemu}"
EASYQEMU_VMS="$EASYQEMU_HOME/vms"
EASYQEMU_VOLUMES="$EASYQEMU_HOME/volumes"
EASYQEMU_CONFIGS="$EASYQEMU_HOME/configs"
EASYQEMU_REPO="$EASYQEMU_HOME/repository"
EASYQEMU_LOCKS="$EASYQEMU_HOME/locks"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
##############################################################################
# Utility Functions
##############################################################################
info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
fatal() {
error "$1"
exit 1
}
# Initialize EasyQEMU directories
init_dirs() {
mkdir -p "$EASYQEMU_HOME"
mkdir -p "$EASYQEMU_VMS"
mkdir -p "$EASYQEMU_VOLUMES"
mkdir -p "$EASYQEMU_CONFIGS"
mkdir -p "$EASYQEMU_REPO"
mkdir -p "$EASYQEMU_LOCKS"
}
# Check if QEMU is installed
check_qemu() {
if ! command -v qemu-system-x86_64 &> /dev/null; then
fatal "QEMU is not installed. Please install qemu-system-x86_64"
fi
if ! command -v qemu-img &> /dev/null; then
fatal "qemu-img is not installed. Please install qemu-utils"
fi
}
# Generate VM ID
generate_vm_id() {
local name="$1"
echo "${name}_$(date +%s)"
}
# Save VM configuration
save_vm_config() {
local vm_id="$1"
local config_file="$EASYQEMU_CONFIGS/${vm_id}.json"
shift
cat > "$config_file" << EOF
{
"id": "$vm_id",
"name": "${VM_NAME:-unknown}",
"created": "$(date -Iseconds)",
"memory": "${VM_MEMORY:-2048}",
"cpus": "${VM_CPUS:-2}",
"disk": "${VM_DISK:-}",
"cdrom": "${VM_CDROM:-}",
"network": "${VM_NETWORK:-user}",
"display": "${VM_DISPLAY:-gtk}",
"boot": "${VM_BOOT:-cd}",
"additional_args": "${VM_ADDITIONAL_ARGS:-}"
}
EOF
success "Configuration saved: $config_file"
}
# Load VM configuration
load_vm_config() {
local vm_id="$1"
local config_file="$EASYQEMU_CONFIGS/${vm_id}.json"
if [[ ! -f "$config_file" ]]; then
error "Configuration not found for VM: $vm_id"
return 1
fi
# Parse JSON (simple extraction)
VM_NAME=$(grep '"name"' "$config_file" | cut -d'"' -f4)
VM_MEMORY=$(grep '"memory"' "$config_file" | cut -d'"' -f4)
VM_CPUS=$(grep '"cpus"' "$config_file" | cut -d'"' -f4)
VM_DISK=$(grep '"disk"' "$config_file" | cut -d'"' -f4)
VM_CDROM=$(grep '"cdrom"' "$config_file" | cut -d'"' -f4)
VM_NETWORK=$(grep '"network"' "$config_file" | cut -d'"' -f4)
VM_DISPLAY=$(grep '"display"' "$config_file" | cut -d'"' -f4)
VM_BOOT=$(grep '"boot"' "$config_file" | cut -d'"' -f4)
VM_ADDITIONAL_ARGS=$(grep '"additional_args"' "$config_file" | cut -d'"' -f4)
}
##############################################################################
# VM Management Functions
##############################################################################
# Create a new VM
vm_create() {
local name=""
local memory="2048"
local cpus="2"
local disk=""
local disk_size="20G"
local cdrom=""
local network="user"
local display="gtk"
local boot="cd"
local additional_args=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--name)
name="$2"
shift 2
;;
--memory)
memory="$2"
shift 2
;;
--cpus)
cpus="$2"
shift 2
;;
--disk)
disk="$2"
shift 2
;;
--disk-size)
disk_size="$2"
shift 2
;;
--cdrom)
cdrom="$2"
shift 2
;;
--network)
network="$2"
shift 2
;;
--display)
display="$2"
shift 2
;;
--boot)
boot="$2"
shift 2
;;
*)
additional_args="$additional_args $1"
shift
;;
esac
done
[[ -z "$name" ]] && fatal "VM name is required. Use --name <name>"
info "Creating VM: $name"
# Generate VM ID
local vm_id=$(generate_vm_id "$name")
# Create disk if not provided
if [[ -z "$disk" ]]; then
disk="$EASYQEMU_VOLUMES/${vm_id}.qcow2"
info "Creating disk: $disk ($disk_size)"
qemu-img create -f qcow2 "$disk" "$disk_size"
fi
# Save configuration
VM_NAME="$name"
VM_MEMORY="$memory"
VM_CPUS="$cpus"
VM_DISK="$disk"
VM_CDROM="$cdrom"
VM_NETWORK="$network"
VM_DISPLAY="$display"
VM_BOOT="$boot"
VM_ADDITIONAL_ARGS="$additional_args"
save_vm_config "$vm_id"
success "VM created successfully: $vm_id"
echo "Use 'easyqemu vm start $vm_id' to start the VM"
}
# List VMs
vm_list() {
info "Available VMs:"
echo ""
printf "%-30s %-20s %-15s %-10s %-10s\n" "VM ID" "NAME" "CREATED" "MEMORY" "CPUS"
echo "--------------------------------------------------------------------------------"
for config in "$EASYQEMU_CONFIGS"/*.json; do
[[ ! -f "$config" ]] && continue
local vm_id=$(basename "$config" .json)
load_vm_config "$vm_id"
local created=$(grep '"created"' "$config" | cut -d'"' -f4 | cut -d'T' -f1)
printf "%-30s %-20s %-15s %-10s %-10s\n" "$vm_id" "$VM_NAME" "$created" "${VM_MEMORY}M" "$VM_CPUS"
done
}
# Start VM
vm_start() {
local vm_id="$1"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
load_vm_config "$vm_id" || fatal "Failed to load VM configuration"
info "Starting VM: $vm_id ($VM_NAME)"
# Build QEMU command
local qemu_cmd="qemu-system-x86_64"
qemu_cmd="$qemu_cmd -name $VM_NAME"
qemu_cmd="$qemu_cmd -m $VM_MEMORY"
qemu_cmd="$qemu_cmd -smp $VM_CPUS"
qemu_cmd="$qemu_cmd -enable-kvm"
# Disk
if [[ -n "$VM_DISK" ]]; then
qemu_cmd="$qemu_cmd -drive file=$VM_DISK,format=qcow2,if=virtio"
fi
# CDROM
if [[ -n "$VM_CDROM" ]]; then
qemu_cmd="$qemu_cmd -cdrom $VM_CDROM"
fi
# Network
qemu_cmd="$qemu_cmd -net nic -net $VM_NETWORK"
# Display
if [[ "$VM_DISPLAY" == "none" ]]; then
qemu_cmd="$qemu_cmd -nographic"
else
qemu_cmd="$qemu_cmd -display $VM_DISPLAY"
fi
# Boot
qemu_cmd="$qemu_cmd -boot $VM_BOOT"
# Additional args
if [[ -n "$VM_ADDITIONAL_ARGS" ]]; then
qemu_cmd="$qemu_cmd $VM_ADDITIONAL_ARGS"
fi
info "Executing: $qemu_cmd"
eval "$qemu_cmd"
}
# Delete VM
vm_delete() {
local vm_id="$1"
local force="$2"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
local config_file="$EASYQEMU_CONFIGS/${vm_id}.json"
[[ ! -f "$config_file" ]] && fatal "VM not found: $vm_id"
load_vm_config "$vm_id"
if [[ "$force" != "-f" ]] && [[ "$force" != "--force" ]]; then
read -p "Delete VM '$VM_NAME' ($vm_id)? This will remove the disk. [y/N] " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && fatal "Deletion cancelled"
fi
info "Deleting VM: $vm_id"
# Remove disk if it exists
if [[ -f "$VM_DISK" ]]; then
rm -f "$VM_DISK"
success "Disk removed: $VM_DISK"
fi
# Remove configuration
rm -f "$config_file"
success "VM deleted: $vm_id"
}
# Show VM info
vm_info() {
local vm_id="$1"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
local config_file="$EASYQEMU_CONFIGS/${vm_id}.json"
[[ ! -f "$config_file" ]] && fatal "VM not found: $vm_id"
info "VM Information:"
echo ""
cat "$config_file" | sed 's/^/ /'
}
# Modify VM
vm_modify() {
local vm_id="$1"
shift
[[ -z "$vm_id" ]] && fatal "VM ID is required"
load_vm_config "$vm_id" || fatal "Failed to load VM configuration"
# Parse modification arguments
while [[ $# -gt 0 ]]; do
case $1 in
--name)
VM_NAME="$2"
shift 2
;;
--memory)
VM_MEMORY="$2"
shift 2
;;
--cpus)
VM_CPUS="$2"
shift 2
;;
--cdrom)
VM_CDROM="$2"
shift 2
;;
--network)
VM_NETWORK="$2"
shift 2
;;
--display)
VM_DISPLAY="$2"
shift 2
;;
--boot)
VM_BOOT="$2"
shift 2
;;
*)
warning "Unknown option: $1"
shift
;;
esac
done
save_vm_config "$vm_id"
success "VM modified: $vm_id"
}
##############################################################################
# Volume Management Functions
##############################################################################
# Create volume
volume_create() {
local name=""
local size="10G"
local format="qcow2"
while [[ $# -gt 0 ]]; do
case $1 in
--name)
name="$2"
shift 2
;;
--size)
size="$2"
shift 2
;;
--format)
format="$2"
shift 2
;;
*)
shift
;;
esac
done
[[ -z "$name" ]] && fatal "Volume name is required. Use --name <name>"
local volume_path="$EASYQEMU_VOLUMES/${name}.${format}"
info "Creating volume: $volume_path ($size)"
qemu-img create -f "$format" "$volume_path" "$size"
success "Volume created: $volume_path"
}
# List volumes
volume_list() {
info "Available volumes:"
echo ""
printf "%-40s %-10s %-15s %-15s\n" "NAME" "FORMAT" "SIZE" "ACTUAL SIZE"
echo "--------------------------------------------------------------------------------"
for volume in "$EASYQEMU_VOLUMES"/*; do
[[ ! -f "$volume" ]] && continue
local name=$(basename "$volume")
local info=$(qemu-img info "$volume")
local format=$(echo "$info" | grep "file format:" | awk '{print $3}')
local vsize=$(echo "$info" | grep "virtual size:" | awk '{print $3}')
local dsize=$(echo "$info" | grep "disk size:" | awk '{print $3}')
printf "%-40s %-10s %-15s %-15s\n" "$name" "$format" "$vsize" "$dsize"
done
}
# Convert volume
volume_convert() {
local source=""
local target=""
local format="qcow2"
while [[ $# -gt 0 ]]; do
case $1 in
--source)
source="$2"
shift 2
;;
--target)
target="$2"
shift 2
;;
--format)
format="$2"
shift 2
;;
*)
shift
;;
esac
done
[[ -z "$source" ]] && fatal "Source volume is required. Use --source <path>"
[[ -z "$target" ]] && fatal "Target volume is required. Use --target <path>"
info "Converting: $source -> $target (format: $format)"
qemu-img convert -f raw -O "$format" "$source" "$target"
success "Volume converted: $target"
}
# Volume info
volume_info() {
local volume="$1"
[[ -z "$volume" ]] && fatal "Volume path is required"
[[ ! -f "$volume" ]] && fatal "Volume not found: $volume"
info "Volume Information:"
echo ""
qemu-img info "$volume"
}
# Import volume
volume_import() {
local source="$1"
local name="$2"
[[ -z "$source" ]] && fatal "Source path is required"
[[ -z "$name" ]] && name=$(basename "$source")
local target="$EASYQEMU_VOLUMES/$name"
info "Importing volume: $source -> $target"
cp "$source" "$target"
success "Volume imported: $target"
}
# Export volume
volume_export() {
local name="$1"
local target="$2"
[[ -z "$name" ]] && fatal "Volume name is required"
[[ -z "$target" ]] && fatal "Target path is required"
local source="$EASYQEMU_VOLUMES/$name"
[[ ! -f "$source" ]] && fatal "Volume not found: $name"
info "Exporting volume: $source -> $target"
cp "$source" "$target"
success "Volume exported: $target"
}
# Delete volume
volume_delete() {
local name="$1"
local force="$2"
[[ -z "$name" ]] && fatal "Volume name is required"
local volume_path="$EASYQEMU_VOLUMES/$name"
[[ ! -f "$volume_path" ]] && fatal "Volume not found: $name"
if [[ "$force" != "-f" ]] && [[ "$force" != "--force" ]]; then
read -p "Delete volume '$name'? [y/N] " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && fatal "Deletion cancelled"
fi
rm -f "$volume_path"
success "Volume deleted: $name"
}
##############################################################################
# Snapshot Management Functions
##############################################################################
# Create snapshot
snapshot_create() {
local vm_id="$1"
local snapshot_name="$2"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
[[ -z "$snapshot_name" ]] && snapshot_name="snapshot_$(date +%Y%m%d_%H%M%S)"
load_vm_config "$vm_id" || fatal "Failed to load VM configuration"
[[ ! -f "$VM_DISK" ]] && fatal "Disk not found: $VM_DISK"
info "Creating snapshot: $snapshot_name for VM: $vm_id"
qemu-img snapshot -c "$snapshot_name" "$VM_DISK"
success "Snapshot created: $snapshot_name"
}
# List snapshots
snapshot_list() {
local vm_id="$1"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
load_vm_config "$vm_id" || fatal "Failed to load VM configuration"
[[ ! -f "$VM_DISK" ]] && fatal "Disk not found: $VM_DISK"
info "Snapshots for VM: $vm_id ($VM_DISK)"
echo ""
qemu-img snapshot -l "$VM_DISK"
}
# Apply snapshot
snapshot_apply() {
local vm_id="$1"
local snapshot_name="$2"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
[[ -z "$snapshot_name" ]] && fatal "Snapshot name is required"
load_vm_config "$vm_id" || fatal "Failed to load VM configuration"
[[ ! -f "$VM_DISK" ]] && fatal "Disk not found: $VM_DISK"
info "Applying snapshot: $snapshot_name for VM: $vm_id"
qemu-img snapshot -a "$snapshot_name" "$VM_DISK"
success "Snapshot applied: $snapshot_name"
}
# Delete snapshot
snapshot_delete() {
local vm_id="$1"
local snapshot_name="$2"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
[[ -z "$snapshot_name" ]] && fatal "Snapshot name is required"
load_vm_config "$vm_id" || fatal "Failed to load VM configuration"
[[ ! -f "$VM_DISK" ]] && fatal "Disk not found: $VM_DISK"
info "Deleting snapshot: $snapshot_name for VM: $vm_id"
qemu-img snapshot -d "$snapshot_name" "$VM_DISK"
success "Snapshot deleted: $snapshot_name"
}
##############################################################################
# Repository Management Functions
##############################################################################
# Save VM to repository
repo_save() {
local vm_id="$1"
local repo_name="$2"
[[ -z "$vm_id" ]] && fatal "VM ID is required"
[[ -z "$repo_name" ]] && repo_name="$vm_id"
load_vm_config "$vm_id" || fatal "Failed to load VM configuration"
local repo_dir="$EASYQEMU_REPO/$repo_name"
mkdir -p "$repo_dir"
info "Saving VM to repository: $repo_name"
# Copy configuration
cp "$EASYQEMU_CONFIGS/${vm_id}.json" "$repo_dir/config.json"
# Copy disk
if [[ -f "$VM_DISK" ]]; then
cp "$VM_DISK" "$repo_dir/disk.qcow2"
fi
# Create metadata
cat > "$repo_dir/metadata.json" << EOF
{
"name": "$repo_name",
"original_vm_id": "$vm_id",
"saved": "$(date -Iseconds)",
"version": "1.0"
}
EOF
success "VM saved to repository: $repo_name"
}
# Load VM from repository
repo_load() {
local repo_name="$1"
local new_name="$2"
[[ -z "$repo_name" ]] && fatal "Repository name is required"
local repo_dir="$EASYQEMU_REPO/$repo_name"
[[ ! -d "$repo_dir" ]] && fatal "Repository not found: $repo_name"
# Generate new VM ID
local vm_id=$(generate_vm_id "${new_name:-$repo_name}")
info "Loading VM from repository: $repo_name -> $vm_id"
# Copy configuration
cp "$repo_dir/config.json" "$EASYQEMU_CONFIGS/${vm_id}.json"
# Update VM ID in config
sed -i "s/\"id\": \"[^\"]*\"/\"id\": \"$vm_id\"/" "$EASYQEMU_CONFIGS/${vm_id}.json"
if [[ -n "$new_name" ]]; then
sed -i "s/\"name\": \"[^\"]*\"/\"name\": \"$new_name\"/" "$EASYQEMU_CONFIGS/${vm_id}.json"
fi
# Copy disk
if [[ -f "$repo_dir/disk.qcow2" ]]; then
cp "$repo_dir/disk.qcow2" "$EASYQEMU_VOLUMES/${vm_id}.qcow2"
sed -i "s|\"disk\": \"[^\"]*\"|\"disk\": \"$EASYQEMU_VOLUMES/${vm_id}.qcow2\"|" "$EASYQEMU_CONFIGS/${vm_id}.json"
fi
success "VM loaded from repository: $vm_id"
echo "Use 'easyqemu vm start $vm_id' to start the VM"
}
# List repository entries
repo_list() {
info "Repository entries:"
echo ""
printf "%-30s %-20s\n" "NAME" "SAVED"
echo "--------------------------------------------------------------------------------"
for repo_dir in "$EASYQEMU_REPO"/*; do
[[ ! -d "$repo_dir" ]] && continue
local name=$(basename "$repo_dir")
local metadata="$repo_dir/metadata.json"
if [[ -f "$metadata" ]]; then
local saved=$(grep '"saved"' "$metadata" | cut -d'"' -f4 | cut -d'T' -f1)
printf "%-30s %-20s\n" "$name" "$saved"
else
printf "%-30s %-20s\n" "$name" "unknown"
fi
done
}
# Delete repository entry
repo_delete() {
local repo_name="$1"
local force="$2"
[[ -z "$repo_name" ]] && fatal "Repository name is required"
local repo_dir="$EASYQEMU_REPO/$repo_name"
[[ ! -d "$repo_dir" ]] && fatal "Repository not found: $repo_name"
if [[ "$force" != "-f" ]] && [[ "$force" != "--force" ]]; then
read -p "Delete repository entry '$repo_name'? [y/N] " -n 1 -r
echo
[[ ! $REPLY =~ ^[Yy]$ ]] && fatal "Deletion cancelled"
fi
rm -rf "$repo_dir"
success "Repository entry deleted: $repo_name"
}
##############################################################################
# Main Command Dispatcher
##############################################################################
show_help() {
cat << EOF
EasyQEMU v${VERSION} - Intuitive QEMU Virtual Machine Management
USAGE:
easyqemu <command> [options]
COMMANDS:
VM Management:
vm create Create a new virtual machine
vm list List all virtual machines
vm start Start a virtual machine
vm stop Stop a virtual machine (use QEMU controls)
vm delete Delete a virtual machine
vm info Show VM information
vm modify Modify VM configuration
Volume Management:
volume create Create a new volume
volume list List all volumes
volume info Show volume information
volume convert Convert volume format
volume import Import external volume
volume export Export volume
volume delete Delete a volume
Snapshot Management:
snapshot create Create a snapshot
snapshot list List snapshots for a VM
snapshot apply Apply/restore a snapshot
snapshot delete Delete a snapshot
Repository Management:
repo save Save VM to repository
repo load Load VM from repository
repo list List repository entries
repo delete Delete repository entry
Other:
version Show version information
help Show this help message
EXAMPLES:
# Create a new VM
easyqemu vm create --name ubuntu --memory 4096 --cpus 4 --disk-size 50G
# Start VM with ISO
easyqemu vm create --name test --cdrom /path/to/iso
easyqemu vm start test_<timestamp>
# List VMs
easyqemu vm list
# Create snapshot
easyqemu snapshot create <vm_id> my_snapshot
# Save VM to repository
easyqemu repo save <vm_id> my_saved_vm
# Convert volume
easyqemu volume convert --source disk.raw --target disk.qcow2 --format qcow2
For more information, see the README.md file.
EOF
}
show_version() {
echo "EasyQEMU version $VERSION"
}
main() {
# Initialize
init_dirs
check_qemu
# Parse command
local command="${1:-help}"
shift || true
case "$command" in
vm)
local subcommand="${1:-list}"
shift || true
case "$subcommand" in
create)
vm_create "$@"
;;
list)
vm_list "$@"
;;
start)
vm_start "$@"
;;
delete)
vm_delete "$@"
;;
info)
vm_info "$@"
;;
modify)
vm_modify "$@"
;;
*)
error "Unknown VM subcommand: $subcommand"
echo "Use 'easyqemu help' for usage information"
exit 1
;;
esac
;;
volume)
local subcommand="${1:-list}"
shift || true
case "$subcommand" in
create)
volume_create "$@"
;;
list)
volume_list "$@"
;;
info)
volume_info "$@"
;;
convert)
volume_convert "$@"
;;
import)
volume_import "$@"
;;
export)
volume_export "$@"
;;
delete)
volume_delete "$@"
;;
*)
error "Unknown volume subcommand: $subcommand"
echo "Use 'easyqemu help' for usage information"
exit 1
;;
esac
;;
snapshot)
local subcommand="${1:-list}"
shift || true
case "$subcommand" in
create)
snapshot_create "$@"
;;
list)
snapshot_list "$@"
;;
apply)
snapshot_apply "$@"
;;
delete)
snapshot_delete "$@"
;;
*)
error "Unknown snapshot subcommand: $subcommand"
echo "Use 'easyqemu help' for usage information"
exit 1
;;
esac
;;
repo)
local subcommand="${1:-list}"
shift || true
case "$subcommand" in
save)
repo_save "$@"
;;
load)
repo_load "$@"
;;
list)
repo_list "$@"
;;
delete)
repo_delete "$@"
;;
*)
error "Unknown repo subcommand: $subcommand"
echo "Use 'easyqemu help' for usage information"
exit 1
;;
esac
;;
version)
show_version
;;
help|--help|-h)
show_help
;;
*)
error "Unknown command: $command"
echo "Use 'easyqemu help' for usage information"
exit 1
;;
esac
}
# Run main function
main "$@"