· 修复了一些已知问题

· 增加了局域网DHCP IP分配适配
· 完善了多盘兼容支持 #17
This commit is contained in:
MengMengCode
2026-07-18 19:44:32 +08:00
parent 3dabd93d2f
commit ebba97f1d6
34 changed files with 3309 additions and 417 deletions
+381
View File
@@ -5,9 +5,12 @@ import (
"encoding/hex"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/bcrypt"
@@ -109,6 +112,8 @@ type Container struct {
LXCName string `json:"lxc_name,omitempty"`
KVMName string `json:"kvm_name,omitempty"`
DiskImage string `json:"disk_image,omitempty"`
StoragePoolID string `json:"storage_pool_id,omitempty"`
StoragePath string `json:"storage_path,omitempty"`
MACAddress string `json:"mac_address,omitempty"`
Template string `json:"template"`
VCPU float64 `json:"vcpu"`
@@ -202,6 +207,308 @@ func (c *Container) UsesLANIPv4() bool {
return c.UsesLANDHCP() || c.UsesLANStaticIPv4()
}
func normalizeStoragePools() bool {
if AppConfig == nil {
return false
}
changed := false
result := make([]StoragePool, 0, len(AppConfig.StoragePools))
seen := map[string]bool{}
defaultSeen := map[string]bool{}
for _, pool := range AppConfig.StoragePools {
pool.ID = strings.TrimSpace(pool.ID)
pool.Name = strings.TrimSpace(pool.Name)
pool.Path = filepath.Clean(strings.TrimSpace(pool.Path))
pool.MountPoint = filepath.Clean(strings.TrimSpace(pool.MountPoint))
if pool.MountPoint == "." {
pool.MountPoint = ""
}
if pool.ID == "" {
pool.ID = storagePoolIDFromName(pool.Name, pool.Path)
changed = true
}
if pool.Name == "" {
pool.Name = pool.ID
changed = true
}
if pool.Path == "." || !filepath.IsAbs(pool.Path) || seen[pool.ID] {
changed = true
continue
}
seen[pool.ID] = true
pool.ContentTypes = normalizeStorageContentTypes(pool.ContentTypes)
pool.DefaultContents = normalizeStorageContentTypes(pool.DefaultContents)
allowed := map[string]bool{}
for _, content := range pool.ContentTypes {
allowed[content] = true
}
defaults := make([]string, 0, len(pool.DefaultContents))
for _, content := range pool.DefaultContents {
if !allowed[content] || defaultSeen[content] {
changed = true
continue
}
defaultSeen[content] = true
defaults = append(defaults, content)
}
pool.DefaultContents = defaults
if pool.ContentTypes == nil {
pool.ContentTypes = []string{}
}
result = append(result, pool)
}
if len(result) != len(AppConfig.StoragePools) {
changed = true
}
AppConfig.StoragePools = result
return changed
}
func storagePoolIDFromName(name, path string) string {
base := strings.ToLower(strings.TrimSpace(name))
if base == "" {
base = filepath.Base(filepath.Clean(path))
}
replacer := strings.NewReplacer(" ", "-", "_", "-", ".", "-", "/", "-")
base = replacer.Replace(base)
base = strings.Trim(base, "-")
if base == "" {
base = "storage"
}
return base
}
func normalizeStorageContentTypes(values []string) []string {
if len(values) == 0 {
return nil
}
valid := map[string]bool{
StorageContentLXC: true,
StorageContentKVM: true,
StorageContentImages: true,
StorageContentSnapshots: true,
StorageContentBackups: true,
}
seen := map[string]bool{}
result := []string{}
for _, value := range values {
next := strings.ToLower(strings.TrimSpace(value))
if !valid[next] || seen[next] {
continue
}
seen[next] = true
result = append(result, next)
}
return result
}
func StoragePoolsForContent(content string) []StoragePool {
if AppConfig == nil {
return nil
}
content = strings.ToLower(strings.TrimSpace(content))
result := []StoragePool{}
for _, pool := range AppConfig.StoragePools {
if !pool.Enabled || !storagePoolAllows(pool, content) {
continue
}
result = append(result, pool)
}
return result
}
func StoragePoolByID(id string) *StoragePool {
if AppConfig == nil {
return nil
}
id = strings.TrimSpace(id)
for i := range AppConfig.StoragePools {
if AppConfig.StoragePools[i].ID == id {
return &AppConfig.StoragePools[i]
}
}
return nil
}
func StoragePoolAllowsContent(pool StoragePool, content string) bool {
return storagePoolAllows(pool, strings.ToLower(strings.TrimSpace(content)))
}
func StoragePathForContent(content, fallback string) string {
if pool := DefaultStoragePoolForContent(content); pool != nil {
return pool.Path
}
return fallback
}
// PreferredStoragePoolForContent returns the configured default without doing
// filesystem probes. Use SelectStoragePoolForContent for new writes.
func PreferredStoragePoolForContent(content string) *StoragePool {
if AppConfig == nil {
return nil
}
content = strings.ToLower(strings.TrimSpace(content))
for i := range AppConfig.StoragePools {
pool := &AppConfig.StoragePools[i]
if !pool.Enabled || !storagePoolAllows(*pool, content) {
continue
}
for _, item := range pool.DefaultContents {
if item == content {
return pool
}
}
}
for i := range AppConfig.StoragePools {
pool := &AppConfig.StoragePools[i]
if pool.Enabled && storagePoolAllows(*pool, content) {
return pool
}
}
return nil
}
func DefaultStoragePoolForContent(content string) *StoragePool {
pool, _ := SelectStoragePoolForContent(content, "", 0)
return pool
}
const storagePoolFreeReserveBytes int64 = 256 * 1024 * 1024
type storagePoolCandidate struct {
pool *StoragePool
freeBytes int64
isDefault bool
}
// SelectStoragePoolForContent picks a writable mounted pool. The requested or
// configured default pool is preferred while it has enough space; remaining
// pools are tried by available space from largest to smallest.
func SelectStoragePoolForContent(content, requestedPoolID string, requiredBytes int64) (*StoragePool, error) {
if AppConfig == nil {
return nil, fmt.Errorf("storage configuration is not loaded")
}
content = strings.ToLower(strings.TrimSpace(content))
requestedPoolID = strings.TrimSpace(requestedPoolID)
if requiredBytes < 0 {
requiredBytes = 0
}
requiredFree := requiredBytes + storagePoolFreeReserveBytes
candidates := make([]storagePoolCandidate, 0, len(AppConfig.StoragePools))
configured := 0
for i := range AppConfig.StoragePools {
pool := &AppConfig.StoragePools[i]
if !pool.Enabled || !storagePoolAllows(*pool, content) {
continue
}
configured++
freeBytes, available := probeStoragePoolFreeBytes(*pool)
if !available {
continue
}
candidate := storagePoolCandidate{pool: pool, freeBytes: freeBytes}
for _, item := range pool.DefaultContents {
if item == content {
candidate.isDefault = true
break
}
}
candidates = append(candidates, candidate)
}
if configured == 0 {
return nil, fmt.Errorf("no storage disk is enabled for %s", storageContentLabel(content))
}
if len(candidates) == 0 {
return nil, fmt.Errorf("all storage disks enabled for %s are unavailable or unmounted", storageContentLabel(content))
}
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].freeBytes > candidates[j].freeBytes
})
preferred := func(match func(storagePoolCandidate) bool) *StoragePool {
for _, candidate := range candidates {
if match(candidate) && candidate.freeBytes >= requiredFree {
return candidate.pool
}
}
return nil
}
if requestedPoolID != "" {
if pool := preferred(func(candidate storagePoolCandidate) bool { return candidate.pool.ID == requestedPoolID }); pool != nil {
return pool, nil
}
}
if pool := preferred(func(candidate storagePoolCandidate) bool { return candidate.isDefault }); pool != nil {
return pool, nil
}
if pool := preferred(func(storagePoolCandidate) bool { return true }); pool != nil {
return pool, nil
}
return nil, fmt.Errorf("storage disks enabled for %s do not have enough free space", storageContentLabel(content))
}
var probeStoragePoolFreeBytes = storagePoolFreeBytes
func storagePoolFreeBytes(pool StoragePool) (int64, bool) {
if strings.TrimSpace(pool.Path) == "" {
return 0, false
}
if _, err := os.Stat(pool.Path); err != nil {
if !os.IsNotExist(err) || filepath.Clean(pool.MountPoint) != string(os.PathSeparator) {
return 0, false
}
if err := os.MkdirAll(pool.Path, 0755); err != nil {
return 0, false
}
}
if mountPoint := strings.TrimSpace(pool.MountPoint); mountPoint != "" {
out, err := exec.Command("findmnt", "-n", "-o", "TARGET", "--target", pool.Path).Output()
if err != nil || filepath.Clean(strings.TrimSpace(string(out))) != filepath.Clean(mountPoint) {
return 0, false
}
}
out, err := exec.Command("df", "-B1", "-P", pool.Path).Output()
if err != nil {
return 0, false
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) < 2 {
return 0, false
}
fields := strings.Fields(lines[len(lines)-1])
if len(fields) < 4 {
return 0, false
}
freeBytes, err := strconv.ParseInt(fields[3], 10, 64)
return freeBytes, err == nil
}
func storageContentLabel(content string) string {
switch content {
case StorageContentLXC:
return "LXC containers"
case StorageContentKVM:
return "KVM disks"
case StorageContentImages:
return "image cache"
case StorageContentSnapshots:
return "snapshots"
case StorageContentBackups:
return "backups"
default:
return content
}
}
func storagePoolAllows(pool StoragePool, content string) bool {
for _, item := range pool.ContentTypes {
if item == content {
return true
}
}
return false
}
func (c *Container) NormalizeNetworkAssignments() bool {
changed := false
lanMode := strings.ToLower(strings.TrimSpace(c.LANIPv4Mode))
@@ -422,6 +729,43 @@ type SSLConfig struct {
LastError string `json:"last_error,omitempty"`
}
const (
StorageContentLXC = "lxc"
StorageContentKVM = "kvm"
StorageContentImages = "images"
StorageContentSnapshots = "snapshots"
StorageContentBackups = "backups"
)
type StoragePool struct {
ID string `json:"id"`
Name string `json:"name"`
Path string `json:"path"`
MountPoint string `json:"mount_point,omitempty"`
ContentTypes []string `json:"content_types"`
DefaultContents []string `json:"default_contents,omitempty"`
Enabled bool `json:"enabled"`
}
func defaultPrimaryStoragePool() StoragePool {
contents := []string{
StorageContentLXC,
StorageContentKVM,
StorageContentImages,
StorageContentSnapshots,
StorageContentBackups,
}
return StoragePool{
ID: "disk-root",
Name: "system (/)",
Path: "/var/lib/clicd",
MountPoint: "/",
ContentTypes: append([]string(nil), contents...),
DefaultContents: append([]string(nil), contents...),
Enabled: true,
}
}
// ClicdConfig is the main configuration structure
type ClicdConfig struct {
AdminUser string `json:"admin_user"`
@@ -447,16 +791,24 @@ type ClicdConfig struct {
PublicIPv6Prefixes []PublicIPv6Prefix `json:"public_ipv6_prefixes"`
WebSSHAllowedOrigins []string `json:"webssh_allowed_origins"`
SecurityAutoShutdown bool `json:"security_auto_shutdown"`
TaskConcurrency int `json:"task_concurrency"`
Language string `json:"language"`
SSL SSLConfig `json:"ssl"`
SSLCertificates map[string]SSLConfig `json:"ssl_certificates"`
StoragePools []StoragePool `json:"storage_pools"`
}
var configPath string
var AppConfig *ClicdConfig
var allocationMu sync.Mutex
const DefaultSnapshotLimit = 3
const (
DefaultTaskConcurrency = 2
MaxTaskConcurrency = 16
)
const (
DefaultNATPortStart = 20000
DefaultNATPortEnd = 65535
@@ -588,6 +940,8 @@ func InitConfig() (*ClicdConfig, error) {
PublicIPv4Pool: []PublicIPv4Assignment{},
PublicIPv6Prefixes: []PublicIPv6Prefix{},
WebSSHAllowedOrigins: []string{},
TaskConcurrency: DefaultTaskConcurrency,
StoragePools: []StoragePool{defaultPrimaryStoragePool()},
}
if err := SaveConfig(); err != nil {
@@ -629,6 +983,10 @@ func normalizeConfigDefaults(dataDir string) bool {
AppConfig.NextContainerID = 1
changed = true
}
if normalized := NormalizeTaskConcurrency(AppConfig.TaskConcurrency); AppConfig.TaskConcurrency != normalized {
AppConfig.TaskConcurrency = normalized
changed = true
}
if AppConfig.DataDir == "" {
AppConfig.DataDir = dataDir
changed = true
@@ -656,6 +1014,13 @@ func normalizeConfigDefaults(dataDir string) bool {
AppConfig.WebSSHAllowedOrigins = normalized
changed = true
}
if len(AppConfig.StoragePools) == 0 {
AppConfig.StoragePools = []StoragePool{defaultPrimaryStoragePool()}
changed = true
}
if normalizeStoragePools() {
changed = true
}
if AppConfig.SubUsers == nil {
AppConfig.SubUsers = make([]SubUser, 0)
changed = true
@@ -701,6 +1066,16 @@ func normalizeConfigDefaults(dataDir string) bool {
return changed
}
func NormalizeTaskConcurrency(value int) int {
if value <= 0 {
return DefaultTaskConcurrency
}
if value > MaxTaskConcurrency {
return MaxTaskConcurrency
}
return value
}
func NormalizeLanguage(language string) string {
switch strings.ToLower(strings.TrimSpace(language)) {
case "en", "en-us", "en_us", "english":
@@ -1047,6 +1422,8 @@ func SaveConfig() error {
// AddContainer adds a container to the config
func AddContainer(c Container) {
allocationMu.Lock()
defer allocationMu.Unlock()
if c.UUID == "" {
c.UUID = NewContainerUUID()
}
@@ -1058,6 +1435,8 @@ func AddContainer(c Container) {
// AllocateContainerID allocates a new container ID
func AllocateContainerID() int {
allocationMu.Lock()
defer allocationMu.Unlock()
id := AppConfig.NextContainerID
AppConfig.NextContainerID++
SaveConfig()
@@ -1334,6 +1713,8 @@ func normalizeNATPortRangeDefaults() bool {
// AllocateSSHPort allocates a new SSH port, skipping ports already used by any container
func AllocateSSHPort() (int, error) {
allocationMu.Lock()
defer allocationMu.Unlock()
used := collectAllHostPorts()
start, end := NATPortRange()
port := AppConfig.NextSSHPort
+90
View File
@@ -0,0 +1,90 @@
package config
import "testing"
func TestSelectStoragePoolForContent(t *testing.T) {
previousConfig := AppConfig
previousProbe := probeStoragePoolFreeBytes
t.Cleanup(func() {
AppConfig = previousConfig
probeStoragePoolFreeBytes = previousProbe
})
AppConfig = &ClicdConfig{StoragePools: []StoragePool{
{
ID: "primary",
Path: "/primary",
ContentTypes: []string{StorageContentLXC},
DefaultContents: []string{StorageContentLXC},
Enabled: true,
},
{
ID: "large",
Path: "/large",
ContentTypes: []string{StorageContentLXC},
Enabled: true,
},
{
ID: "small",
Path: "/small",
ContentTypes: []string{StorageContentLXC},
Enabled: true,
},
}}
free := map[string]int64{
"primary": 20 * 1024 * 1024 * 1024,
"large": 50 * 1024 * 1024 * 1024,
"small": 10 * 1024 * 1024 * 1024,
}
probeStoragePoolFreeBytes = func(pool StoragePool) (int64, bool) {
value, ok := free[pool.ID]
return value, ok
}
pool, err := SelectStoragePoolForContent(StorageContentLXC, "", 5*1024*1024*1024)
if err != nil {
t.Fatal(err)
}
if pool.ID != "primary" {
t.Fatalf("selected %q, want configured default primary", pool.ID)
}
free["primary"] = 128 * 1024 * 1024
pool, err = SelectStoragePoolForContent(StorageContentLXC, "", 5*1024*1024*1024)
if err != nil {
t.Fatal(err)
}
if pool.ID != "large" {
t.Fatalf("selected %q, want largest fallback pool", pool.ID)
}
pool, err = SelectStoragePoolForContent(StorageContentLXC, "small", 5*1024*1024*1024)
if err != nil {
t.Fatal(err)
}
if pool.ID != "small" {
t.Fatalf("selected %q, want requested pool", pool.ID)
}
}
func TestSelectStoragePoolRequiresEnabledContent(t *testing.T) {
previousConfig := AppConfig
previousProbe := probeStoragePoolFreeBytes
t.Cleanup(func() {
AppConfig = previousConfig
probeStoragePoolFreeBytes = previousProbe
})
AppConfig = &ClicdConfig{StoragePools: []StoragePool{{
ID: "primary",
Path: "/primary",
ContentTypes: []string{StorageContentLXC},
Enabled: true,
}}}
probeStoragePoolFreeBytes = func(StoragePool) (int64, bool) { return 100 * 1024 * 1024 * 1024, true }
if _, err := SelectStoragePoolForContent(StorageContentSnapshots, "", 0); err == nil {
t.Fatal("expected snapshots selection to fail when no pool enables snapshots")
}
}
+25 -5
View File
@@ -23,6 +23,7 @@ type savedTaskConfig struct {
Name string `json:"name"`
Virtualization string `json:"virtualization,omitempty"`
TemplateID string `json:"template_id"`
StoragePoolID string `json:"storage_pool_id,omitempty"`
VCPU float64 `json:"vcpu"`
CPUPercent int `json:"cpu_percent"`
RAMMB int `json:"ram_mb"`
@@ -190,6 +191,8 @@ func ensureSchema() error {
lxc_name TEXT,
kvm_name TEXT,
disk_image TEXT,
storage_pool_id TEXT,
storage_path TEXT,
mac_address TEXT,
template TEXT,
vcpu REAL,
@@ -461,6 +464,8 @@ func ensureSchemaMigrations() error {
{"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", "storage_pool_id", "TEXT"},
{"containers", "storage_path", "TEXT"},
{"containers", "lan_ipv4_mode", "TEXT"},
{"containers", "lan_interface", "TEXT"},
{"containers", "lan_ipv4_address", "TEXT NOT NULL DEFAULT ''"},
@@ -515,6 +520,11 @@ func ensureSchemaMigrations() error {
lan_ipv4_gateway = COALESCE(lan_ipv4_gateway, '')`); err != nil {
return err
}
if _, err := db.Exec(`UPDATE containers
SET storage_pool_id = COALESCE(storage_pool_id, ''),
storage_path = COALESCE(storage_path, '')`); err != nil {
return err
}
if _, err := db.Exec(`UPDATE tasks
SET cfg_lan_ipv4_mode = COALESCE(cfg_lan_ipv4_mode, ''),
cfg_lan_interface = COALESCE(cfg_lan_interface, ''),
@@ -585,6 +595,7 @@ func loadConfigFromDB() (*ClicdConfig, bool, error) {
NATPortEnd: atoi(meta["nat_port_end"]),
SetupComplete: atob(meta["setup_complete"]),
SecurityAutoShutdown: atob(meta["security_auto_shutdown"]),
TaskConcurrency: atoi(meta["task_concurrency"]),
Language: meta["language"],
}
if raw := strings.TrimSpace(meta["ssl"]); raw != "" {
@@ -602,6 +613,9 @@ func loadConfigFromDB() (*ClicdConfig, bool, error) {
if raw := strings.TrimSpace(meta["webssh_allowed_origins"]); raw != "" {
_ = json.Unmarshal([]byte(raw), &cfg.WebSSHAllowedOrigins)
}
if raw := strings.TrimSpace(meta["storage_pools"]); raw != "" {
_ = json.Unmarshal([]byte(raw), &cfg.StoragePools)
}
if cfg.Containers, err = loadContainers(); err != nil {
return nil, false, err
@@ -701,6 +715,7 @@ func saveMeta(tx *sql.Tx) error {
publicIPv4PoolJSON, _ := json.Marshal(AppConfig.PublicIPv4Pool)
publicIPv6PrefixesJSON, _ := json.Marshal(AppConfig.PublicIPv6Prefixes)
webSSHAllowedOriginsJSON, _ := json.Marshal(AppConfig.WebSSHAllowedOrigins)
storagePoolsJSON, _ := json.Marshal(AppConfig.StoragePools)
values := map[string]string{
"admin_user": AppConfig.AdminUser,
"admin_pass_hash": AppConfig.AdminPassHash,
@@ -714,12 +729,14 @@ func saveMeta(tx *sql.Tx) error {
"nat_port_end": strconv.Itoa(AppConfig.NATPortEnd),
"setup_complete": btoa(AppConfig.SetupComplete),
"security_auto_shutdown": btoa(AppConfig.SecurityAutoShutdown),
"task_concurrency": strconv.Itoa(AppConfig.TaskConcurrency),
"language": NormalizeLanguage(AppConfig.Language),
"ssl": string(sslJSON),
"ssl_certificates": string(sslCertificatesJSON),
"public_ipv4_pool": string(publicIPv4PoolJSON),
"public_ipv6_prefixes": string(publicIPv6PrefixesJSON),
"webssh_allowed_origins": string(webSSHAllowedOriginsJSON),
"storage_pools": string(storagePoolsJSON),
"schema_version": "1",
"updated_at": time.Now().Format("2006-01-02 15:04:05"),
}
@@ -736,7 +753,7 @@ func saveContainers(tx *sql.Tx) error {
NormalizeContainerResourceAliases(&c)
allowedImageIDs := encodeStringSlice(c.AllowedImageIDs)
if _, err := tx.Exec(`INSERT INTO containers (
id, uuid, name, virtualization, lxc_name, kvm_name, disk_image, mac_address, template,
id, uuid, name, virtualization, lxc_name, kvm_name, disk_image, storage_pool_id, storage_path, mac_address, template,
vcpu, ram_mb, disk_gb, network_bw_mbps, network_down_mbps, network_up_mbps,
monthly_traffic_gb, traffic_mode, traffic_in_gb,
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
@@ -748,8 +765,8 @@ func saveContainers(tx *sql.Tx) error {
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.MACAddress, c.Template,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.StoragePoolID, c.StoragePath, 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,
@@ -957,7 +974,7 @@ func saveSnapshots(tx *sql.Tx) error {
func loadContainers() ([]Container, error) {
rows, err := db.Query(`SELECT
id, uuid, name, virtualization, lxc_name, kvm_name, disk_image, mac_address, template,
id, uuid, name, virtualization, lxc_name, kvm_name, disk_image, storage_pool_id, storage_path, mac_address, template,
vcpu, ram_mb, disk_gb, network_bw_mbps, network_down_mbps, network_up_mbps,
monthly_traffic_gb, traffic_mode, traffic_in_gb,
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
@@ -981,11 +998,12 @@ func loadContainers() ([]Container, error) {
var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured, restoreOnHostBoot int
var firewallDefaultAction string
var firewallRulesJSON, allowedImageIDs sql.NullString
var storagePoolID, storagePath sql.NullString
var lanIPv4Mode, lanInterface sql.NullString
var lanIPv4Address, lanIPv4Gateway sql.NullString
var lanIPv4PrefixLen sql.NullInt64
if err := rows.Scan(
&c.ID, &c.UUID, &c.Name, &c.Virtualization, &c.LXCName, &c.KVMName, &c.DiskImage, &c.MACAddress, &c.Template,
&c.ID, &c.UUID, &c.Name, &c.Virtualization, &c.LXCName, &c.KVMName, &c.DiskImage, &storagePoolID, &storagePath, &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,
@@ -1000,6 +1018,8 @@ func loadContainers() ([]Container, error) {
); err != nil {
return nil, err
}
c.StoragePoolID = storagePoolID.String
c.StoragePath = storagePath.String
c.LANIPv4Mode = lanIPv4Mode.String
c.LANInterface = lanInterface.String
c.LANIPv4Address = lanIPv4Address.String
@@ -93,11 +93,15 @@ func TestSQLiteConfigMigratesLegacyJSONAndPersists(t *testing.T) {
if len(cfg.Tasks) != 1 || !strings.Contains(cfg.Tasks[0].Config, `"extra_ports":[80,443]`) {
t.Fatalf("task config was not restored from sqlite columns: %+v", cfg.Tasks)
}
if cfg.TaskConcurrency != DefaultTaskConcurrency {
t.Fatalf("legacy task concurrency = %d, want default %d", cfg.TaskConcurrency, DefaultTaskConcurrency)
}
if _, err := os.Stat(filepath.Join(dir, "config.db")); err != nil {
t.Fatalf("sqlite database was not created: %v", err)
}
cfg.Containers[0].Status = "stopped"
cfg.TaskConcurrency = 6
if err := SaveConfig(); err != nil {
t.Fatal(err)
}
@@ -111,6 +115,9 @@ func TestSQLiteConfigMigratesLegacyJSONAndPersists(t *testing.T) {
if got := cfg.Containers[0].Status; got != "stopped" {
t.Fatalf("expected sqlite value to win after migration, got %q", got)
}
if got := cfg.TaskConcurrency; got != 6 {
t.Fatalf("persisted task concurrency = %d, want 6", got)
}
}
func resetConfigStoreForTest(t *testing.T) {