Files
CLICD/backend/internal/config/config.go
T
2026-06-07 21:08:51 +08:00

919 lines
25 KiB
Go

package config
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
)
// PortMapping represents a port mapping rule
type PortMapping struct {
ContainerPort int `json:"container_port"`
HostPort int `json:"host_port"`
Protocol string `json:"protocol"`
Description string `json:"description"`
}
// SavedTask for persisting task queue across restarts
type SavedTask struct {
ID string `json:"id"`
Type string `json:"type"`
ContainerID int `json:"container_id"`
ContainerName string `json:"container_name"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
CreatedAt string `json:"created_at"`
TemplateID string `json:"template_id,omitempty"`
Config string `json:"config,omitempty"`
User string `json:"user,omitempty"`
}
// SavedLoginLog for persisting login logs
type SavedLoginLog struct {
Time string `json:"time"`
Username string `json:"username"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Success bool `json:"success"`
}
// AuditLog represents an operation log entry
type AuditLog struct {
Time string `json:"time"`
Action string `json:"action"`
Target string `json:"target"`
Detail string `json:"detail"`
User string `json:"user"`
IP string `json:"ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
Success *bool `json:"success,omitempty"`
Error string `json:"error,omitempty"`
}
type VMReadinessCheck struct {
Key string `json:"key"`
Label string `json:"label"`
OK bool `json:"ok"`
Detail string `json:"detail,omitempty"`
}
// Container represents an LXC container configuration
type Container struct {
ID int `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Virtualization string `json:"virtualization,omitempty"`
LXCName string `json:"lxc_name,omitempty"`
KVMName string `json:"kvm_name,omitempty"`
DiskImage string `json:"disk_image,omitempty"`
MACAddress string `json:"mac_address,omitempty"`
Template string `json:"template"`
VCPU float64 `json:"vcpu"`
RAMMB int `json:"ram_mb"`
DiskGB int `json:"disk_gb"`
NetworkBWMbps int `json:"network_bw_mbps"`
MonthlyTrafficGB int `json:"monthly_traffic_gb"`
TrafficMode string `json:"traffic_mode"` // "total" or "in_out"
TrafficInGB int `json:"traffic_in_gb"` // 0 = unlimited
TrafficOutGB int `json:"traffic_out_gb"` // 0 = unlimited
TrafficUsedRX int64 `json:"traffic_used_rx"`
TrafficUsedTX int64 `json:"traffic_used_tx"`
TrafficResetDate string `json:"traffic_reset_date"`
IOSpeedMBps int `json:"io_speed_mbps"`
Status string `json:"status"`
IP string `json:"ip"`
IPv6 string `json:"ipv6"`
IPv6PrefixLen int `json:"ipv6_prefix_len"`
IPv6Interface string `json:"ipv6_interface"`
VNCPort int `json:"vnc_port"`
SSHPort int `json:"ssh_port"`
SSHPassword string `json:"ssh_password"`
SSHHostKey string `json:"ssh_host_key,omitempty"`
PortMappings []PortMapping `json:"port_mappings"`
PortMappingLimit int `json:"port_mapping_limit"`
SnapshotLimit int `json:"snapshot_limit"`
CreatedAt string `json:"created_at"`
ExpiresAt string `json:"expires_at"`
SnapshotScheduleEnabled bool `json:"snapshot_schedule_enabled"`
SnapshotScheduleIntervalHours int `json:"snapshot_schedule_interval_hours"`
SnapshotScheduleTime string `json:"snapshot_schedule_time"`
SnapshotScheduleLastRun string `json:"snapshot_schedule_last_run"`
SnapshotScheduleNextRun string `json:"snapshot_schedule_next_run"`
SnapshotScheduleCreatedBy string `json:"snapshot_schedule_created_by"`
PolicyBlocked bool `json:"policy_blocked"`
PolicyBlockedReason string `json:"policy_blocked_reason,omitempty"`
PolicyBlockedAt string `json:"policy_blocked_at,omitempty"`
}
const (
VirtualizationLXC = "lxc"
VirtualizationKVM = "kvm"
)
func NormalizeVirtualization(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case VirtualizationKVM:
return VirtualizationKVM
default:
return VirtualizationLXC
}
}
func (c *Container) Runtime() string {
return NormalizeVirtualization(c.Virtualization)
}
func (c *Container) IsKVM() bool {
return c.Runtime() == VirtualizationKVM
}
// LxcName returns the internal LXC container name (ct-{id})
func (c *Container) LxcName() string {
if c.LXCName != "" {
return c.LXCName
}
return fmt.Sprintf("ct-%d", c.ID)
}
// VirshName returns the internal libvirt domain name for KVM instances.
func (c *Container) VirshName() string {
if c.KVMName != "" {
return c.KVMName
}
return fmt.Sprintf("vm-%d", c.ID)
}
// SubUser represents a sub-user with access to specific containers
type ApiKeyConfig struct {
ID string `json:"id"`
Name string `json:"name"`
KeyHash string `json:"key_hash"`
Prefix string `json:"prefix"`
IPWhitelist string `json:"ip_whitelist"`
CreatedAt string `json:"created_at"`
LastUsed string `json:"last_used"`
}
// DeleteApiKey removes an API key by ID
func DeleteApiKey(id string) {
filtered := make([]ApiKeyConfig, 0, len(AppConfig.ApiKeys))
for _, k := range AppConfig.ApiKeys {
if k.ID != id {
filtered = append(filtered, k)
}
}
AppConfig.ApiKeys = filtered
SaveConfig()
}
type SubUser struct {
ID string `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
PassHash string `json:"pass_hash"`
ContainerNames []string `json:"container_names"`
ContainerUUIDs []string `json:"container_uuids,omitempty"`
Token string `json:"-"`
AccessCode string `json:"access_code"`
CreatedAt string `json:"created_at"`
TokenVersion int `json:"token_version"`
}
type Snapshot struct {
ID string `json:"id"`
ContainerID int `json:"container_id"`
ContainerName string `json:"container_name"`
LXCName string `json:"lxc_name"`
CreatedAt string `json:"created_at"`
CreatedBy string `json:"created_by"`
Scheduled bool `json:"scheduled"`
Path string `json:"path"`
SizeBytes int64 `json:"size_bytes"`
}
// ClicdConfig is the main configuration structure
type ClicdConfig struct {
AdminUser string `json:"admin_user"`
AdminPassHash string `json:"admin_pass_hash"`
JWTSecret string `json:"jwt_secret"`
Port int `json:"port"`
DataDir string `json:"data_dir"`
Containers []Container `json:"containers"`
NextContainerID int `json:"next_container_id"`
NextVNCPort int `json:"next_vnc_port"`
NextSSHPort int `json:"next_ssh_port"`
SetupComplete bool `json:"setup_complete"`
SubUsers []SubUser `json:"sub_users"`
ApiKeys []ApiKeyConfig `json:"api_keys"`
AuditLogs []AuditLog `json:"audit_logs"`
Tasks []SavedTask `json:"tasks"`
LoginLogs []SavedLoginLog `json:"login_logs"`
EnabledImages []string `json:"enabled_images"`
Snapshots []Snapshot `json:"snapshots"`
SecurityAutoShutdown bool `json:"security_auto_shutdown"`
}
var configPath string
var AppConfig *ClicdConfig
const DefaultSnapshotLimit = 3
func getConfigPath() string {
if configPath != "" {
return configPath
}
home, err := os.UserHomeDir()
if err != nil {
home = "/root"
}
return filepath.Join(home, ".clicd", "config.json")
}
func SetConfigPath(path string) {
configPath = path
}
func getDataDir() string {
home, err := os.UserHomeDir()
if err != nil {
home = "/root"
}
return filepath.Join(home, ".clicd")
}
func generateRandomString(length int) string {
b := make([]byte, length)
rand.Read(b)
return hex.EncodeToString(b)[:length]
}
func generateUUIDString() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return generateRandomString(32)
}
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
// NewContainerUUID returns a UUID that is unique within the current config.
func NewContainerUUID() string {
for {
uuid := generateUUIDString()
if FindContainerByUUID(uuid) == nil {
return uuid
}
}
}
// InitConfig initializes or loads the configuration
func InitConfig() (*ClicdConfig, error) {
cfgPath := getConfigPath()
dataDir := getDataDir()
if err := os.MkdirAll(filepath.Dir(cfgPath), 0700); err != nil {
return nil, fmt.Errorf("failed to create data directory: %v", err)
}
if err := os.MkdirAll(dataDir, 0700); err != nil {
return nil, fmt.Errorf("failed to create data directory: %v", err)
}
if err := openConfigDB(); err != nil {
return nil, err
}
cfg, ok, err := loadConfigFromDB()
if err != nil {
return nil, err
}
if ok {
AppConfig = cfg
normalizeConfigDefaults(dataDir)
if migrateLoadedConfig() {
if err := SaveConfig(); err != nil {
return nil, err
}
}
return AppConfig, nil
}
legacy, ok, err := loadLegacyJSONConfig(cfgPath)
if err != nil {
return nil, err
}
if ok {
AppConfig = legacy
normalizeConfigDefaults(dataDir)
if migrateLoadedConfig() {
// Save below persists normalized legacy data into SQLite.
}
if err := SaveConfig(); err != nil {
return nil, err
}
return AppConfig, nil
}
adminUser := "admin"
adminPass := generateRandomString(16)
jwtSecret := generateRandomString(32)
hash, err := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %v", err)
}
AppConfig = &ClicdConfig{
AdminUser: adminUser,
AdminPassHash: string(hash),
JWTSecret: jwtSecret,
Port: 8999,
DataDir: dataDir,
Containers: []Container{},
NextContainerID: 1,
NextVNCPort: 5900,
NextSSHPort: 22000,
SetupComplete: false,
SubUsers: []SubUser{},
AuditLogs: []AuditLog{},
Tasks: []SavedTask{},
LoginLogs: []SavedLoginLog{},
Snapshots: []Snapshot{},
}
if err := SaveConfig(); err != nil {
return nil, err
}
fmt.Println("\n========================================")
fmt.Println(" CLICD - LXC Container Manager")
fmt.Println("========================================")
fmt.Printf(" Username: %s\n", adminUser)
fmt.Printf(" Password: %s\n", adminPass)
fmt.Println("========================================")
fmt.Println(" Please save these credentials!")
fmt.Println(" Web Interface: http://0.0.0.0:8999")
fmt.Println("========================================")
fmt.Println()
return AppConfig, nil
}
func normalizeConfigDefaults(dataDir string) {
if AppConfig.Port == 0 {
AppConfig.Port = 8999
}
if AppConfig.NextVNCPort == 0 {
AppConfig.NextVNCPort = 5900
}
if AppConfig.NextSSHPort == 0 {
AppConfig.NextSSHPort = 22000
}
if AppConfig.NextContainerID == 0 {
AppConfig.NextContainerID = 1
}
if AppConfig.DataDir == "" {
AppConfig.DataDir = dataDir
}
if AppConfig.Containers == nil {
AppConfig.Containers = make([]Container, 0)
}
if AppConfig.Snapshots == nil {
AppConfig.Snapshots = make([]Snapshot, 0)
}
if AppConfig.SubUsers == nil {
AppConfig.SubUsers = make([]SubUser, 0)
}
if AppConfig.ApiKeys == nil {
AppConfig.ApiKeys = make([]ApiKeyConfig, 0)
}
if AppConfig.AuditLogs == nil {
AppConfig.AuditLogs = make([]AuditLog, 0)
}
if AppConfig.Tasks == nil {
AppConfig.Tasks = make([]SavedTask, 0)
}
if AppConfig.LoginLogs == nil {
AppConfig.LoginLogs = make([]SavedLoginLog, 0)
}
if AppConfig.EnabledImages == nil {
AppConfig.EnabledImages = make([]string, 0)
}
}
func migrateLoadedConfig() bool {
changed := ensureContainerUUIDs()
if ensureContainerVirtualization() {
changed = true
}
if ensureContainerPortMappingLimits() {
changed = true
}
if ensureContainerSnapshotLimits() {
changed = true
}
if ensureContainerSnapshotScheduleDefaults() {
changed = true
}
if migrateSubUsers() {
changed = true
}
if removeLegacyVNCMappings() {
changed = true
}
return changed
}
func ensureContainerVirtualization() bool {
changed := false
for i := range AppConfig.Containers {
next := NormalizeVirtualization(AppConfig.Containers[i].Virtualization)
if AppConfig.Containers[i].Virtualization != next {
AppConfig.Containers[i].Virtualization = next
changed = true
}
}
return changed
}
func ensureContainerSnapshotScheduleDefaults() bool {
changed := false
for i := range AppConfig.Containers {
if AppConfig.Containers[i].SnapshotScheduleEnabled && AppConfig.Containers[i].SnapshotScheduleIntervalHours < 24 {
AppConfig.Containers[i].SnapshotScheduleIntervalHours = 24
changed = true
}
if AppConfig.Containers[i].SnapshotScheduleEnabled && AppConfig.Containers[i].SnapshotScheduleTime == "" {
AppConfig.Containers[i].SnapshotScheduleTime = "03:00"
changed = true
}
}
return changed
}
func ensureContainerUUIDs() bool {
changed := false
used := make(map[string]bool)
for i := range AppConfig.Containers {
uuid := AppConfig.Containers[i].UUID
if uuid == "" || used[uuid] {
for {
uuid = generateUUIDString()
if !used[uuid] {
break
}
}
AppConfig.Containers[i].UUID = uuid
changed = true
}
used[uuid] = true
}
return changed
}
func ensureContainerPortMappingLimits() bool {
changed := false
for i := range AppConfig.Containers {
if AppConfig.Containers[i].PortMappingLimit <= 0 {
limit := len(AppConfig.Containers[i].PortMappings)
if limit < 2 {
limit = 2
}
AppConfig.Containers[i].PortMappingLimit = limit
changed = true
}
}
return changed
}
func ensureContainerSnapshotLimits() bool {
changed := false
for i := range AppConfig.Containers {
if AppConfig.Containers[i].SnapshotLimit <= 0 {
AppConfig.Containers[i].SnapshotLimit = DefaultSnapshotLimit
changed = true
}
}
return changed
}
func migrateSubUsers() bool {
changed := false
for i := range AppConfig.SubUsers {
su := &AppConfig.SubUsers[i]
if su.PassHash == "" && su.Password != "" {
if hash, err := bcrypt.GenerateFromPassword([]byte(su.Password), bcrypt.DefaultCost); err == nil {
su.PassHash = string(hash)
changed = true
}
}
if su.Token != "" {
su.Token = ""
changed = true
}
if len(su.ContainerUUIDs) == 0 && len(su.ContainerNames) > 0 {
for _, name := range su.ContainerNames {
if c := FindContainerByName(name); c != nil && c.UUID != "" {
su.ContainerUUIDs = appendUniqueString(su.ContainerUUIDs, c.UUID)
}
}
if len(su.ContainerUUIDs) > 0 {
changed = true
}
}
}
return changed
}
func appendUniqueString(values []string, value string) []string {
for _, existing := range values {
if existing == value {
return values
}
}
return append(values, value)
}
func NormalizeSnapshotLimit(limit int) int {
if limit <= 0 {
return DefaultSnapshotLimit
}
return limit
}
func ContainerSnapshotLimit(c *Container) int {
if c == nil {
return DefaultSnapshotLimit
}
return NormalizeSnapshotLimit(c.SnapshotLimit)
}
func removeLegacyVNCMappings() bool {
changed := false
for i := range AppConfig.Containers {
mappings := AppConfig.Containers[i].PortMappings
if len(mappings) == 0 {
continue
}
filtered := mappings[:0]
for _, pm := range mappings {
isLegacyVNC := strings.EqualFold(pm.Description, "VNC") || pm.ContainerPort == 5901
if isLegacyVNC {
changed = true
continue
}
filtered = append(filtered, pm)
}
AppConfig.Containers[i].PortMappings = filtered
}
return changed
}
// SaveConfig saves configuration to disk
func SaveConfig() error {
return saveConfigToDB()
}
// AddContainer adds a container to the config
func AddContainer(c Container) {
if c.UUID == "" {
c.UUID = NewContainerUUID()
}
c.Virtualization = NormalizeVirtualization(c.Virtualization)
AppConfig.Containers = append(AppConfig.Containers, c)
SaveConfig()
}
// AllocateContainerID allocates a new container ID
func AllocateContainerID() int {
id := AppConfig.NextContainerID
AppConfig.NextContainerID++
SaveConfig()
return id
}
// RemoveContainer removes a container from config by ID
func RemoveContainer(id int) bool {
for i, c := range AppConfig.Containers {
if c.ID == id {
removeSubUserContainerAccess(c.Name, c.UUID)
removeContainerSnapshotMetadata(id)
// Clear snapshot schedule for this container
clearContainerSnapshotSchedule(&AppConfig.Containers[i])
AppConfig.Containers = append(AppConfig.Containers[:i], AppConfig.Containers[i+1:]...)
SaveConfig()
return true
}
}
return false
}
func clearContainerSnapshotSchedule(c *Container) {
c.SnapshotScheduleEnabled = false
c.SnapshotScheduleIntervalHours = 0
c.SnapshotScheduleTime = ""
c.SnapshotScheduleLastRun = ""
c.SnapshotScheduleNextRun = ""
c.SnapshotScheduleCreatedBy = ""
}
func AddSnapshot(snapshot Snapshot) {
AppConfig.Snapshots = append(AppConfig.Snapshots, snapshot)
SaveConfig()
}
func FindSnapshot(id string) *Snapshot {
for i := range AppConfig.Snapshots {
if AppConfig.Snapshots[i].ID == id {
return &AppConfig.Snapshots[i]
}
}
return nil
}
func RemoveSnapshot(id string) bool {
for i := range AppConfig.Snapshots {
if AppConfig.Snapshots[i].ID == id {
AppConfig.Snapshots = append(AppConfig.Snapshots[:i], AppConfig.Snapshots[i+1:]...)
SaveConfig()
return true
}
}
return false
}
func ContainerSnapshots(containerID int) []Snapshot {
result := make([]Snapshot, 0)
for _, snapshot := range AppConfig.Snapshots {
if snapshot.ContainerID == containerID {
result = append(result, snapshot)
}
}
return result
}
func removeContainerSnapshotMetadata(containerID int) {
filtered := make([]Snapshot, 0, len(AppConfig.Snapshots))
for _, snapshot := range AppConfig.Snapshots {
if snapshot.ContainerID != containerID {
filtered = append(filtered, snapshot)
}
}
AppConfig.Snapshots = filtered
}
func RemoveSubUserContainerAccess(containerName string, containerUUID string) {
removeSubUserContainerAccess(containerName, containerUUID)
SaveConfig()
}
func removeSubUserContainerAccess(containerName string, containerUUID string) {
if containerName == "" && containerUUID == "" || len(AppConfig.SubUsers) == 0 {
return
}
filteredUsers := make([]SubUser, 0, len(AppConfig.SubUsers))
for _, su := range AppConfig.SubUsers {
filteredNames := make([]string, 0, len(su.ContainerNames))
for _, name := range su.ContainerNames {
if name != containerName {
filteredNames = append(filteredNames, name)
}
}
filteredUUIDs := make([]string, 0, len(su.ContainerUUIDs))
for _, uuid := range su.ContainerUUIDs {
if uuid != containerUUID {
filteredUUIDs = append(filteredUUIDs, uuid)
}
}
if len(filteredNames) == 0 && len(filteredUUIDs) == 0 {
continue
}
su.ContainerNames = filteredNames
su.ContainerUUIDs = filteredUUIDs
filteredUsers = append(filteredUsers, su)
}
AppConfig.SubUsers = filteredUsers
}
// FindContainer finds a container by ID
func FindContainer(id int) *Container {
for i, c := range AppConfig.Containers {
if c.ID == id {
return &AppConfig.Containers[i]
}
}
return nil
}
// FindContainerByUUID finds a container by UUID.
func FindContainerByUUID(uuid string) *Container {
for i, c := range AppConfig.Containers {
if c.UUID == uuid {
return &AppConfig.Containers[i]
}
}
return nil
}
// FindContainerByName finds a container by name
func FindContainerByName(name string) *Container {
for i, c := range AppConfig.Containers {
if c.Name == name {
return &AppConfig.Containers[i]
}
}
return nil
}
// FindContainerByIdentifier finds a container by ID, UUID, or name.
func FindContainerByIdentifier(identifier string) *Container {
if id, err := strconv.Atoi(identifier); err == nil {
if c := FindContainer(id); c != nil {
return c
}
}
if c := FindContainerByUUID(identifier); c != nil {
return c
}
return FindContainerByName(identifier)
}
// UpdateContainerStatus updates container status by ID
func UpdateContainerStatus(id int, status string) {
c := FindContainer(id)
if c != nil {
c.Status = status
SaveConfig()
}
}
func SetContainerPolicyBlock(id int, blocked bool, reason string) {
c := FindContainer(id)
if c == nil {
return
}
c.PolicyBlocked = blocked
if blocked {
c.PolicyBlockedReason = reason
c.PolicyBlockedAt = time.Now().Format("2006-01-02 15:04:05")
} else {
c.PolicyBlockedReason = ""
c.PolicyBlockedAt = ""
}
SaveConfig()
}
// UpdateVNC refreshes all container statuses
func UpdateVNC(containers []Container) {
AppConfig.Containers = containers
SaveConfig()
}
// AllocateSSHPort allocates a new SSH port, skipping ports already used by any container
func AllocateSSHPort() int {
used := collectAllHostPorts()
port := 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)
}
// IsValidContainerNameSyntax checks only the container name format.
func IsValidContainerNameSyntax(name string) bool {
if len(name) == 0 || len(name) > 63 {
return false
}
// Only allow alphanumeric, hyphens, underscores
for _, c := range name {
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_') {
return false
}
}
return true
}
// AddAuditLog adds an audit log entry
func AddAuditLog(action, target, detail, user string) {
log := AuditLog{
Time: time.Now().Format("2006-01-02 15:04:05"),
Action: action,
Target: target,
Detail: detail,
User: user,
}
AppConfig.AuditLogs = append(AppConfig.AuditLogs, log)
if len(AppConfig.AuditLogs) > 500 {
AppConfig.AuditLogs = AppConfig.AuditLogs[len(AppConfig.AuditLogs)-500:]
}
SaveConfig()
}
func AddAuditLogFull(action, target, detail, user, ip, userAgent string, success bool, errMsg string) {
s := success
log := AuditLog{
Time: time.Now().Format("2006-01-02 15:04:05"),
Action: action,
Target: target,
Detail: detail,
User: user,
IP: ip,
UserAgent: userAgent,
Success: &s,
Error: errMsg,
}
AppConfig.AuditLogs = append(AppConfig.AuditLogs, log)
if len(AppConfig.AuditLogs) > 500 {
AppConfig.AuditLogs = AppConfig.AuditLogs[len(AppConfig.AuditLogs)-500:]
}
SaveConfig()
}
// SaveTasks persists the task queue to config
func SaveTasks(tasks []SavedTask) {
AppConfig.Tasks = tasks
SaveConfig()
}
// AddLoginLog persists a login log entry
func AddLoginLog(username, ip, userAgent string, success bool) {
log := SavedLoginLog{
Time: time.Now().Format("2006-01-02 15:04:05 MST"),
Username: username,
IP: ip,
UserAgent: userAgent,
Success: success,
}
AppConfig.LoginLogs = append(AppConfig.LoginLogs, log)
if len(AppConfig.LoginLogs) > 200 {
AppConfig.LoginLogs = AppConfig.LoginLogs[len(AppConfig.LoginLogs)-200:]
}
SaveConfig()
}
// ResetAdminPassword resets the admin password from CLI
func ResetAdminPassword(newPassword string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return err
}
AppConfig.AdminPassHash = string(hash)
return SaveConfig()
}
// CleanStaleContainers removes containers from config if their LXC directory doesn't exist
func CleanStaleContainers() {
valid := make([]Container, 0)
changed := false
for _, c := range AppConfig.Containers {
if c.IsKVM() {
if c.DiskImage == "" {
valid = append(valid, c)
continue
}
if _, err := os.Stat(c.DiskImage); os.IsNotExist(err) {
fmt.Printf("Cleaning stale KVM config: %s (disk image not found)\n", c.VirshName())
changed = true
continue
}
valid = append(valid, c)
continue
}
lxcDir := "/var/lib/lxc/" + c.LxcName()
if _, err := os.Stat(lxcDir); os.IsNotExist(err) {
fmt.Printf("Cleaning stale container config: %s (LXC dir not found)\n", c.LxcName())
changed = true
continue
}
valid = append(valid, c)
}
if changed {
AppConfig.Containers = valid
SaveConfig()
}
}