Virtual machine automation in macOS

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

QEMU with socket_vmnet VM automation script

Run Qemu in terminal, connect via port

  1. 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

  2. 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.

  3. 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

  4. Then you could SSH into the machine by using the host machine’s IP address and port 5555. ssh localhost -p 5555 or ssh 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

  1. Install socket_vmnet

    1. Download the zip file, build from source, or download from Brew. Release v1.1.0 · lima-vm/socket_vmnet · GitHub

    2. If downloading, move the files to opt: ls /opt/socket_vmnet/bin/

    3. 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)

    4. 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

  2. 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.

    1. 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.

  3. Make sure your etc/resolv.conf exists; if not create etc/resolv.conf with the nameserver configuration like:

    nameserver 8.8.8.8
    options edns0 trust-ad single-request-reopen
    
  4. run service networking restart

  5. Test ip a, ping it. Machine IP will be 192.168.105.115.

  6. Update and install ssh

    apk update && apk upgrade
    apk add openssh
    
  7. 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.

  1. SSH into the virtual machine: root@192.168.105.115

  2. 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.

  3. 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:

  • Creating Launch Daemons and Agents
  • About launchd
  • MacOS ‘launchd’ examples (launchd plist example files) | alvinalexander.com