diff --git a/pkg/driver/qemu/qemu.go b/pkg/driver/qemu/qemu.go index c139348a5a6..4867fea89c0 100644 --- a/pkg/driver/qemu/qemu.go +++ b/pkg/driver/qemu/qemu.go @@ -846,12 +846,28 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er if audiodev == "default" { audiodev = audioDevice() } + + // Check if SPICE is configured with audio + var usingSPICEAudio bool + if *y.Video.Display != "" && strings.HasPrefix(*y.Video.Display, "spice") { + // Check if SPICE audio is enabled in config + if y.Video.SPICE.Audio != nil && *y.Video.SPICE.Audio { + usingSPICEAudio = true + audiodev = "spice" + } + } + audiodev += fmt.Sprintf(",id=%s", id) args = append(args, "-audiodev", audiodev) - // audio controller - args = append(args, "-device", "ich9-intel-hda") - // audio codec - args = append(args, "-device", fmt.Sprintf("hda-output,audiodev=%s", id)) + + // Only add HDA controller/codec if not using SPICE audio + // SPICE handles audio streaming differently + if !usingSPICEAudio { + // audio controller + args = append(args, "-device", "ich9-intel-hda") + // audio codec + args = append(args, "-device", fmt.Sprintf("hda-output,audiodev=%s", id)) + } } // Graphics if *y.Video.Display != "" { diff --git a/pkg/driver/qemu/qemu_driver.go b/pkg/driver/qemu/qemu_driver.go index 3cb7bed29d7..2a07af12b7f 100644 --- a/pkg/driver/qemu/qemu_driver.go +++ b/pkg/driver/qemu/qemu_driver.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -390,10 +391,18 @@ func (l *LimaQemuDriver) Stop(ctx context.Context) error { } func (l *LimaQemuDriver) ChangeDisplayPassword(_ context.Context, password string) error { + // Determine if we're using SPICE or VNC based on the display configuration + if l.Instance.Config.Video.Display != nil && strings.HasPrefix(*l.Instance.Config.Video.Display, "spice") { + return l.changeSPICEPassword(password) + } return l.changeVNCPassword(password) } func (l *LimaQemuDriver) DisplayConnection(_ context.Context) (string, error) { + // Check if SPICE is configured + if l.Instance.Config.Video.Display != nil && strings.HasPrefix(*l.Instance.Config.Video.Display, "spice") { + return l.getSPICEDisplayPort() + } return l.getVNCDisplayPort() } @@ -458,6 +467,46 @@ func (l *LimaQemuDriver) changeVNCPassword(password string) error { return nil } +func (l *LimaQemuDriver) changeSPICEPassword(password string) error { + qmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock) + err := waitFileExists(qmpSockPath, 30*time.Second) + if err != nil { + return err + } + qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) + if err != nil { + return err + } + if err := qmpClient.Connect(); err != nil { + return err + } + defer func() { _ = qmpClient.Disconnect() }() + + // Execute set_password command for SPICE + cmd := struct { + Execute string `json:"execute"` + Arguments map[string]any `json:"arguments"` + }{ + Execute: "set_password", + Arguments: map[string]any{ + "protocol": "spice", + "password": password, + }, + } + + cmdBytes, err := json.Marshal(cmd) + if err != nil { + return fmt.Errorf("failed to marshal QMP command: %w", err) + } + + // Use qmpClient.Run directly + _, err = qmpClient.Run(cmdBytes) + if err != nil { + return fmt.Errorf("failed to set SPICE password: %w", err) + } + return nil +} + func (l *LimaQemuDriver) getVNCDisplayPort() (string, error) { qmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock) qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) @@ -476,6 +525,36 @@ func (l *LimaQemuDriver) getVNCDisplayPort() (string, error) { return *info.Service, nil } +func (l *LimaQemuDriver) getSPICEDisplayPort() (string, error) { + qmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock) + qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) + if err != nil { + return "", err + } + if err := qmpClient.Connect(); err != nil { + return "", err + } + defer func() { _ = qmpClient.Disconnect() }() + rawClient := raw.NewMonitor(qmpClient) + + // Query SPICE info using raw monitor + info, err := rawClient.QuerySpice() + if err != nil { + return "", fmt.Errorf("failed to query SPICE: %w", err) + } + + if info.Port == nil || *info.Port == 0 { + return "", errors.New("SPICE port not available") + } + + host := "127.0.0.1" + if info.Host != nil && *info.Host != "" { + host = *info.Host + } + + return fmt.Sprintf("%s:%d", host, *info.Port), nil +} + func (l *LimaQemuDriver) removeVNCFiles() error { vncfile := filepath.Join(l.Instance.Dir, filenames.VNCDisplayFile) err := os.RemoveAll(vncfile) diff --git a/pkg/driver/qemu/qemu_test.go b/pkg/driver/qemu/qemu_test.go index 422e1fbbbe5..c74f36e147a 100644 --- a/pkg/driver/qemu/qemu_test.go +++ b/pkg/driver/qemu/qemu_test.go @@ -4,6 +4,7 @@ package qemu import ( + "strings" "testing" "gotest.tools/v3/assert" @@ -89,3 +90,56 @@ func TestParseQemuVersion(t *testing.T) { assert.Equal(t, tc.expectedValue, v.String()) } } + +func TestSPICEAudioDetection(t *testing.T) { + // Test that SPICE audio is properly detected and configured + testCases := []struct { + name string + displayString string + audioDevice string + spiceAudio bool + expectSPICE bool + }{ + { + name: "SPICE display with audio enabled", + displayString: "spice,port=5930", + audioDevice: "default", + spiceAudio: true, + expectSPICE: true, + }, + { + name: "SPICE display without audio config", + displayString: "spice,port=5930", + audioDevice: "default", + spiceAudio: false, + expectSPICE: false, + }, + { + name: "VNC display with audio", + displayString: "vnc=:0", + audioDevice: "default", + spiceAudio: false, + expectSPICE: false, + }, + { + name: "No display", + displayString: "none", + audioDevice: "default", + spiceAudio: false, + expectSPICE: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // This tests the logic of detecting SPICE audio configuration + usingSPICEAudio := false + if tc.displayString != "" && strings.HasPrefix(tc.displayString, "spice") { + if tc.spiceAudio { + usingSPICEAudio = true + } + } + assert.Equal(t, tc.expectSPICE, usingSPICEAudio) + }) + } +} diff --git a/pkg/limatype/lima_yaml.go b/pkg/limatype/lima_yaml.go index fc48766cc85..e2b13935d73 100644 --- a/pkg/limatype/lima_yaml.go +++ b/pkg/limatype/lima_yaml.go @@ -221,10 +221,22 @@ type VNCOptions struct { Display *string `yaml:"display,omitempty" json:"display,omitempty" jsonschema:"nullable"` } +type SPICEOptions struct { + // Enable SPICE GL (OpenGL acceleration) + GL *bool `yaml:"gl,omitempty" json:"gl,omitempty" jsonschema:"nullable"` + // Enable SPICE streaming video + StreamingVideo *string `yaml:"streamingVideo,omitempty" json:"streamingVideo,omitempty" jsonschema:"nullable"` + // Enable SPICE agent + Agent *bool `yaml:"agent,omitempty" json:"agent,omitempty" jsonschema:"nullable"` + // Enable SPICE audio streaming + Audio *bool `yaml:"audio,omitempty" json:"audio,omitempty" jsonschema:"nullable"` +} + type Video struct { // Display is a QEMU display string - Display *string `yaml:"display,omitempty" json:"display,omitempty" jsonschema:"nullable"` - VNC VNCOptions `yaml:"vnc,omitempty" json:"vnc,omitempty"` + Display *string `yaml:"display,omitempty" json:"display,omitempty" jsonschema:"nullable"` + VNC VNCOptions `yaml:"vnc,omitempty" json:"vnc,omitempty"` + SPICE SPICEOptions `yaml:"spice,omitempty" json:"spice,omitempty"` } type ProvisionMode = string diff --git a/templates/experimental/spice-audio.yaml b/templates/experimental/spice-audio.yaml new file mode 100644 index 00000000000..8789f23d364 --- /dev/null +++ b/templates/experimental/spice-audio.yaml @@ -0,0 +1,108 @@ +# Example: QEMU with SPICE display and audio +# This template demonstrates SPICE protocol with audio streaming support + +# VM type must be qemu for SPICE support +vmType: qemu + +# SPICE display configuration +video: + # SPICE display with basic settings + # For production, consider using password protection or TLS + display: "spice,port=5930,addr=127.0.0.1,disable-ticketing=on" + + # SPICE-specific options + spice: + # Enable audio streaming over SPICE + audio: true + # Enable OpenGL acceleration (requires spice-app or gl=on in display) + gl: false + # Enable SPICE agent for enhanced integration + agent: true + # Streaming video mode: "off", "all", or "filter" + streamingVideo: "filter" + +# Audio device configuration +audio: + # Use "default" for platform audio (coreaudio on macOS, pulseaudio on Linux) + # When SPICE audio is enabled, this will use SPICE audiodev instead + device: "default" + +# Standard VM configuration +images: +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" + arch: "x86_64" +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" + arch: "aarch64" + +cpus: 4 +memory: "4GiB" +disk: "30GiB" + +mounts: +- location: "~" +- location: "/tmp/lima" + writable: true + +# SSH configuration +ssh: + localPort: 0 + loadDotSSHPubKeys: true + +# Firmware: Use UEFI +firmware: + legacyBIOS: false + +# Provision scripts +provision: +- mode: system + script: | + #!/bin/bash + set -eux -o pipefail + # Install audio utilities for testing + apt-get update + apt-get install -y alsa-utils pulseaudio +- mode: user + script: | + #!/bin/bash + set -eux -o pipefail + echo "SPICE display and audio are configured" + echo "Connect with: remote-viewer spice://127.0.0.1:5930" + echo "" + echo "Audio testing commands:" + echo " speaker-test -t wav -c 2 # Test audio output" + echo " arecord -d 5 test.wav # Test audio input (microphone)" + +# Host resolver +hostResolver: + enabled: true + +# Port forwarding +portForwards: +- guestSocket: "/run/user/{{.UID}}/podman/podman.sock" + hostSocket: "{{.Dir}}/sock/podman.sock" + +# Message to display after instance is created +message: | + ==================================================================== + SPICE Display and Audio Enabled + ==================================================================== + + To connect to the graphical display: + remote-viewer spice://127.0.0.1:5930 + + SPICE Viewer Required: + macOS: brew install virt-viewer + Linux: sudo apt-get install virt-viewer (Ubuntu/Debian) + sudo dnf install virt-viewer (Fedora/RHEL) + + Audio Testing: + # Inside the VM, test audio output: + speaker-test -t wav -c 2 + + # Test audio input (if configured): + arecord -d 5 test.wav && aplay test.wav + + Configuration: + Display: {{.Video.Display}} + Audio: SPICE streaming (enabled) + ==================================================================== diff --git a/templates/experimental/ubuntu-desktop-iso.yaml b/templates/experimental/ubuntu-desktop-iso.yaml new file mode 100644 index 00000000000..9a0a48a0eb2 --- /dev/null +++ b/templates/experimental/ubuntu-desktop-iso.yaml @@ -0,0 +1,56 @@ +# Ubuntu Desktop (Xubuntu) with SPICE Display +# Uses the actual Ubuntu Desktop ISO + +vmType: qemu + +# SPICE display with audio +video: + display: "spice,port=5930,addr=127.0.0.1,disable-ticketing=on" + +audio: + device: "default" + +# Use Ubuntu Desktop ISO (Xubuntu is lighter than full Ubuntu Desktop) +images: +- location: "https://cdimage.ubuntu.com/xubuntu/releases/24.04/release/xubuntu-24.04.1-desktop-amd64.iso" + arch: "x86_64" +- location: "https://cdimage.ubuntu.com/xubuntu/releases/24.04/release/xubuntu-24.04.1-desktop-arm64.iso" + arch: "aarch64" + +cpus: 4 +memory: "8GiB" +disk: "50GiB" + +# Don't mount host directories for a desktop install +mounts: [] + +ssh: + localPort: 0 + # Desktop ISO won't have cloud-init, so no automatic SSH setup + loadDotSSHPubKeys: false + +firmware: + legacyBIOS: false + +message: | + ====================================================================== + Xubuntu Desktop ISO Booted - Manual Installation Required + ====================================================================== + + This boots the Xubuntu Desktop installer ISO. + + 1. Connect to the SPICE display: + remote-viewer spice://127.0.0.1:5930 + + 2. Follow the graphical installer in the window + - Choose "Install Xubuntu" + - Set username/password during installation + - Complete the installation process + + Note: This is a manual desktop installation, not automated. + + Required: Install SPICE viewer first + brew install virt-viewer + + After installation, the desktop will boot into the GUI. + ====================================================================== diff --git a/templates/experimental/ubuntu-desktop-simple.yaml b/templates/experimental/ubuntu-desktop-simple.yaml new file mode 100644 index 00000000000..e7062a0c64b --- /dev/null +++ b/templates/experimental/ubuntu-desktop-simple.yaml @@ -0,0 +1,84 @@ +# Ubuntu Desktop with SPICE Display +# A full desktop environment using Xfce (Xubuntu) + +vmType: qemu + +# SPICE display - basic configuration that works now +video: + display: "spice,port=5930,addr=127.0.0.1,disable-ticketing=on" + +# Enable audio +audio: + device: "default" + +# Ubuntu Desktop image +images: +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" + arch: "x86_64" +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" + arch: "aarch64" + +cpus: 4 +memory: "8GiB" +disk: "50GiB" + +mounts: +- location: "~" +- location: "/tmp/lima" + writable: true + +ssh: + localPort: 0 + loadDotSSHPubKeys: true + +firmware: + legacyBIOS: false + +# Install Xfce desktop environment +provision: +- mode: system + script: | + #!/bin/bash + set -eux -o pipefail + + # Update package lists + apt-get update + + # Install Xfce desktop environment (lightweight) + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + xubuntu-desktop \ + spice-vdagent \ + xserver-xorg-video-qxl \ + pulseaudio + + # Configure automatic login for lima user + mkdir -p /etc/lightdm/lightdm.conf.d + cat > /etc/lightdm/lightdm.conf.d/50-autologin.conf <<'EOF' + [Seat:*] + autologin-user=lima + autologin-user-timeout=0 + EOF + + # Enable graphical target + systemctl set-default graphical.target + + echo "Desktop environment installed. Rebooting..." + sleep 2 + systemctl reboot || true + +hostResolver: + enabled: true + +message: | + Ubuntu Desktop (Xfce) with SPICE Display is starting. + + The VM is installing Xubuntu desktop (this takes 5-10 minutes). + The VM will reboot automatically when installation is complete. + + After reboot, connect to the SPICE display: + remote-viewer spice://127.0.0.1:5930 + + Install SPICE viewer first: + brew install virt-viewer + + Desktop will auto-login as user 'lima' diff --git a/templates/experimental/ubuntu-desktop-spice.yaml b/templates/experimental/ubuntu-desktop-spice.yaml new file mode 100644 index 00000000000..d1f3104af2b --- /dev/null +++ b/templates/experimental/ubuntu-desktop-spice.yaml @@ -0,0 +1,98 @@ +# Ubuntu Desktop with SPICE Display and Audio +# A full desktop environment using Xfce (Xubuntu) + +vmType: qemu + +# SPICE display with audio support +video: + display: "spice,port=5930,addr=127.0.0.1,disable-ticketing=on,gl=on" + spice: + audio: true + gl: true + agent: true + streamingVideo: "filter" + +# Enable audio +audio: + device: "default" + +# Ubuntu Desktop image +images: +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img" + arch: "x86_64" +- location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img" + arch: "aarch64" + +cpus: 4 +memory: "8GiB" +disk: "50GiB" + +mounts: +- location: "~" +- location: "/tmp/lima" + writable: true + +ssh: + localPort: 0 + loadDotSSHPubKeys: true + +firmware: + legacyBIOS: false + +# Install Xfce desktop environment +provision: +- mode: system + script: | + #!/bin/bash + set -eux -o pipefail + + # Update package lists + apt-get update + + # Install Xfce desktop environment (lightweight) + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + xubuntu-desktop \ + spice-vdagent \ + xserver-xorg-video-qxl \ + pulseaudio + + # Configure automatic login for the lima user + mkdir -p /etc/lightdm/lightdm.conf.d + cat > /etc/lightdm/lightdm.conf.d/50-autologin.conf <<'EOF' + [Seat:*] + autologin-user=lima + autologin-user-timeout=0 + EOF + + # Enable graphical target + systemctl set-default graphical.target + + echo "Desktop environment installed. Rebooting to start GUI..." + sleep 2 + systemctl reboot || true + +hostResolver: + enabled: true + +portForwards: [] + +message: | + Ubuntu Desktop (Xfce) with SPICE Display is starting. + + The VM is installing the Xubuntu desktop environment. + This will take several minutes and the VM will reboot automatically. + + After the VM reboots, connect to the SPICE display: + remote-viewer spice://127.0.0.1:5930 + + SPICE Viewer Required: + macOS: brew install virt-viewer + Linux: sudo apt-get install virt-viewer + + The desktop will auto-login as user: lima + + Features: + - Xfce Desktop Environment (lightweight) + - SPICE graphics with OpenGL + - Audio output support + - SPICE agent for clipboard sharing