mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-14 00:46:34 +03:00
364 lines
9.2 KiB
Go
364 lines
9.2 KiB
Go
// SPDX-License-Identifier: BSD-2-Clause
|
|
//
|
|
// Copyright (c) 2025 The FreeBSD Foundation.
|
|
//
|
|
// This software was developed by Hayzam Sherif <hayzam@alchemilla.io>
|
|
// of Alchemilla Ventures Pvt. Ltd. <hello@alchemilla.io>,
|
|
// under sponsorship from the FreeBSD Foundation.
|
|
|
|
package db
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/alchemillahq/sylve/internal"
|
|
"github.com/alchemillahq/sylve/internal/db/models"
|
|
clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster"
|
|
infoModels "github.com/alchemillahq/sylve/internal/db/models/info"
|
|
jailModels "github.com/alchemillahq/sylve/internal/db/models/jail"
|
|
networkModels "github.com/alchemillahq/sylve/internal/db/models/network"
|
|
sambaModels "github.com/alchemillahq/sylve/internal/db/models/samba"
|
|
taskModels "github.com/alchemillahq/sylve/internal/db/models/task"
|
|
utilitiesModels "github.com/alchemillahq/sylve/internal/db/models/utilities"
|
|
vmModels "github.com/alchemillahq/sylve/internal/db/models/vm"
|
|
zfsModels "github.com/alchemillahq/sylve/internal/db/models/zfs"
|
|
"github.com/alchemillahq/sylve/internal/logger"
|
|
"github.com/alchemillahq/sylve/pkg/system"
|
|
"github.com/alchemillahq/sylve/pkg/utils"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
gormLogger "gorm.io/gorm/logger"
|
|
)
|
|
|
|
func SetupDatabase(cfg *internal.SylveConfig, isTest bool) *gorm.DB {
|
|
var logMode gormLogger.Interface
|
|
|
|
switch cfg.Environment {
|
|
case internal.Development:
|
|
logMode = gormLogger.Default.LogMode(gormLogger.Warn)
|
|
case internal.Debug:
|
|
logMode = gormLogger.Default.LogMode(gormLogger.Info)
|
|
case internal.Production:
|
|
logMode = gormLogger.Default.LogMode(gormLogger.Silent)
|
|
}
|
|
|
|
ormConfig := &gorm.Config{
|
|
Logger: logMode,
|
|
TranslateError: true,
|
|
DisableForeignKeyConstraintWhenMigrating: true,
|
|
}
|
|
|
|
var db *gorm.DB
|
|
var err error
|
|
|
|
if isTest {
|
|
db, err = gorm.Open(sqlite.Open(":memory:"), ormConfig)
|
|
} else {
|
|
db, err = gorm.Open(sqlite.Open(cfg.DataPath+"/sylve.db"), ormConfig)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.L.Fatal().Msgf("Error connecting to database: %v", err)
|
|
}
|
|
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
logger.L.Fatal().Msgf("Error getting sql database handle: %v", err)
|
|
}
|
|
|
|
db.Exec("PRAGMA busy_timeout = 5000")
|
|
db.Exec("PRAGMA journal_mode = WAL")
|
|
db.Exec("PRAGMA synchronous = NORMAL")
|
|
|
|
// Pre-migration fixups use the migrations tracking table, so ensure it
|
|
// exists before running any pre-migration logic.
|
|
if err := db.AutoMigrate(&models.Migrations{}); err != nil {
|
|
logger.L.Fatal().Msgf("Error bootstrapping migrations table: %v", err)
|
|
}
|
|
|
|
PreMigrationFixups(db)
|
|
|
|
err = db.AutoMigrate(
|
|
&models.BasicSettings{},
|
|
|
|
&models.System{},
|
|
&models.User{},
|
|
&models.PAMIdentity{},
|
|
&models.Group{},
|
|
&models.Token{},
|
|
&models.WebAuthnCredential{},
|
|
&models.WebAuthnChallenge{},
|
|
&models.SystemSecrets{},
|
|
|
|
&vmModels.Storage{},
|
|
&vmModels.Network{},
|
|
&vmModels.VMStats{},
|
|
&vmModels.VMCPUPinning{},
|
|
&vmModels.VMSnapshot{},
|
|
&vmModels.VM{},
|
|
|
|
&jailModels.Network{},
|
|
&jailModels.Storage{},
|
|
&jailModels.JailStats{},
|
|
&jailModels.JailHooks{},
|
|
&jailModels.JailSnapshot{},
|
|
&jailModels.JailTemplate{},
|
|
&jailModels.Jail{},
|
|
|
|
&models.PassedThroughIDs{},
|
|
&models.Triggers{},
|
|
&models.NetlinkEvent{},
|
|
|
|
&networkModels.Object{},
|
|
&networkModels.ObjectEntry{},
|
|
&networkModels.ObjectResolution{},
|
|
|
|
&networkModels.DHCPConfig{},
|
|
&networkModels.DHCPRange{},
|
|
&networkModels.DHCPStaticLease{},
|
|
// &networkModels.DHCPOption{},
|
|
|
|
&infoModels.CPU{},
|
|
&infoModels.RAM{},
|
|
&infoModels.Swap{},
|
|
&infoModels.NetworkInterface{},
|
|
&infoModels.Note{},
|
|
&infoModels.AuditRecord{},
|
|
|
|
&infoModels.ZPoolHistorical{},
|
|
|
|
&zfsModels.PeriodicSnapshot{},
|
|
|
|
&networkModels.ManualSwitch{},
|
|
&networkModels.StandardSwitch{},
|
|
&networkModels.NetworkPort{},
|
|
|
|
&utilitiesModels.CloudInitTemplate{},
|
|
&utilitiesModels.DownloadedFile{},
|
|
&utilitiesModels.Downloads{},
|
|
&utilitiesModels.WoL{},
|
|
|
|
&sambaModels.SambaSettings{},
|
|
&sambaModels.SambaShare{},
|
|
&sambaModels.SambaAuditLog{},
|
|
|
|
&clusterModels.Cluster{},
|
|
&clusterModels.ClusterNode{},
|
|
&clusterModels.ClusterOption{},
|
|
&clusterModels.ClusterNote{},
|
|
&clusterModels.BackupTarget{},
|
|
&clusterModels.BackupJob{},
|
|
&clusterModels.BackupEvent{},
|
|
&clusterModels.ReplicationPolicy{},
|
|
&clusterModels.ReplicationPolicyTarget{},
|
|
&clusterModels.ReplicationLease{},
|
|
&clusterModels.ReplicationEvent{},
|
|
&clusterModels.ReplicationReceipt{},
|
|
&clusterModels.ClusterSSHIdentity{},
|
|
&taskModels.GuestLifecycleTask{},
|
|
|
|
&models.Migrations{},
|
|
)
|
|
|
|
if err != nil {
|
|
logger.L.Fatal().Msgf("Error migrating database: %v", err)
|
|
}
|
|
|
|
sqlDB.SetMaxOpenConns(1)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
|
|
err = setupInitUsers(db, cfg)
|
|
if err != nil {
|
|
logger.L.Fatal().Msgf("Error setting up initial users: %v", err)
|
|
}
|
|
|
|
err = initClusterRecord(db)
|
|
if err != nil {
|
|
logger.L.Fatal().Msgf("Error initializing cluster record: %v", err)
|
|
}
|
|
|
|
err = initDHCPConfig(db)
|
|
if err != nil {
|
|
logger.L.Fatal().Msgf("Error initializing DHCP config: %v", err)
|
|
}
|
|
|
|
err = Fixups(db)
|
|
|
|
if err != nil {
|
|
logger.L.Fatal().Msgf("Error applying database fixups: %v", err)
|
|
}
|
|
|
|
err = PruneJobs(db)
|
|
|
|
if err != nil {
|
|
logger.L.Error().Err(err).Msgf("Error pruning database of unnecessary records: %v", err)
|
|
}
|
|
|
|
if !isTest {
|
|
if err := db.Exec("VACUUM").Error; err != nil {
|
|
logger.L.Warn().Msgf("VACUUM failed: %v", err)
|
|
}
|
|
}
|
|
|
|
db.Model(&models.BasicSettings{}).
|
|
Where("id = ? AND (SELECT COUNT(*) FROM basic_settings) = 1", 1).
|
|
Update("restarted", true)
|
|
|
|
return db
|
|
}
|
|
|
|
func setupInitUsers(db *gorm.DB, cfg *internal.SylveConfig) error {
|
|
const username = "admin"
|
|
adminCfg := cfg.Admin
|
|
|
|
var user models.User
|
|
result := db.Where("username = ?", username).First(&user)
|
|
|
|
if result.Error != nil {
|
|
if result.Error == gorm.ErrRecordNotFound {
|
|
hashed, err := utils.HashPassword(adminCfg.Password)
|
|
if err != nil {
|
|
logger.L.Error().Msgf("Failed to hash password for admin user: %v", err)
|
|
return err
|
|
}
|
|
|
|
newUser := models.User{
|
|
Username: username,
|
|
Email: adminCfg.Email,
|
|
Password: hashed,
|
|
Admin: true,
|
|
}
|
|
if err := db.Create(&newUser).Error; err != nil {
|
|
logger.L.Error().Msgf("Failed to create admin user: %v", err)
|
|
return err
|
|
}
|
|
logger.L.Info().Msg("Admin user created")
|
|
} else {
|
|
logger.L.Error().Msgf("Error querying admin user: %v", result.Error)
|
|
return result.Error
|
|
}
|
|
} else {
|
|
updates := map[string]any{}
|
|
needsUpdate := false
|
|
|
|
if user.Email != adminCfg.Email {
|
|
updates["email"] = adminCfg.Email
|
|
needsUpdate = true
|
|
}
|
|
|
|
if !user.Admin {
|
|
updates["admin"] = true
|
|
needsUpdate = true
|
|
}
|
|
|
|
if adminCfg.Password != "" {
|
|
if !utils.CheckPasswordHash(adminCfg.Password, user.Password) {
|
|
hashed, err := utils.HashPassword(adminCfg.Password)
|
|
if err != nil {
|
|
logger.L.Error().Msgf("Failed to hash password for admin update: %v", err)
|
|
return err
|
|
}
|
|
updates["password"] = hashed
|
|
needsUpdate = true
|
|
}
|
|
}
|
|
|
|
if !needsUpdate {
|
|
logger.L.Debug().Msg("Admin user up to date, no changes needed")
|
|
return nil
|
|
}
|
|
|
|
if err := db.Model(&user).Updates(updates).Error; err != nil {
|
|
logger.L.Error().Msgf("Failed to update admin user: %v", err)
|
|
return err
|
|
}
|
|
|
|
logger.L.Info().Msg("Admin user updated")
|
|
}
|
|
|
|
exists, err := system.UnixUserExists(username)
|
|
if err != nil {
|
|
logger.L.Error().Msgf("Error checking Unix user 'admin': %v", err)
|
|
}
|
|
if !exists {
|
|
err := system.CreateUnixUser(username, "/usr/sbin/nologin", "/nonexistent", "")
|
|
if err != nil {
|
|
logger.L.Error().Msgf("Failed to create Unix user 'admin': %v", err)
|
|
return err
|
|
}
|
|
logger.L.Info().Msg("Unix user 'admin' created")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func initClusterRecord(db *gorm.DB) error {
|
|
var keepID uint
|
|
|
|
err := db.Model(&clusterModels.Cluster{}).
|
|
Order("key DESC, id ASC").
|
|
Select("id").
|
|
First(&keepID).Error
|
|
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
defaultCluster := &clusterModels.Cluster{
|
|
Enabled: false,
|
|
Key: "",
|
|
RaftBootstrap: nil,
|
|
RaftIP: "",
|
|
RaftPort: 8180,
|
|
}
|
|
|
|
if err := db.Create(defaultCluster).Error; err != nil {
|
|
logger.L.Error().Msgf("Failed to create initial cluster record: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
logger.L.Error().Msgf("Failed to query best cluster record: %v", err)
|
|
return err
|
|
}
|
|
|
|
res := db.Where("id != ?", keepID).Delete(&clusterModels.Cluster{})
|
|
if res.Error != nil {
|
|
logger.L.Error().Msgf("Failed to clean up cluster records: %v", res.Error)
|
|
return res.Error
|
|
}
|
|
|
|
if res.RowsAffected > 0 {
|
|
logger.L.Info().Msgf("Purged %d duplicate cluster records!", res.RowsAffected)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func initDHCPConfig(db *gorm.DB) error {
|
|
var count int64
|
|
if err := db.Model(&networkModels.DHCPConfig{}).Count(&count).Error; err != nil {
|
|
logger.L.Error().Msgf("Failed to query DHCP config count: %v", err)
|
|
return err
|
|
}
|
|
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
|
|
dhcpConfig := &networkModels.DHCPConfig{
|
|
StandardSwitches: []networkModels.StandardSwitch{},
|
|
ManualSwitches: []networkModels.ManualSwitch{},
|
|
DNSServers: []string{"1.1.1.1", "1.0.0.1", "8.8.8.8"},
|
|
Domain: "lan",
|
|
ExpandHosts: true,
|
|
}
|
|
|
|
if err := db.Create(dhcpConfig).Error; err != nil {
|
|
logger.L.Error().Msgf("Failed to create initial DHCP config record: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|