重构架构

This commit is contained in:
2026-02-20 18:44:43 +08:00
parent aba7b68439
commit cc71248ef4
52 changed files with 1404 additions and 2360 deletions

46
internal/cli/init/cfg.go Normal file
View File

@@ -0,0 +1,46 @@
package initcmd
import (
"sunhpc/internal/middler/auth"
"sunhpc/pkg/logger"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
// NewConfigCmd 创建 "init config" 命令
func NewInitCfgCmd() *cobra.Command {
var (
force bool
path string
verbose bool
)
cmd := &cobra.Command{
Use: "config",
Short: "生成默认配置文件",
Long: `
在指定路径生成 SunHPC 默认配置文件 (sunhpc.yaml)
示例:
sunhpc init config # 生成默认配置文件
sunhpc init config -f # 强制覆盖已有配置文件
sunhpc init config -p /etc/sunhpc/sunhpc.yaml # 指定路径
`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.RequireRoot(); err != nil {
return err
}
logger.Info("✅ 配置文件已生成", zap.String("path", path))
return nil
},
}
// 定义局部 flags
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制覆盖已有配置文件")
cmd.Flags().StringVarP(&path, "path", "p", "", "指定配置文件路径")
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "显示详细日志")
return cmd
}

53
internal/cli/init/db.go Normal file
View File

@@ -0,0 +1,53 @@
package initcmd
import (
"fmt"
"sunhpc/internal/middler/auth"
"sunhpc/pkg/config"
"sunhpc/pkg/database"
"sunhpc/pkg/logger"
"github.com/spf13/cobra"
)
func NewInitDBCmd() *cobra.Command {
var force bool
cmd := &cobra.Command{
Use: "db",
Short: "初始化数据库",
Long: `初始化SQLite数据库,创建所有表结构和默认数据。
示例:
sunhpc init db # 初始化数据库
sunhpc init db --force # 强制重新初始化`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.RequireRoot(); err != nil {
return err
}
logger.Debug("执行数据库初始化...")
cfg, err := config.LoadConfig()
if err != nil {
return fmt.Errorf("加载配置失败: %w", err)
}
// 初始化数据库
db, err := database.GetInstance(&cfg.Database, nil)
if err != nil {
return fmt.Errorf("数据库连接失败: %w", err)
}
defer db.Close()
if err := db.InitTables(force); err != nil {
return fmt.Errorf("数据库初始化失败: %w", err)
}
return nil
},
}
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制重新初始化")
return cmd
}

19
internal/cli/init/init.go Normal file
View File

@@ -0,0 +1,19 @@
package initcmd
import (
"github.com/spf13/cobra"
)
// 仅定义 Cmd 注册子命令,只负责组装命令树,尽量不包含业务逻辑
func NewInitCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "初始化集群配置",
Long: "初始化 SunHPC 配置文件、数据库、系统参数及相关服务",
}
cmd.AddCommand(NewInitDBCmd())
cmd.AddCommand(NewInitCfgCmd())
return cmd
}

69
internal/cli/root.go Normal file
View File

@@ -0,0 +1,69 @@
package cli
import (
initcmd "sunhpc/internal/cli/init"
"sunhpc/pkg/config"
"sunhpc/pkg/logger"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
verbose bool
noColor bool
)
func NewRootCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "sunhpc",
Short: "SunHPC - HPC集群一体化运维工具",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// 加载全局配置(只加载一次)
cfg, err := config.LoadConfig()
if err != nil {
// 配置加载失败,使用默认日志配置初始化
logger.Warnf("加载配置失败,使用默认日志配置: %v", err)
logger.Init(logger.LogConfig{})
return
}
// 3. 初始化全局日志(全局只执行一次)
logger.Init(logger.LogConfig{
Verbose: cfg.Log.Verbose,
ShowColor: !cfg.Log.ShowColor,
LogFile: cfg.Log.LogFile,
})
},
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}
cmd.PersistentFlags().StringVarP(
&config.CLIParams.Config,
"config", "c",
"", "配置文件路径 (默认:/etc/sunhpc/config.yaml)")
cmd.PersistentFlags().BoolVarP(
&config.CLIParams.Verbose,
"verbose", "v", false, "启用详细日志输出")
cmd.PersistentFlags().BoolVar(
&config.CLIParams.NoColor,
"no-color", false, "禁用彩色输出")
// 如果指定了 --config 参数,优先使用该配置文件
if config.CLIParams.Config != "" {
viper.SetConfigFile(config.CLIParams.Config)
}
cmd.AddCommand(initcmd.NewInitCmd())
return cmd
}
func Execute() error {
return NewRootCmd().Execute()
}

View File

@@ -0,0 +1,41 @@
package soft
import (
"fmt"
"sunhpc/pkg/logger"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
func NewSoftInstallCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "install [software]",
Short: "安装软件",
Long: "在集群节点上安装指定软件",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
software := args[0]
logger.Info("开始安装软件", zap.String("software", software))
// TODO: 实现软件安装逻辑
// 1. 检查软件包
// 2. 分发到节点
// 3. 执行安装
fmt.Printf("✓ 软件 %s 安装完成\n", software)
return nil
},
}
// 添加安装命令的标志
cmd.Flags().StringSlice("nodes", []string{}, "目标节点列表")
cmd.Flags().String("version", "", "软件版本")
cmd.Flags().Bool("force", false, "强制安装")
return cmd
}

18
internal/cli/soft/soft.go Normal file
View File

@@ -0,0 +1,18 @@
package soft
import (
"github.com/spf13/cobra"
)
func NewSoftCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "soft",
Short: "软件管理命令",
Long: "管理集群软件安装、更新、卸载等操作",
}
// 添加 soft 的子命令
cmd.AddCommand(NewSoftInstallCmd())
return cmd
}

58
internal/cli/tmpl/dump.go Normal file
View File

@@ -0,0 +1,58 @@
package tmpl
import (
"fmt"
log "sunhpc/pkg/logger"
"sunhpc/pkg/templating"
"github.com/spf13/cobra"
)
func newDumpCmd() *cobra.Command {
var output string
cmd := &cobra.Command{
Use: "dump <template-name>",
Short: "导出内置模板到文件",
Long: `
将内置的 YAML 模板导出为可编辑的文件。
示例:
sunhpc tmpl dump autofs --output ./my-autofs.yaml`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
// 检查模板是否存在
available, _ := templating.ListEmbeddedTemplates()
found := false
for _, n := range available {
if n == name {
found = true
break
}
}
if !found {
return fmt.Errorf("内置模板 '%s' 不存在。可用模板: %v", name, available)
}
outPath := output
if outPath == "" {
outPath = name + ".yaml"
}
if err := templating.DumpEmbeddedTemplateToFile(name, outPath); err != nil {
return err
}
log.Infof("内置模板 '%s' 已导出到: %s", name, outPath)
log.Infof("你可以编辑此文件,然后用以下命令使用它:")
log.Infof(" sunhpc tmpl render %s -f %s [flags]", name, outPath)
return nil
},
}
cmd.Flags().StringVarP(&output, "output", "o", "", "输出文件路径(默认: <name>.yaml")
return cmd
}

17
internal/cli/tmpl/init.go Normal file
View File

