重构架构
This commit is contained in:
@@ -1,44 +0,0 @@
|
|||||||
package initcmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sunhpc/internal/auth"
|
|
||||||
"sunhpc/internal/db"
|
|
||||||
"sunhpc/internal/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewDatabaseCmd() *cobra.Command {
|
|
||||||
var force bool
|
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "database",
|
|
||||||
Short: "初始化数据库",
|
|
||||||
Long: `初始化SQLite数据库,创建所有表结构和默认数据。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
sunhpc init database # 初始化数据库
|
|
||||||
sunhpc init database --force # 强制重新初始化`,
|
|
||||||
|
|
||||||
Annotations: map[string]string{
|
|
||||||
"skip-db-check": "true", // 标记此命令跳过数据库检查
|
|
||||||
},
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := auth.RequireRoot(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("初始化数据库...")
|
|
||||||
|
|
||||||
dbInst := db.MustGetDB() // panic if fail (ok for CLI tool)
|
|
||||||
if err := dbInst.InitSchema(force); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("数据库初始化完成")
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制重新初始化")
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package initcmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 仅定义 Cmd, 注册子命令,只负责组装命令树,尽量不包含业务逻辑
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "init",
|
|
||||||
Short: "初始化集群配置",
|
|
||||||
Long: "初始化 SunHPC 配置文件、数据库、系统参数及相关服务",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// 注册所有子命令(通过工厂函数创建, 例如 DatabaseCmd())
|
|
||||||
Cmd.AddCommand(NewDatabaseCmd())
|
|
||||||
Cmd.AddCommand(NewConfigCmd())
|
|
||||||
}
|
|
||||||
9
cmd/insert/main.go
Normal file
9
cmd/insert/main.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("✓ 计算节点添加成功")
|
||||||
|
}
|
||||||
71
cmd/root.go
71
cmd/root.go
@@ -1,71 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
initcmd "sunhpc/cmd/init"
|
|
||||||
"sunhpc/cmd/soft"
|
|
||||||
"sunhpc/cmd/tmpl"
|
|
||||||
"sunhpc/internal/auth"
|
|
||||||
"sunhpc/internal/db"
|
|
||||||
"sunhpc/internal/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfgFile string
|
|
||||||
verbose bool
|
|
||||||
noColor bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "sunhpc",
|
|
||||||
Short: "SunHPC - HPC集群一体化运维工具",
|
|
||||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
// 初始化日志(verbose=false 不显示调试信息)
|
|
||||||
log.Init(verbose)
|
|
||||||
|
|
||||||
// 是否禁用颜色
|
|
||||||
log.EnableColor(!noColor)
|
|
||||||
|
|
||||||
log.Debugf("当前命令 Annotations: %+v", cmd.Annotations)
|
|
||||||
|
|
||||||
_, err := db.CheckDB()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("数据库检查失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 需要 root 权限
|
|
||||||
if cmd.Annotations["require-root"] == "true" {
|
|
||||||
if err := auth.RequireRoot(); err != nil {
|
|
||||||
log.Fatalf("需要 root 权限: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("当前命令: %s", cmd.Name())
|
|
||||||
log.Debugf("详细模式: %v", verbose)
|
|
||||||
log.Debugf("禁用颜色: %v", noColor)
|
|
||||||
},
|
|
||||||
PersistentPostRun: func(cmd *cobra.Command, args []string) {
|
|
||||||
// 同步日志
|
|
||||||
log.Sync()
|
|
||||||
log.Close()
|
|
||||||
},
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
cmd.Help()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() error {
|
|
||||||
return rootCmd.Execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (默认为 /etc/sunhpc/sunhpc.yaml)")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "启用详细日志输出")
|
|
||||||
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "禁用彩色输出")
|
|
||||||
|
|
||||||
// 注册一级子命令下的子命令树
|
|
||||||
rootCmd.AddCommand(initcmd.Cmd)
|
|
||||||
rootCmd.AddCommand(soft.Cmd)
|
|
||||||
rootCmd.AddCommand(tmpl.Cmd)
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// cmd/soft/install.go
|
|
||||||
package soft
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sunhpc/internal/auth"
|
|
||||||
"sunhpc/internal/log"
|
|
||||||
"sunhpc/internal/soft"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
installType string // --type, -t
|
|
||||||
srcPath string // --src-path, -s
|
|
||||||
binPath string // --bin-path, -b
|
|
||||||
prefix string // --prefix, -p
|
|
||||||
version string // --version, -v
|
|
||||||
forceInstall bool // --force, -f
|
|
||||||
dryRun bool // --dry-run, -n
|
|
||||||
keepSource bool // --keep-source, -k
|
|
||||||
jobs int // --jobs, -j
|
|
||||||
offlineMode bool // --offline, -o
|
|
||||||
)
|
|
||||||
|
|
||||||
var installCmd = &cobra.Command{
|
|
||||||
Use: "install <software>",
|
|
||||||
Short: "安装软件",
|
|
||||||
Long: `安装指定的软件包,支持多种安装方式。
|
|
||||||
|
|
||||||
安装类型:
|
|
||||||
source - 从源码编译安装
|
|
||||||
binary - 从二进制压缩包安装
|
|
||||||
rpm - 通过 RPM 包管理器安装
|
|
||||||
deb - 通过 APT 包管理器安装
|
|
||||||
|
|
||||||
示例:
|
|
||||||
sunhpc soft install vasp --type source --src-path /tmp/vasp.tar.gz
|
|
||||||
sunhpc soft install openmpi --type binary --bin-path openmpi.tar.gz -p /opt/openmpi
|
|
||||||
sunhpc soft install htop --type rpm --force
|
|
||||||
sunhpc soft install nginx --type deb --dry-run`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := auth.RequireRoot(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
software := args[0]
|
|
||||||
|
|
||||||
if dryRun {
|
|
||||||
log.Infof("[干运行] 将要安装 %s", software)
|
|
||||||
log.Infof(" 安装类型: %s", installType)
|
|
||||||
if srcPath != "" {
|
|
||||||
log.Infof(" 源码路径: %s", srcPath)
|
|
||||||
}
|
|
||||||
if binPath != "" {
|
|
||||||
log.Infof(" 二进制包: %s", binPath)
|
|
||||||
}
|
|
||||||
if prefix != "" {
|
|
||||||
log.Infof(" 安装路径: %s", prefix)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &soft.InstallContext{
|
|
||||||
Force: forceInstall,
|
|
||||||
DryRun: dryRun,
|
|
||||||
KeepSource: keepSource,
|
|
||||||
Jobs: jobs,
|
|
||||||
Offline: offlineMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch installType {
|
|
||||||
case "source":
|
|
||||||
return soft.InstallFromSource(software, srcPath, prefix, version, ctx)
|
|
||||||
case "binary":
|
|
||||||
return soft.InstallFromBinary(software, binPath, prefix, ctx)
|
|
||||||
case "rpm", "deb":
|
|
||||||
return soft.InstallFromPackage(software, installType, ctx)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("不支持的安装类型: %s", installType)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// 必选参数
|
|
||||||
installCmd.Flags().StringVarP(&installType, "type", "t", "", "安装类型: source/binary/rpm/deb")
|
|
||||||
installCmd.MarkFlagRequired("type")
|
|
||||||
|
|
||||||
// 路径参数
|
|
||||||
installCmd.Flags().StringVarP(&srcPath, "src-path", "s", "", "源码路径或URL")
|
|
||||||
installCmd.Flags().StringVarP(&binPath, "bin-path", "b", "", "二进制压缩包路径")
|
|
||||||
installCmd.Flags().StringVarP(&prefix, "prefix", "p", "/opt/sunhpc/software", "安装路径")
|
|
||||||
|
|
||||||
// 版本参数
|
|
||||||
installCmd.Flags().StringVarP(&version, "version", "v", "", "软件版本号")
|
|
||||||
|
|
||||||
// 行为控制
|
|
||||||
installCmd.Flags().BoolVarP(&forceInstall, "force", "f", false, "强制安装,覆盖已有版本")
|
|
||||||
installCmd.Flags().BoolVarP(&dryRun, "dry-run", "n", false, "仅显示将要执行的操作")
|
|
||||||
installCmd.Flags().BoolVarP(&keepSource, "keep-source", "k", false, "保留源码文件")
|
|
||||||
installCmd.Flags().IntVarP(&jobs, "jobs", "j", 4, "编译线程数")
|
|
||||||
installCmd.Flags().BoolVarP(&offlineMode, "offline", "o", false, "离线模式,不联网下载")
|
|
||||||
|
|
||||||
// 参数互斥
|
|
||||||
installCmd.MarkFlagsMutuallyExclusive("src-path", "bin-path")
|
|
||||||
installCmd.MarkFlagsOneRequired("src-path", "bin-path")
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package soft
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "soft",
|
|
||||||
Short: "软件包管理",
|
|
||||||
Long: "安装、卸载、编译软件,支持源码、RPM、DEB、二进制压缩包",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Cmd.AddCommand(installCmd)
|
|
||||||
// 后续可添加 remove、list 等子命令
|
|
||||||
}
|
|
||||||
13
cmd/sunhpc/main.go
Normal file
13
cmd/sunhpc/main.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sunhpc/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rootCmd := cli.NewRootCmd()
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
cmd/sunhpc/sunhpc
Executable file
BIN
cmd/sunhpc/sunhpc
Executable file
Binary file not shown.
@@ -1,16 +0,0 @@
|
|||||||
// cmd/tmpl/init.go
|
|
||||||
package tmpl
|
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
// Cmd 是 sunhpc tmpl 的根命令
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "tmpl",
|
|
||||||
Short: "管理配置模板",
|
|
||||||
Long: "从 YAML 模板生成配置文件或脚本,支持变量替换和多阶段执行",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Cmd.AddCommand(newRenderCmd())
|
|
||||||
Cmd.AddCommand(newDumpCmd())
|
|
||||||
}
|
|
||||||
0
configs/config.yaml
Normal file
0
configs/config.yaml
Normal file
161
data/confs/confs.go
Normal file
161
data/confs/confs.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package tmpls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 核心:
|
||||||
|
// 1. 从嵌入的文件系统中读取指定目录下的单个配置文件内容
|
||||||
|
// 2. 检查文件是否存在
|
||||||
|
// 3. 读取文件内容
|
||||||
|
// 通配符说明:
|
||||||
|
// - db/*/*.yaml : 匹配data/一级子目录下的所有yaml文件.
|
||||||
|
// - 如需递归匹配子目录(如data/db/sub/*.yaml),用 data/**/*.yaml(Go.18+)
|
||||||
|
//
|
||||||
|
//go:embed db/*.yaml firewall/*.yaml
|
||||||
|
var ConfigFS embed.FS
|
||||||
|
|
||||||
|
// GetConfigFile 获取指定目录下的的单个配置文件内容
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - dir : 目录名(db/services/firewall...)
|
||||||
|
// - name: 文件名(如base.yaml)
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - []byte: YAML 文件的内容
|
||||||
|
// - error: 如果读取文件失败,返回错误信息
|
||||||
|
func GetConfigFile(dir, name string) ([]byte, error) {
|
||||||
|
// 拼接完整路径(基于tmpls.go所在的 data 目录)
|
||||||
|
fullPath := filepath.Join(dir, name)
|
||||||
|
|
||||||
|
// 检查文件是否存在(避免ReadFile直接报错)
|
||||||
|
_, err := fs.Stat(ConfigFS, fullPath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("配置文件不存在: %s", fullPath)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
content, err := ConfigFS.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取配置文件内容失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigDir 获取指定目录下的所有配置文件内容
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - dir : 目录名(db/services/firewall...)
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - map[string][]byte: 键为文件名(如base.yaml), 值为文件内容
|
||||||
|
// - error: 如果读取文件失败,返回错误信息
|
||||||
|
func GetConfigDir(dir string) (map[string][]byte, error) {
|
||||||
|
// 存储结果: key=文件名 value=文件内容
|
||||||
|
configMap := make(map[string][]byte)
|
||||||
|
|
||||||
|
// 遍历指定目录下的所有文件
|
||||||
|
err := fs.WalkDir(ConfigFS, dir,
|
||||||
|
func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("遍历目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过目录,只处理文件
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理yaml文件(双重效验,避免非yaml文件)
|
||||||
|
if !strings.HasSuffix(d.Name(), ".yaml") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
content, err := ConfigFS.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取文件内容失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储: key用文件名(如base.yaml),而非完整路径.
|
||||||
|
configMap[d.Name()] = content
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return configMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllConfigDirs 获取所有嵌入目录(db/services/firewall...)的配置
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - map[目录名]map[文件名]文件内容
|
||||||
|
// - error: 如果读取文件失败,返回错误信息
|
||||||
|
func GetAllConfigDirs() (map[string]map[string][]byte, error) {
|
||||||
|
allConfigs := make(map[string]map[string][]byte)
|
||||||
|
|
||||||
|
// 动态获取所有一级子目录(如db/services/firewall)
|
||||||
|
dirs, err := GetAllSubDirs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有嵌入目录
|
||||||
|
for _, dir := range dirs {
|
||||||
|
dirConfigs, err := GetConfigDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
// 可选: 忽略空目录、错误目录,继续处理其他目录
|
||||||
|
// 如不需要、直接返回错误、return nil, err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allConfigs[dir] = dirConfigs
|
||||||
|
}
|
||||||
|
|
||||||
|
return allConfigs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSubDirs 动态获取data/下的所有一级子目录
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - []string: 所有一级子目录名(如db/services/firewall)
|
||||||
|
// - error: 如果读取目录失败,返回错误信息
|
||||||
|
func GetAllSubDirs() ([]string, error) {
|
||||||
|
var subDirs []string
|
||||||
|
|
||||||
|
// 遍历data/目录下的所有一级子目录
|
||||||
|
err := fs.WalkDir(ConfigFS, "",
|
||||||
|
func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("遍历目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤条件:
|
||||||
|
// 1. 是目录(d.IsDir())
|
||||||
|
// 2. 是一级子目录(path不含/,排除嵌套子目录如db/sub)
|
||||||
|
// 3. 排除根目录本身(path != "")
|
||||||
|
if d.IsDir() && !strings.Contains(path, "/") && path != "" {
|
||||||
|
subDirs = append(subDirs, path)
|
||||||
|
// 跳过该目录的子目录遍历(提升性能)
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return subDirs, nil
|
||||||
|
}
|
||||||
123
data/confs/db/base.yaml
Normal file
123
data/confs/db/base.yaml
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# 基础数据配置文件
|
||||||
|
version: 1.0
|
||||||
|
description: "SunHPC 基础数据配置"
|
||||||
|
|
||||||
|
# 节点基础数据
|
||||||
|
nodes:
|
||||||
|
- name: frontend
|
||||||
|
cpus: 4
|
||||||
|
memory: 8192
|
||||||
|
disk: 100
|
||||||
|
rack: null
|
||||||
|
rank: null
|
||||||
|
arch: x86_64
|
||||||
|
os: linux
|
||||||
|
runaction: os
|
||||||
|
installaction: os
|
||||||
|
status: active
|
||||||
|
description: "管理节点"
|
||||||
|
|
||||||
|
# 属性基础数据
|
||||||
|
attributes:
|
||||||
|
# 国家地区
|
||||||
|
- node_name: frontend # 通过节点名称关联
|
||||||
|
attr: country
|
||||||
|
value: CN
|
||||||
|
shadow: ""
|
||||||
|
- node_name: frontend
|
||||||
|
attr: state
|
||||||
|
value: Liaoning
|
||||||
|
shadow: ""
|
||||||
|
- node_name: frontend
|
||||||
|
attr: city
|
||||||
|
value: Shenyang
|
||||||
|
shadow: ""
|
||||||
|
|
||||||
|
# 网络配置
|
||||||
|
- node_name: frontend
|
||||||
|
attr: network_type
|
||||||
|
value: management
|
||||||
|
shadow: ""
|
||||||
|
- node_name: frontend
|
||||||
|
attr: ip_address
|
||||||
|
value: 192.168.1.100
|
||||||
|
shadow: ""
|
||||||
|
- node_name: frontend
|
||||||
|
attr: subnet_mask
|
||||||
|
value: 255.255.255.0
|
||||||
|
shadow: ""
|
||||||
|
|
||||||
|
# 硬件信息
|
||||||
|
- node_name: frontend
|
||||||
|
attr: manufacturer
|
||||||
|
value: Dell
|
||||||
|
shadow: ""
|
||||||
|
- node_name: frontend
|
||||||
|
attr: model
|
||||||
|
value: PowerEdge R740
|
||||||
|
shadow: ""
|
||||||
|
|
||||||
|
# 系统配置
|
||||||
|
- node_name: frontend
|
||||||
|
attr: timezone
|
||||||
|
value: Asia/Shanghai
|
||||||
|
shadow: ""
|
||||||
|
- node_name: frontend
|
||||||
|
attr: language
|
||||||
|
value: zh_CN.UTF-8
|
||||||
|
shadow: ""
|
||||||
|
- node_name: frontend
|
||||||
|
attr: kernel_version
|
||||||
|
value: "5.10.0"
|
||||||
|
shadow: ""
|
||||||
|
|
||||||
|
# 软件基础数据
|
||||||
|
software:
|
||||||
|
- name: openssl
|
||||||
|
version: "1.1.1k"
|
||||||
|
vendor: OpenSSL
|
||||||
|
install_method: source
|
||||||
|
is_installed: 0
|
||||||
|
description: "加密库"
|
||||||
|
|
||||||
|
- name: slurm
|
||||||
|
version: "23.02"
|
||||||
|
vendor: SchedMD
|
||||||
|
install_method: source
|
||||||
|
is_installed: 0
|
||||||
|
description: "作业调度系统"
|
||||||
|
|
||||||
|
- name: openmpi
|
||||||
|
version: "4.1.5"
|
||||||
|
vendor: OpenMPI
|
||||||
|
install_method: source
|
||||||
|
is_installed: 0
|
||||||
|
description: "MPI 并行计算库"
|
||||||
|
|
||||||
|
# 网络基础数据
|
||||||
|
networks:
|
||||||
|
- node_name: frontend
|
||||||
|
interface: eth0
|
||||||
|
ip_address: 192.168.1.100
|
||||||
|
netmask: 255.255.255.0
|
||||||
|
gateway: 192.168.1.1
|
||||||
|
type: management
|
||||||
|
mac: "00:11:22:33:44:55"
|
||||||
|
|
||||||
|
# 分区基础数据
|
||||||
|
partitions:
|
||||||
|
- node_name: frontend
|
||||||
|
device: /dev/sda1
|
||||||
|
mount_point: /boot
|
||||||
|
size: 1024
|
||||||
|
fs_type: ext4
|
||||||
|
- node_name: frontend
|
||||||
|
device: /dev/sda2
|
||||||
|
mount_point: /
|
||||||
|
size: 102400
|
||||||
|
fs_type: ext4
|
||||||
|
- node_name: frontend
|
||||||
|
device: /dev/sda3
|
||||||
|
mount_point: /home
|
||||||
|
size: 51200
|
||||||
|
fs_type: ext4
|
||||||
0
data/confs/firewall/rule1.yaml
Normal file
0
data/confs/firewall/rule1.yaml
Normal file
161
data/tmpls/tmpl.go
Normal file
161
data/tmpls/tmpl.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package tmpls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 核心:
|
||||||
|
// 1. 从嵌入的文件系统中读取指定目录下的单个配置文件内容
|
||||||
|
// 2. 检查文件是否存在
|
||||||
|
// 3. 读取文件内容
|
||||||
|
// 通配符说明:
|
||||||
|
// - db/*/*.yaml : 匹配data/一级子目录下的所有yaml文件.
|
||||||
|
// - 如需递归匹配子目录(如data/db/sub/*.yaml),用 data/**/*.yaml(Go.18+)
|
||||||
|
//
|
||||||
|
//go:embed db/*.yaml services/*.yaml firewall/*.yaml
|
||||||
|
var ConfigFS embed.FS
|
||||||
|
|
||||||
|
// GetConfigFile 获取指定目录下的的单个配置文件内容
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - dir : 目录名(db/services/firewall...)
|
||||||
|
// - name: 文件名(如base.yaml)
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - []byte: YAML 文件的内容
|
||||||
|
// - error: 如果读取文件失败,返回错误信息
|
||||||
|
func GetConfigFile(dir, name string) ([]byte, error) {
|
||||||
|
// 拼接完整路径(基于tmpls.go所在的 data 目录)
|
||||||
|
fullPath := filepath.Join(dir, name)
|
||||||
|
|
||||||
|
// 检查文件是否存在(避免ReadFile直接报错)
|
||||||
|
_, err := fs.Stat(ConfigFS, fullPath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil, fmt.Errorf("配置文件不存在: %s", fullPath)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
content, err := ConfigFS.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取配置文件内容失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigDir 获取指定目录下的所有配置文件内容
|
||||||
|
//
|
||||||
|
// 参数:
|
||||||
|
// - dir : 目录名(db/services/firewall...)
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - map[string][]byte: 键为文件名(如base.yaml), 值为文件内容
|
||||||
|
// - error: 如果读取文件失败,返回错误信息
|
||||||
|
func GetConfigDir(dir string) (map[string][]byte, error) {
|
||||||
|
// 存储结果: key=文件名 value=文件内容
|
||||||
|
configMap := make(map[string][]byte)
|
||||||
|
|
||||||
|
// 遍历指定目录下的所有文件
|
||||||
|
err := fs.WalkDir(ConfigFS, dir,
|
||||||
|
func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("遍历目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过目录,只处理文件
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理yaml文件(双重效验,避免非yaml文件)
|
||||||
|
if !strings.HasSuffix(d.Name(), ".yaml") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
content, err := ConfigFS.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("读取文件内容失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储: key用文件名(如base.yaml),而非完整路径.
|
||||||
|
configMap[d.Name()] = content
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return configMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllConfigDirs 获取所有嵌入目录(db/services/firewall...)的配置
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - map[目录名]map[文件名]文件内容
|
||||||
|
// - error: 如果读取文件失败,返回错误信息
|
||||||
|
func GetAllConfigDirs() (map[string]map[string][]byte, error) {
|
||||||
|
allConfigs := make(map[string]map[string][]byte)
|
||||||
|
|
||||||
|
// 动态获取所有一级子目录(如db/services/firewall)
|
||||||
|
dirs, err := GetAllSubDirs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遍历所有嵌入目录
|
||||||
|
for _, dir := range dirs {
|
||||||
|
dirConfigs, err := GetConfigDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
// 可选: 忽略空目录、错误目录,继续处理其他目录
|
||||||
|
// 如不需要、直接返回错误、return nil, err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allConfigs[dir] = dirConfigs
|
||||||
|
}
|
||||||
|
|
||||||
|
return allConfigs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSubDirs 动态获取data/下的所有一级子目录
|
||||||
|
//
|
||||||
|
// 返回值:
|
||||||
|
// - []string: 所有一级子目录名(如db/services/firewall)
|
||||||
|
// - error: 如果读取目录失败,返回错误信息
|
||||||
|
func GetAllSubDirs() ([]string, error) {
|
||||||
|
var subDirs []string
|
||||||
|
|
||||||
|
// 遍历data/目录下的所有一级子目录
|
||||||
|
err := fs.WalkDir(ConfigFS, "",
|
||||||
|
func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("遍历目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤条件:
|
||||||
|
// 1. 是目录(d.IsDir())
|
||||||
|
// 2. 是一级子目录(path不含/,排除嵌套子目录如db/sub)
|
||||||
|
// 3. 排除根目录本身(path != "")
|
||||||
|
if d.IsDir() && !strings.Contains(path, "/") && path != "" {
|
||||||
|
subDirs = append(subDirs, path)
|
||||||
|
// 跳过该目录的子目录遍历(提升性能)
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return subDirs, nil
|
||||||
|
}
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
# 数据库使用指南
|
|
||||||
|
|
||||||
## 📋 概述
|
|
||||||
|
|
||||||
SunHPC 使用 SQLite 数据库,支持自定义数据库路径和名称。数据库配置通过 `sunhpc init database` 命令初始化,之后所有命令都可以通过单例模式访问同一个数据库实例。
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 1. 初始化数据库
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 使用默认路径 (/var/lib/sunhpc/sunhpc.db)
|
|
||||||
sunhpc init database
|
|
||||||
|
|
||||||
# 使用自定义配置文件
|
|
||||||
sunhpc init database --config /path/to/config.yaml
|
|
||||||
|
|
||||||
# 使用环境变量
|
|
||||||
DB_PATH=/opt/sunhpc/data DB_NAME=cluster.db sunhpc init database
|
|
||||||
|
|
||||||
# 强制重新初始化
|
|
||||||
sunhpc init database --force
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 在其他命令中使用数据库
|
|
||||||
|
|
||||||
```go
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sunhpc/internal/db"
|
|
||||||
"sunhpc/internal/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var myCmd = &cobra.Command{
|
|
||||||
Use: "mycommand",
|
|
||||||
Short: "我的命令",
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
// 获取数据库实例(自动使用配置的路径)
|
|
||||||
database, err := db.GetInstance()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取数据库连接失败: %v", err)
|
|
||||||
}
|
|
||||||
defer database.Close()
|
|
||||||
|
|
||||||
// 执行查询
|
|
||||||
_, err = database.Execute("SELECT * FROM nodes")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("查询失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取结果
|
|
||||||
rows, err := database.FetchAll()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取结果失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理结果
|
|
||||||
for _, row := range rows {
|
|
||||||
log.Infof("节点: %v", row)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 配置说明
|
|
||||||
|
|
||||||
### 配置文件格式
|
|
||||||
|
|
||||||
创建 `config.yaml` 文件:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
db:
|
|
||||||
path: "/opt/sunhpc/data" # 数据库目录路径
|
|
||||||
name: "my_cluster.db" # 数据库文件名
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置优先级
|
|
||||||
|
|
||||||
从高到低:
|
|
||||||
|
|
||||||
1. **配置文件**:`config.yaml` 中的 `db.path` 和 `db.name`
|
|
||||||
2. **环境变量**:`DB_PATH` 和 `DB_NAME`
|
|
||||||
3. **默认值**:`/var/lib/sunhpc` 和 `sunhpc.db`
|
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 设置数据库路径
|
|
||||||
export DB_PATH=/tmp/sunhpc
|
|
||||||
export DB_NAME=test.db
|
|
||||||
|
|
||||||
# 使用环境变量
|
|
||||||
sunhpc init database
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 数据库 API
|
|
||||||
|
|
||||||
### 获取数据库实例
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 方式1:使用默认配置(推荐)
|
|
||||||
database, err := db.GetInstance()
|
|
||||||
|
|
||||||
// 方式2:指定路径和名称(仅在初始化时使用)
|
|
||||||
database, err := db.GetInstanceWithConfig("/path/to/db", "mydb.db")
|
|
||||||
|
|
||||||
// 检查实例是否已配置
|
|
||||||
if db.IsInstanceConfigured() {
|
|
||||||
dbPath, dbName := db.GetInstanceConfig()
|
|
||||||
log.Infof("数据库: %s/%s", dbPath, dbName)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 执行 SQL
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 执行查询
|
|
||||||
_, err := database.Execute("SELECT * FROM nodes WHERE name = ?", "node1")
|
|
||||||
|
|
||||||
// 执行插入
|
|
||||||
_, err := database.Execute(
|
|
||||||
"INSERT INTO nodes (name, cpus, memory) VALUES (?, ?, ?)",
|
|
||||||
"node2", 32, 128,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 执行更新
|
|
||||||
_, err := database.Execute(
|
|
||||||
"UPDATE nodes SET cpus = ? WHERE name = ?",
|
|
||||||
64, "node1",
|
|
||||||
)
|
|
||||||
|
|
||||||
// 执行删除
|
|
||||||
_, err := database.Execute("DELETE FROM nodes WHERE name = ?", "node1")
|
|
||||||
```
|
|
||||||
|
|
||||||
### 获取查询结果
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 获取单行
|
|
||||||
row, err := database.FetchOne()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if row != nil {
|
|
||||||
log.Infof("节点名称: %s", row["name"])
|
|
||||||
log.Infof("CPU数量: %v", row["cpus"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有行
|
|
||||||
rows, err := database.FetchAll()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, row := range rows {
|
|
||||||
log.Infof("节点: %v", row)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 使用示例
|
|
||||||
|
|
||||||
### 示例1:查询节点列表
|
|
||||||
|
|
||||||
```go
|
|
||||||
func listNodes() error {
|
|
||||||
database, err := db.GetInstance()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer database.Close()
|
|
||||||
|
|
||||||
_, err = database.Execute("SELECT id, name, cpus, memory FROM nodes ORDER BY name")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, err := database.FetchAll()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
fmt.Printf("%-5s %-20s %-8s %-10s\n",
|
|
||||||
row["id"], row["name"], row["cpus"], row["memory"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例2:添加节点
|
|
||||||
|
|
||||||
```go
|
|
||||||
func addNode(name string, cpus, memory int) error {
|
|
||||||
database, err := db.GetInstance()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer database.Close()
|
|
||||||
|
|
||||||
_, err = database.Execute(
|
|
||||||
"INSERT INTO nodes (name, cpus, memory) VALUES (?, ?, ?)",
|
|
||||||
name, cpus, memory,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("添加节点失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例3:更新节点
|
|
||||||
|
|
||||||
```go
|
|
||||||
func updateNode(name string, cpus, memory int) error {
|
|
||||||
database, err := db.GetInstance()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer database.Close()
|
|
||||||
|
|
||||||
_, err = database.Execute(
|
|
||||||
"UPDATE nodes SET cpus = ?, memory = ? WHERE name = ?",
|
|
||||||
cpus, memory, name,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("更新节点失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例4:删除节点
|
|
||||||
|
|
||||||
```go
|
|
||||||
func deleteNode(name string) error {
|
|
||||||
database, err := db.GetInstance()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer database.Close()
|
|
||||||
|
|
||||||
_, err = database.Execute("DELETE FROM nodes WHERE name = ?", name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("删除节点失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 数据库表结构
|
|
||||||
|
|
||||||
### nodes 表
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE 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
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### networks 表
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE 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
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### software_installs 表
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE 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
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
|
||||||
|
|
||||||
1. **单例模式**:`GetInstance()` 使用单例模式,整个程序只创建一个数据库实例
|
|
||||||
2. **路径一致性**:所有命令都使用同一个数据库路径,确保数据一致性
|
|
||||||
3. **关闭连接**:使用完毕后调用 `database.Close()` 释放资源
|
|
||||||
4. **错误处理**:始终检查错误返回值
|
|
||||||
5. **SQL注入防护**:使用参数化查询(`?` 占位符)
|
|
||||||
|
|
||||||
## 📝 最佳实践
|
|
||||||
|
|
||||||
1. **初始化优先**:在程序启动时先执行 `sunhpc init database`
|
|
||||||
2. **配置管理**:使用配置文件统一管理数据库路径
|
|
||||||
3. **事务处理**:复杂操作使用事务确保数据一致性
|
|
||||||
4. **日志记录**:记录所有数据库操作,便于调试
|
|
||||||
5. **资源释放**:使用 `defer database.Close()` 确保连接关闭
|
|
||||||
|
|
||||||
## 🆘 常见问题
|
|
||||||
|
|
||||||
### Q: 如何切换数据库路径?
|
|
||||||
|
|
||||||
A: 重新运行 `sunhpc init database` 命令,指定新的配置文件或环境变量。
|
|
||||||
|
|
||||||
### Q: 多个命令会创建多个数据库实例吗?
|
|
||||||
|
|
||||||
A: 不会。`GetInstance()` 使用单例模式,整个程序只创建一个实例。
|
|
||||||
|
|
||||||
### Q: 如何查看当前使用的数据库路径?
|
|
||||||
|
|
||||||
A: 使用 `db.GetInstanceConfig()` 获取配置信息。
|
|
||||||
|
|
||||||
### Q: 数据库文件不存在会怎样?
|
|
||||||
|
|
||||||
A: 首次调用 `GetInstance()` 时会自动创建数据库文件和表结构。
|
|
||||||
2
go.mod
2
go.mod
@@ -26,6 +26,8 @@ require (
|
|||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -56,6 +56,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
package initcmd
|
package initcmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"sunhpc/internal/middler/auth"
|
||||||
"os"
|
"sunhpc/pkg/logger"
|
||||||
|
|
||||||
"sunhpc/internal/auth"
|
|
||||||
"sunhpc/internal/config"
|
|
||||||
"sunhpc/internal/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewConfigCmd 创建 "init config" 命令
|
// NewConfigCmd 创建 "init config" 命令
|
||||||
func NewConfigCmd() *cobra.Command {
|
func NewInitCfgCmd() *cobra.Command {
|
||||||
var (
|
var (
|
||||||
force bool
|
force bool
|
||||||
path string
|
path string
|
||||||
@@ -30,31 +27,12 @@ func NewConfigCmd() *cobra.Command {
|
|||||||
sunhpc init config -f # 强制覆盖已有配置文件
|
sunhpc init config -f # 强制覆盖已有配置文件
|
||||||
sunhpc init config -p /etc/sunhpc/sunhpc.yaml # 指定路径
|
sunhpc init config -p /etc/sunhpc/sunhpc.yaml # 指定路径
|
||||||
`,
|
`,
|
||||||
|
|
||||||
Annotations: map[string]string{
|
|
||||||
"require-root": "true", // 假设需要 root(你可自定义策略)
|
|
||||||
},
|
|
||||||
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := auth.RequireRoot(); err != nil {
|
if err := auth.RequireRoot(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if path == "" {
|
logger.Info("✅ 配置文件已生成", zap.String("path", path))
|
||||||
path = "/etc/sunhpc/sunhpc.yaml"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !force {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
return fmt.Errorf("配置文件已存在: %s (使用 --force 覆盖)", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := config.WriteDefaultConfig(path); err != nil {
|
|
||||||
return fmt.Errorf("写入配置失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("✅ 配置文件已生成: %s", path)
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ package tmpl
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"sunhpc/internal/log"
|
log "sunhpc/pkg/logger"
|
||||||
"sunhpc/internal/templating"
|
"sunhpc/pkg/templating"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
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
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ package tmpl
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"sunhpc/internal/log"
|
log "sunhpc/pkg/logger"
|
||||||
"sunhpc/internal/templating"
|
templating "sunhpc/pkg/templating"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -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,7 +1,7 @@
|
|||||||
package soft
|
package soft
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sunhpc/internal/log"
|
log "sunhpc/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InstallContext 安装上下文,包含所有命令行参数
|
// InstallContext 安装上下文,包含所有命令行参数
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sunhpc/internal/log"
|
log "sunhpc/pkg/logger"
|
||||||
"sunhpc/pkg/utils"
|
"sunhpc/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sunhpc/internal/log"
|
log "sunhpc/pkg/logger"
|
||||||
"sunhpc/pkg/utils"
|
"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
|
|
||||||
}
|
|
||||||
12
main.go
12
main.go
@@ -1,12 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"sunhpc/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := cmd.Execute(); err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
169
pkg/config/config.go
Normal file
169
pkg/config/config.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.yaml.in/yaml/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
Log LogConfig `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
DSN string `yaml:"dsn"` // 数据库连接字符串
|
||||||
|
Path string `yaml:"path"` // SQLite: 目录路径
|
||||||
|
Name string `yaml:"name"` // SQLite: 文件名
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `yaml:"level"`
|
||||||
|
Format string `yaml:"format"`
|
||||||
|
Output string `yaml:"output"`
|
||||||
|
Verbose bool `yaml:"verbose"`
|
||||||
|
LogFile string `yaml:"log_file"`
|
||||||
|
ShowColor bool `yaml:"show_color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------- 全局单例配置(核心) ---------------------------------
|
||||||
|
var (
|
||||||
|
// GlobalConfig 全局配置单例实例
|
||||||
|
GlobalConfig *Config
|
||||||
|
// 命令行参数配置(全局、由root命令绑定)
|
||||||
|
CLIParams = struct {
|
||||||
|
Verbose bool // -v/--verbose
|
||||||
|
NoColor bool // --no-color
|
||||||
|
Config string // -c/--config
|
||||||
|
}{}
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ----------------------------------- 配置加载(只加载一次) -----------------------------------
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
// 如果已经加载过,直接返回
|
||||||
|
if GlobalConfig != nil {
|
||||||
|
return GlobalConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigName("sunhpc")
|
||||||
|
viper.SetConfigType("yaml")
|
||||||
|
viper.AddConfigPath(BaseDir)
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.AddConfigPath(filepath.Join(os.Getenv("HOME"), "."))
|
||||||
|
|
||||||
|
// Step 1: 设置默认值(最低优先级)
|
||||||
|
viper.SetDefault("log.level", "info")
|
||||||
|
viper.SetDefault("log.format", "text")
|
||||||
|
viper.SetDefault("log.output", "stdout")
|
||||||
|
viper.SetDefault("log.verbose", false)
|
||||||
|
viper.SetDefault("log.log_file", filepath.Join(LogDir, "sunhpc.log"))
|
||||||
|
viper.SetDefault("database.name", "sunhpc.db")
|
||||||
|
viper.SetDefault("database.path", "/var/lib/sunhpc")
|
||||||
|
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
// 配置文件不存在时,使用默认值
|
||||||
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并命令行参数(最高优先级)
|
||||||
|
if CLIParams.Verbose {
|
||||||
|
viper.Set("log.verbose", true)
|
||||||
|
viper.Set("log.level", "debug")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并noColor参数
|
||||||
|
if CLIParams.NoColor {
|
||||||
|
viper.Set("log.show_color", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(
|
||||||
|
viper.GetString("database.path"), viper.GetString("database.name"))
|
||||||
|
dsn := fmt.Sprintf(
|
||||||
|
"%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000", fullPath)
|
||||||
|
viper.Set("database.dsn", dsn)
|
||||||
|
|
||||||
|
// 解码到结构体
|
||||||
|
var cfg Config
|
||||||
|
if err := viper.Unmarshal(&cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalConfig = &cfg
|
||||||
|
|
||||||
|
return GlobalConfig, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WriteDefaultConfig(path string) error {
|
||||||
|
// 确保目录存在
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建目录失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成默认配置
|
||||||
|
cfg := DefaultConfig(path)
|
||||||
|
|
||||||
|
// 序列化为 YAML
|
||||||
|
data, err := yaml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("序列化配置失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件(0644 权限)
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig(path string) *Config {
|
||||||
|
return &Config{
|
||||||
|
Database: DatabaseConfig{
|
||||||
|
DSN: fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000",
|
||||||
|
filepath.Join(filepath.Dir(path), defaultDBName)),
|
||||||
|
Path: filepath.Dir(path),
|
||||||
|
Name: defaultDBName,
|
||||||
|
},
|
||||||
|
Log: LogConfig{
|
||||||
|
Level: "info",
|
||||||
|
Format: "text",
|
||||||
|
Output: "stdout",
|
||||||
|
LogFile: filepath.Join(filepath.Dir(path), "sunhpc.log"),
|
||||||
|
Verbose: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetConfig 重置全局配置为默认值
|
||||||
|
func ResetConfig() {
|
||||||
|
GlobalConfig = nil
|
||||||
|
viper.Reset()
|
||||||
|
CLIParams = struct {
|
||||||
|
Verbose bool // -v/--verbose
|
||||||
|
NoColor bool // --no-color
|
||||||
|
Config string // -c/--config
|
||||||
|
}{}
|
||||||
|
}
|
||||||
176
pkg/database/database.go
Normal file
176
pkg/database/database.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"sunhpc/pkg/config"
|
||||||
|
"sunhpc/pkg/logger"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
db *sql.DB
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
dbInstance *DB
|
||||||
|
dbOnce sync.Once
|
||||||
|
dbErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetInstance(dbConfig *config.DatabaseConfig, log logger.Logger) (*DB, error) {
|
||||||
|
dbOnce.Do(func() {
|
||||||
|
// 兜底: 未注入则使用全局默认日志实例
|
||||||
|
if log == nil {
|
||||||
|
log = logger.DefaultLogger
|
||||||
|
}
|
||||||
|
log.Debugf("开始初始化数据库,路径: %s", dbConfig.Path)
|
||||||
|
|
||||||
|
// 确认数据库目录存在
|
||||||
|
if err := os.MkdirAll(dbConfig.Path, 0755); err != nil {
|
||||||
|
log.Errorf("创建数据库目录失败: %v", err)
|
||||||
|
dbErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(dbConfig.Path, dbConfig.Name)
|
||||||
|
log.Debugf("数据库路径: %s", fullPath)
|
||||||
|
|
||||||
|
// 构建DSN
|
||||||
|
dsn := fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000&cache=shared",
|
||||||
|
fullPath)
|
||||||
|
log.Debugf("DSN: %s", dsn)
|
||||||
|
|
||||||
|
// 打开SQLite 连接
|
||||||
|
sqlDB, err := sql.Open("sqlite3", dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("数据库打开失败: %v", err)
|
||||||
|
dbErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置连接池参数
|
||||||
|
sqlDB.SetMaxOpenConns(1) // SQLite 只支持单连接
|
||||||
|
sqlDB.SetMaxIdleConns(1) // 保持一个空闲连接
|
||||||
|
sqlDB.SetConnMaxLifetime(0) // 禁用连接生命周期超时
|
||||||
|
sqlDB.SetConnMaxIdleTime(0) // 禁用空闲连接超时
|
||||||
|
|
||||||
|
// 测试数据库连接
|
||||||
|
if err := sqlDB.Ping(); err != nil {
|
||||||
|
sqlDB.Close()
|
||||||
|
log.Errorf("数据库连接失败: %v", err)
|
||||||
|
dbErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 赋值 *DB 类型的单例(而非直接复制 *sql.DB)
|
||||||
|
log.Info("数据库连接成功")
|
||||||
|
dbInstance = &DB{sqlDB, log}
|
||||||
|
})
|
||||||
|
|
||||||
|
if dbErr != nil {
|
||||||
|
return nil, dbErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbInstance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭数据库连接
|
||||||
|
func (d *DB) Close() error {
|
||||||
|
if d.db != nil {
|
||||||
|
d.logger.Debug("关闭数据库连接")
|
||||||
|
err := d.db.Close()
|
||||||
|
if err != nil {
|
||||||
|
d.logger.Errorf("数据库连接关闭失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.logger.Info("数据库连接关闭成功")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmAction(prompt string) bool {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
logger.Warnf("%s [Y/Yes]: ", prompt)
|
||||||
|
response, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
return response == "y" || response == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DB) InitTables(force bool) error {
|
||||||
|
d.logger.Info("开始初始化数据库表...")
|
||||||
|
|
||||||
|
if force {
|
||||||
|
// 确认是否强制删除
|
||||||
|
if !confirmAction("确认强制删除所有表和触发器?") {
|
||||||
|
d.logger.Info("操作已取消")
|
||||||
|
os.Exit(0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制删除所有表和触发器
|
||||||
|
d.logger.Debug("强制删除所有表和触发器...")
|
||||||
|
if err := dropTables(d.db); err != nil {
|
||||||
|
return fmt.Errorf("删除表失败: %w", err)
|
||||||
|
}
|
||||||
|
d.logger.Debug("删除所有表和触发器成功")
|
||||||
|
|
||||||
|
if err := dropTriggers(d.db); err != nil {
|
||||||
|
return fmt.Errorf("删除触发器失败: %w", err)
|
||||||
|
}
|
||||||
|
d.logger.Debug("删除所有触发器成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 调用 schema.go 中的函数
|
||||||
|
for _, ddl := range CreateTableStatements() {
|
||||||
|
d.logger.Debugf("执行: %s", ddl)
|
||||||
|
if _, err := d.db.Exec(ddl); err != nil {
|
||||||
|
return fmt.Errorf("数据表创建失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.logger.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
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package db defines the database schema.
|
// Package db defines the database schema.
|
||||||
package db
|
package database
|
||||||
|
|
||||||
// CurrentSchemaVersion returns the current schema version (for migrations)
|
// CurrentSchemaVersion returns the current schema version (for migrations)
|
||||||
func CurrentSchemaVersion() int {
|
func CurrentSchemaVersion() int {
|
||||||
298
pkg/logger/logger.go
Normal file
298
pkg/logger/logger.go
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
// logger/logger.go
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------------------------- 1. ANSI 颜色码常量(关键) --------------------------
|
||||||
|
const (
|
||||||
|
// 颜色重置
|
||||||
|
ColorReset = "\033[0m"
|
||||||
|
// 灰色(日期、文件行号)
|
||||||
|
ColorGray = "\033[90m"
|
||||||
|
// 日志级别颜色
|
||||||
|
ColorDebug = "\033[36m" // 青色 [d]
|
||||||
|
ColorInfo = "\033[32m" // 绿色 [i]
|
||||||
|
ColorWarn = "\033[33m" // 黄色 [w]
|
||||||
|
ColorError = "\033[31m" // 红色 [e]
|
||||||
|
ColorFatal = "\033[35m" // 紫色 [f]
|
||||||
|
)
|
||||||
|
|
||||||
|
// -------------------------- 1. 定义日志接口 --------------------------
|
||||||
|
// Logger 日志核心接口,定义所有需要的日志方法
|
||||||
|
// 所有模块都依赖这个接口,而非具体实现
|
||||||
|
type Logger interface {
|
||||||
|
Debug(args ...interface{})
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
Info(args ...interface{})
|
||||||
|
Infof(format string, args ...interface{})
|
||||||
|
Warn(args ...interface{})
|
||||||
|
Warnf(format string, args ...interface{})
|
||||||
|
Error(args ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Fatal(args ...interface{})
|
||||||
|
Fatalf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------- 2. 基于logrus的默认实现 --------------------------
|
||||||
|
// logrusLogger 是 Logger 接口的具体实现(基于logrus)
|
||||||
|
type logrusLogger struct {
|
||||||
|
*logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实现 Logger 接口的所有方法(直接转发给logrus)
|
||||||
|
func (l *logrusLogger) Debug(args ...interface{}) { l.Logger.Debug(args...) }
|
||||||
|
func (l *logrusLogger) Debugf(format string, args ...interface{}) { l.Logger.Debugf(format, args...) }
|
||||||
|
func (l *logrusLogger) Info(args ...interface{}) { l.Logger.Info(args...) }
|
||||||
|
func (l *logrusLogger) Infof(format string, args ...interface{}) { l.Logger.Infof(format, args...) }
|
||||||
|
func (l *logrusLogger) Warn(args ...interface{}) { l.Logger.Warn(args...) }
|
||||||
|
func (l *logrusLogger) Warnf(format string, args ...interface{}) { l.Logger.Warnf(format, args...) }
|
||||||
|
func (l *logrusLogger) Error(args ...interface{}) { l.Logger.Error(args...) }
|
||||||
|
func (l *logrusLogger) Errorf(format string, args ...interface{}) { l.Logger.Errorf(format, args...) }
|
||||||
|
func (l *logrusLogger) Fatal(args ...interface{}) { l.Logger.Fatal(args...) }
|
||||||
|
func (l *logrusLogger) Fatalf(format string, args ...interface{}) { l.Logger.Fatalf(format, args...) }
|
||||||
|
|
||||||
|
// -------------------------- 3. 全局默认实例 + 初始化逻辑 --------------------------
|
||||||
|
var (
|
||||||
|
// DefaultLogger 全局默认日志实例(所有模块可直接用,也可注入自定义实现)
|
||||||
|
DefaultLogger Logger
|
||||||
|
// once 保证日志只初始化一次
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogConfig 日志配置结构体(和项目的config包对齐)
|
||||||
|
type LogConfig struct {
|
||||||
|
Verbose bool // 是否开启详细模式(Debug级别)
|
||||||
|
Level string // 日志级别:debug/info/warn/error
|
||||||
|
ShowColor bool // 是否显示彩色输出
|
||||||
|
LogFile string // 日志文件路径(可选,空则只输出到控制台)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
var defaultConfig = LogConfig{
|
||||||
|
Verbose: false,
|
||||||
|
Level: "info",
|
||||||
|
ShowColor: true,
|
||||||
|
LogFile: "/var/log/sunhpc/sunhpc.log",
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------- 5. 自定义Formatter(核心配色逻辑) --------------------------
|
||||||
|
type CustomFormatter struct {
|
||||||
|
ShowColor bool // 是否显示彩色输出
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format 实现 logrus.Formatter 接口,自定义日志格式和颜色
|
||||||
|
func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||||
|
// 1. 获取日志级别标识和对应颜色
|
||||||
|
levelStr, levelColor := getLevelInfo(entry.Level)
|
||||||
|
|
||||||
|
// 2. 获取调用文件和行号(简化路径,只保留文件名+行号)
|
||||||
|
file, line := getCallerInfo()
|
||||||
|
// 3. 格式化时间(灰色)
|
||||||
|
timeStr := entry.Time.Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// 构建日志行(按你的格式:日期 [级别] 内容 文件:行号)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
// 颜色开关:如果禁用颜色,所有颜色码置空
|
||||||
|
colorReset := ColorReset
|
||||||
|
colorGray := ColorGray
|
||||||
|
if !f.ShowColor {
|
||||||
|
levelColor = ""
|
||||||
|
colorGray = ""
|
||||||
|
colorReset = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接格式:
|
||||||
|
// 灰色日期 + 空格 + 带颜色的[级别] + 空格 + 日志内容 + 空格 + 灰色文件行号 + 重置
|
||||||
|
fmt.Fprintf(&buf, "%s%s%s %s[%s]%s %s %s%s:%d%s\n",
|
||||||
|
colorGray, // 日期开始灰色
|
||||||
|
timeStr, // 日期字符串
|
||||||
|
colorReset, // 日期结束重置颜色
|
||||||
|
levelColor, // 级别标识颜色
|
||||||
|
levelStr, // 级别标识(i/d/e/w/f)
|
||||||
|
colorReset, // 级别标识结束重置
|
||||||
|
entry.Message, // 日志内容(默认色)
|
||||||
|
colorGray, // 文件行号开始灰色
|
||||||
|
file, // 文件名(如db.go)
|
||||||
|
line, // 行号(如64)
|
||||||
|
colorReset, // 文件行号结束重置
|
||||||
|
)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLevelInfo 获取日志级别对应的标识和颜色
|
||||||
|
func getLevelInfo(level logrus.Level) (string, string) {
|
||||||
|
switch level {
|
||||||
|
case logrus.DebugLevel:
|
||||||
|
return "d", ColorDebug
|
||||||
|
case logrus.InfoLevel:
|
||||||
|
return "i", ColorInfo
|
||||||
|
case logrus.WarnLevel:
|
||||||
|
return "w", ColorWarn
|
||||||
|
case logrus.ErrorLevel:
|
||||||
|
return "e", ColorError
|
||||||
|
case logrus.FatalLevel:
|
||||||
|
return "f", ColorFatal
|
||||||
|
default:
|
||||||
|
return "i", ColorInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCallerInfo 获取调用日志的文件和行号(跳过logrus内部调用)
|
||||||
|
func _getCallerInfo() (string, int) {
|
||||||
|
// 跳过的调用栈深度:根据实际情况调整(这里跳过logrus和logger包的调用)
|
||||||
|
skip := 6
|
||||||
|
pc, file, line, ok := runtime.Caller(skip)
|
||||||
|
if !ok {
|
||||||
|
return "unknown.go", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只保留文件名(如 db.go),去掉完整路径
|
||||||
|
fileName := filepath.Base(file)
|
||||||
|
|
||||||
|
// 过滤logrus内部调用(可选)
|
||||||
|
funcName := runtime.FuncForPC(pc).Name()
|
||||||
|
if funcName == "" || filepath.Base(funcName) == "logrus" {
|
||||||
|
return getCallerInfoWithSkip(skip + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName, line
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCallerInfo() (string, int) {
|
||||||
|
// 从当前调用开始,逐层向上查找
|
||||||
|
for i := 2; i < 15; i++ { // i从2开始(跳过getCallerInfo自身)
|
||||||
|
pc, file, line, ok := runtime.Caller(i)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取函数名
|
||||||
|
funcName := runtime.FuncForPC(pc).Name()
|
||||||
|
|
||||||
|
// 跳过logrus和logger包内部的调用
|
||||||
|
if shouldSkipPackage(funcName, file) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到第一个非内部调用的栈帧
|
||||||
|
return filepath.Base(file), line
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown.go", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldSkipPackage 判断是否需要跳过该调用
|
||||||
|
func shouldSkipPackage(funcName, file string) bool {
|
||||||
|
// 跳过logrus包
|
||||||
|
if strings.Contains(funcName, "logrus") ||
|
||||||
|
strings.Contains(file, "logrus") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过logger包(你自己的包装包)
|
||||||
|
if strings.Contains(funcName, "your/package/logger") ||
|
||||||
|
strings.Contains(file, "logger") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳过runtime包
|
||||||
|
if strings.Contains(funcName, "runtime.") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归调整调用栈深度(兼容不同场景)
|
||||||
|
func getCallerInfoWithSkip(skip int) (string, int) {
|
||||||
|
_, file, line, ok := runtime.Caller(skip)
|
||||||
|
if !ok {
|
||||||
|
return "unknown.go", 0
|
||||||
|
}
|
||||||
|
return filepath.Base(file), line
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init 初始化全局默认日志实例(全局只执行一次)
|
||||||
|
func Init(cfg LogConfig) {
|
||||||
|
once.Do(func() {
|
||||||
|
// 合并配置:传入的配置为空则用默认值
|
||||||
|
if cfg.Level == "" {
|
||||||
|
cfg.Level = defaultConfig.Level
|
||||||
|
}
|
||||||
|
if cfg.LogFile == "" {
|
||||||
|
cfg.LogFile = defaultConfig.LogFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 创建logrus实例
|
||||||
|
logrusInst := logrus.New()
|
||||||
|
|
||||||
|
// 2. 配置输出(控制台 + 文件,可选)
|
||||||
|
var outputs []io.Writer
|
||||||
|
outputs = append(outputs, os.Stdout) // 控制台输出
|
||||||
|
// 如果配置了日志文件,添加文件输出
|
||||||
|
if cfg.LogFile != "" {
|
||||||
|
// 确保日志目录存在
|
||||||
|
dir := filepath.Dir(cfg.LogFile)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
// 目录创建失败,只输出警告,不影响程序运行
|
||||||
|
logrusInst.Warnf("创建日志目录失败: %v,仅输出到控制台", err)
|
||||||
|
} else {
|
||||||
|
file, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err == nil {
|
||||||
|
outputs = append(outputs, file)
|
||||||
|
} else {
|
||||||
|
logrusInst.Warnf("打开日志文件失败: %v,仅输出到控制台", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logrusInst.SetOutput(io.MultiWriter(outputs...))
|
||||||
|
|
||||||
|
// 3. 配置格式
|
||||||
|
logrusInst.SetFormatter(&CustomFormatter{
|
||||||
|
ShowColor: cfg.ShowColor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 配置日志级别
|
||||||
|
lvl, err := logrus.ParseLevel(cfg.Level)
|
||||||
|
if err != nil {
|
||||||
|
lvl = logrus.InfoLevel // 解析失败默认Info级别
|
||||||
|
}
|
||||||
|
// 开启Verbose则强制设为Debug级别
|
||||||
|
if cfg.Verbose {
|
||||||
|
lvl = logrus.DebugLevel
|
||||||
|
}
|
||||||
|
logrusInst.SetLevel(lvl)
|
||||||
|
|
||||||
|
// 启用文件行号(必须开启,否则getCallerInfo拿不到数据)
|
||||||
|
logrusInst.SetReportCaller(true)
|
||||||
|
|
||||||
|
// 5. 赋值给全局默认实例
|
||||||
|
DefaultLogger = &logrusLogger{logrusInst}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------- 4. 全局快捷调用方法(可选,简化使用) --------------------------
|
||||||
|
// 如果你不想每次都写 logger.DefaultLogger.Info(),可以封装快捷方法
|
||||||
|
func Debug(args ...any) { DefaultLogger.Debug(args...) }
|
||||||
|
func Debugf(format string, args ...any) { DefaultLogger.Debugf(format, args...) }
|
||||||
|
func Info(args ...any) { DefaultLogger.Info(args...) }
|
||||||
|
func Infof(format string, args ...any) { DefaultLogger.Infof(format, args...) }
|
||||||
|
func Warn(args ...any) { DefaultLogger.Warn(args...) }
|
||||||
|
func Warnf(format string, args ...any) { DefaultLogger.Warnf(format, args...) }
|
||||||
|
func Error(args ...any) { DefaultLogger.Error(args...) }
|
||||||
|
func Errorf(format string, args ...any) { DefaultLogger.Errorf(format, args...) }
|
||||||
|
func Fatal(args ...any) { DefaultLogger.Fatal(args...) }
|
||||||
|
func Fatalf(format string, args ...any) { DefaultLogger.Fatalf(format, args...) }
|
||||||
@@ -6,15 +6,14 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sunhpc/data/tmpls"
|
||||||
"sunhpc/tmpls"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListEmbeddedTemplates 返回所有内置模板名称(不含路径和扩展名)
|
// ListEmbeddedTemplates 返回所有内置模板名称(不含路径和扩展名)
|
||||||
func ListEmbeddedTemplates() ([]string, error) {
|
func ListEmbeddedTemplates() ([]string, error) {
|
||||||
entries, err := fs.ReadDir(tmpls.FS, ".")
|
entries, err := fs.ReadDir(tmpls.ConfigFS, ".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -30,7 +29,7 @@ func ListEmbeddedTemplates() ([]string, error) {
|
|||||||
|
|
||||||
// LoadEmbeddedTemplate 从二进制加载内置模板
|
// LoadEmbeddedTemplate 从二进制加载内置模板
|
||||||
func LoadEmbeddedTemplate(name string) (*Template, error) {
|
func LoadEmbeddedTemplate(name string) (*Template, error) {
|
||||||
data, err := tmpls.FS.ReadFile(name + ".yaml")
|
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, fmt.Errorf("内置模板 '%s' 不存在", name)
|
return nil, fmt.Errorf("内置模板 '%s' 不存在", name)
|
||||||
@@ -46,7 +45,7 @@ func LoadEmbeddedTemplate(name string) (*Template, error) {
|
|||||||
|
|
||||||
// DumpEmbeddedTemplateToFile 将内置模板写入文件
|
// DumpEmbeddedTemplateToFile 将内置模板写入文件
|
||||||
func DumpEmbeddedTemplateToFile(name, outputPath string) error {
|
func DumpEmbeddedTemplateToFile(name, outputPath string) error {
|
||||||
data, err := tmpls.FS.ReadFile(name + ".yaml")
|
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("找不到内置模板 '%s': %w", name, err)
|
return fmt.Errorf("找不到内置模板 '%s': %w", name, err)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommandExists 检查命令是否存在
|
// CommandExists 检查命令是否存在
|
||||||
@@ -16,3 +19,15 @@ func FileExists(path string) bool {
|
|||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
return err == nil || !os.IsNotExist(err)
|
return err == nil || !os.IsNotExist(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateID() (string, error) {
|
||||||
|
bytes := make([]byte, 16)
|
||||||
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(bytes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTimestamp() string {
|
||||||
|
return time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|||||||
185
readme.md
185
readme.md
@@ -1,146 +1,39 @@
|
|||||||
## 其他包使用数据库模块
|
project/
|
||||||
```go
|
├── cmd/
|
||||||
package repository
|
│ ├── sunhpc/
|
||||||
|
│ │ └── main.go
|
||||||
import (
|
│ └── insert-nodes/
|
||||||
"your-project/database"
|
│ └── main.go
|
||||||
"your-project/log"
|
├── pkg/
|
||||||
)
|
│ ├── app/
|
||||||
|
│ │ └── app.go
|
||||||
type NodeRepository struct {
|
│ ├── config/
|
||||||
db *database.DB
|
│ │ └── config.go
|
||||||
}
|
│ ├── database/
|
||||||
|
│ │ └── database.go
|
||||||
// NewNodeRepository 创建仓库(延迟连接)
|
│ ├── logger/
|
||||||
func NewNodeRepository() (*NodeRepository, error) {
|
│ │ └── logger.go
|
||||||
db, err := database.GetDB()
|
│ └── utils/
|
||||||
if err != nil {
|
│ └── utils.go
|
||||||
return nil, err
|
├── internal/
|
||||||
}
|
│ ├── cli/
|
||||||
|
│ │ ├── root.go # 根命令
|
||||||
return &NodeRepository{db: db}, nil
|
│ │ ├── init/ # init 子命令目录
|
||||||
}
|
│ │ │ ├── init.go # init 命令定义
|
||||||
|
│ │ │ ├── db.go # init db 子命令
|
||||||
// GetNode 获取节点(自动连接)
|
│ │ │ └── cfg.go # init cfg 子命令
|
||||||
func (r *NodeRepository) GetNode(id int) (*Node, error) {
|
│ │ ├── soft/ # soft 子命令目录
|
||||||
// 获取数据库引擎(会自动连接)
|
│ │ │ ├── soft.go # soft 命令定义
|
||||||
engine, err := r.db.GetEngine()
|
│ │ │ └── install.go # soft install 子命令
|
||||||
if err != nil {
|
│ │ └── node/ # node 子命令目录(可选扩展)
|
||||||
return nil, fmt.Errorf("数据库连接失败: %w", err)
|
│ │ ├── node.go
|
||||||
}
|
│ │ └── list.go
|
||||||
|
│ ├── handler/
|
||||||
var node Node
|
│ │ └── node_handler.go
|
||||||
err = engine.QueryRow("SELECT id, name FROM nodes WHERE id = ?", id).
|
│ └── middleware/
|
||||||
Scan(&node.ID, &node.Name)
|
│ └── auth.go
|
||||||
if err != nil {
|
├── configs/
|
||||||
return nil, err
|
│ └── config.yaml
|
||||||
}
|
├── data/
|
||||||
|
├── go.mod
|
||||||
return &node, nil
|
└── go.sum
|
||||||
}
|
|
||||||
|
|
||||||
// CreateNode 创建节点
|
|
||||||
func (r *NodeRepository) CreateNode(name string) error {
|
|
||||||
engine, err := r.db.GetEngine()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = engine.Exec("INSERT INTO nodes (name) VALUES (?)", name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 服务层使用
|
|
||||||
```go
|
|
||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"your-project/repository"
|
|
||||||
"your-project/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NodeService struct {
|
|
||||||
repo *repository.NodeRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewNodeService() (*NodeService, error) {
|
|
||||||
repo, err := repository.NewNodeRepository()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &NodeService{repo: repo}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NodeService) ListNode() error {
|
|
||||||
// 自动连接数据库
|
|
||||||
nodes, err := s.repo.GetAllNodes()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, node := range nodes {
|
|
||||||
log.Infof("Node: %v", node)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
```
|
|
||||||
## 命令处理
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"your-project/cmd"
|
|
||||||
"your-project/database"
|
|
||||||
"your-project/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var rootCmd = &cobra.Command{Use: "sunhpc"}
|
|
||||||
|
|
||||||
// init database 命令
|
|
||||||
var initCmd = &cobra.Command{
|
|
||||||
Use: "init database",
|
|
||||||
Short: "初始化数据库",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
force, _ := cmd.Flags().GetBool("force")
|
|
||||||
|
|
||||||
// 获取DB实例(只加载配置)
|
|
||||||
db, err := database.GetDB()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化schema(会根据force参数决定行为)
|
|
||||||
if err := db.InitSchema(force); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("数据库初始化成功")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
initCmd.Flags().BoolP("force", "f", false, "强制重新初始化")
|
|
||||||
rootCmd.AddCommand(initCmd)
|
|
||||||
|
|
||||||
// node list 命令 - 自动连接已存在的数据库
|
|
||||||
var nodeCmd = &cobra.Command{
|
|
||||||
Use: "node list",
|
|
||||||
Short: "列出所有节点",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
service, err := service.NewNodeService()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err) // 如果数据库不存在,这里会报错
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := service.ListNode(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rootCmd.AddCommand(nodeCmd)
|
|
||||||
|
|
||||||
rootCmd.Execute()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
description: AutoFS server for SunHPC clusters
|
|
||||||
copyright: |
|
|
||||||
Copyright (c) 2026 SunHPC Project.
|
|
||||||
Licensed under Apache 2.0.
|
|
||||||
|
|
||||||
stages:
|
|
||||||
post:
|
|
||||||
- type: file
|
|
||||||
path: /etc/auto.master
|
|
||||||
content: |
|
|
||||||
/share /etc/auto.share --timeout=1200
|
|
||||||
/home /etc/auto.home --timeout=1200
|
|
||||||
|
|
||||||
- type: file
|
|
||||||
path: /etc/auto.share
|
|
||||||
content: |
|
|
||||||
apps {{ .Node.Hostname }}.{{ .Cluster.Domain }}:/export/&
|
|
||||||
|
|
||||||
- type: script
|
|
||||||
content: |
|
|
||||||
mkdir -p /export/apps
|
|
||||||
echo "AutoFS 配置已生成"
|
|
||||||
|
|
||||||
configure:
|
|
||||||
- type: script
|
|
||||||
condition: "{{ if .Node.OldHostname }}true{{ end }}"
|
|
||||||
content: |
|
|
||||||
sed -i 's/{{ .Node.OldHostname }}/{{ .Node.Hostname }}/g' /etc/auto.share
|
|
||||||
systemctl restart autofs
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package tmpls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
_ "embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed *.yaml
|
|
||||||
var FS embed.FS
|
|
||||||
Reference in New Issue
Block a user