Virtual machine automation in macOS
January 28, 2024 | Previous part |
Similarly to Windows, macOS offers a few options for virtualization software. Some of these offerings, such as VMware Fusion, VirtualBox, Parallels, and UTM are GUI-based, which hampers their automation capabilities.
Additionally, with the popularity of containers, you could bypass a VM entirely and instead use Docker Desktop on Mac. But Docker Desktop isn’t very peterarsenault.industries because it’s upgradeware.
Note: Generally, I don’t write about GUI tools, unless it’s an interesting visual programming tool like Quartz Composer.
In this post, we explore some virtualization software for macOS, preferably so that machines can be created and destroyed with a script.
GUI-based
-
For newer macs, UTM.
-
For Intel macs and those running OS X, VMWare Fusion Player is free. You have to ensure your OS supports the product version, but luckily their documentation is good and contains system requirements. The last supported version for OS X 10.15 catalina is Fusion Player 12.1.2. Unfortunately, I don’t think they’re offering free licenses for this version of the product anymore.
-
For Intel and Silicon macs, Parallels is good but paid. Parallels can be administrated through the CLI utility
prtctl
.prlctl list -a prlctl start <name>
This could help automate VM creation. The utility is documented here: Parallels Desktop Command-Line Reference
CLI-based
-
Lima VM - Linux Virtual Machines On macOS.
-
Apple Virtualization framework
- You can use QEMU for M1 or Intel Macs but there are some limitations, for example, on Mac, the VM would be accessed through the localhost using a non-standard port. For a good recipe, see the next section: QEMU with socket_vmnet VM automation script
- If you’re going with QEMU, you should use the
socket_vmnet
plugin.
- If you have the socket_vmnet plugin installed, you can run Qemu headlessly as a LaunchDaemon. See Qemu for more info.s
- If you’re going with QEMU, you should use the
socket_vmnet
QEMU with socket_vmnet VM automation script
Run Qemu in terminal, connect via port
-
Make a directory and go into it. Make the drive in the directory:
mkdir xx; cd xx
qemu-img create -f qcow2 xxx_drive.qcow2 10G
-
Put together a script to create the machine. This is a create script:
qemu-system-aarch64 \ -machine type=q35,accel=hvf \ -m 2048 \ -vga virtio \ -usb \ -device usb-tablet \ -cdrom ubuntu-22.04.1-live-server-amd64.iso \ -netdev user,id=user.0 -device e1000,netdev=user.0 \ -drive file=/Users/petera/Sites/ubuntu/ubuntu_drive.qcow2,if=virtio \ -cpu host \ -display default,show-cursor=on
Note: For Intel macs, use the
qemu-system-x86_64
command instead. -
Actually, I think after creating the drive and installing ubuntu, you can change the script so that the image loads from the drive, doesn’t require the CDRom because the OS is alredy installed in the qcow2 image, and include hostfowarding (and nice things like resize display/ allow cursor to move in and out). This is a run script:
DRIVE='/Users/petera/Sites/ubuntu/ubuntu_drive.qcow2' qemu-system-x86_64 \ -machine type=q35,accel=hvf \ -m 3G \ -vga virtio \ -usb \ -device usb-tablet \ -device e1000,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5556-:22 \ -drive file=$DRIVE,if=virtio \ -cpu host \ -display default,show-cursor=on
The above code runs. Another working code example can be found
/Users/petera/Sites/ubuntu/runVm.sh
-
Then you could SSH into the machine by using the host machine’s IP address and port 5555.
ssh localhost -p 5555
orssh 192.168.0.x -p 5555
from the LAN. More explained here: networking - How to SSH from host to guest using QEMU? - Unix & Linux Stack Exchange
But we may want to have the image use a different IP, so we may want to explore other networking ideas.
-
More Home Made Qemu KVM Recipes! — Give Each VM Its Own IP Address! - LowEndBox
-
GitHub - joshkunz/qemu-docker: A docker container for running x86_64 virtual machines using qemu
-
Setting up Qemu with a tap interface · GitHub
-
Really Simple Network Bridging With qemu
-
GitHub - alessiodionisi/qemu-vmnet: Native macOS networking for QEMU using vmnet.framework and socket networking.
-
Networking bridging on Mac OS X (Mavericks) with QEMU
also, we would like to be able to run it in background.
Qemu with socket_vmnet, connect via IP
-
Install socket_vmnet
-
Download the zip file, build from source, or download from Brew. Release v1.1.0 · lima-vm/socket_vmnet · GitHub
-
If downloading, move the files to opt:
ls /opt/socket_vmnet/bin/
-
Start the service as Root:
sudo /opt/socket_vmnet/bin/socket_vmnet --vmnet-gateway=192.168.105.1 /var/run/socket_vmnet`
(You can be able to run this service in the background using launchd. I will test this, but it could involve adding this file socket_vmnet/io.github.lima-vm.socket_vmnet.plist at master · lima-vm/socket_vmnet · GitHub
as
/Library/LaunchDaemons/io.github.lima-vm.socket_vmnet.plist
) -
Create that file. Fill the contents of it with the linked file above. then load the service:
launchctl load -w /Library/LaunchDaemons/io.github.lima-vm.socket_vmnet.plist
-
-
In another terminal Window, you run a modified qemu-system-x86_64 command that is prefaced with the socket_vmnet code. For example:
DRIVE='/Users/petera/Sites/alpine/alpine.qcow2' ISO= '/Users/petera/Sites/alpine/alpine-virt-3.17.0-x86_64.iso' rm $DRIVE qemu-img create -f qcow2 $DRIVE 10G /opt/socket_vmnet/bin/socket_vmnet_client /var/run/socket_vmnet \ qemu-system-x86_64 \ -device virtio-net-pci,netdev=net0 -netdev socket,id=net0,fd=3 \ -m 4096 -accel hvf -cdrom $ISO \ -vga virtio \ -usb \ -device usb-tablet \ -drive file=$DRIVE,if=virtio \ -display default,show-cursor=on
This should give us networking. But let’s do the Alpine configurations.
-
Add the interfaces file
vi /etc/network/interfaces auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 192.168.105.115/24 gateway 192.168.105.1
Make sure there is no whitespace at end of lines in interfaces file.
-
-
Make sure your
etc/resolv.conf
exists; if not createetc/resolv.conf
with the nameserver configuration like:nameserver 8.8.8.8 options edns0 trust-ad single-request-reopen
-
run
service networking restart
-
Test ip a, ping it. Machine IP will be 192.168.105.115.
-
Update and install ssh
apk update && apk upgrade apk add openssh
-
Tweak the login things, for example add a password and allow root login:
passwd <change password> vi /etc/ssh/sshd_config PermitRootLogin yes service sshd restart
A bit later we can implement key login with PubKeyAuthentication.
-
SSH into the virtual machine: root@192.168.105.115
-
Install alpine to the disk:
setup-alpine
. it remembers the configurations you’ve made but you need to do some things, or just validate the settings. Reboot. -
Then, you can start the same VM in the following way through the socket_vmnet add-on:
DRIVE='/Users/petera/Sites/alpine/alpine.qcow2'
/opt/socket_vmnet/bin/socket_vmnet_client /var/run/socket_vmnet \
qemu-system-x86_64 \
-device virtio-net-pci,netdev=net0 -netdev socket,id=net0,fd=3 \
-m 4096 -accel hvf \
-vga virtio \
-usb \
-device usb-tablet \
-drive file=$DRIVE,if=virtio \
-display default,show-cursor=on #\
#-daemonize
As long as the process is running, you can just SSH into it. You could probably even daemonize it.
Launch VM as a launchd service with socket_vmnet
If you’ve followed the previous steps and installed socket_vmnet, you have a VM that can be be reached via its own IP address.
I recommended you create the plist entry in /Library/LaunchDaemons
and load the socket_vmnet service using launchctl load -w /Library/LaunchDaemons/io.github.vmnet_socket.plist
. This will launch the socket_vmnet
service at runtime and set the
Actually, they include the file for you in the following directory:
/opt/socket_vmnet/share/doc/socket_vmnet/launchd/io.github.lima-vm.socket_vmnet.plist
So copy that to /Library/LaunchDaemons/
.
Anyway, with that in place, you can go ahead and write your own plist file and place it in the same directory. The plist file contains the same commands used in your qemu script, just written in XML. Here’s a sample:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- make install: yes -->
<plist version="1.0">
<dict>
<key>Label</key>
<string>dev.peterai.runqemu</string>
<key>ProgramArguments</key>
<array>
<string>/opt/socket_vmnet/bin/socket_vmnet_client</string>
<string>/var/run/socket_vmnet</string>
<string>/opt/local/bin/qemu-system-x86_64</string>
<string>-device</string>
<string>virtio-net-pci,netdev=net0</string>
<string>-netdev</string>
<string>socket,id=net0,fd=3</string>
<string>-m</string>
<string>4096</string>
<string>-accel</string>
<string>hvf</string>
<string>-usb</string>
<string>-device</string>
<string>usb-tablet</string>
<string>-drive</string>
<string>file=/Users/petera/Sites/alpine/alpine.qcow2,if=virtio</string>
<string>-display</string>
<string>default,show-cursor=on</string>
<string>-nographic</string>
</array>
<key>StandardErrorPath</key>
<string>/var/run/runqemu.stderr</string>
<key>StandardOutPath</key>
<string>/var/run/runqemu.stdout</string>
<key>RunAtLoad</key>
<true />
</dict>
</plist>
You would then probably want to load it as sudo.
sudo -s
launchctl unload dev.peterai.runqemu.plist
launchctl load -w dev.peterai.runqemu.plist
launchctl list | grep dev
- 0 com.apple.avbdeviced
721 0 com.apple.icloud.findmydeviced
15650 0 dev.peterai.runqemu
exit
Then ssh root@192.168.105.115.
If things get messed up, just unload and reload:
sudo -s
launchctl unload dev.peterai.runqemu.plist
launchctl unload io.github.lima-vm.socket_vmnet.plist
launchctl list | grep -i 'peterai\|github'
launchctl load -w io.github.lima-vm.socket_vmnet.plist
launchctl load -w dev.peterai.runqemu.plist
launchctl list | grep -i 'peterai\|github'
16556 0 dev.peterai.runqemu
16542 0 io.github.lima-vm.socket_vmnet
References: