Fix some problem.

This commit is contained in:
MengMengCode
2026-07-17 00:44:47 +08:00
parent 8bad52bd9e
commit 3dabd93d2f
6 changed files with 158 additions and 14 deletions
+104
View File
@@ -0,0 +1,104 @@
package api
import (
"fmt"
"time"
"clicd/internal/config"
"clicd/internal/kvm"
"clicd/internal/lxc"
)
// CaptureRuntimeRestoreState records which managed workloads are actually
// running before the CLICD service exits. On the next host boot, only those
// workloads are started again.
func CaptureRuntimeRestoreState() {
if config.AppConfig == nil {
return
}
lxcManager := lxc.NewManager()
kvmManager := kvm.NewManager()
changed := false
for i := range config.AppConfig.Containers {
c := &config.AppConfig.Containers[i]
status, err := runtimeStatus(*c, lxcManager, kvmManager)
if err != nil {
fmt.Printf("Warning: failed to capture runtime state for %s: %v\n", c.Name, err)
continue
}
restore := status == "running"
if c.RestoreOnHostBoot != restore {
c.RestoreOnHostBoot = restore
changed = true
}
if status != "" && c.Status != status {
c.Status = status
changed = true
}
}
if changed {
if err := config.SaveConfig(); err != nil {
fmt.Printf("Warning: failed to save host boot restore state: %v\n", err)
}
}
}
func StartHostBootRestore() {
go RestoreHostBootState()
}
func RestoreHostBootState() {
if config.AppConfig == nil {
return
}
time.Sleep(2 * time.Second)
lxcManager := lxc.NewManager()
kvmManager := kvm.NewManager()
containers := append([]config.Container(nil), config.AppConfig.Containers...)
for _, c := range containers {
if !c.RestoreOnHostBoot {
continue
}
if c.PolicyBlocked {
fmt.Printf("Skipping host boot restore for %s: policy blocked\n", c.Name)
continue
}
if lxc.IsExpired(c) {
fmt.Printf("Skipping host boot restore for %s: expired at %s\n", c.Name, c.ExpiresAt)
continue
}
status, err := runtimeStatus(c, lxcManager, kvmManager)
if err == nil && status == "running" {
config.UpdateContainerStatusAndRestore(c.ID, "running", true)
if !c.IsKVM() {
_ = lxcManager.ApplyPortMappings(c.ID)
} else {
_ = lxc.NewManager().ApplyPortMappings(c.ID)
}
continue
}
fmt.Printf("Restoring workload after host boot: %s (ID=%d)\n", c.Name, c.ID)
if c.IsKVM() {
if err := kvmManager.StartContainer(c.ID); err != nil {
fmt.Printf("Warning: failed to restore KVM %s: %v\n", c.Name, err)
}
continue
}
if err := lxcManager.StartContainer(c.ID); err != nil {
fmt.Printf("Warning: failed to restore LXC %s: %v\n", c.Name, err)
}
}
lxc.EnsureAllRunningPortMappings()
}
func runtimeStatus(c config.Container, lxcManager *lxc.Manager, kvmManager *kvm.Manager) (string, error) {
if c.IsKVM() {
return kvmManager.GetContainerStatus(c.VirshName())
}
return lxcManager.GetContainerStatus(c.LxcName())
}
+18
View File
@@ -128,6 +128,7 @@ type Container struct {
IOReadMBps int `json:"io_read_mbps"`
IOWriteMBps int `json:"io_write_mbps"`
Status string `json:"status"`
RestoreOnHostBoot bool `json:"restore_on_host_boot,omitempty"`
IP string `json:"ip"`
LANIPv4Mode string `json:"lan_ipv4_mode,omitempty"`
LANInterface string `json:"lan_interface,omitempty"`
@@ -1218,6 +1219,23 @@ func UpdateContainerStatus(id int, status string) {
}
}
func UpdateContainerStatusAndRestore(id int, status string, restoreOnHostBoot bool) {
c := FindContainer(id)
if c != nil {
c.Status = status
c.RestoreOnHostBoot = restoreOnHostBoot
SaveConfig()
}
}
func SetContainerRestoreOnHostBoot(id int, restore bool) {
c := FindContainer(id)
if c != nil {
c.RestoreOnHostBoot = restore
SaveConfig()
}
}
func SetContainerPolicyBlock(id int, blocked bool, reason string) {
c := FindContainer(id)
if c == nil {
+9 -6
View File
@@ -209,6 +209,7 @@ func ensureSchema() error {
io_read_mbps INTEGER NOT NULL DEFAULT 0,
io_write_mbps INTEGER NOT NULL DEFAULT 0,
status TEXT,
restore_on_host_boot INTEGER NOT NULL DEFAULT 0,
ip TEXT,
lan_ipv4_mode TEXT,
lan_interface TEXT,
@@ -459,6 +460,7 @@ func ensureSchemaMigrations() error {
{"containers", "firewall_rules", "TEXT"},
{"containers", "allowed_image_ids", "TEXT"},
{"containers", "image_limit_configured", "INTEGER NOT NULL DEFAULT 0"},
{"containers", "restore_on_host_boot", "INTEGER NOT NULL DEFAULT 0"},
{"containers", "lan_ipv4_mode", "TEXT"},
{"containers", "lan_interface", "TEXT"},
{"containers", "lan_ipv4_address", "TEXT NOT NULL DEFAULT ''"},
@@ -739,20 +741,20 @@ func saveContainers(tx *sql.Tx) error {
monthly_traffic_gb, traffic_mode, traffic_in_gb,
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
io_speed_mbps, io_read_mbps, io_write_mbps,
status, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway,
status, restore_on_host_boot, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway,
ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
policy_blocked, policy_blocked_reason, policy_blocked_at,
firewall_enabled, firewall_default_action, firewall_rules, allowed_image_ids, image_limit_configured
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.MACAddress, c.Template,
c.VCPU, c.RAMMB, c.DiskGB, c.NetworkBWMbps, c.NetworkDownMbps, c.NetworkUpMbps,
c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB,
c.TrafficOutGB, c.TrafficUsedRX, c.TrafficUsedTX, c.TrafficResetDate,
c.IOSpeedMBps, c.IOReadMBps, c.IOWriteMBps,
c.Status, c.IP, c.LANIPv4Mode, c.LANInterface, c.LANIPv4Address, c.LANIPv4PrefixLen, c.LANIPv4Gateway,
c.Status, boolInt(c.RestoreOnHostBoot), c.IP, c.LANIPv4Mode, c.LANInterface, c.LANIPv4Address, c.LANIPv4PrefixLen, c.LANIPv4Gateway,
c.IPv6, c.IPv6PrefixLen, c.IPv6Interface, c.VNCPort, c.SSHPort, c.SSHPassword,
c.SSHHostKey, c.PortMappingLimit, c.SnapshotLimit, c.CreatedAt, c.ExpiresAt,
boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime,
@@ -960,7 +962,7 @@ func loadContainers() ([]Container, error) {
monthly_traffic_gb, traffic_mode, traffic_in_gb,
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
io_speed_mbps, io_read_mbps, io_write_mbps,
status, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway,
status, restore_on_host_boot, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway,
ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
@@ -976,7 +978,7 @@ func loadContainers() ([]Container, error) {
result := []Container{}
for rows.Next() {
var c Container
var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured int
var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured, restoreOnHostBoot int
var firewallDefaultAction string
var firewallRulesJSON, allowedImageIDs sql.NullString
var lanIPv4Mode, lanInterface sql.NullString
@@ -988,7 +990,7 @@ func loadContainers() ([]Container, error) {
&c.MonthlyTrafficGB, &c.TrafficMode, &c.TrafficInGB,
&c.TrafficOutGB, &c.TrafficUsedRX, &c.TrafficUsedTX, &c.TrafficResetDate,
&c.IOSpeedMBps, &c.IOReadMBps, &c.IOWriteMBps,
&c.Status, &c.IP, &lanIPv4Mode, &lanInterface, &lanIPv4Address, &lanIPv4PrefixLen, &lanIPv4Gateway,
&c.Status, &restoreOnHostBoot, &c.IP, &lanIPv4Mode, &lanInterface, &lanIPv4Address, &lanIPv4PrefixLen, &lanIPv4Gateway,
&c.IPv6, &c.IPv6PrefixLen, &c.IPv6Interface, &c.VNCPort, &c.SSHPort, &c.SSHPassword,
&c.SSHHostKey, &c.PortMappingLimit, &c.SnapshotLimit, &c.CreatedAt, &c.ExpiresAt,
&scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime,
@@ -1006,6 +1008,7 @@ func loadContainers() ([]Container, error) {
}
c.LANIPv4Gateway = lanIPv4Gateway.String
c.SnapshotScheduleEnabled = scheduleEnabled != 0
c.RestoreOnHostBoot = restoreOnHostBoot != 0
c.PolicyBlocked = policyBlocked != 0
c.FirewallEnabled = firewallEnabled != 0
c.FirewallDefaultAction = normalizeFirewallDefaultAction(firewallDefaultAction)
+4 -4
View File
@@ -649,7 +649,7 @@ func (m *Manager) StartContainer(id int) error {
if !isWindows && c.IP != "" {
m.waitForCloudInitReady(name, c.IP, c.SSHPassword)
}
config.UpdateContainerStatus(id, "running")
config.UpdateContainerStatusAndRestore(id, "running", true)
if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
if err := m.applyIPv6Runtime(c); err != nil {
return err
@@ -728,13 +728,13 @@ func (m *Manager) StopContainer(id int) error {
name := c.VirshName()
status, _ := m.GetContainerStatus(name)
if status != "running" {
config.UpdateContainerStatus(id, "stopped")
config.UpdateContainerStatusAndRestore(id, "stopped", false)
return nil
}
exec.Command("virsh", "shutdown", name).Run()
for i := 0; i < 20; i++ {
if status, _ := m.GetContainerStatus(name); status != "running" {
config.UpdateContainerStatus(id, "stopped")
config.UpdateContainerStatusAndRestore(id, "stopped", false)
return nil
}
time.Sleep(1 * time.Second)
@@ -743,7 +743,7 @@ func (m *Manager) StopContainer(id int) error {
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("virsh destroy failed: %v, output: %s", err, string(output))
}
config.UpdateContainerStatus(id, "stopped")
config.UpdateContainerStatusAndRestore(id, "stopped", false)
return nil
}
+4 -4
View File
@@ -1730,7 +1730,7 @@ func (m *Manager) StartContainer(id int) error {
return err
}
config.UpdateContainerStatus(id, "running")
config.UpdateContainerStatusAndRestore(id, "running", true)
var ip string
for retry := 0; retry < 10; retry++ {
@@ -1951,7 +1951,7 @@ func (m *Manager) StopContainer(id int) error {
status, _ := m.GetContainerStatus(lxcName)
if status != "running" {
config.UpdateContainerStatus(id, "stopped")
config.UpdateContainerStatusAndRestore(id, "stopped", false)
m.CleanPortMappings(id)
CleanFirewallRules(id)
m.cleanupBandwidthLimit(lxcName)
@@ -1966,13 +1966,13 @@ func (m *Manager) StopContainer(id int) error {
output, err := cmd.CombinedOutput()
if err != nil {
if strings.Contains(string(output), "not running") {
config.UpdateContainerStatus(id, "stopped")
config.UpdateContainerStatusAndRestore(id, "stopped", false)
return nil
}
return fmt.Errorf("failed to stop container: %v, output: %s", err, string(output))
}
config.UpdateContainerStatus(id, "stopped")
config.UpdateContainerStatusAndRestore(id, "stopped", false)
fmt.Printf("Container %d (%s) stopped\n", id, c.Name)
return nil
}
+19
View File
@@ -4,7 +4,10 @@ import (
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"syscall"
"clicd/internal/api"
"clicd/internal/cli"
@@ -16,6 +19,8 @@ import (
"golang.org/x/term"
)
var shutdownCaptureOnce sync.Once
func main() {
isTerminal := term.IsTerminal(int(os.Stdin.Fd()))
@@ -44,6 +49,8 @@ func main() {
_ = cfg
if isServerMode || (!isTerminal && !isCliMode) {
installShutdownStateCapture()
// Restore persisted state
api.RestoreTasks()
api.RestoreLoginLogs()
@@ -75,6 +82,7 @@ func main() {
// Clean up stale container configs (LXC dir was deleted but config remains)
config.CleanStaleContainers()
api.StartHostBootRestore()
lxc.EnsureAllRunningPortMappings()
// Pre-warm SSH for containers already running after host boot or service restart.
@@ -97,6 +105,17 @@ func main() {
}
}
func installShutdownStateCapture() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signals
fmt.Fprintf(os.Stderr, "Received %s, capturing workload restore state...\n", sig)
shutdownCaptureOnce.Do(api.CaptureRuntimeRestoreState)
os.Exit(0)
}()
}
func isWebPanelSystemdRunning() bool {
cmd := exec.Command("systemctl", "is-active", "clicd")
output, err := cmd.Output()