@@ -0,0 +1,17 @@
// cmd/tmpl/init.go
package tmpl
import "github.com/spf13/cobra"
// Cmd 是 sunhpc tmpl 的根命令
func NewTmplCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "tmpl",
Short: "管理配置模板",
Long: "从 YAML 模板生成配置文件或脚本,支持变量替换和多阶段执行",
}
cmd.AddCommand(newRenderCmd())
cmd.AddCommand(newDumpCmd())
return cmd
}

View File

@@ -0,0 +1,96 @@
package tmpl
import (
"fmt"
log "sunhpc/pkg/logger"
templating "sunhpc/pkg/templating"
"github.com/spf13/cobra"
)
var (
tmplFile string
hostname string
domain string
oldHostname string
ip string
clusterName string
outputRoot string
)
func newRenderCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "render <template-name>",
Short: "渲染配置模板",
Long: "根据 YAML 模板和上下文变量生成配置文件或脚本",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
tmplName := args[0]
var template *templating.Template
var err error
// 优先使用 -f 指定的外部模版文件
if tmplFile != "" {
template, err = templating.LoadTemplate(tmplFile)
if err != nil {
return fmt.Errorf("加载外部模板失败: %w", err)
}
log.Infof("✅ 外部模板 '%s' 已加载\n", tmplFile)
} else {
// 否则从内置模板加载
template, err = templating.LoadEmbeddedTemplate(tmplName)
if err != nil {
return err
}
log.Infof("✅ 内置模板 '%s' 已加载\n", tmplName)
}
ctx := templating.Context{
Node: templating.NodeInfo{
Hostname: hostname,
OldHostname: oldHostname,
Domain: domain,
IP: ip,
},
Cluster: templating.ClusterInfo{
Name: clusterName,
},
}
rendered, err := template.Render(ctx)
if err != nil {
return fmt.Errorf("模板渲染失败: %w", err)
}
// 处理 post 阶段
if steps, ok := rendered["post"]; ok {
fmt.Println(">>> 执行 post 阶段")
if err := templating.WriteFiles(steps, outputRoot); err != nil {
return err
}
templating.PrintScripts(steps)
}
// 处理 configure 阶段
if steps, ok := rendered["configure"]; ok {
fmt.Println(">>> 执行 configure 阶段")
templating.PrintScripts(steps)
}
fmt.Println("✅ 模板渲染完成")
return nil
},
}
cmd.Flags().StringVarP(&tmplFile, "file", "f", "", "指定模板文件路径(覆盖默认查找)")
cmd.Flags().StringVar(&hostname, "hostname", "", "节点主机名")
cmd.Flags().StringVar(&domain, "domain", "cluster.local", "DNS 域名")
cmd.Flags().StringVar(&oldHostname, "old-hostname", "", "旧主机名(用于迁移)")
cmd.Flags().StringVar(&ip, "ip", "", "节点 IP 地址")
cmd.Flags().StringVar(&clusterName, "cluster", "default", "集群名称")
cmd.Flags().StringVarP(&outputRoot, "output", "o", "/", "文件输出根目录")
_ = cmd.MarkFlagRequired("hostname")
return cmd
}

View File

