From 2ab42e7f57164b939994d754c46481e5530beae9 Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Sun, 7 Jun 2026 23:10:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=83=A8=E7=BD=B2KVM=20XFCE?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/api/images.go | 4 +- backend/internal/kvm/kvm.go | 98 +++++++++++++++++++++++++--- backend/internal/kvm/templates.go | 15 +++++ backend/internal/server/web/.gitkeep | 1 - frontend/src/services/api.ts | 2 + 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/backend/internal/api/images.go b/backend/internal/api/images.go index fa8d4d4..46b3120 100644 --- a/backend/internal/api/images.go +++ b/backend/internal/api/images.go @@ -28,6 +28,7 @@ type ImageInfo struct { Downloading bool `json:"downloading"` SizeBytes int64 `json:"size_bytes"` ManualPath string `json:"manual_path,omitempty"` + Desktop string `json:"desktop,omitempty"` } var imageDownloadsMu sync.Mutex @@ -140,6 +141,7 @@ func HandleImages(w http.ResponseWriter, r *http.Request) { Downloading: downloading, SizeBytes: size, ManualPath: manualPath, + Desktop: t.Desktop, }) } @@ -336,7 +338,7 @@ func HandleEnabledImages(w http.ResponseWriter, r *http.Request) { if downloaded, _ := kvm.ImageDownloadedInfo(t.ID); enabledSet[t.ID] && downloaded { result = append(result, map[string]string{ "id": t.ID, "name": t.Name, "distro": t.Distro, "release": t.Release, "arch": t.Arch, - "description": t.Description, "type": config.VirtualizationKVM, + "description": t.Description, "type": config.VirtualizationKVM, "desktop": t.Desktop, }) } } diff --git a/backend/internal/kvm/kvm.go b/backend/internal/kvm/kvm.go index 17c1769..403c4c9 100644 --- a/backend/internal/kvm/kvm.go +++ b/backend/internal/kvm/kvm.go @@ -371,13 +371,21 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig } xml = windowsDomainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, ImagePath(image.ID), unattendPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps) } else { + if image.Desktop != "" { + if cfg.RAMMB < 2048 { + cfg.RAMMB = 2048 + } + if cfg.DiskGB < 20 { + cfg.DiskGB = 20 + } + } if err := createOverlayDisk(ImagePath(image.ID), diskPath, cfg.DiskGB); err != nil { return nil, err } - if err := createSeedISO(seedPath, vmName, cfg.Name, sshPassword, mac, ipv6); err != nil { + if err := createSeedISO(seedPath, vmName, cfg.Name, sshPassword, mac, ipv6, *image); err != nil { return nil, err } - xml = domainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, seedPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps) + xml = domainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, seedPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps, image.Desktop != "") } xmlPath := filepath.Join(m.instanceDir(vmName), "domain.xml") if err := os.WriteFile(xmlPath, []byte(xml), 0644); err != nil { @@ -706,7 +714,7 @@ func (m *Manager) ApplyContainerLimits(c *config.Container) error { xml = windowsDomainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, winISO, unattendISO, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps) } else { seedPath := filepath.Join(m.instanceDir(c.VirshName()), "seed.iso") - xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps) + xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps, isKVMDesktopTemplate(c.Template)) } xmlPath := filepath.Join(m.instanceDir(c.VirshName()), "domain.xml") if err := os.WriteFile(xmlPath, []byte(xml), 0644); err != nil { @@ -731,7 +739,7 @@ func (m *Manager) ensureDomainDefinition(c *config.Container) error { xml = windowsDomainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, winISO, unattendISO, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps) } else { seedPath := filepath.Join(m.instanceDir(c.VirshName()), "seed.iso") - xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps) + xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps, isKVMDesktopTemplate(c.Template)) } if err := os.WriteFile(xmlPath, []byte(xml), 0644); err != nil { return err @@ -1668,8 +1676,11 @@ func shellQuoteWindows(value string) string { return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"` } -func createSeedISO(seedPath, instanceID, hostname, password, mac, ipv6 string) error { +func createSeedISO(seedPath, instanceID, hostname, password, mac, ipv6 string, image Image) error { guestSetup := kvmSSHSetupScript(password) + if desktopSetup := kvmDesktopSetupScript(image); desktopSetup != "" { + guestSetup += "\n" + desktopSetup + } if strings.TrimSpace(ipv6) != "" { guestSetup += "\n" + kvmIPv6SetupScript(ipv6) } @@ -1743,7 +1754,12 @@ func indentScript(script string, spaces int) string { return strings.Join(lines, "\n") } -func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string, ioSpeedMBps int, networkBWMbps int) string { +func isKVMDesktopTemplate(templateID string) bool { + image := FindImage(templateID) + return image != nil && image.Desktop != "" +} + +func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string, ioSpeedMBps int, networkBWMbps int, desktop bool) string { if vcpu < 1 { vcpu = 1 } @@ -1767,6 +1783,12 @@ func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string, `, averageKiB, averageKiB) } + video := "" + input := "" + if desktop { + video = "" + input = "\n\t " + } return fmt.Sprintf(` %s %s @@ -1810,10 +1832,10 @@ func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string, - - + %s + %s -`, xmlEscape(name), domainUUIDXML(name), ramMB, ramMB, vcpu, vcpu, xmlEscape(diskPath), iotune, xmlEscape(seedPath), xmlEscape(mac), bandwidth) +`, xmlEscape(name), domainUUIDXML(name), ramMB, ramMB, vcpu, vcpu, xmlEscape(diskPath), iotune, xmlEscape(seedPath), xmlEscape(mac), bandwidth, input, video) } func windowsDomainXML(name string, vcpu int, ramMB int, diskPath, winISOPath, unattendISOPath, mac string, ioSpeedMBps int, networkBWMbps int) string { @@ -2263,6 +2285,64 @@ fi ` } +func kvmDesktopSetupScript(image Image) string { + if strings.ToLower(strings.TrimSpace(image.Desktop)) != "xfce" { + return "" + } + packages := "" + switch image.Distro { + case "ubuntu": + packages = "xubuntu-desktop" + case "debian": + packages = "task-xfce-desktop" + default: + return "" + } + return `if command -v apt-get >/dev/null 2>&1; then + { + exec >>/var/log/clicd-desktop-setup.log 2>&1 + echo "CLICD XFCE setup started at $(date -Is)" + export DEBIAN_FRONTEND=noninteractive + export APT_LISTCHANGES_FRONTEND=none + apt-get update || true + apt-get install -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold ` + packages + ` || apt-get install -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold xfce4 lightdm lightdm-gtk-greeter dbus-x11 xorg || true + if command -v useradd >/dev/null 2>&1 && ! id clicd >/dev/null 2>&1; then + useradd -m -s /bin/bash clicd || true + fi + if command -v chpasswd >/dev/null 2>&1 && id clicd >/dev/null 2>&1; then + printf 'clicd:%s\n' "$ROOT_PASSWORD" | chpasswd || true + fi + usermod -aG sudo clicd >/dev/null 2>&1 || true + usermod -aG autologin clicd >/dev/null 2>&1 || true + if id clicd >/dev/null 2>&1; then + printf 'startxfce4\n' >/home/clicd/.xsession || true + chown clicd:clicd /home/clicd/.xsession >/dev/null 2>&1 || true + fi + mkdir -p /etc/lightdm/lightdm.conf.d + cat >/etc/lightdm/lightdm.conf.d/50-clicd-autologin.conf <<'EOF' +[Seat:*] +autologin-user=clicd +autologin-user-timeout=0 +user-session=xfce +greeter-session=lightdm-gtk-greeter +EOF + if [ -x /usr/sbin/lightdm ]; then + printf '/usr/sbin/lightdm\n' >/etc/X11/default-display-manager || true + fi + if command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload >/dev/null 2>&1 || true + systemctl set-default graphical.target >/dev/null 2>&1 || true + systemctl enable display-manager.service >/dev/null 2>&1 || true + systemctl enable lightdm.service >/dev/null 2>&1 || true + systemctl restart lightdm.service >/dev/null 2>&1 || systemctl start lightdm.service >/dev/null 2>&1 || true + fi + apt-get clean || true + echo "CLICD XFCE setup finished at $(date -Is)" + } || true +fi +` +} + func qemuGuestPing(name string) error { out, err := exec.Command("virsh", "qemu-agent-command", name, `{"execute":"guest-ping"}`).CombinedOutput() if err != nil { diff --git a/backend/internal/kvm/templates.go b/backend/internal/kvm/templates.go index a7f7e04..04f971e 100644 --- a/backend/internal/kvm/templates.go +++ b/backend/internal/kvm/templates.go @@ -12,6 +12,7 @@ type Image struct { Arch string `json:"arch"` Description string `json:"description"` URL string `json:"url"` + Desktop string `json:"desktop,omitempty"` } func GetImages() []Image { @@ -22,6 +23,13 @@ func GetImages() []Image { Description: "Ubuntu 24.04 LTS cloud image for KVM", URL: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img", }, + { + ID: "kvm-ubuntu-noble-xfce", Name: "Ubuntu 24.04 XFCE KVM", + Distro: "ubuntu", Release: "noble", Arch: "amd64", + Description: "Ubuntu 24.04 LTS cloud image with XFCE desktop provisioned via cloud-init", + URL: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img", + Desktop: "xfce", + }, { ID: "kvm-ubuntu-jammy", Name: "Ubuntu 22.04 KVM", Distro: "ubuntu", Release: "jammy", Arch: "amd64", @@ -34,6 +42,13 @@ func GetImages() []Image { Description: "Debian 12 generic cloud image for KVM", URL: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2", }, + { + ID: "kvm-debian-bookworm-xfce", Name: "Debian 12 XFCE KVM", + Distro: "debian", Release: "bookworm", Arch: "amd64", + Description: "Debian 12 generic cloud image with XFCE desktop provisioned via cloud-init", + URL: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2", + Desktop: "xfce", + }, { ID: "kvm-debian-bullseye", Name: "Debian 11 KVM", Distro: "debian", Release: "bullseye", Arch: "amd64", diff --git a/backend/internal/server/web/.gitkeep b/backend/internal/server/web/.gitkeep index 30259b2..e69de29 100644 --- a/backend/internal/server/web/.gitkeep +++ b/backend/internal/server/web/.gitkeep @@ -1 +0,0 @@ - diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 1e457fe..dd6ad3a 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -94,6 +94,7 @@ export interface Template { release: string arch: string variant?: string + desktop?: string description: string } @@ -359,6 +360,7 @@ export interface ImageInfo { downloading: boolean size_bytes: number manual_path?: string + desktop?: string } export const getImages = () =>