重构架构
This commit is contained in:
46
internal/cli/init/cfg.go
Normal file
46
internal/cli/init/cfg.go
Normal 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
53
internal/cli/init/db.go
Normal 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
19
internal/cli/init/init.go
Normal 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
69
internal/cli/root.go
Normal 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()
|
||||
}
|
||||
41
internal/cli/soft/install.go
Normal file
41
internal/cli/soft/install.go
Normal 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
18
internal/cli/soft/soft.go
Normal 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
58
internal/cli/tmpl/dump.go
Normal 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
17
internal/cli/tmpl/init.go
Normal 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
|
||||
}
|
||||
96
internal/cli/tmpl/render.go
Normal file
96
internal/cli/tmpl/render.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
// 支持 sqlite,mysql的常见别名
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
`
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package soft
|
||||
|
||||
import (
|
||||
"sunhpc/internal/log"
|
||||
log "sunhpc/pkg/logger"
|
||||
)
|
||||
|
||||
// InstallContext 安装上下文,包含所有命令行参数
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sunhpc/internal/log"
|
||||
log "sunhpc/pkg/logger"
|
||||
"sunhpc/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sunhpc/internal/log"
|
||||
log "sunhpc/pkg/logger"
|
||||
"sunhpc/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package model
|
||||
|
||||
type DBConfig struct {
|
||||
ForceDB bool
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user