diff --git a/.gitignore b/.gitignore index 6d0afd9..68e6c36 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ backend/tmp/ Thumbs.db linux.txt push-release.ps1 +deploy.ps1 diff --git a/backend/internal/api/handlers.go b/backend/internal/api/handlers.go index dd48f44..cae48a9 100644 --- a/backend/internal/api/handlers.go +++ b/backend/internal/api/handlers.go @@ -298,7 +298,11 @@ func updateResourceLimit(w http.ResponseWriter, r *http.Request, id int) { } } - jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Resource limits updated"}) + msg := "Resource limits updated" + if c.IsKVM() && c.Status == "running" { + msg = "资源已保存,请关机重启虚拟机后生效" + } + jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: msg}) } func getRandomPort(w http.ResponseWriter, r *http.Request, id int) { diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 3827a85..bba8541 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -716,14 +716,29 @@ func UpdateVNC(containers []Container) { SaveConfig() } -// AllocateSSHPort allocates a new SSH port +// AllocateSSHPort allocates a new SSH port, skipping ports already used by any container func AllocateSSHPort() int { + used := collectAllHostPorts() port := AppConfig.NextSSHPort - AppConfig.NextSSHPort++ + for used[port] { + port++ + } + AppConfig.NextSSHPort = port + 1 SaveConfig() return port } +// collectAllHostPorts collects all host ports used by any container (LXC + KVM) +func collectAllHostPorts() map[int]bool { + used := map[int]bool{} + for _, c := range AppConfig.Containers { + for _, pm := range c.PortMappings { + used[pm.HostPort] = true + } + } + return used +} + // IsValidContainerName checks if container name is valid (no duplicate check needed, ID is primary key) func IsValidContainerName(name string) bool { return IsValidContainerNameSyntax(name) diff --git a/backend/internal/kvm/kvm.go b/backend/internal/kvm/kvm.go index 25b2af3..ef8cef5 100644 --- a/backend/internal/kvm/kvm.go +++ b/backend/internal/kvm/kvm.go @@ -503,7 +503,8 @@ func (m *Manager) ApplyContainerLimits(c *config.Container) error { return nil } if c.Status == "running" { - return fmt.Errorf("KVM resource changes require shutdown and start") + // Config already saved; domain definition will be refreshed on next start + return nil } if c.DiskImage == "" || c.MACAddress == "" { return nil @@ -901,6 +902,9 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) { "disk_write_bytes": writeBytes, "disk_read_bps": 0.0, "disk_write_bps": 0.0, + "load1": 0.0, + "load5": 0.0, + "load15": 0.0, } if c.DiskImage != "" { if info, err := os.Stat(c.DiskImage); err == nil { @@ -2264,10 +2268,20 @@ func allocateDefaultEqualPorts(c *config.Container, count int) []int { return nil } used := map[int]bool{} + // Mark current container's ports for _, pm := range c.PortMappings { used[pm.HostPort] = true used[pm.ContainerPort] = true } + // Also mark all other containers' host ports (LXC + KVM) + for _, oc := range config.AppConfig.Containers { + if oc.ID == c.ID { + continue + } + for _, pm := range oc.PortMappings { + used[pm.HostPort] = true + } + } ports := make([]int, 0, count) for next := 20000; next <= 65535 && len(ports) < count; next++ { if !used[next] { diff --git a/backend/internal/lxc/lxc.go b/backend/internal/lxc/lxc.go index 6aae523..5cf537a 100644 --- a/backend/internal/lxc/lxc.go +++ b/backend/internal/lxc/lxc.go @@ -2272,6 +2272,11 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) { usage := make(map[string]interface{}) // Read raw values + load1, load5, load15 := m.getContainerLoadAvg(lxcName) + usage["load1"] = load1 + usage["load5"] = load5 + usage["load15"] = load15 + memUsage := readIntCommand(fmt.Sprintf( "cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+ "cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+ @@ -2319,6 +2324,25 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) { return usage, nil } +func (m *Manager) getContainerLoadAvg(lxcName string) (float64, float64, float64) { + pid := m.getContainerInitPID(lxcName) + if pid == "" { + return 0, 0, 0 + } + out, err := exec.Command("nsenter", "-t", pid, "-m", "-p", "cat", "/proc/loadavg").Output() + if err != nil { + return 0, 0, 0 + } + parts := strings.Fields(string(out)) + if len(parts) < 3 { + return 0, 0, 0 + } + load1, _ := strconv.ParseFloat(parts[0], 64) + load5, _ := strconv.ParseFloat(parts[1], 64) + load15, _ := strconv.ParseFloat(parts[2], 64) + return load1, load5, load15 +} + func (m *Manager) getContainerNetworkBytes(lxcName string) (uint64, uint64) { pid := m.getContainerInitPID(lxcName) if pid == "" { diff --git a/backend/internal/lxc/portmap.go b/backend/internal/lxc/portmap.go index 4ccd42f..d41c473 100644 --- a/backend/internal/lxc/portmap.go +++ b/backend/internal/lxc/portmap.go @@ -174,12 +174,24 @@ func normalizePortMapping(c *config.Container, skipIndex int, pm config.PortMapp if pm.HostPort <= 0 { pm.HostPort = pm.ContainerPort } + // Check current container's own mappings for i, existing := range c.PortMappings { if i == skipIndex { continue } if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol { - return pm, fmt.Errorf("host port %d/%s already mapped", pm.HostPort, pm.Protocol) + return pm, fmt.Errorf("host port %d/%s already mapped in this container", pm.HostPort, pm.Protocol) + } + } + // Check all other containers (LXC + KVM) for port conflicts + for _, oc := range config.AppConfig.Containers { + if oc.ID == c.ID { + continue + } + for _, existing := range oc.PortMappings { + if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol { + return pm, fmt.Errorf("host port %d/%s already used by container %s (ID: %d)", pm.HostPort, pm.Protocol, oc.Name, oc.ID) + } } } return pm, nil @@ -190,10 +202,20 @@ func allocateDefaultEqualPorts(c *config.Container, count int) []int { return nil } used := map[int]bool{} + // Mark current container's ports for _, pm := range c.PortMappings { used[pm.HostPort] = true used[pm.ContainerPort] = true } + // Also mark all other containers' host ports (LXC + KVM) + for _, oc := range config.AppConfig.Containers { + if oc.ID == c.ID { + continue + } + for _, pm := range oc.PortMappings { + used[pm.HostPort] = true + } + } ports := make([]int, 0, count) next := 20000 for len(ports) < count { diff --git a/backend/internal/server/web/.gitkeep b/backend/internal/server/web/.gitkeep index e69de29..30259b2 100644 --- a/backend/internal/server/web/.gitkeep +++ b/backend/internal/server/web/.gitkeep @@ -0,0 +1 @@ + diff --git a/frontend/src/components/RingStats.tsx b/frontend/src/components/RingStats.tsx index 35bfc74..52caf26 100644 --- a/frontend/src/components/RingStats.tsx +++ b/frontend/src/components/RingStats.tsx @@ -16,8 +16,8 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke const radius = (size - strokeWidth) / 2 const circumference = radius * 2 * Math.PI - const percentage = Math.min(Math.max(value / max * 100, 0), 100) - const strokeDashoffset = circumference - (percentage / 100) * circumference + const percentage = max === Infinity ? Math.max(value, 0) : Math.min(Math.max(value / max * 100, 0), 100) + const strokeDashoffset = circumference - (Math.min(percentage, 100) / 100) * circumference const bgStroke = isDark ? '#374151' : '#f3f4f6' const progressStroke = isDark ? '#f9fafb' : '#000000' @@ -51,7 +51,7 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke {/* Center value */}