修复了一些已知问题

This commit is contained in:
MengMengCode
2026-06-10 14:51:22 +08:00
parent bfc98d043b
commit eedb2d7fb0
6 changed files with 81 additions and 10 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ var cliTranslations = map[string]string{
"请选择操作": "Select an action",
"再见": "Goodbye",
"无效选择": "Invalid choice",
"CLICD - LXC 容器管理器": "CLICD - LXC Container Manager",
"CLICD - LXC 容器管理器": "CLICD - Container Manager",
"Web 面板": "Web panel",
"端口": "port",
"运行中": "running",
+54 -2
View File
@@ -588,7 +588,7 @@ func (m *Manager) StartContainer(id int) error {
}
}
}
config.UpdateContainerStatus(id, "running")
config.UpdateContainerStatus(id, "initializing")
// Detect VNC port
if _, err := m.RefreshVNCPort(id); err != nil {
fmt.Printf("Warning: failed to refresh VNC port for %s: %v\n", name, err)
@@ -625,6 +625,11 @@ func (m *Manager) StartContainer(id int) error {
return err
}
}
// Wait for cloud-init to finish and SSH to be reachable (password-only mode)
if !isWindows && c.IP != "" {
m.waitForCloudInitReady(name, c.IP, c.SSHPassword)
}
config.UpdateContainerStatus(id, "running")
if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
if err := m.applyIPv6Runtime(c); err != nil {
return err
@@ -635,6 +640,50 @@ func (m *Manager) StartContainer(id int) error {
return nil
}
// waitForCloudInitReady waits for cloud-init to finish and SSH to be reachable.
func (m *Manager) waitForCloudInitReady(vmName, ip, password string) {
if ip == "" || password == "" {
return
}
deadline := time.Now().Add(3 * time.Minute)
target := net.JoinHostPort(ip, "22")
sshWasUp := false
qgaAttempted := false
for time.Now().Before(deadline) {
client, err := ssh.Dial("tcp", target, &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{ssh.Password(password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
})
if err == nil {
client.Close()
if !sshWasUp {
sshWasUp = true
fmt.Printf("KVM %s SSH up, waiting for cloud-init to settle...\n", vmName)
time.Sleep(10 * time.Second)
continue
}
fmt.Printf("KVM %s ready\n", vmName)
return
}
// Try guest agent ONCE to speed things up, with timeout to avoid blocking
if !qgaAttempted && qemuGuestPing(vmName) == nil {
qgaAttempted = true
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
cmd := exec.CommandContext(ctx, "virsh", "qemu-agent-command", vmName,
`{"execute":"guest-exec","arguments":{"path":"/bin/sh","arg":["-c","cloud-init status --wait 2>/dev/null; systemctl restart sshd 2>/dev/null || systemctl restart ssh 2>/dev/null; true"],"capture-output":false}}`)
cmd.Run()
cancel()
}
time.Sleep(5 * time.Second)
}
fmt.Printf("Warning: KVM %s not ready after 3 minutes\n", vmName)
}
func (m *Manager) StopContainer(id int) error {
c := config.FindContainer(id)
if c == nil {
@@ -2530,7 +2579,10 @@ if [ -n "$SSH_PUBLIC_KEY" ]; then
chown -R root:root /root/.ssh 2>/dev/null || true
fi
if command -v chpasswd >/dev/null 2>&1; then
printf 'root:%s\n' "$ROOT_PASSWORD" | chpasswd || true
printf 'root:%s\n' "$ROOT_PASSWORD" | chpasswd 2>/tmp/clicd-chpasswd.log && echo "root password set via chpasswd" || echo "WARNING: chpasswd failed: $(cat /tmp/clicd-chpasswd.log 2>/dev/null)"
elif command -v openssl >/dev/null 2>&1 && command -v usermod >/dev/null 2>&1; then
HASH=$(echo "$ROOT_PASSWORD" | openssl passwd -6 -stdin 2>/dev/null)
[ -n "$HASH" ] && usermod -p "$HASH" root 2>/dev/null && echo "root password set via openssl/usermod" || echo "WARNING: openssl/usermod failed"
fi
ssh-keygen -A >/dev/null 2>&1 || true
if command -v systemctl >/dev/null 2>&1; then
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CLICD - LXC Container Manager</title>
<title>CLICD - Container Manager</title>
<script>
(function() {
var theme = localStorage.getItem('clicd_theme');
+11 -2
View File
@@ -753,6 +753,7 @@ export default function ContainerDetail() {
}
const isRunning = container.status === 'running'
const isInitializing = container.status === 'initializing'
const isKVM = (container.virtualization || 'lxc') === 'kvm'
const isWindows = container.template?.includes('windows')
const reinstallLinuxTemplate = !isWindowsTemplate(selectedTemplate)
@@ -872,7 +873,7 @@ export default function ContainerDetail() {
<div>
<div className="flex items-center gap-2 flex-wrap">
<h1 className="text-xl font-bold text-black">{container.name}</h1>
<StatusBadge running={isRunning} />
<StatusBadge running={isRunning} initializing={isInitializing} />
</div>
<div className="flex items-center gap-2 flex-wrap mt-2">
<InfoTag color="blue"> {container.template}</InfoTag>
@@ -1670,7 +1671,15 @@ function RangeSwitch({ value, onChange }: { value: StatsRangeKey; onChange: (val
)
}
function StatusBadge({ running }: { running: boolean }) {
function StatusBadge({ running, initializing }: { running: boolean; initializing?: boolean }) {
if (initializing) {
return (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium whitespace-nowrap bg-amber-50 text-amber-700">
<span className="w-1.5 h-1.5 rounded-full flex-shrink-0 bg-amber-500 animate-pulse"></span>
</span>
)
}
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium whitespace-nowrap ${running ? 'bg-emerald-100 text-emerald-700' : 'bg-rose-100 text-rose-700'}`}>
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${running ? 'bg-emerald-500' : 'bg-rose-500'}`}></span>
+13 -3
View File
@@ -390,6 +390,7 @@ export default function Containers() {
<tbody className="divide-y divide-gray-100">
{pageContainers.map((container) => {
const isRunning = container.status === 'running'
const isInitializing = container.status === 'initializing'
const task = (container.id > 0 ? taskStatusMap[container.id] : taskNameMap[container.name]) || container.createTask
const isPlaceholder = !!container.isPlaceholder
const isPolicyBlocked = !!container.policy_blocked
@@ -437,7 +438,7 @@ export default function Containers() {
</button>
</td>
<td className="px-2.5 py-2 align-top">
<StatusBadge running={isRunning} task={task} placeholder={isPlaceholder} policyBlocked={isPolicyBlocked} />
<StatusBadge running={isRunning} initializing={isInitializing} task={task} placeholder={isPlaceholder} policyBlocked={isPolicyBlocked} />
</td>
<td className="px-2.5 py-2 align-top text-xs text-gray-600 whitespace-nowrap">
<span className="inline-flex items-center gap-1">
@@ -483,7 +484,7 @@ export default function Containers() {
try {
const { default: api } = await import('../services/api')
await api.delete(`/tasks/${task.id}`)
fetchData()
await Promise.all([fetchData(), fetchTasks()])
} catch { /* ignore */ }
}}
className="inline-flex items-center gap-1 px-2 py-1 rounded-md border border-red-200 text-[11px] text-red-600 hover:bg-red-50 transition-colors whitespace-nowrap"
@@ -581,7 +582,7 @@ type DisplayContainer = Container & {
createTask?: Task
}
function StatusBadge({ running, task, placeholder, policyBlocked }: { running: boolean; task?: Task; placeholder?: boolean; policyBlocked?: boolean }) {
function StatusBadge({ running, initializing, task, placeholder, policyBlocked }: { running: boolean; initializing?: boolean; task?: Task; placeholder?: boolean; policyBlocked?: boolean }) {
const baseClass = "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium whitespace-nowrap"
if (policyBlocked) {
return (
@@ -640,6 +641,15 @@ function StatusBadge({ running, task, placeholder, policyBlocked }: { running: b
)
}
if (initializing) {
return (
<span className={`${baseClass} bg-amber-50 text-amber-700`}>
<span className="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
</span>
)
}
return (
<span className={`${baseClass} ${running ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-600'}`}>
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${running ? 'bg-green-500' : 'bg-red-500'}`}></span>
+1 -1
View File
@@ -66,7 +66,7 @@ export default function Login() {
<AppIcon className="w-10 h-10" />
</div>
<h1 className="text-2xl font-bold text-gray-950">CLICD</h1>
<p className="text-gray-500 mt-1 text-sm">{isAccessCodeLogin ? '容器管理登录' : 'LXC Container Manager'}</p>
<p className="text-gray-500 mt-1 text-sm">{isAccessCodeLogin ? '容器管理登录' : 'Container Manager'}</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">