Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions pkg/driver/qemu/qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
79 changes: 79 additions & 0 deletions pkg/driver/qemu/qemu_driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions pkg/driver/qemu/qemu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package qemu

import (
"strings"
"testing"

"gotest.tools/v3/assert"
Expand Down Expand Up @@ -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)
})
}
}
16 changes: 14 additions & 2 deletions pkg/limatype/lima_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
108 changes: 108 additions & 0 deletions templates/experimental/spice-audio.yaml
Original file line number Diff line number Diff line change
@@ -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)
====================================================================
56 changes: 56 additions & 0 deletions templates/experimental/ubuntu-desktop-iso.yaml
Original file line number Diff line number Diff line change
@@ -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.
======================================================================
Loading
Loading