From 2df92be501489102d31700409912caea6553ee1a Mon Sep 17 00:00:00 2001 From: MengMengCode <227010654+MengMengCode@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:15:59 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=B7=B2=E7=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + backend/internal/api/handlers.go | 6 +++++- backend/internal/config/config.go | 19 +++++++++++++++-- backend/internal/kvm/kvm.go | 16 ++++++++++++++- backend/internal/lxc/lxc.go | 24 ++++++++++++++++++++++ backend/internal/lxc/portmap.go | 24 +++++++++++++++++++++- backend/internal/server/web/.gitkeep | 1 + frontend/src/components/RingStats.tsx | 10 ++++----- frontend/src/pages/ContainerDetail.tsx | 5 +++-- frontend/src/pages/Dashboard.tsx | 3 +-- frontend/src/pages/Routing.tsx | 28 +++++++++++++++++++++----- frontend/src/services/api.ts | 3 +++ 12 files changed, 120 insertions(+), 20 deletions(-) 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 */}
- {value.toFixed(percentage < 1 ? 2 : 1)}% + {percentage.toFixed(percentage < 1 ? 2 : 1)}%
@@ -73,7 +73,6 @@ interface RingStatsProps { swapUsed?: number swapTotal?: number loadPercent: number - loadStatus: string diskPercent: number diskUsed: number diskTotal: number @@ -90,7 +89,6 @@ export default function RingStats({ swapUsed = 0, swapTotal = 0, loadPercent, - loadStatus, diskPercent, diskUsed, diskTotal, @@ -125,8 +123,8 @@ export default function RingStats({ )} 0 ? clamp(((usage?.memory_usage_bytes || 0) / (container.ram_mb * 1024 * 1024)) * 100) : 0 + const loadPct = container.vcpu > 0 ? ((usage?.load1 || 0) / container.vcpu) * 100 : 0 const diskPct = container.disk_gb > 0 ? clamp(((usage?.disk_usage_bytes || 0) / (container.disk_gb * 1024 * 1024 * 1024)) * 100) : 0 const networkBps = (usage?.network_rx_bps || 0) + (usage?.network_tx_bps || 0) const rx = usage?.network_rx_bps || 0 @@ -873,9 +874,9 @@ export default function ContainerDetail() { subLabel={`${formatMB(usage?.memory_usage_bytes || 0)} / ${formatMB(container.ram_mb * 1024 * 1024)}`} /> 0 ? (host.ram.used_mb / host.ram.total_mb) * 100 : 0} ramUsed={host.ram.used_mb} ramTotal={host.ram.total_mb} - loadPercent={Math.min((host.load.load1 / host.cpu.cores) * 100, 100)} - loadStatus={host.load.load1 < host.cpu.cores * 0.7 ? '正常' : host.load.load1 < host.cpu.cores * 1.0 ? '中等' : '高'} + loadPercent={(host.load.load1 / Math.max(host.cpu.cores, 1)) * 100} diskPercent={host.disk.total_gb > 0 ? (host.disk.used_gb / host.disk.total_gb) * 100 : 0} diskUsed={host.disk.used_gb * 1024} diskTotal={host.disk.total_gb * 1024} diff --git a/frontend/src/pages/Routing.tsx b/frontend/src/pages/Routing.tsx index 55dedb6..049f818 100644 --- a/frontend/src/pages/Routing.tsx +++ b/frontend/src/pages/Routing.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { Network, RefreshCw, Route, Search, Server, X } from 'lucide-react' +import { RefreshCw, Search, Server, X } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { getRoutingInfo, RoutingInfo, NAT4Route, IPv6Route } from '../services/api' @@ -96,7 +96,7 @@ export default function Routing() {
} + icon={} remaining={routing?.nat4.remaining || '0'} total={routing?.nat4.total || '0'} used={routing?.nat4.used || 0} @@ -104,7 +104,7 @@ export default function Routing() { /> } + icon={} remaining={formatCapacity(routing?.ipv6.remaining || '0')} total={formatCapacity(routing?.ipv6.total || '0')} used={routing?.ipv6.used || 0} @@ -137,7 +137,7 @@ export default function Routing() {
{nat4Mappings.length === 0 ? ( - } text="暂无 NAT4 端口映射" /> + } text="暂无 NAT4 端口映射" /> ) : ( <>
@@ -214,7 +214,7 @@ export default function Routing() {
{ipv6Assignments.length === 0 ? ( - } text="暂无 IPv6 地址分配" /> + } text="暂无 IPv6 地址分配" /> ) : ( <>
@@ -357,3 +357,21 @@ function formatCapacity(value: string): string { if (value === 'large') return '充足' return value } + +function Nat4Icon({ className }: { className?: string }) { + return ( + + + + + ) +} + +function IPv6Icon({ className }: { className?: string }) { + return ( + + + + + ) +} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 60b22ce..d522e86 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -170,6 +170,9 @@ export interface ContainerUsage { disk_write_bytes: number disk_read_bps: number disk_write_bps: number + load1: number + load5: number + load15: number } export interface APIResponse {