重构架构
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/pflag v1.0.10 // 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
|
||||
golang.org/x/sys v0.29.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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
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/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package initcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"sunhpc/internal/auth"
|
||||
"sunhpc/internal/config"
|
||||
"sunhpc/internal/log"
|
||||
"sunhpc/internal/middler/auth"
|
||||
"sunhpc/pkg/logger"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// NewConfigCmd 创建 "init config" 命令
|
||||
func NewConfigCmd() *cobra.Command {
|
||||
func NewInitCfgCmd() *cobra.Command {
|
||||
var (
|
||||
force bool
|
||||
path string
|
||||
@@ -30,31 +27,12 @@ func NewConfigCmd() *cobra.Command {
|
||||
sunhpc init config -f # 强制覆盖已有配置文件
|
||||
sunhpc init config -p /etc/sunhpc/sunhpc.yaml # 指定路径
|
||||
`,
|
||||
|
||||
Annotations: map[string]string{
|
||||
"require-root": "true", // 假设需要 root(你可自定义策略)
|
||||
},
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := auth.RequireRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if 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)
|
||||
logger.Info("✅ 配置文件已生成", zap.String("path", path))
|
||||
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 (
|
||||
"fmt"
|
||||
|
||||
"sunhpc/internal/log"
|
||||
"sunhpc/internal/templating"
|
||||
log "sunhpc/pkg/logger"
|
||||
"sunhpc/pkg/templating"
|
||||
|
||||
"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 (
|
||||
"fmt"
|
||||
|
||||
"sunhpc/internal/log"
|
||||
"sunhpc/internal/templating"
|
||||
log "sunhpc/pkg/logger"
|
||||
templating "sunhpc/pkg/templating"
|
||||
|
||||
"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
|
||||
|
||||
import (
|
||||
"sunhpc/internal/log"
|
||||
log "sunhpc/pkg/logger"
|
||||
)
|
||||
|
||||
// InstallContext 安装上下文,包含所有命令行参数
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sunhpc/internal/log"
|
||||
log "sunhpc/pkg/logger"
|
||||
"sunhpc/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sunhpc/internal/log"
|
||||
log "sunhpc/pkg/logger"
|
||||
"sunhpc/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// 日志级别
|
||||
type Level int
|
||||
|
||||
const (
|
||||
DebugLevel Level = iota
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
)
|
||||
|
||||
// 级别名称
|
||||
var levelNames = map[Level]string{
|
||||
DebugLevel: "DEBUG",
|
||||
InfoLevel: "INFO",
|
||||
WarnLevel: "WARN",
|
||||
ErrorLevel: "ERROR",
|
||||
FatalLevel: "FATAL",
|
||||
}
|
||||
|
||||
// 级别简写
|
||||
var levelShort = map[Level]string{
|
||||
DebugLevel: "[d]",
|
||||
InfoLevel: "[i]",
|
||||
WarnLevel: "[w]",
|
||||
ErrorLevel: "[e]",
|
||||
FatalLevel: "[f]",
|
||||
}
|
||||
|
||||
// 级别颜色
|
||||
var levelColor = map[Level]func(format string, a ...interface{}) string{
|
||||
DebugLevel: color.CyanString, // 青色
|
||||
InfoLevel: color.GreenString, // 绿色
|
||||
WarnLevel: color.YellowString, // 黄色
|
||||
ErrorLevel: color.RedString, // 红色
|
||||
FatalLevel: color.MagentaString, // 品红
|
||||
}
|
||||
|
||||
// Logger 日志器结构体
|
||||
type Logger struct {
|
||||
mu sync.Mutex
|
||||
consoleOut io.Writer // 控制台输出
|
||||
fileOut io.Writer // 文件输出
|
||||
minLevel Level // 最小输出级别
|
||||
showColor bool // 是否显示颜色
|
||||
showCaller bool // 是否显示调用者信息
|
||||
callerSkip int // 调用者跳过的层级
|
||||
timeFormat string // 时间格式
|
||||
}
|
||||
|
||||
// 默认日志器实例
|
||||
var defaultLogger *Logger
|
||||
|
||||
const (
|
||||
defaultTimeFormat = "2006-01-02 15:04:05"
|
||||
logFile = "/var/log/sunhpc/sunhpc.log"
|
||||
)
|
||||
|
||||
// Init 初始化日志系统
|
||||
func Init(verbose bool) {
|
||||
// 确保日志目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(logFile), 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "创建日志目录失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 打开日志文件
|
||||
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "打开日志文件失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 控制台输出
|
||||
consoleOut := os.Stdout
|
||||
|
||||
// 创建日志器
|
||||
defaultLogger = &Logger{
|
||||
consoleOut: consoleOut,
|
||||
fileOut: file,
|
||||
minLevel: InfoLevel,
|
||||
showColor: true,
|
||||
showCaller: false,
|
||||
callerSkip: 2,
|
||||
timeFormat: defaultTimeFormat,
|
||||
}
|
||||
|
||||
// 详细模式下显示调试信息
|
||||
if verbose {
|
||||
defaultLogger.minLevel = DebugLevel
|
||||
defaultLogger.showCaller = true
|
||||
}
|
||||
|
||||
// 初始化颜色支持
|
||||
if runtime.GOOS == "windows" {
|
||||
color.NoColor = false
|
||||
}
|
||||
}
|
||||
|
||||
// log 核心日志输出方法
|
||||
func (l *Logger) log(level Level, format string, args ...interface{}) {
|
||||
if level < l.minLevel {
|
||||
return
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// 生成时间戳
|
||||
timestamp := time.Now().Format(l.timeFormat)
|
||||
|
||||
// 获取调用者信息
|
||||
caller := ""
|
||||
if l.showCaller {
|
||||
_, file, line, ok := runtime.Caller(l.callerSkip)
|
||||
if ok {
|
||||
// 只保留文件名和行号
|
||||
file = filepath.Base(file)
|
||||
caller = fmt.Sprintf(" %s:%d", file, line)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化消息
|
||||
var message string
|
||||
if format == "" {
|
||||
message = fmt.Sprint(args...)
|
||||
} else {
|
||||
message = fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
// ---- 控制台输出(带颜色和简写)----
|
||||
if l.consoleOut != nil {
|
||||
// 获取级别简写
|
||||
shortPrefix := levelShort[level]
|
||||
|
||||
// 构建控制台行
|
||||
var consoleLine string
|
||||
|
||||
if l.showColor {
|
||||
// 带颜色输出 - 简写有颜色,时间戳灰色
|
||||
colorFunc := levelColor[level]
|
||||
consoleLine = fmt.Sprintf("%s %s %s",
|
||||
color.HiBlackString(timestamp), // 时间戳灰色
|
||||
colorFunc(shortPrefix), // 级别简写彩色
|
||||
message) // 消息普通颜色
|
||||
} else {
|
||||
// 不带颜色输出
|
||||
consoleLine = fmt.Sprintf("%s %s %s",
|
||||
timestamp,
|
||||
shortPrefix,
|
||||
message)
|
||||
}
|
||||
|
||||
// 添加调用者信息(灰色)
|
||||
if caller != "" {
|
||||
if l.showColor {
|
||||
consoleLine += fmt.Sprintf(" %s", color.HiBlackString(caller))
|
||||
} else {
|
||||
consoleLine += fmt.Sprintf(" %s", caller)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(l.consoleOut, consoleLine)
|
||||
}
|
||||
|
||||
// ---- 文件输出(完整格式)----
|
||||
if l.fileOut != nil {
|
||||
// 获取级别全名
|
||||
levelName := levelNames[level]
|
||||
|
||||
// 文件使用完整格式:时间 [级别] 消息 调用者
|
||||
fileLine := fmt.Sprintf("%s [%s] %s%s\n",
|
||||
timestamp,
|
||||
levelName,
|
||||
message,
|
||||
caller)
|
||||
|
||||
fmt.Fprint(l.fileOut, fileLine)
|
||||
}
|
||||
|
||||
// 致命错误退出程序
|
||||
if level == FatalLevel {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 全局日志函数
|
||||
|
||||
// Debug 调试日志
|
||||
func Debug(args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(DebugLevel, "", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Debugf 格式化调试日志
|
||||
func Debugf(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(DebugLevel, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Info 信息日志
|
||||
func Info(args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(InfoLevel, "", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Infof 格式化信息日志
|
||||
func Infof(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(InfoLevel, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Warn 警告日志
|
||||
func Warn(args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(WarnLevel, "", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Warnf 格式化警告日志
|
||||
func Warnf(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(WarnLevel, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Error 错误日志
|
||||
func Error(args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(ErrorLevel, "", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Errorf 格式化错误日志
|
||||
func Errorf(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(ErrorLevel, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Fatal 致命错误日志,输出后退出程序
|
||||
func Fatal(args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(FatalLevel, "", args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Fatalf 格式化致命错误日志,输出后退出程序
|
||||
func Fatalf(format string, args ...interface{}) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.log(FatalLevel, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Writer 返回一个 io.Writer,可将子命令的输出写入日志(Debug级别)
|
||||
func Writer() *io.PipeWriter {
|
||||
r, w := io.Pipe()
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := r.Read(buf)
|
||||
if n > 0 {
|
||||
Debug(string(buf[:n]))
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
return w
|
||||
}
|
||||
|
||||
// SetLevel 设置日志级别
|
||||
func SetLevel(level Level) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.mu.Lock()
|
||||
defer defaultLogger.mu.Unlock()
|
||||
defaultLogger.minLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
// EnableColor 启用/禁用颜色输出
|
||||
func EnableColor(enable bool) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.mu.Lock()
|
||||
defer defaultLogger.mu.Unlock()
|
||||
defaultLogger.showColor = enable
|
||||
}
|
||||
}
|
||||
|
||||
// EnableCaller 启用/禁用调用者信息
|
||||
func EnableCaller(enable bool) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.mu.Lock()
|
||||
defer defaultLogger.mu.Unlock()
|
||||
defaultLogger.showCaller = enable
|
||||
}
|
||||
}
|
||||
|
||||
// SetTimeFormat 设置时间格式
|
||||
func SetTimeFormat(format string) {
|
||||
if defaultLogger != nil {
|
||||
defaultLogger.mu.Lock()
|
||||
defer defaultLogger.mu.Unlock()
|
||||
defaultLogger.timeFormat = format
|
||||
}
|
||||
}
|
||||
|
||||
// Sync 同步日志文件
|
||||
func Sync() {
|
||||
if defaultLogger != nil && defaultLogger.fileOut != nil {
|
||||
if f, ok := defaultLogger.fileOut.(*os.File); ok {
|
||||
f.Sync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭日志文件
|
||||
func Close() error {
|
||||
if defaultLogger != nil && defaultLogger.fileOut != nil {
|
||||
if f, ok := defaultLogger.fileOut.(*os.File); ok {
|
||||
return f.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package model
|
||||
|
||||
type DBConfig struct {
|
||||
ForceDB bool
|
||||
}
|
||||
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
|
||||
package database
|
||||
|
||||
// CurrentSchemaVersion returns the current schema version (for migrations)
|
||||
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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"sunhpc/tmpls"
|
||||
"sunhpc/data/tmpls"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// ListEmbeddedTemplates 返回所有内置模板名称(不含路径和扩展名)
|
||||
func ListEmbeddedTemplates() ([]string, error) {
|
||||
entries, err := fs.ReadDir(tmpls.FS, ".")
|
||||
entries, err := fs.ReadDir(tmpls.ConfigFS, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -30,7 +29,7 @@ func ListEmbeddedTemplates() ([]string, error) {
|
||||
|
||||
// LoadEmbeddedTemplate 从二进制加载内置模板
|
||||
func LoadEmbeddedTemplate(name string) (*Template, error) {
|
||||
data, err := tmpls.FS.ReadFile(name + ".yaml")
|
||||
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("内置模板 '%s' 不存在", name)
|
||||
@@ -46,7 +45,7 @@ func LoadEmbeddedTemplate(name string) (*Template, error) {
|
||||
|
||||
// DumpEmbeddedTemplateToFile 将内置模板写入文件
|
||||
func DumpEmbeddedTemplateToFile(name, outputPath string) error {
|
||||
data, err := tmpls.FS.ReadFile(name + ".yaml")
|
||||
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("找不到内置模板 '%s': %w", name, err)
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CommandExists 检查命令是否存在
|
||||
@@ -16,3 +19,15 @@ func FileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
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 @@
|
||||
## 其他包使用数据库模块
|
||||
```go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"your-project/database"
|
||||
"your-project/log"
|
||||
)
|
||||
|
||||
type NodeRepository struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
// NewNodeRepository 创建仓库(延迟连接)
|
||||
func NewNodeRepository() (*NodeRepository, error) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &NodeRepository{db: db}, nil
|
||||
}
|
||||
|
||||
// GetNode 获取节点(自动连接)
|
||||
func (r *NodeRepository) GetNode(id int) (*Node, error) {
|
||||
// 获取数据库引擎(会自动连接)
|
||||
engine, err := r.db.GetEngine()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("数据库连接失败: %w", err)
|
||||
}
|
||||
|
||||
var node Node
|
||||
err = engine.QueryRow("SELECT id, name FROM nodes WHERE id = ?", id).
|
||||
Scan(&node.ID, &node.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &node, nil
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
```
|
||||
project/
|
||||
├── cmd/
|
||||
│ ├── sunhpc/
|
||||
│ │ └── main.go
|
||||
│ └── insert-nodes/
|
||||
│ └── main.go
|
||||
├── pkg/
|
||||
│ ├── app/
|
||||
│ │ └── app.go
|
||||
│ ├── config/
|
||||
│ │ └── config.go
|
||||
│ ├── database/
|
||||
│ │ └── database.go
|
||||
│ ├── logger/
|
||||
│ │ └── logger.go
|
||||
│ └── utils/
|
||||
│ └── utils.go
|
||||
├── internal/
|
||||
│ ├── cli/
|
||||
│ │ ├── root.go # 根命令
|
||||
│ │ ├── init/ # init 子命令目录
|
||||
│ │ │ ├── init.go # init 命令定义
|
||||
│ │ │ ├── db.go # init db 子命令
|
||||
│ │ │ └── cfg.go # init cfg 子命令
|
||||
│ │ ├── soft/ # soft 子命令目录
|
||||
│ │ │ ├── soft.go # soft 命令定义
|
||||
│ │ │ └── install.go # soft install 子命令
|
||||
│ │ └── node/ # node 子命令目录(可选扩展)
|
||||
│ │ ├── node.go
|
||||
│ │ └── list.go
|
||||
│ ├── handler/
|
||||
│ │ └── node_handler.go
|
||||
│ └── middleware/
|
||||
│ └── auth.go
|
||||
├── configs/
|
||||
│ └── config.yaml
|
||||
├── data/
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
@@ -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