mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
867 lines
24 KiB
Go
867 lines
24 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"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"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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 config directory: %v", err)
|
|
}
|
|
if err := os.MkdirAll(dataDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to create data directory: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
|
// First run: generate new config
|
|
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
|
|
}
|
|
|
|
// Load existing config
|
|
data, err := os.ReadFile(cfgPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read config: %v", err)
|
|
}
|
|
|
|
AppConfig = &ClicdConfig{}
|
|
if err := json.Unmarshal(data, AppConfig); err != nil {
|
|
return nil, fmt.Errorf("failed to parse config: %v", err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
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
|
|
}
|
|
if changed {
|
|
if err := SaveConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return AppConfig, nil
|
|
}
|
|
|
|
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 {
|
|
data, err := json.MarshalIndent(AppConfig, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal config: %v", err)
|
|
}
|
|
return os.WriteFile(getConfigPath(), data, 0600)
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|