@@ -1,147 +0,0 @@
package config
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/viper"
)
const (
BaseDir string = "/etc/sunhpc"
LogDir string = "/var/log/sunhpc"
TmplDir string = BaseDir + "/tmpl.d"
appName string = "sunhpc"
defaultDBPath string = "/var/lib/sunhpc"
defaultDBName string = "sunhpc.db"
)
type Config struct {
DB DBConfig `yaml:"db"`
Log LogConfig `yaml:"log"`
Cluster ClusterConfig `yaml:"cluster"`
}
type DBConfig struct {
Type string `yaml:"type"`
Path string `yaml:"path"` // SQLite: 目录路径
Name string `yaml:"name"` // SQLite: 文件名
User string `yaml:"user"`
Password string `yaml:"password"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Socket string `yaml:"socket"`
Verbose bool `yaml:"verbose"`
}
type LogConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
Output string `yaml:"output"`
FilePath string `yaml:"file_path"`
}
type ClusterConfig struct {
Name string `yaml:"name"`
AdminEmail string `yaml:"admin_email"`
TimeZone string `yaml:"time_zone"`
NodePrefix string `yaml:"node_prefix"`
}
// LoadConfig loads configuration with the following precedence:
// 优先级排序:
// 1. 环境变量 (prefix: SUNHPC_)
// 2. ~/.sunhpc.yaml
// 3. ./sunhpc.yaml
// 4. /etc/sunhpc/sunhpc.yaml
// 5. Default values
/*
示例配置文件:
```yaml
db:
type: sqlite
name: sunhpc.db
path: /var/lib/sunhpc
socket: /var/lib/sunhpc/mysql/mysqld.sock
user: root
password: ""
host: localhost
```
环境变量配置示例:
```bash
export SUNHPC_DATABASE_TYPE=mysql
export SUNHPC_DATABASE_NAME=sunhpc
export SUNHPC_DATABASE_USER=root
export SUNHPC_DATABASE_PASSWORD=123456
export SUNHPC_DATABASE_HOST=localhost
```
*/
func LoadConfig() (*Config, error) {
v := viper.New()
// Step 1: 设置默认值(最低优先级)
v.SetDefault("db.type", "sqlite")
v.SetDefault("db.name", "sunhpc.db")
v.SetDefault("db.path", "/var/lib/sunhpc")
v.SetDefault("db.socket", "/var/lib/sunhpc/mysql/mysqld.sock")
v.SetDefault("db.user", "")
v.SetDefault("db.password", "")
v.SetDefault("db.host", "localhost")
v.SetDefault("db.port", 3306)
v.SetDefault("db.verbose", false)
// Step 2: 启用环境变量(高优先级)
v.SetEnvPrefix("SUNHPC") // e.g., SUNHPC_DATABASE_NAME
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // db.type -> SUNHPC_DB_TYPE
v.AutomaticEnv() // Auto bind env vars matching config keys
// Step 3: 按优先级从高到低加载配置文件
// 优先级: env > ./sunhpc.yaml > ~/.sunhpc.yaml > /etc/sunhpc/sunhpc.yaml > defaults
configFiles := []string{
"./sunhpc.yaml",
filepath.Join(os.Getenv("HOME"), ".sunhpc.yaml"),
filepath.Join(BaseDir, "sunhpc.yaml"),
}
var configFile string
for _, cfgFile := range configFiles {
if _, err := os.Stat(cfgFile); err == nil {
configFile = cfgFile
break // 找到第一个就停止.
}
}
// 如果找到配置文件,就加载它.
if configFile != "" {
v.SetConfigFile(configFile)
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("加载配置文件 %s 失败: %w", configFile, err)
}
}
// 解码到结构体
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("解码配置到结构体失败: %w", err)
}
return &cfg, nil
}
// InitDirs 创建所有必需目录
func InitDirs() error {
dirs := []string{
BaseDir,
TmplDir,
LogDir,
}
for _, d := range dirs {
if err := os.MkdirAll(d, 0755); err != nil {
return fmt.Errorf("创建目录 %s 失败: %w", d, err)
}
}
return nil
}

View File

@@ -1,60 +0,0 @@
package config
import (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// DefaultConfig 返回 SunHPC 的默认配置结构体
func DefaultConfig() *Config {
return &Config{
DB: DBConfig{
Type: "sqlite",
Path: "/var/lib/sunhpc", // SQLite 数据库存放目录
Name: "sunhpc.db", // 数据库文件名
User: "", // SQLite 不需要
Password: "",
Host: "",
Port: 0,
Socket: "",
Verbose: false,
},
Log: LogConfig{
Level: "info",
Format: "text", // or "json"
Output: "stdout",
FilePath: "/var/log/sunhpc/sunhpc.log",
},
Cluster: ClusterConfig{
Name: "default-cluster",
AdminEmail: "admin@example.com",
TimeZone: "Asia/Shanghai",
NodePrefix: "node",
},
}
}
// WriteDefaultConfig 将默认配置写入指定路径
// 如果目录不存在,会自动创建(需有权限)
// 如果文件已存在且非空,会返回错误(除非调用方先删除)
func WriteDefaultConfig(path string) error {
// 确保目录存在
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
// 生成默认配置
cfg := DefaultConfig()
// 序列化为 YAML
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
// 写入文件0644 权限)
return os.WriteFile(path, data, 0644)
}

View File

@@ -1,312 +0,0 @@
package db
import (
"bufio"
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
_ "github.com/go-sql-driver/mysql"
_ "github.com/mattn/go-sqlite3"
"sunhpc/internal/config"
"sunhpc/internal/log"
)
// DB wraps the sql.DB connection pool.
type DB struct {
engine *sql.DB
config *config.DBConfig // 保存配置
}
/*
// Engine returns the underlying *sql.DB.
func (d *DB) Engine() *sql.DB {
return d.engine
}
*/
func ConfirmWithRetry(prompt string, maxAttempts int) bool {
reader := bufio.NewReader(os.Stdin)
for attempt := 1; attempt <= maxAttempts; attempt++ {
log.Infof("%s [y/n]", prompt)
response, err := reader.ReadString('\n')
if err != nil {
continue
}
response = strings.ToLower(strings.TrimSpace(response))
switch response {
case "y", "yes":
return true
case "n", "no", "":
return false
default:
if attempt < maxAttempts {
log.Warnf(
"⚠️ 无效输入、请输入 'y' 或 'n'(剩余尝试次数: %d)",
maxAttempts-attempt)
}
}
}
log.Warn("⚠️ 警告:尝试次数过多、操作已取消")
return false
}
// InitSchema initializes the database schema.
// If force is true, drops existing tables before recreating them.
func (d *DB) InitSchema(force bool) error {
fullPath := filepath.Join(d.config.Path, d.config.Name)
// 检查文件是否存在
_, err := os.Stat(fullPath)
fileExists := err == nil
// 处理不同的场景
switch {
case !fileExists:
// 场景1文件不存在连接并创建(allowCreate = true).
log.Infof("数据库文件不存在,将创建: %s", fullPath)
if err := d.Connect(true); err != nil {
return err
}
return createTables(d.engine)
case fileExists && !force:
// 场景2文件存在、无 force 参数、提示友好退出.
log.Warnf("数据库文件已存在: %s", fullPath)
log.Warn("如果需要强制重新初始化,请添加 --force 参数")
log.Warn("数据库已存在、退出初始化操作.")
os.Exit(1)
case fileExists && force:
// 场景3文件存在、force 参数 -> 需要用户确认并重建.
log.Warn("警告:强制重新初始化将清空数据库中的所有数据!")
if !ConfirmWithRetry("是否继续?", 3) {
return fmt.Errorf("用户取消操作")
}
// 连接现有数据库(allowCreate = true, 因为文件已经存在)
if err := d.Connect(true); err != nil {
return err
}
// 清空现有数据.
if err := dropTables(d.engine); err != nil {
return err
}
// 清空表
if err := dropTriggers(d.engine); err != nil {
return err
}
log.Info("已清空现有--数据库触发器")
return createTables(d.engine)
}
return nil
}
// 辅助函数:检查文件是否存在
func fileExists(path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
// 其他问题(如权限问题)也视为文件不存在,但应该记录日志
log.Debugf("检查数据库文件状态失败: %v", err)
return false
}
// 辅助函数: 创建数据库表
func createTables(db *sql.DB) error {
// ✅ 调用 schema.go 中的函数
for _, ddl := range CreateTableStatements() {
log.Debugf("执行: %s", ddl)
if _, err := db.Exec(ddl); err != nil {
return fmt.Errorf("数据表创建失败: %w", err)
}
}
log.Info("数据库表创建成功")
/*
使用sqlite3命令 测试数据库是否存在表
✅ 查询所有表
sqlite3 /var/lib/sunhpc/sunhpc.db
.tables # 查看所有表
select * from sqlite_master where type='table'; # 查看表定义
PRAGMA integrity_check; # 检查数据库完整性
*/
return nil
}
func dropTables(db *sql.DB) error {
// ✅ 调用 schema.go 中的函数
for _, table := range DropTableOrder() {
if _, err := db.Exec(fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)); err != nil {
return err
}
}
return nil
}
func dropTriggers(db *sql.DB) error {
// ✅ 调用 schema.go 中的函数
for _, trigger := range DropTriggerStatements() {
if _, err := db.Exec(fmt.Sprintf("DROP TRIGGER IF EXISTS `%s`", trigger)); err != nil {
return err
}
}
return nil
}
// --- Singleton DB Instance ---
var (
globalDB *DB
initOnce sync.Once
initErr error
)
func GetDB() (*DB, error) {
initOnce.Do(func() {
cfg, err := config.LoadConfig()
if err != nil {
initErr = fmt.Errorf("数据库配置文件加载失败: %w", err)
return
}
globalDB = &DB{
config: &cfg.DB,
}
})
return globalDB, initErr
}
func (d *DB) Connect(allowCreate bool) error {
// 如果已经连接,直接返回
if d.engine != nil {
return nil
}
switch d.config.Type {
case "sqlite":
fullPath := filepath.Join(d.config.Path, d.config.Name)
// 检查文件是否存在
_, err := os.Stat(fullPath)
fileExists := err == nil
// 如果文件不存在且不允许创建,返回错误
if !fileExists && !allowCreate {
return fmt.Errorf("数据库文件不存在: %s, 请先初始化.", fullPath)
}
// 确保目录存在
if err := os.MkdirAll(d.config.Path, 0755); err != nil {
return fmt.Errorf("创建数据库目录失败: %w", err)
}
// 连接参数, 开启外键约束(PRAGMA foreign_keys = ON)、WAL 模式、5秒超时
dsn := fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000",
fullPath)
engine, err := sql.Open("sqlite3", dsn)
if err != nil {
return fmt.Errorf("数据库打开失败: %w", err)
}
if err := engine.Ping(); err != nil {
engine.Close()
return fmt.Errorf("数据库连接失败: %w", err)
}
d.engine = engine
case "mysql":
// TODO: 实现 MySQL 连接逻辑
return fmt.Errorf("mysql 数据库连接未实现")
}
return nil
}
// Close 关闭数据库连接
func (d *DB) Close() error {
if d.engine != nil {
return d.engine.Close()
}
return nil
}
// GetEngine 获取数据库引擎(自动连接)
func (d *DB) GetEngine() (*sql.DB, error) {
// 如果还没有连接,自动连接(但不创建新文件)
if d.engine == nil {
if err := d.Connect(false); err != nil {
return nil, err
}
}
return d.engine, nil
}
// MustGetDB is a helper that panics on error (use in main/init only).
func MustGetDB() *DB {
db, err := GetDB()
if err != nil {
log.Fatalf("数据库初始化失败: %v", err)
}
return db
}
func GetDBConfig() (*config.DBConfig, error) {
cfg, err := config.LoadConfig()
if err != nil {
return nil, fmt.Errorf("数据库配置文件加载失败: %w", err)
}
return &cfg.DB, nil
}
func CheckDB() (*config.Config, error) {
cfg, err := config.LoadConfig()
if err != nil {
log.Warnf("加载配置失败: %v", err)
}
// 统一转为小写,避免用户输入错误
dbType := strings.ToLower(cfg.DB.Type)
// 打印配置(调试用)
log.Debugf("数据库类型: %s", dbType)
log.Debugf("数据库名称: %s", cfg.DB.Name)
log.Debugf("数据库路径: %s", cfg.DB.Path)
log.Debugf("数据库用户: %s", cfg.DB.User)
log.Debugf("数据库主机: %s", cfg.DB.Host)
log.Debugf("数据库套接字: %s", cfg.DB.Socket)
log.Debugf("数据库详细日志: %v", cfg.DB.Verbose)
// 支持 sqlitemysql的常见别名
isSQLite := dbType == "sqlite" || dbType == "sqlite3"
isMySQL := dbType == "mysql"
// 检查数据库类型,只允许 sqlite 和 mysql
if !isSQLite && !isMySQL {
log.Fatalf("不支持的数据库类型: %s(仅支持 sqlite、sqlite3、mysql)", dbType)
}
// 检查数据库路径是否存在
if isSQLite {
if _, err := os.Stat(cfg.DB.Path); os.IsNotExist(err) {
log.Warnf("SQLite 数据库路径 %s 不存在", cfg.DB.Path)
log.Warn("必须先执行 'sunhpc init database' 初始化数据库")
}
}
return cfg, nil
}

View File

@@ -1,637 +0,0 @@
package db
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"sunhpc/internal/log"
_ "github.com/mattn/go-sqlite3"
)
// 全局单例
var (
globalDB *DB
once sync.Once
)
// DB 核心数据库类 - 对应Rocks的Database类
type DB struct {
// 连接参数
dbUser string
dbPasswd string
dbHost string
dbName string
dbPath string
dbSocket string
verbose bool
forceInit bool
// 连接对象
engine *sql.DB // 连接池
conn *sql.Conn // 当前连接
results *sql.Rows // 当前结果集
// 线程本地存储模拟
sessions sync.Map
mu sync.RWMutex
}
// NewDB 创建新实例
func NewDB() *DB {
return &DB{
dbUser: "",
dbPasswd: "",
dbHost: "localhost",
dbName: "sunhpc",
dbPath: "/var/lib/sunhpc",
dbSocket: "/var/lib/sunhpc/mysql/mysql.sock",
verbose: false,
}
}
// ==================== 连接管理 ====================
// Connect 连接数据库
func (db *DB) Connect() error {
log.Debug("连接数据库...")
db.mu.Lock()
defer db.mu.Unlock()
log.Debug("检查 SUNHPCDEBUG 环境变量...")
if os.Getenv("SUNHPCDEBUG") != "" {
db.verbose = true
}
// 使用SQLite
dbFullPath := filepath.Join(db.dbPath, db.dbName+".db")
log.Debugf("数据库路径: %s", dbFullPath)
// 确保目录存在
log.Debug("确保数据库目录存在...")
os.MkdirAll(db.dbPath, 0755)
engine, err := sql.Open("sqlite3", dbFullPath+"?_foreign_keys=on&_journal_mode=WAL")
log.Debugf("打开数据库连接...")
if err != nil {
return fmt.Errorf("打开数据库失败: %v", err)
}
engine.SetMaxOpenConns(10)
engine.SetMaxIdleConns(5)
engine.SetConnMaxLifetime(time.Hour)
db.engine = engine
conn, err := engine.Conn(context.Background())
log.Debugf("获取数据库连接...")
if err != nil {
return fmt.Errorf("获取连接失败: %v", err)
}
db.conn = conn
// 初始化数据库表
if err := db.initSchema(); err != nil {
return fmt.Errorf("初始化数据库表失败: %v", err)
}
if db.verbose {
log.Infof("数据库连接成功: %s", dbFullPath)
}
return nil
}
// initSchema 初始化数据库表结构 - 所有表定义在这里
func (db *DB) initSchema() error {
log.Debug("初始化数据库表结构...")
// 检查 nodes 表是否已存在
var tableName string
err := db.engine.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='nodes'").Scan(&tableName)
if err == nil && !db.forceInit {
log.Debug("数据库表已存在,跳过初始化")
return nil
}
if db.forceInit {
log.Warn("强制重新初始化数据库表结构...")
} else {
log.Info("首次初始化数据库表结构...")
}
// 如果强制初始化,先删除所有表
if db.forceInit {
log.Info("删除现有表...")
dropSQLs := []string{
`DROP TABLE IF EXISTS resolvechain;`,
`DROP TABLE IF EXISTS hostselections;`,
`DROP TABLE IF EXISTS attributes;`,
`DROP TABLE IF EXISTS catindexes;`,
`DROP TABLE IF EXISTS categories;`,
`DROP TABLE IF EXISTS node_attrs;`,
`DROP TABLE IF EXISTS aliases;`,
`DROP TABLE IF EXISTS networks;`,
`DROP TABLE IF EXISTS subnets;`,
`DROP TABLE IF EXISTS software_installs;`,
`DROP TABLE IF EXISTS memberships;`,
`DROP TABLE IF EXISTS appliances;`,
`DROP TABLE IF EXISTS nodes;`,
}
for _, sql := range dropSQLs {
if _, err := db.engine.Exec(sql); err != nil {
log.Warnf("删除表失败: %v", err)
}
}
log.Info("现有表已删除")
}
// 开启事务
tx, err := db.engine.Begin()
if err != nil {
return fmt.Errorf("开启事务失败: %v", err)
}
// 使用exec执行每条SQL单独执行
sqls := []string{
// 创建表 - 注意创建顺序(先创建主表,再创建有外键的表)
`CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
rack INTEGER DEFAULT 0,
rank INTEGER DEFAULT 0,
membership_id INTEGER,
cpus INTEGER DEFAULT 0,
memory INTEGER DEFAULT 0,
disk INTEGER DEFAULT 0,
os TEXT,
kernel TEXT,
last_state_change DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);`,
`CREATE TABLE IF NOT EXISTS appliances (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
node_type TEXT DEFAULT 'compute'
);`,
`CREATE TABLE IF NOT EXISTS memberships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
appliance_id INTEGER,
FOREIGN KEY (appliance_id) REFERENCES appliances(id) ON DELETE SET NULL
);`,
`CREATE TABLE IF NOT EXISTS subnets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
network TEXT,
netmask TEXT,
gateway TEXT,
dns_zone TEXT,
is_private INTEGER DEFAULT 1
);`,
`CREATE TABLE IF NOT EXISTS networks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
name TEXT,
ip TEXT UNIQUE,
mac TEXT UNIQUE,
subnet_id INTEGER,
interface TEXT DEFAULT 'eth0',
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE,
FOREIGN KEY (subnet_id) REFERENCES subnets(id) ON DELETE SET NULL
);`,
`CREATE TABLE IF NOT EXISTS aliases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
name TEXT NOT NULL,
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE,
UNIQUE(node_id, name)
);`,
`CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);`,
`CREATE TABLE IF NOT EXISTS catindexes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category_id INTEGER NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
UNIQUE(name, category_id)
);`,
`CREATE TABLE IF NOT EXISTS attributes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
attr TEXT NOT NULL,
value TEXT,
category_id INTEGER NOT NULL,
catindex_id INTEGER NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
FOREIGN KEY (catindex_id) REFERENCES catindexes(id) ON DELETE CASCADE,
UNIQUE(attr, category_id, catindex_id)
);`,
`CREATE TABLE IF NOT EXISTS node_attrs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
attr TEXT NOT NULL,
value TEXT,
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE,
UNIQUE(node_id, attr)
);`,
`CREATE TABLE IF NOT EXISTS hostselections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
host_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
selection TEXT NOT NULL,
FOREIGN KEY (host_id) REFERENCES nodes(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
UNIQUE(host_id, category_id, selection)
);`,
`CREATE TABLE IF NOT EXISTS resolvechain (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
precedence INTEGER NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
UNIQUE(category_id, precedence)
);`,
`CREATE TABLE IF NOT EXISTS software_installs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
version TEXT,
install_type TEXT,
node_id INTEGER,
status TEXT,
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
installed_by TEXT,
FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE SET NULL
);`,
// 创建索引
`CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);`,
`CREATE INDEX IF NOT EXISTS idx_networks_ip ON networks(ip);`,
`CREATE INDEX IF NOT EXISTS idx_networks_mac ON networks(mac);`,
`CREATE INDEX IF NOT EXISTS idx_attributes_lookup ON attributes(attr, category_id, catindex_id);`,
`CREATE INDEX IF NOT EXISTS idx_node_attrs_lookup ON node_attrs(node_id, attr);`,
`CREATE INDEX IF NOT EXISTS idx_hostselections_host ON hostselections(host_id);`,
`CREATE INDEX IF NOT EXISTS idx_resolvechain_precedence ON resolvechain(precedence);`,
}
// 逐条执行SQL
for i, sql := range sqls {
if strings.TrimSpace(sql) == "" {
continue
}
log.Debugf("执行SQL[%d]: %s", i, strings.TrimSpace(strings.Split(sql, "\n")[0]))
_, err := tx.Exec(sql)
if err != nil {
tx.Rollback()
return fmt.Errorf("执行SQL[%d]失败: %v\nSQL: %s", i, err, sql)
}
}
// 提交事务
if err := tx.Commit(); err != nil {
return fmt.Errorf("提交事务失败: %v", err)
}
log.Info("数据库表结构创建成功")
// 插入默认数据
return db.insertDefaultData()
}
// insertDefaultData 插入默认数据
func (db *DB) insertDefaultData() error {
log.Debug("插入默认数据...")
// 默认类别
categories := []string{"global", "host", "os", "appliance", "network"}
for _, cat := range categories {
_, err := db.engine.Exec(
"INSERT OR IGNORE INTO categories (name) VALUES (?)",
cat,
)
if err != nil {
return err
}
}
log.Debug("插入默认类别索引...")
// 默认类别索引
catIndexes := []struct {
catName string
idxName string
}{
{"global", "global"},
{"os", "linux"},
{"network", "private"},
}
for _, ci := range catIndexes {
_, err := db.engine.Exec(`
INSERT OR IGNORE INTO catindexes (name, category_id)
SELECT ?, id FROM categories WHERE name = ?
`, ci.idxName, ci.catName)
if err != nil {
return err
}
}
log.Debug("插入默认解析链优先级...")
// 默认解析链优先级
precedence := []struct {
catName string
level int
}{
{"global", 1},
{"os", 2},
{"appliance", 3},
{"host", 4},
{"network", 5},
}
for _, p := range precedence {
_, err := db.engine.Exec(`
INSERT OR IGNORE INTO resolvechain (category_id, precedence)
SELECT id, ? FROM categories WHERE name = ?
`, p.level, p.catName)
if err != nil {
return err
}
}
log.Debug("插入默认设备类型...")
// 默认设备类型
appliances := []struct {
name string
desc string
typ string
}{
{"frontend", "管理节点", "master"},
{"compute", "计算节点", "compute"},
{"login", "登录节点", "login"},
{"storage", "存储节点", "storage"},
}
for _, a := range appliances {
_, err := db.engine.Exec(
"INSERT OR IGNORE INTO appliances (name, description, node_type) VALUES (?, ?, ?)",
a.name, a.desc, a.typ,
)
if err != nil {
return err
}
}
log.Debug("插入默认数据完成...")
return nil
}
// ==================== 核心查询方法 ====================
// Execute 执行SQL语句 - 对应Rocks的execute()
func (db *DB) Execute(query string, args ...interface{}) (int64, error) {
db.mu.RLock()
conn := db.conn
verbose := db.verbose
db.mu.RUnlock()
if conn == nil {
return 0, fmt.Errorf("没有活动数据库连接")
}
if verbose {
log.Debugf("执行SQL: %s %v", query, args)
}
// 判断SQL类型
upperQuery := strings.ToUpper(strings.TrimSpace(query))
isSelect := strings.HasPrefix(upperQuery, "SELECT")
if isSelect {
// SELECT 查询使用 QueryContext
rows, err := conn.QueryContext(context.Background(), query, args...)
if err != nil {
// 尝试重连一次
db.RenewConnection()
db.mu.RLock()
conn = db.conn
db.mu.RUnlock()
rows, err = conn.QueryContext(context.Background(), query, args...)
}
if err != nil {
return 0, err
}
// 关闭旧结果
db.mu.Lock()
if db.results != nil {
db.results.Close()
}
db.results = rows
db.mu.Unlock()
return 0, nil
} else {
// INSERT/UPDATE/DELETE 使用 Exec自动提交
result, err := conn.ExecContext(context.Background(), query, args...)
if err != nil {
// 尝试重连一次
db.RenewConnection()
db.mu.RLock()
conn = db.conn
db.mu.RUnlock()
result, err = conn.ExecContext(context.Background(), query, args...)
}
if err != nil {
return 0, err
}
// 获取影响行数
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, err
}
if verbose {
log.Debugf("影响行数: %d", rowsAffected)
}
return rowsAffected, nil
}
}
// FetchOne 获取一行 - 对应Rocks的fetchone()
// 返回map[string]interface{}格式key为列名
func (db *DB) FetchOne() (map[string]interface{}, error) {
db.mu.RLock()
results := db.results
db.mu.RUnlock()
if results == nil {
return nil, nil
}
if !results.Next() {
return nil, nil
}
columns, err := results.Columns()
if err != nil {
return nil, err
}
values := make([]interface{}, len(columns))
scanArgs := make([]interface{}, len(columns))
for i := range values {
scanArgs[i] = &values[i]
}
err = results.Scan(scanArgs...)
if err != nil {
return nil, err
}
row := make(map[string]interface{})
for i, col := range columns {
val := values[i]
if b, ok := val.([]byte); ok {
row[col] = string(b)
} else {
row[col] = val
}
}
return row, nil
}
// FetchAll 获取所有行 - 对应Rocks的fetchall()
// 返回[]map[string]interface{}格式
func (db *DB) FetchAll() ([]map[string]interface{}, error) {
db.mu.RLock()
results := db.results
db.mu.RUnlock()
if results == nil {
return nil, nil
}
columns, err := results.Columns()
if err != nil {
return nil, err
}
var rows []map[string]interface{}
for results.Next() {
values := make([]interface{}, len(columns))
scanArgs := make([]interface{}, len(columns))
for i := range values {
scanArgs[i] = &values[i]
}
err = results.Scan(scanArgs...)
if err != nil {
return nil, err
}
row := make(map[string]interface{})
for i, col := range columns {
val := values[i]
if b, ok := val.([]byte); ok {
row[col] = string(b)
} else {
row[col] = val
}
}
rows = append(rows, row)
}
return rows, nil
}
// ==================== 连接维护 ====================
// RenewConnection 续期连接
func (db *DB) RenewConnection() error {
db.mu.Lock()
defer db.mu.Unlock()
if db.conn != nil {
db.conn.Close()
}
conn, err := db.engine.Conn(context.Background())
if err != nil {
return err
}
db.conn = conn
return nil
}
// Close 关闭连接
func (db *DB) Close() error {
db.mu.Lock()
defer db.mu.Unlock()
if db.results != nil {
db.results.Close()
db.results = nil
}
if db.conn != nil {
db.conn.Close()
db.conn = nil
}
if db.engine != nil {
return db.engine.Close()
}
return nil
}
// CloseConnection 只关闭当前连接,不关闭连接池
func (db *DB) CloseConnection() error {
db.mu.Lock()
defer db.mu.Unlock()
if db.results != nil {
db.results.Close()
db.results = nil
}
if db.conn != nil {
db.conn.Close()
db.conn = nil
}
return nil
}
// ==================== 单例模式 ====================
var (
instanceConfigured bool
instanceDBPath string
instanceDBName string
)

View File

@@ -1,294 +0,0 @@
// Package db defines the database schema.
package db
// CurrentSchemaVersion returns the current schema version (for migrations)
func CurrentSchemaVersion() int {
return 1
}
// CreateTableStatements returns a list of CREATE TABLE statements.
func CreateTableStatements() []string {
return []string{
createAliasesTable(),
createAttributesTable(),
createBootactionTable(),
createDistributionsTable(),
createFirewallsTable(),
createNetworksTable(),
createPartitionsTable(),
createPublicKeysTable(),
createSoftwareTable(),
createNodesTable(),
createSubnetsTable(),
createTrg_nodes_before_delete(),
}
}
// DropTableOrder returns table names in reverse dependency order for safe DROP.
func DropTableOrder() []string {
return []string{
"aliases",
"attributes",
"bootactions",
"distributions",
"firewalls",
"networks",
"partitions",
"publickeys",
"software",
"nodes",
"subnets",
}
}
func DropTriggerStatements() []string {
return []string{
"trg_nodes_before_delete",
}
}
// --- Private DDL Functions ---
func createAliasesTable() string {
return `
CREATE TABLE IF NOT EXISTS aliases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
alias TEXT NOT NULL,
CONSTRAINT fk_aliases_node FOREIGN KEY(node_id) REFERENCES nodes(id),
UNIQUE(node_id, alias)
);
create index if not exists idx_aliases_node on aliases (node_id);
`
}
func createAttributesTable() string {
return `
CREATE TABLE IF NOT EXISTS attributes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
attr TEXT NOT NULL,
value TEXT,
shadow TEXT,
CONSTRAINT fk_attributes_node FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_attributes_node on attributes (node_id);
`
}
func createBootactionTable() string {
return `
CREATE TABLE IF NOT EXISTS bootactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
action TEXT,
kernel TEXT,
initrd TEXT,
cmdline TEXT,
CONSTRAINT fk_bootactions_node FOREIGN KEY(node_id) REFERENCES nodes(id),
UNIQUE(node_id)
);
create index if not exists idx_bootactions_node on bootactions (node_id);
`
}
func createDistributionsTable() string {
return `
CREATE TABLE IF NOT EXISTS distributions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
name TEXT NOT NULL,
version TEXT,
lang TEXT,
os_release TEXT,
constraint distributions_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_distributions_node on distributions (node_id);
`
}
func createFirewallsTable() string {
return `
CREATE TABLE IF NOT EXISTS firewalls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
rulename TEXT NOT NULL,
rulesrc TEXT NOT NULL,
insubnet INTEGER,
outsubnet INTEGER,
service TEXT,
protocol TEXT,
action TEXT,
chain TEXT,
flags TEXT,
comment TEXT,
constraint firewalls_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_firewalls_node on firewalls (node_id);
`
}
func createNetworksTable() string {
return `
CREATE TABLE IF NOT EXISTS networks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
subnet_id INTEGER,
mac TEXT,
ip TEXT,
name TEXT,
device TEXT,
module TEXT,
vlanid INTEGER,
options TEXT,
channel TEXT,
disable_kvm INTEGER NOT NULL DEFAULT 0 CHECK (disable_kvm IN (0, 1)),
constraint networks_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id),
constraint networks_subnets_fk FOREIGN KEY(subnet_id) REFERENCES subnets(id)
);
create index if not exists idx_networks_node on networks (node_id);
`
}
func createNodesTable() string {
return `
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cpus INTEGER NOT NULL,
rack INTEGER NOT NULL,
rank INTEGER NOT NULL,
arch TEXT,
os TEXT,
runaction TEXT,
installaction TEXT
);
create index if not exists idx_nodes_name on nodes (name);
`
}
func createPartitionsTable() string {
return `
CREATE TABLE IF NOT EXISTS partitions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
device TEXT NOT NULL,
formatflags TEXT NOT NULL,
fstype TEXT NOT NULL,
mountpoint TEXT NOT NULL,
partitionflags TEXT NOT NULL,
partitionid TEXT NOT NULL,
partitionsize TEXT NOT NULL,
sectorstart TEXT NOT NULL,
constraint partitions_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_partitions_node on partitions (node_id);
`
}
func createPublicKeysTable() string {
return `
CREATE TABLE IF NOT EXISTS publickeys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
publickey TEXT NOT NULL,
description TEXT,
constraint publickeys_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_publickeys_node on publickeys (node_id);
`
}
func createSubnetsTable() string {
return `
CREATE TABLE IF NOT EXISTS subnets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
dnszone TEXT NOT NULL,
subnet TEXT NOT NULL,
netmask TEXT NOT NULL,
mtu INTEGER NOT NULL DEFAULT 1500,
servedns INTEGER NOT NULL DEFAULT 0 CHECK (servedns IN (0, 1)),
UNIQUE(name, dnszone)
);`
}
func createSoftwareTable() string {
return `
CREATE TABLE IF NOT EXISTS software (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
pathversion TEXT,
fullversion TEXT,
description TEXT,
website TEXT,
license TEXT,
install_method TEXT NOT NULL CHECK (install_method IN (
'source',
'binary',
'rpm',
'docker',
'apptainer',
'conda',
'mamba',
'spack',
'tarball',
'zipball',
'pip',
'npm',
'custom'
)),
-- 源码编译相关参数
source_url TEXT, -- 源码下载地址
source_checksum TEXT, -- 源码校验和
source_checksum_type TEXT NOT NULL CHECK (source_checksum_type IN (
'md5',
'sha1',
'sha256',
'sha512'
)),
build_dependencies TEXT, -- 编译依赖(JSON格式)
configure_params TEXT, -- 配置参数(JSON格式)
make_params TEXT, -- make参数(JSON格式)
make_install_params TEXT, -- make install参数(JSON格式)
-- 安装路径参数
install_path TEXT NOT NULL, -- 安装路径
env_vars TEXT, -- 环境变量(JSON格式)
-- 状态信息
is_installed INTEGER NOT NULL DEFAULT 0 CHECK (is_installed IN (0, 1)), -- 是否安装
install_date TEXT, -- 安装日期
updated_date TEXT, -- 更新日期
install_user TEXT, -- 安装用户
notes TEXT, -- 安装备注
-- 元数据
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 更新时间
UNIQUE(name)
);
create index if not exists idx_software_name on software (name);
create index if not exists idx_software_install_method on software (install_method);
create index if not exists idx_software_is_installed on software (is_installed);
`
}
func createTrg_nodes_before_delete() string {
return `
CREATE TRIGGER IF NOT EXISTS trg_nodes_before_delete
BEFORE DELETE ON nodes
FOR EACH ROW
BEGIN
-- 先删除子表的关联记录
DELETE FROM aliases WHERE node_id = OLD.id;
DELETE FROM attributes WHERE node_id = OLD.id;
DELETE FROM bootactions WHERE node_id = OLD.id;
DELETE FROM distributions WHERE node_id = OLD.id;
DELETE FROM firewalls WHERE node_id = OLD.id;
DELETE FROM networks WHERE node_id = OLD.id;
DELETE FROM partitions WHERE node_id = OLD.id;
DELETE FROM publickeys WHERE node_id = OLD.id;
END;
`
}

View File

@@ -1,7 +1,7 @@
package soft
import (
"sunhpc/internal/log"
log "sunhpc/pkg/logger"
)
// InstallContext 安装上下文,包含所有命令行参数

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"sunhpc/internal/log"
log "sunhpc/pkg/logger"
"sunhpc/pkg/utils"
)

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"sunhpc/internal/log"
log "sunhpc/pkg/logger"
"sunhpc/pkg/utils"
)

View File

@@ -1,344 +0,0 @@
package log
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/fatih/color"
)
// 日志级别
type Level int
const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
FatalLevel
)
// 级别名称
var levelNames = map[Level]string{
DebugLevel: "DEBUG",
InfoLevel: "INFO",
WarnLevel: "WARN",
ErrorLevel: "ERROR",
FatalLevel: "FATAL",
}
// 级别简写
var levelShort = map[Level]string{
DebugLevel: "[d]",
InfoLevel: "[i]",
WarnLevel: "[w]",
ErrorLevel: "[e]",
FatalLevel: "[f]",
}
// 级别颜色
var levelColor = map[Level]func(format string, a ...interface{}) string{
DebugLevel: color.CyanString, // 青色
InfoLevel: color.GreenString, // 绿色
WarnLevel: color.YellowString, // 黄色
ErrorLevel: color.RedString, // 红色
FatalLevel: color.MagentaString, // 品红
}
// Logger 日志器结构体
type Logger struct {
mu sync.Mutex
consoleOut io.Writer // 控制台输出
fileOut io.Writer // 文件输出
minLevel Level // 最小输出级别
showColor bool // 是否显示颜色
showCaller bool // 是否显示调用者信息
callerSkip int // 调用者跳过的层级
timeFormat string // 时间格式
}
// 默认日志器实例
var defaultLogger *Logger
const (
defaultTimeFormat = "2006-01-02 15:04:05"
logFile = "/var/log/sunhpc/sunhpc.log"
)
// Init 初始化日志系统
func Init(verbose bool) {
// 确保日志目录存在
if err := os.MkdirAll(filepath.Dir(logFile), 0755); err != nil {
fmt.Fprintf(os.Stderr, "创建日志目录失败: %v\n", err)
os.Exit(1)
}
// 打开日志文件
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "打开日志文件失败: %v\n", err)
os.Exit(1)
}
// 控制台输出
consoleOut := os.Stdout
// 创建日志器
defaultLogger = &Logger{
consoleOut: consoleOut,
fileOut: file,
minLevel: InfoLevel,
showColor: true,
showCaller: false,
callerSkip: 2,
timeFormat: defaultTimeFormat,
}
// 详细模式下显示调试信息
if verbose {
defaultLogger.minLevel = DebugLevel
defaultLogger.showCaller = true
}
// 初始化颜色支持
if runtime.GOOS == "windows" {
color.NoColor = false
}
}
// log 核心日志输出方法
func (l *Logger) log(level Level, format string, args ...interface{}) {
if level < l.minLevel {
return
}
l.mu.Lock()
defer l.mu.Unlock()
// 生成时间戳
timestamp := time.Now().Format(l.timeFormat)
// 获取调用者信息
caller := ""
if l.showCaller {
_, file, line, ok := runtime.Caller(l.callerSkip)
if ok {
// 只保留文件名和行号
file = filepath.Base(file)
caller = fmt.Sprintf(" %s:%d", file, line)
}
}
// 格式化消息
var message string
if format == "" {
message = fmt.Sprint(args...)
} else {
message = fmt.Sprintf(format, args...)
}
// ---- 控制台输出(带颜色和简写)----
if l.consoleOut != nil {
// 获取级别简写
shortPrefix := levelShort[level]
// 构建控制台行
var consoleLine string
if l.showColor {
// 带颜色输出 - 简写有颜色,时间戳灰色
colorFunc := levelColor[level]
consoleLine = fmt.Sprintf("%s %s %s",
color.HiBlackString(timestamp), // 时间戳灰色
colorFunc(shortPrefix), // 级别简写彩色
message) // 消息普通颜色
} else {
// 不带颜色输出
consoleLine = fmt.Sprintf("%s %s %s",
timestamp,
shortPrefix,
message)
}
// 添加调用者信息(灰色)
if caller != "" {
if l.showColor {
consoleLine += fmt.Sprintf(" %s", color.HiBlackString(caller))
} else {
consoleLine += fmt.Sprintf(" %s", caller)
}
}
fmt.Fprintln(l.consoleOut, consoleLine)
}
// ---- 文件输出(完整格式)----
if l.fileOut != nil {
// 获取级别全名
levelName := levelNames[level]
// 文件使用完整格式:时间 [级别] 消息 调用者
fileLine := fmt.Sprintf("%s [%s] %s%s\n",
timestamp,
levelName,
message,
caller)
fmt.Fprint(l.fileOut, fileLine)
}
// 致命错误退出程序
if level == FatalLevel {
os.Exit(1)
}
}
// 全局日志函数
// Debug 调试日志
func Debug(args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(DebugLevel, "", args...)
}
}
// Debugf 格式化调试日志
func Debugf(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(DebugLevel, format, args...)
}
}
// Info 信息日志
func Info(args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(InfoLevel, "", args...)
}
}
// Infof 格式化信息日志
func Infof(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(InfoLevel, format, args...)
}
}
// Warn 警告日志
func Warn(args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(WarnLevel, "", args...)
}
}
// Warnf 格式化警告日志
func Warnf(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(WarnLevel, format, args...)
}
}
// Error 错误日志
func Error(args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(ErrorLevel, "", args...)
}
}
// Errorf 格式化错误日志
func Errorf(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(ErrorLevel, format, args...)
}
}
// Fatal 致命错误日志,输出后退出程序
func Fatal(args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(FatalLevel, "", args...)
}
}
// Fatalf 格式化致命错误日志,输出后退出程序
func Fatalf(format string, args ...interface{}) {
if defaultLogger != nil {
defaultLogger.log(FatalLevel, format, args...)
}
}
// Writer 返回一个 io.Writer可将子命令的输出写入日志Debug级别
func Writer() *io.PipeWriter {
r, w := io.Pipe()
go func() {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
if n > 0 {
Debug(string(buf[:n]))
}
if err != nil {
break
}
}
}()
return w
}
// SetLevel 设置日志级别
func SetLevel(level Level) {
if defaultLogger != nil {
defaultLogger.mu.Lock()
defer defaultLogger.mu.Unlock()
defaultLogger.minLevel = level
}
}
// EnableColor 启用/禁用颜色输出
func EnableColor(enable bool) {
if defaultLogger != nil {
defaultLogger.mu.Lock()
defer defaultLogger.mu.Unlock()
defaultLogger.showColor = enable
}
}
// EnableCaller 启用/禁用调用者信息
func EnableCaller(enable bool) {
if defaultLogger != nil {
defaultLogger.mu.Lock()
defer defaultLogger.mu.Unlock()
defaultLogger.showCaller = enable
}
}
// SetTimeFormat 设置时间格式
func SetTimeFormat(format string) {
if defaultLogger != nil {
defaultLogger.mu.Lock()
defer defaultLogger.mu.Unlock()
defaultLogger.timeFormat = format
}
}
// Sync 同步日志文件
func Sync() {
if defaultLogger != nil && defaultLogger.fileOut != nil {
if f, ok := defaultLogger.fileOut.(*os.File); ok {
f.Sync()
}
}
}
// Close 关闭日志文件
func Close() error {
if defaultLogger != nil && defaultLogger.fileOut != nil {
if f, ok := defaultLogger.fileOut.(*os.File); ok {
return f.Close()
}
}
return nil
}

View File

@@ -1,5 +0,0 @@
package model
type DBConfig struct {
ForceDB bool
}

View File

@@ -1,54 +0,0 @@
// internal/templating/embedded.go
package templating
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sunhpc/tmpls"
"gopkg.in/yaml.v3"
)
// ListEmbeddedTemplates 返回所有内置模板名称(不含路径和扩展名)
func ListEmbeddedTemplates() ([]string, error) {
entries, err := fs.ReadDir(tmpls.FS, ".")
if err != nil {
return nil, err
}
var names []string
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".yaml" {
continue
}
names = append(names, entry.Name()[:len(entry.Name())-5]) // 去掉 .yaml
}
return names, nil
}
// LoadEmbeddedTemplate 从二进制加载内置模板
func LoadEmbeddedTemplate(name string) (*Template, error) {
data, err := tmpls.FS.ReadFile(name + ".yaml")
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("内置模板 '%s' 不存在", name)
}
return nil, err
}
var tmpl Template
if err := yaml.Unmarshal(data, &tmpl); err != nil {
return nil, fmt.Errorf("解析内置模板失败: %w", err)
}
return &tmpl, nil
}
// DumpEmbeddedTemplateToFile 将内置模板写入文件
func DumpEmbeddedTemplateToFile(name, outputPath string) error {
data, err := tmpls.FS.ReadFile(name + ".yaml")
if err != nil {
return fmt.Errorf("找不到内置模板 '%s': %w", name, err)
}
return os.WriteFile(outputPath, data, 0644)
}

View File

@@ -1,104 +0,0 @@
// internal/templating/engine.go
package templating
import (
"bytes"
"fmt"
"os"
"path/filepath"
"text/template"
"gopkg.in/yaml.v3"
)
// LoadTemplate 从文件加载 YAML 模板
func LoadTemplate(path string) (*Template, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("无法读取模板文件 %s: %w", path, err)
}
var tmpl Template
if err := yaml.Unmarshal(data, &tmpl); err != nil {
return nil, fmt.Errorf("YAML 解析失败: %w", err)
}
return &tmpl, nil
}
// Render 渲染模板为具体操作
func (t *Template) Render(ctx Context) (map[string][]RenderedStep, error) {
result := make(map[string][]RenderedStep)
for stageName, steps := range t.Stages {
var renderedSteps []RenderedStep
for _, step := range steps {
// 处理 condition
if step.Condition != "" {
condTmpl, err := template.New("condition").Parse(step.Condition)
if err != nil {
return nil, fmt.Errorf("条件模板语法错误: %w", err)
}
var buf bytes.Buffer
if err := condTmpl.Execute(&buf, ctx); err != nil {
return nil, fmt.Errorf("执行条件模板失败: %w", err)
}
if buf.String() == "" {
continue // 条件不满足,跳过
}
}
// 渲染 content
contentTmpl, err := template.New("content").Parse(step.Content)
if err != nil {
return nil, fmt.Errorf("内容模板语法错误: %w", err)
}
var buf bytes.Buffer
if err := contentTmpl.Execute(&buf, ctx); err != nil {
return nil, fmt.Errorf("执行内容模板失败: %w", err)
}
renderedSteps = append(renderedSteps, RenderedStep{
Type: step.Type,
Path: step.Path,
Content: buf.String(),
})
}
result[stageName] = renderedSteps
}
return result, nil
}
// RenderedStep 是渲染后的步骤
type RenderedStep struct {
Type string
Path string
Content string
}
// WriteFiles 将 file 类型步骤写入磁盘
func WriteFiles(steps []RenderedStep, rootDir string) error {
for _, s := range steps {
if s.Type != "file" {
continue
}
fullPath := s.Path
if !filepath.IsAbs(s.Path) {
fullPath = filepath.Join(rootDir, s.Path)
}
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return err
}
if err := os.WriteFile(fullPath, []byte(s.Content), 0644); err != nil {
return fmt.Errorf("写入文件 %s 失败: %w", fullPath, err)
}
}
return nil
}
// PrintScripts 打印 script 内容(安全起见,先不自动执行)
func PrintScripts(steps []RenderedStep) {
for _, s := range steps {
if s.Type == "script" {
fmt.Printf("# --- 脚本开始 ---\n%s\n# --- 脚本结束 ---\n", s.Content)
}
}
}

View File

@@ -1,38 +0,0 @@
package templating
// Template 是 YAML 模板的顶层结构
type Template struct {
Description string `yaml:"description,omitempty"`
Copyright string `yaml:"copyright,omitempty"`
Stages map[string][]Step `yaml:"stages"`
}
// Step 表示一个操作步骤
type Step struct {
Type string `yaml:"type"` // "file" 或 "script"
Path string `yaml:"path,omitempty"` // 文件路径(仅 type=file
Content string `yaml:"content"` // 多行内容
Condition string `yaml:"condition,omitempty"` // 条件表达式Go template
}
// Context 是渲染模板时的上下文数据
type Context struct {
Node NodeInfo `json:"node"`
Cluster ClusterInfo `json:"cluster"`
}
// NodeInfo 节点信息
type NodeInfo struct {
Hostname string `json:"hostname"`
OldHostname string `json:"old_hostname,omitempty"`
Domain string `json:"domain"`
IP string `json:"ip"`
}
// ClusterInfo 集群信息
type ClusterInfo struct {
Name string `json:"name"`
Domain string `json:"domain"`
AdminEmail string `json:"admin_email"`
TimeZone string `json:"time_zone"`
}