mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-06 05:52:19 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d48889eea | |||
| 2bcdb9e095 | |||
| 2ab42e7f57 | |||
| 3257cbb2a3 | |||
| 1ff5d7a85e | |||
| a99781d418 | |||
| b993e57d05 | |||
| 7ae0c91813 | |||
| aab58aca6e | |||
| 0b27604f95 | |||
| 460614e274 |
+5
-5
@@ -1,18 +1,18 @@
|
||||
module clicd
|
||||
|
||||
go 1.23.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.5
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/term v0.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/sys v0.38.0
|
||||
modernc.org/sqlite v1.29.10
|
||||
)
|
||||
|
||||
|
||||
+8
-8
@@ -1,7 +1,7 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -18,8 +18,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
|
||||
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
@@ -27,10 +27,10 @@ golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+103
-10
@@ -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
|
||||
@@ -1399,7 +1407,7 @@ func ensureDefaultNetwork() error {
|
||||
}
|
||||
// Start and autostart the default network
|
||||
if out, err := exec.Command("virsh", "net-info", "default").Output(); err == nil {
|
||||
if !strings.Contains(strings.ToLower(string(out)), "active:") || !strings.Contains(strings.ToLower(string(out)), "yes") {
|
||||
if !libvirtNetworkActive(string(out)) {
|
||||
if startOut, startErr := exec.Command("virsh", "net-start", "default").CombinedOutput(); startErr != nil {
|
||||
return fmt.Errorf("failed to start libvirt default network: %v, output: %s", startErr, string(startOut))
|
||||
}
|
||||
@@ -1411,6 +1419,19 @@ func ensureDefaultNetwork() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func libvirtNetworkActive(info string) bool {
|
||||
for _, line := range strings.Split(info, "\n") {
|
||||
key, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(key), "Active") {
|
||||
return strings.EqualFold(strings.TrimSpace(value), "yes")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func createOverlayDisk(base, target string, diskGB int) error {
|
||||
if diskGB < 1 {
|
||||
diskGB = 5
|
||||
@@ -1668,8 +1689,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 +1767,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 +1796,12 @@ func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string,
|
||||
<outbound average='%d'/>
|
||||
</bandwidth>`, averageKiB, averageKiB)
|
||||
}
|
||||
video := "<video><model type='virtio'/></video>"
|
||||
input := ""
|
||||
if desktop {
|
||||
video = "<video><model type='qxl' ram='65536' vram='65536' heads='1' primary='yes'/></video>"
|
||||
input = "\n\t <input type='tablet' bus='usb'/>"
|
||||
}
|
||||
return fmt.Sprintf(`<domain type='kvm'>
|
||||
<name>%s</name>
|
||||
%s
|
||||
@@ -1810,10 +1845,10 @@ func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string,
|
||||
<memballoon model='virtio'>
|
||||
<stats period='10'/>
|
||||
</memballoon>
|
||||
<graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'/>
|
||||
<video><model type='virtio'/></video>
|
||||
<graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'/>%s
|
||||
%s
|
||||
</devices>
|
||||
</domain>`, xmlEscape(name), domainUUIDXML(name), ramMB, ramMB, vcpu, vcpu, xmlEscape(diskPath), iotune, xmlEscape(seedPath), xmlEscape(mac), bandwidth)
|
||||
</domain>`, 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 +2298,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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package version
|
||||
|
||||
var (
|
||||
Version = "1.1.0"
|
||||
Version = "1.1.1"
|
||||
Repo = "MengMengCode/CLICD"
|
||||
)
|
||||
|
||||
@@ -11,12 +11,3 @@ func Current() string {
|
||||
}
|
||||
return Version
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clicd-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -3,13 +3,6 @@ import { Lock, User } from 'lucide-react'
|
||||
import AppIcon from '../components/AppIcon'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
async function sha256Hash(input: string): Promise<string> {
|
||||
const msgBuffer = new TextEncoder().encode(input)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
|
||||
export default function Login() {
|
||||
const { login, accessCodeLogin } = useAuth()
|
||||
const [username, setUsername] = useState('')
|
||||
@@ -113,7 +106,7 @@ export default function Login() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.0.0</p>
|
||||
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.1</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 = () =>
|
||||
|
||||
+232
-85
@@ -5,17 +5,36 @@ REPO="${CLICD_REPO:-MengMengCode/CLICD}"
|
||||
CLICD_INSTALL_VERSION="${CLICD_VERSION:-latest}"
|
||||
ASSET="clicd-linux-amd64.tar.gz"
|
||||
ACTION="${1:-install}"
|
||||
ACTION_CONFIRM="${2:-}"
|
||||
ISSUE_URL="https://github.com/${REPO}/issues"
|
||||
LOG_FILE="${CLICD_LOG_FILE:-/var/log/clicd-install.log}"
|
||||
|
||||
echo "====================================="
|
||||
echo " CLICD Installer"
|
||||
echo " CLICD 中文安装/卸载脚本"
|
||||
echo "====================================="
|
||||
|
||||
write_log_file() {
|
||||
if [ "$(id -u 2>/dev/null || echo 1)" = "0" ]; then
|
||||
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || true)" "$*" >> "$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
log() {
|
||||
echo "[clicd] $*"
|
||||
write_log_file "[clicd] $*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo "[clicd][警告] $*" >&2
|
||||
write_log_file "[警告] $*"
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
echo "[clicd][错误] $*" >&2
|
||||
write_log_file "[错误] $*"
|
||||
echo "" >&2
|
||||
echo "安装/卸载未完成。请查看日志:$LOG_FILE" >&2
|
||||
echo "如果你确认这是程序问题,请提交 issue:$ISSUE_URL" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -31,13 +50,70 @@ is_openrc() {
|
||||
has_cmd rc-service && has_cmd rc-update
|
||||
}
|
||||
|
||||
run_step() {
|
||||
step_name="$1"
|
||||
shift
|
||||
log "开始:$step_name"
|
||||
if "$@" >> "$LOG_FILE" 2>&1; then
|
||||
log "完成:$step_name"
|
||||
return 0
|
||||
fi
|
||||
rc="$?"
|
||||
echo "" >&2
|
||||
echo "[clicd][错误] 步骤失败:$step_name,退出码:$rc" >&2
|
||||
echo "[clicd][错误] 最近 80 行日志:$LOG_FILE" >&2
|
||||
tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true
|
||||
echo "" >&2
|
||||
echo "请将上述日志和系统信息提交到:$ISSUE_URL" >&2
|
||||
exit "$rc"
|
||||
}
|
||||
|
||||
check_os_compatibility() {
|
||||
log "系统检测:ID=${OS_ID} ID_LIKE=${OS_LIKE} ARCH=$(uname -m 2>/dev/null || echo unknown)"
|
||||
case "$(uname -m 2>/dev/null || echo unknown)" in
|
||||
x86_64|amd64)
|
||||
;;
|
||||
*)
|
||||
die "当前安装包仅支持 x86_64/amd64,当前架构:$(uname -m 2>/dev/null || echo unknown)。"
|
||||
;;
|
||||
esac
|
||||
if ! is_systemd && ! is_openrc; then
|
||||
die "未检测到 systemd 或 OpenRC,无法安装服务。"
|
||||
fi
|
||||
case "$OS_ID" in
|
||||
ubuntu|debian|alpine|centos|rhel|rocky|almalinux|fedora)
|
||||
;;
|
||||
*)
|
||||
if ! has_cmd apt-get && ! has_cmd apk && ! has_cmd dnf && ! has_cmd yum; then
|
||||
die "暂不支持当前 Linux 发行版:${OS_ID} ${OS_LIKE}。请提交 issue 并附上 /etc/os-release。"
|
||||
fi
|
||||
warn "发行版 ${OS_ID} 不在主要支持列表,将按检测到的软件包管理器尝试安装。"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_storage_compatibility() {
|
||||
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || echo unknown)"
|
||||
avail_kb="$(df -Pk / 2>/dev/null | awk 'NR==2 {print $4}' || echo 0)"
|
||||
log "存储检测:根文件系统=${root_fs} 可用空间=${avail_kb}KB"
|
||||
if [ "${avail_kb:-0}" -lt 5242880 ]; then
|
||||
warn "根分区可用空间低于 5GB,下载镜像或创建 KVM/LXC 时可能失败。"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Please run as root: sudo ./install.sh"
|
||||
echo "Or: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh"
|
||||
echo "Uninstall: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall"
|
||||
echo "请使用 root 权限运行:sudo ./install.sh"
|
||||
echo "或执行:curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh"
|
||||
echo "卸载:curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall"
|
||||
echo "问题反馈:$ISSUE_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
: > "$LOG_FILE" 2>/dev/null || true
|
||||
log "日志文件:$LOG_FILE"
|
||||
log "仓库地址:https://github.com/${REPO}"
|
||||
log "问题反馈:$ISSUE_URL"
|
||||
|
||||
OS_ID="unknown"
|
||||
OS_LIKE=""
|
||||
if [ -r /etc/os-release ]; then
|
||||
@@ -48,17 +124,21 @@ fi
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage:
|
||||
./install.sh
|
||||
./install.sh uninstall
|
||||
用法:
|
||||
./install.sh 安装或升级 CLICD
|
||||
./install.sh uninstall 卸载 CLICD(会删除容器、虚拟机、镜像缓存和配置数据)
|
||||
|
||||
Environment:
|
||||
CLICD_REPO=owner/repo
|
||||
CLICD_VERSION=latest|v1.0.0
|
||||
环境变量:
|
||||
CLICD_REPO=owner/repo 默认:${REPO}
|
||||
CLICD_VERSION=latest|v1.0.0 默认:latest
|
||||
CLICD_LOG_FILE=/path/file.log 默认:${LOG_FILE}
|
||||
|
||||
Examples:
|
||||
示例:
|
||||
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh
|
||||
curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | sudo sh -s -- uninstall
|
||||
|
||||
日志:${LOG_FILE}
|
||||
问题反馈:${ISSUE_URL}
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -68,7 +148,7 @@ remove_path() {
|
||||
return
|
||||
fi
|
||||
rm -rf "$path"
|
||||
log "Removed $path"
|
||||
log "已删除 $path"
|
||||
}
|
||||
|
||||
unmount_path_tree() {
|
||||
@@ -125,17 +205,17 @@ remove_lxc_container_dir() {
|
||||
detach_container_loop_devices "$container_dir"
|
||||
|
||||
if rm -rf "$container_dir" >/dev/null 2>&1; then
|
||||
log "Removed $container_dir"
|
||||
log "已删除 $container_dir"
|
||||
return
|
||||
fi
|
||||
|
||||
log "Retrying removal after terminating processes using $container_dir..."
|
||||
log "检测到 $container_dir 被占用,终止占用进程后重试删除..."
|
||||
kill_path_users "$container_dir/rootfs"
|
||||
kill_path_users "$container_dir"
|
||||
unmount_path_tree "$container_dir"
|
||||
detach_container_loop_devices "$container_dir"
|
||||
rm -rf "$container_dir"
|
||||
log "Removed $container_dir"
|
||||
log "已删除 $container_dir"
|
||||
}
|
||||
|
||||
remove_kvm_domain() {
|
||||
@@ -158,7 +238,7 @@ remove_kvm_domain() {
|
||||
return
|
||||
fi
|
||||
|
||||
log "Removing KVM domain $domain..."
|
||||
log "正在删除 KVM 虚拟机域 $domain..."
|
||||
virsh destroy "$domain" >/dev/null 2>&1 || true
|
||||
virsh undefine "$domain" --remove-all-storage --nvram >/dev/null 2>&1 ||
|
||||
virsh undefine "$domain" --nvram >/dev/null 2>&1 ||
|
||||
@@ -171,7 +251,7 @@ destroy_clicd_kvm_domains() {
|
||||
return
|
||||
fi
|
||||
|
||||
log "Destroying CLICD KVM domains..."
|
||||
log "正在销毁 CLICD 创建的 KVM 虚拟机..."
|
||||
virsh list --all --name 2>/dev/null | while IFS= read -r domain; do
|
||||
[ -n "$domain" ] || continue
|
||||
remove_kvm_domain "$domain"
|
||||
@@ -231,7 +311,7 @@ delete_ip6tables_bridge_rules() {
|
||||
}
|
||||
|
||||
cleanup_clicd_networking() {
|
||||
log "Cleaning CLICD firewall and bridge rules..."
|
||||
log "正在清理 CLICD 防火墙和网桥规则..."
|
||||
delete_iptables_lines nat PREROUTING 'clicd-'
|
||||
delete_iptables_rule nat POSTROUTING -s 10.0.3.0/24 -o eth+ -j MASQUERADE
|
||||
delete_iptables_rule nat POSTROUTING -s 192.168.122.0/24 -o eth+ -j MASQUERADE
|
||||
@@ -269,7 +349,7 @@ remove_clicd_quota_records() {
|
||||
grep -v 'clicd-' "$file" > "$tmp" || true
|
||||
cat "$tmp" > "$file"
|
||||
rm -f "$tmp"
|
||||
log "Cleaned CLICD quota records from $file"
|
||||
log "已清理 $file 中的 CLICD 配额记录"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -277,7 +357,7 @@ remove_clicd_tmp_files() {
|
||||
for path in /tmp/clicd-* /tmp/clicd.*; do
|
||||
[ -e "$path" ] || [ -L "$path" ] || continue
|
||||
rm -rf "$path"
|
||||
log "Removed $path"
|
||||
log "已删除 $path"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -289,8 +369,28 @@ remove_clicd_swapfile() {
|
||||
remove_path /swapfile
|
||||
}
|
||||
|
||||
|
||||
confirm_uninstall() {
|
||||
if [ "${CLICD_UNINSTALL_CONFIRM:-}" = "1" ] || [ "${CLICD_UNINSTALL_CONFIRM:-}" = "yes" ] || [ "$ACTION_CONFIRM" = "--yes" ] || [ "$ACTION_CONFIRM" = "-y" ]; then
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
echo "[clicd][警告] 卸载会停止并删除 CLICD 服务、配置数据库、CLICD 创建的 LXC/KVM 实例和缓存数据。" >&2
|
||||
echo "[clicd][警告] 为避免误删生产数据,脚本只会删除名称形如 ct-数字 的 LXC 容器和 vm-数字 的 KVM 域。" >&2
|
||||
echo "如需确认卸载,请输入:YES" >&2
|
||||
if [ -t 0 ]; then
|
||||
read answer
|
||||
else
|
||||
answer=""
|
||||
fi
|
||||
if [ "$answer" != "YES" ]; then
|
||||
die "已取消卸载。如需非交互卸载,请设置 CLICD_UNINSTALL_CONFIRM=1。"
|
||||
fi
|
||||
}
|
||||
|
||||
uninstall_clicd() {
|
||||
log "Uninstalling CLICD..."
|
||||
confirm_uninstall
|
||||
log "正在卸载 CLICD..."
|
||||
|
||||
if has_cmd systemctl; then
|
||||
systemctl stop clicd >/dev/null 2>&1 || true
|
||||
@@ -304,8 +404,8 @@ uninstall_clicd() {
|
||||
rc-update del clicd default >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
log "Destroying LXC containers under /var/lib/lxc..."
|
||||
for container_dir in /var/lib/lxc/*; do
|
||||
log "正在删除 CLICD 创建的 LXC 容器(/var/lib/lxc/ct-数字)..."
|
||||
for container_dir in /var/lib/lxc/ct-[0-9]*; do
|
||||
[ -d "$container_dir" ] || continue
|
||||
remove_lxc_container_dir "$container_dir"
|
||||
done
|
||||
@@ -321,13 +421,12 @@ uninstall_clicd() {
|
||||
remove_path /var/log/clicd.log
|
||||
remove_path /var/log/clicd.err
|
||||
remove_path /root/.clicd
|
||||
unmount_path_tree /var/lib/lxc
|
||||
remove_path /var/lib/lxc
|
||||
# /var/lib/lxc 可能包含非 CLICD 容器,生产环境不整体删除。
|
||||
unmount_path_tree /var/lib/clicd
|
||||
remove_path /var/lib/clicd
|
||||
remove_path /var/cache/lxc
|
||||
# /var/cache/lxc 是 LXC 全局镜像缓存,可能被其他工具复用,生产环境不整体删除。
|
||||
remove_path /var/cache/clicd
|
||||
remove_path /root/clicd-backups
|
||||
warn "保留 /root/clicd-backups,避免误删部署/回滚备份。确认不需要后可手动删除。"
|
||||
remove_clicd_tmp_files
|
||||
remove_clicd_swapfile
|
||||
|
||||
@@ -341,11 +440,13 @@ uninstall_clicd() {
|
||||
|
||||
echo ""
|
||||
echo "====================================="
|
||||
echo " CLICD Uninstalled"
|
||||
echo " CLICD 卸载完成"
|
||||
echo "====================================="
|
||||
echo " Removed service, binary, SQLite/config data, LXC containers,"
|
||||
echo " CLICD KVM domains, VM images, image caches, firewall rules,"
|
||||
echo " host hooks, quota records, temp files, backups, and swapfile."
|
||||
echo " 已删除服务、二进制、SQLite/配置数据、CLICD LXC/KVM 实例、"
|
||||
echo " CLICD 镜像缓存、防火墙规则、主机钩子、配额记录和临时文件。"
|
||||
echo " 已保留 /root/clicd-backups 和 LXC 全局缓存,避免误删生产备份/共享镜像。"
|
||||
echo " 日志:$LOG_FILE"
|
||||
echo " 问题反馈:$ISSUE_URL"
|
||||
echo "====================================="
|
||||
}
|
||||
|
||||
@@ -361,12 +462,12 @@ case "$ACTION" in
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown action: $ACTION"
|
||||
die "未知操作:$ACTION"
|
||||
;;
|
||||
esac
|
||||
|
||||
install_apk() {
|
||||
log "Installing dependencies with apk..."
|
||||
log "正在使用 apk 安装依赖..."
|
||||
apk update
|
||||
apk add --no-cache \
|
||||
ca-certificates \
|
||||
@@ -393,12 +494,12 @@ install_apk() {
|
||||
libvirt-qemu
|
||||
|
||||
for pkg in lxcfs shadow conntrack-tools quota-tools e2fsprogs xfsprogs cloud-utils genisoimage xorriso; do
|
||||
apk add --no-cache "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg"
|
||||
apk add --no-cache "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
install_apt() {
|
||||
log "Installing dependencies with apt..."
|
||||
log "正在使用 apt 安装依赖..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
@@ -445,7 +546,7 @@ enable_el_repos() {
|
||||
}
|
||||
|
||||
install_dnf() {
|
||||
log "Installing dependencies with dnf..."
|
||||
log "正在使用 dnf 安装依赖..."
|
||||
enable_el_repos
|
||||
dnf install -y \
|
||||
ca-certificates \
|
||||
@@ -475,12 +576,12 @@ install_dnf() {
|
||||
genisoimage
|
||||
|
||||
for pkg in lxcfs xorriso edk2-ovmf; do
|
||||
dnf install -y "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg"
|
||||
dnf install -y "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
install_yum() {
|
||||
log "Installing dependencies with yum..."
|
||||
log "正在使用 yum 安装依赖..."
|
||||
enable_el_repos
|
||||
yum install -y \
|
||||
ca-certificates \
|
||||
@@ -510,7 +611,7 @@ install_yum() {
|
||||
genisoimage
|
||||
|
||||
for pkg in lxcfs xorriso edk2-ovmf; do
|
||||
yum install -y "$pkg" >/dev/null 2>&1 || log "Optional package not installed: $pkg"
|
||||
yum install -y "$pkg" >/dev/null 2>&1 || warn "可选依赖未安装:$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -528,7 +629,7 @@ install_dependencies() {
|
||||
elif has_cmd yum; then
|
||||
install_yum
|
||||
else
|
||||
die "dnf/yum not found on $OS_ID"
|
||||
die "当前系统 $OS_ID 未找到 dnf/yum,无法安装依赖。"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
@@ -541,27 +642,27 @@ install_dependencies() {
|
||||
elif has_cmd yum; then
|
||||
install_yum
|
||||
else
|
||||
die "Unsupported Linux distribution: ${OS_ID} ${OS_LIKE}"
|
||||
die "暂不支持当前 Linux 发行版:${OS_ID} ${OS_LIKE}。请提交 issue 并附上 /etc/os-release。"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
has_cmd lxc-create || die "lxc-create is still missing after dependency installation."
|
||||
has_cmd iptables || die "iptables is still missing after dependency installation."
|
||||
has_cmd ip || die "iproute2/ip command is still missing after dependency installation."
|
||||
has_cmd virsh || die "virsh is still missing after dependency installation."
|
||||
has_cmd qemu-img || die "qemu-img is still missing after dependency installation."
|
||||
has_cmd cloud-localds || die "cloud-localds is still missing after dependency installation."
|
||||
has_cmd lxc-create || die "依赖安装后仍未找到 lxc-create,请检查 LXC 软件源/安装日志。"
|
||||
has_cmd iptables || die "依赖安装后仍未找到 iptables,请检查系统网络工具包。"
|
||||
has_cmd ip || die "依赖安装后仍未找到 ip 命令,请检查 iproute2 安装。"
|
||||
has_cmd virsh || die "依赖安装后仍未找到 virsh,请检查 libvirt-client/libvirt-clients 安装。"
|
||||
has_cmd qemu-img || die "依赖安装后仍未找到 qemu-img,请检查 qemu-utils/qemu-img 安装。"
|
||||
has_cmd cloud-localds || die "依赖安装后仍未找到 cloud-localds,请检查 cloud-image-utils/cloud-utils 安装。"
|
||||
if ! has_cmd genisoimage && ! has_cmd mkisofs && ! has_cmd xorriso; then
|
||||
die "one of genisoimage, mkisofs, or xorriso is required for Windows KVM setup."
|
||||
die "Windows KVM 初始化需要 genisoimage、mkisofs 或 xorriso 中任意一个。"
|
||||
fi
|
||||
if [ ! -e /dev/kvm ]; then
|
||||
log "Warning: /dev/kvm was not found. KVM VMs require hardware virtualization or nested virtualization."
|
||||
warn "未检测到 /dev/kvm。LXC 可用,但 KVM 虚拟机需要硬件虚拟化或嵌套虚拟化。"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_kernel_networking() {
|
||||
log "Enabling kernel forwarding settings..."
|
||||
log "正在启用内核转发配置..."
|
||||
cat > /etc/sysctl.d/99-clicd.conf << 'EOF'
|
||||
net.ipv4.ip_forward = 1
|
||||
net.ipv6.conf.all.forwarding = 1
|
||||
@@ -574,7 +675,7 @@ EOF
|
||||
}
|
||||
|
||||
setup_runtime_services() {
|
||||
log "Configuring LXC and KVM services..."
|
||||
log "正在配置 LXC 和 KVM 服务..."
|
||||
|
||||
if is_systemd; then
|
||||
systemctl enable --now lxcfs >/dev/null 2>&1 || true
|
||||
@@ -603,11 +704,49 @@ setup_runtime_services() {
|
||||
return
|
||||
fi
|
||||
|
||||
die "No supported service manager found. CLICD supports systemd or OpenRC."
|
||||
die "未检测到支持的服务管理器。CLICD 当前支持 systemd 或 OpenRC。"
|
||||
}
|
||||
|
||||
|
||||
libvirt_network_active() {
|
||||
virsh net-info default 2>/dev/null | awk -F: 'tolower($1) ~ /^[[:space:]]*active[[:space:]]*$/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); print tolower($2)}' | grep -qx yes
|
||||
}
|
||||
|
||||
setup_default_libvirt_network() {
|
||||
if ! has_cmd virsh; then
|
||||
warn "未找到 virsh,跳过 libvirt default NAT 网络检查。"
|
||||
return
|
||||
fi
|
||||
log "正在检查 libvirt default NAT 网络..."
|
||||
if ! virsh net-info default >/dev/null 2>&1; then
|
||||
net_xml="$(mktemp /tmp/clicd-default-net.XXXXXX.xml)"
|
||||
cat > "$net_xml" << 'EOF'
|
||||
<network>
|
||||
<name>default</name>
|
||||
<bridge name='virbr0'/>
|
||||
<forward mode='nat'/>
|
||||
<ip address='192.168.122.1' netmask='255.255.255.0'>
|
||||
<dhcp>
|
||||
<range start='192.168.122.2' end='192.168.122.254'/>
|
||||
</dhcp>
|
||||
</ip>
|
||||
</network>
|
||||
EOF
|
||||
virsh net-define "$net_xml"
|
||||
rm -f "$net_xml"
|
||||
fi
|
||||
if ! libvirt_network_active; then
|
||||
virsh net-start default
|
||||
fi
|
||||
virsh net-autostart default >/dev/null
|
||||
if ! libvirt_network_active; then
|
||||
die "libvirt default 网络仍未启动。请执行 virsh net-info default 查看详情。"
|
||||
fi
|
||||
log "libvirt default NAT 网络已启用。"
|
||||
}
|
||||
|
||||
setup_subids() {
|
||||
log "Setting up subordinate UID/GID ranges..."
|
||||
log "正在配置 subordinate UID/GID 范围..."
|
||||
touch /etc/subuid /etc/subgid
|
||||
grep -q '^root:' /etc/subuid 2>/dev/null || echo 'root:100000:65536' >> /etc/subuid
|
||||
grep -q '^root:' /etc/subgid 2>/dev/null || echo 'root:100000:65536' >> /etc/subgid
|
||||
@@ -618,21 +757,21 @@ try_enable_project_quota() {
|
||||
root_fs="$(findmnt -no FSTYPE / 2>/dev/null || true)"
|
||||
|
||||
if [ "$root_fs" != "ext4" ] || [ -z "$root_src" ] || [ ! -b "$root_src" ]; then
|
||||
log "Project quota auto-enable skipped for root filesystem: ${root_fs:-unknown}"
|
||||
warn "根文件系统 ${root_fs:-unknown} 不适合自动启用 project quota,将使用兼容模式。"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! has_cmd tune2fs; then
|
||||
log "Project quota auto-enable skipped because tune2fs is unavailable."
|
||||
warn "未找到 tune2fs,跳过 project quota 检查,将使用兼容模式。"
|
||||
return
|
||||
fi
|
||||
|
||||
if tune2fs -l "$root_src" 2>/dev/null | grep -q 'project'; then
|
||||
log "Ext4 project quota support already appears to be enabled."
|
||||
log "检测到 ext4 project quota 已可用。"
|
||||
return
|
||||
fi
|
||||
|
||||
log "Ext4 project quota is not enabled. Disk limits will fall back to loopback images."
|
||||
warn "ext4 project quota 未启用,磁盘限制将回退到 loopback 镜像模式。"
|
||||
}
|
||||
|
||||
download_release_if_needed() {
|
||||
@@ -646,8 +785,8 @@ download_release_if_needed() {
|
||||
download_url="https://github.com/${REPO}/releases/download/${CLICD_INSTALL_VERSION}/${ASSET}"
|
||||
fi
|
||||
|
||||
log "clicd binary not found in current directory."
|
||||
log "Downloading release package: ${download_url}"
|
||||
log "当前目录未找到 clicd 二进制,将下载发行版包。"
|
||||
log "正在下载发行版包:${download_url}"
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' 0
|
||||
@@ -657,12 +796,12 @@ download_release_if_needed() {
|
||||
elif has_cmd wget; then
|
||||
wget -O "$tmp_dir/$ASSET" "$download_url"
|
||||
else
|
||||
die "curl or wget is required to download the release package."
|
||||
die "下载发行版包需要 curl 或 wget。"
|
||||
fi
|
||||
|
||||
tar -xzf "$tmp_dir/$ASSET" -C "$tmp_dir"
|
||||
cd "$tmp_dir/clicd-linux-amd64"
|
||||
[ -f "./clicd" ] || die "Downloaded release package did not contain clicd."
|
||||
[ -f "./clicd" ] || die "下载的发行版包中未找到 clicd 二进制。"
|
||||
}
|
||||
|
||||
install_binary() {
|
||||
@@ -678,21 +817,24 @@ install_binary() {
|
||||
chmod +x "$tmp_bin"
|
||||
mv -f "$tmp_bin" /usr/local/bin/clicd
|
||||
chmod +x /usr/local/bin/clicd
|
||||
log "Installed binary: /usr/local/bin/clicd"
|
||||
log "已安装二进制:/usr/local/bin/clicd"
|
||||
}
|
||||
|
||||
install_systemd_service() {
|
||||
cat > /etc/systemd/system/clicd.service << 'EOF'
|
||||
[Unit]
|
||||
Description=CLICD - LXC/KVM Container Manager
|
||||
After=network.target lxc.service libvirtd.service virtqemud.service
|
||||
Wants=libvirtd.service
|
||||
After=network-online.target lxc.service lxcfs.service libvirtd.service virtqemud.service virtqemud.socket virtlogd.socket
|
||||
Wants=network-online.target libvirtd.service virtqemud.socket virtlogd.socket
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/clicd server
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StartLimitIntervalSec=60
|
||||
StartLimitBurst=10
|
||||
LimitNOFILE=1048576
|
||||
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
[Install]
|
||||
@@ -729,51 +871,56 @@ EOF
|
||||
}
|
||||
|
||||
install_service() {
|
||||
log "Installing CLICD service..."
|
||||
log "正在安装 CLICD 服务..."
|
||||
|
||||
if is_systemd; then
|
||||
install_systemd_service
|
||||
elif is_openrc; then
|
||||
install_openrc_service
|
||||
else
|
||||
die "No supported service manager found. CLICD supports systemd or OpenRC."
|
||||
die "未检测到支持的服务管理器。CLICD 当前支持 systemd 或 OpenRC。"
|
||||
fi
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
echo ""
|
||||
echo "====================================="
|
||||
echo " Installation Complete"
|
||||
echo " 安装完成"
|
||||
echo "====================================="
|
||||
echo " Web: http://YOUR_SERVER_IP:8999"
|
||||
echo " Binary: /usr/local/bin/clicd"
|
||||
echo " Web 面板:http://YOUR_SERVER_IP:8999"
|
||||
echo " 二进制:/usr/local/bin/clicd"
|
||||
echo " 安装日志:$LOG_FILE"
|
||||
echo " 问题反馈:$ISSUE_URL"
|
||||
if is_systemd; then
|
||||
echo " Service: systemctl {start|stop|restart|status} clicd"
|
||||
echo " Logs: journalctl -u clicd -f"
|
||||
echo " 服务:systemctl {start|stop|restart|status} clicd"
|
||||
echo " 运行日志:journalctl -u clicd -f"
|
||||
elif is_openrc; then
|
||||
echo " Service: rc-service clicd {start|stop|restart|status}"
|
||||
echo " Logs: tail -f /var/log/clicd.log /var/log/clicd.err"
|
||||
echo " 服务:rc-service clicd {start|stop|restart|status}"
|
||||
echo " 运行日志:tail -f /var/log/clicd.log /var/log/clicd.err"
|
||||
fi
|
||||
echo "====================================="
|
||||
echo ""
|
||||
echo "Initial credentials, if this was the first run:"
|
||||
echo "首次安装时的初始账号信息:"
|
||||
if is_systemd; then
|
||||
journalctl -u clicd --no-pager -n 80 | grep -E "Username:|Password:" || true
|
||||
else
|
||||
grep -E "Username:|Password:" /var/log/clicd.log /var/log/clicd.err 2>/dev/null || true
|
||||
fi
|
||||
echo ""
|
||||
echo "If no password is shown, this server already had /root/.clicd/config.db."
|
||||
echo "The existing admin password cannot be recovered from the bcrypt hash."
|
||||
echo "如果没有显示密码,说明服务器已有 /root/.clicd/config.db。"
|
||||
echo "已有管理员密码使用 bcrypt 存储,无法反查;请使用面板内修改密码或重置配置。"
|
||||
}
|
||||
|
||||
install_dependencies
|
||||
configure_kernel_networking
|
||||
setup_runtime_services
|
||||
setup_subids
|
||||
try_enable_project_quota
|
||||
download_release_if_needed
|
||||
install_binary
|
||||
install_service
|
||||
run_step "兼容性检查" check_os_compatibility
|
||||
run_step "存储环境检查" check_storage_compatibility
|
||||
run_step "安装系统依赖" install_dependencies
|
||||
run_step "配置内核网络参数" configure_kernel_networking
|
||||
run_step "配置运行时服务" setup_runtime_services
|
||||
run_step "配置 libvirt default NAT 网络" setup_default_libvirt_network
|
||||
run_step "配置 UID/GID 映射" setup_subids
|
||||
run_step "检查 project quota" try_enable_project_quota
|
||||
run_step "下载发行版包" download_release_if_needed
|
||||
run_step "安装 CLICD 二进制" install_binary
|
||||
run_step "安装并启动 CLICD 服务" install_service
|
||||
sleep 2
|
||||
print_summary
|
||||
|
||||
Reference in New Issue
Block a user