environment/lib/vm/qemu.rb
2025-08-29 15:52:53 +02:00

344 lines
No EOL
11 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

require "fileutils"
require "ostruct"
module DisplayMode
def self.none
0
end
def self.fullscreen
1
end
def self.window
2
end
def self.vnc
3
end
end
module Qemu
def self.qemu_bin_for(arch)
{
x86_64: "qemu-system-x86_64",
x86_32: "qemu-system-i386",
arm64: "qemu-system-aarch64",
armv7: "qemu-system-arm",
armv6: "qemu-system-arm",
armv5: "qemu-system-arm",
riscv64: "qemu-system-riscv64",
ppc64le: "qemu-system-ppc64",
ppc64: "qemu-system-ppc64",
ppc32: "qemu-system-ppc",
s390x: "qemu-system-s390x",
mips64el: "qemu-system-mips64el",
mips64: "qemu-system-mips64",
mipsel: "qemu-system-mipsel",
mips: "qemu-system-mips",
loongarch64: "qemu-system-loongarch64",
sparc64: "qemu-system-sparc64",
alpha: "qemu-system-alpha",
hppa: "qemu-system-hppa",
ia64: "qemu-system-ia64",
}.fetch(arch) { raise "Unsupported arch: #{arch.inspect}" }
end
def self.accel_args
host = RbConfig::CONFIG["host_os"]
if host =~ /linux/i && File.exist?("/dev/kvm")
["-accel", "kvm"]
elsif host =~ /darwin/i
# hvf exists on Apple Silicon + Intel macOS; QEMU falls back if not available
["-accel", "hvf"]
elsif host =~ /freebsd/i
# if QEMU was built with it; otherwise it will ignore
["-accel", "bhyve"]
else
["-accel", "tcg"]
end
end
def self.machine_args_for(arch)
case arch
when :x86_64, :x86_32
[]
when :arm64
# -machine type=virt
# -cpu cortex-a72
["-machine", "virt", "-cpu", "max"]
when :armv7, :armv6, :armv5
["-machine", "virt", "-cpu", "cortex-a15"]
when :riscv64
["-machine", "virt"]
when :loongarch64
["-machine", "virt"]
else
[]
end
end
#def self.launch(arch, disk_path, cdrom = nil, detach = true)
def self.launch(arch, disk_path, **options)
defaults = {
arch: System::ARCH,
cdrom: nil,
detach: true,
shell: false,
ram: 2048 * 8,
cpus: 1,
display: DisplayMode::none,
mount: {
wd: nil,
home: nil
}
}
# for testing only
defaults[:detach] = false
defaults[:display] = DisplayMode.fullscreen
defaults[:display] = DisplayMode.window
# defaults[:display] = DisplayMode.none
#defaults[:display] = DisplayMode.vnc
opts = defaults.merge(options)
puts options
puts opts
qemu = qemu_bin_for(arch)
args = []
if System::OS == :macos && arch == :arm64
# args += ["-bios", "/opt/homebrew/share/qemu/edk2-aarch64-code.fd"]
# cp /opt/homebrew/share/qemu/edk2-arm-vars.fd ~/edk2-arm-vars.fd
unless File.exist?(opts[:vars_fd])
#System.qemu_paths
FileUtils.cp(System.qemu_vars_fd_path, opts[:vars_fd])
end
unless File.exist?(opts[:code_fd])
FileUtils.cp(System.qemu_code_fd_path, opts[:code_fd])
end
args += ["-drive", "if=pflash,format=raw,unit=0,readonly=on,file=#{opts[:code_fd]}"]
args += ["-drive", "if=pflash,format=raw,unit=1,file=#{opts[:vars_fd]}"]
ssh_port = nil
if opts[:shell]
ssh_port = rand(4000..9999)
# args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{ssh_port}-:22"]
# args += ['-device', 'virtio-net-device,netdev=net0']
# args += ['-netdev', "user,id=n0,hostfwd=tcp:127.0.0.1:#{ssh_port}-10.0.2.15:22"]
# args += ['-device', 'virtio-net,netdev=n0']
# args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{ssh_port}-10.0.2.15:22"]
# args += ['-device', 'virtio-net-device,netdev=net0"']
args += ['-nic', "user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:#{ssh_port}-:22"]
puts "ssh -p #{ssh_port} user@localhost"
# conf that works
#args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"]
end
# -virtfs local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs
# mount -t 9p -o trans=virtio,version=9p2000.L hostfs /mnt
# sudo mount -t 9p hostfs /home/user/Share -o trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap
# hostfs /home 9p trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap,nofail 0 0
# hostfs /share 9p trans=virtio,version=9p2000.L,uid=1000,gid=1000,msize=262144,cache=mmap,nofail 0 0
if opts[:mount][:wd]
args += ['-virtfs', 'local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs']
end
if opts[:mount][:home]
#args += ['-homefs', "local,path=#{VMDATA},mount_tag=hostfs,security_model=passthrough,id=hostfs"]
end
if opts[:display] == DisplayMode::none
port = 2222
args += ['-nographic']
args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22,udp:127.0.0.1:6544-:6544=on"]
#args += ['-device', 'e1000,netdev=net0']
args += ['-device', 'virtio-net-pci,netdev=net0']
puts "ssh -p #{port} user@localhost"
elsif opts[:display] == DisplayMode::vnc
# Note: this outputs serial on the console
#args += ['-nographic']
args += ["-display", "none"]
args += ["-device", "virtio-gpu-pci"]
args += ["-device", "virtio-keyboard-pci"]
args += ["-device", "virtio-mouse-pci"]
args += ['-vnc', '127.0.0.1:0,password=on']
# tunnel ssh -L 5900:127.0.0.1:5900 user@your-host
# -monitor unix:/tmp/qemu-mon,server,nowait
# -vnc 127.0.0.1:0,password=on
# printf 'change vnc password\nMySecret\n' | socat - UNIX-CONNECT:/tmp/qemu-mon
# SASL auth (username + password)
# -vnc :0,sasl
# TLS (certificates, optional password)
# -object tls-creds-x509,id=tls0,...
# -vnc :0,tls-creds=tls0
# qemu-system-x86_64 \
# -object tls-creds-x509,id=tls0,dir=/etc/pki/qemu,endpoint=server \
# -vnc :0,tls-creds=tls0,sasl
else
#args += ["-device", "virtio-gpu-device"]
if opts[:display] == DisplayMode::fullscreen
# #args += ["-display", "cocoa,full-screen=on"]
# # attempts:
# #args += ["-display", "cocoa,full-screen=on,retina=on"]
# # brew install gtk+3 sdl2
# # args += ["-display", "sdl,gl=on,full-screen=on"]
# #args += ["-display", "gtk,gl=on,full-screen=on"]
# #args += ["-display", "cocoa,full-screen=on"]
# #args += ["-display", "cocoa,gl=es,full-screen=on"]
#### TODO: try make it work with custom build
# args += ["-device", "virtio-gpu-gl-pci"]
# args += ["-display", "sdl,gl=on,full-screen=on"]
args += ["-display", "cocoa,full-screen=on"]
else
args += ["-display", "cocoa"]
end
args += ["-device", "qemu-xhci,id=xhci"]
args += ["-device", "usb-kbd"]
args += ["-device", "usb-tablet"]
args += ["-device", "virtio-keyboard-device"]
args += ["-device", "virtio-mouse-device"]
args += ["-device", "virtio-gpu"]
#args += ['-nic', 'user,model=virtio-net-pci']
unless opts[:shell]
args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"]
end
### TODO: remove
port = 2222
args += ['-device', 'virtio-net-pci,netdev=net0']
args += ['-netdev', "user,id=net0,hostfwd=tcp:127.0.0.1:#{port}-:22,hostfwd=udp:127.0.0.1:6544-:6544"]
args += ['-virtfs', 'local,path=.,mount_tag=hostfs,security_model=passthrough,id=hostfs']
### TODO END
# macOS vmnet (shares Macs LAN)
# -netdev vmnet-shared,id=n1 \
# -device virtio-net-pci,netdev=n1
end
# copy to /Users/artur/.cache/dat/vm/debian/arm64/debian/vars.fd
# /opt/homebrew/share/qemu/edk2-aarch64-vars.fd
# args += ["-drive", "if=pflash,format=raw,unit=1,file=/Users/artur/.cache/dat/vm/debian/arm64/debian/vars.fd"]
end
args += accel_args
args += machine_args_for(arch)
args += ["-m", opts[:ram].to_s, "-smp", opts[:cpus].to_s]
args += ["-name", "FirstVM", "-boot", "order=d"] # boot from CD first
args += ["-drive", "file=#{disk_path},if=virtio,cache=writeback,format=raw,id=nvme0"]
#args += ["-device", "nvme,serial=nvme0,drive=nvme0,bootindex=2"]
if opts[:cdrom] != nil
#args += ["-cdrom", opts[:cdrom]]
# args += ["-device", "virtio-scsi-pci,id=scsi"]
# args += ["-drive", "if=none,id=cd,format=raw,file=#{opts[:cdrom]},media=cdrom"]
# args += ["-device", "scsi-cd,drive=cd,bootindex=1"]
args += ["-drive", "id=cd,format=raw,file=#{opts[:cdrom]},media=cdrom"]
args += ["-device", "usb-storage,drive=cd,bootindex=1"]
args += ["-device", "ramfb"]
# args += ["-device", "virtio-gpu-pci"]
# args += ["-display", "default,show-cursor=on"]
end
if opts[:tpm]
# brew install swtpm
# swtpm socket --tpm2 --ctrl type=unixio,path=./tpm/tpm.sock --tpmstate dir=./tpm --daemon
["swtpm", "socket", "--tpm2", "--ctrl", "type=unixio,path=./tpm/tpm.sock", "--tpmstate", "dir=./tpm"]
# args += ["-chardev", "socket,id=chrtpm,path=/Users/agurgul/Downloads/tpm/tpm.sock"]
# args += ["-tpmdev", "emulator,id=tpm0,chardev=chrtpm"]
# args += ["-device", "tpm-crb-device,tpmdev=tpm0"]
args += ["-chardev", "socket,id=chrtpm,path=/Users/agurgul/Downloads/tpm/tpm.sock"]
args += ["-tpmdev", "emulator,id=tpm0,chardev=chrtpm"]
#args += ["-device", "tpm-tis,tpmdev=tpm0"]
args += ["-device", "tpm-tis-device,tpmdev=tpm0"]
# nic user,ipv6=off,model=rtl8139,mac=84:1b:77:c9:03:a6
# TODO: Shared network on macOS
# -netdev vmnet-shared,id=net0
end
args += ['-monitor', 'stdio']
# args += ["-device", "virtio-net,netdev=n0", "-netdev", "user,id=n0"] # user-mode NAT
# optional: uncomment to run headless with VNC on :5901
# args += ["-display", "none", "-vnc", "127.0.0.1:1"]
cmd = [qemu, *args]
puts "Launching: #{cmd.join(' ')}"
if opts[:detach]
log = File.open("log.txt", "w")
pid = Process.spawn(*cmd, pgroup: false, out: log, err: log)
Process.detach(pid)
puts "QEMU pid=#{pid}"
if opts[:shell]
System.exec_ssh(ssh_port)
end
else
pid = Process.spawn(*cmd, pgroup: true, out: $stdout, err: $stderr)
Process.wait(pid)
status = $?
puts "Exit status: #{status.exitstatus}"
end
end
end
## Works on MacOS=
# -monitor unix:/tmp/qemu-monitor.sock,server,nowait
# nc -U /tmp/qemu-monitor.sock
# instead of args += ['-monitor', 'stdio']
# 9P
# sudo mount -t 9p hostfs /home/user/Share \
# -o trans=virtio,version=9p2000.L,msize=262144,cache=mmap,access=any,dfltuid=1000,dfltgid=1000
# sudo chown -hR 1000:1000 /home/user/Share