重构架构

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

View File

@@ -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
}

View File

@@ -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
View File

@@ -0,0 +1,9 @@
package main
import (
"fmt"
)
func main() {
fmt.Println("✓ 计算节点添加成功")
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -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
View 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

Binary file not shown.

View File

@@ -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
View File

161
data/confs/confs.go Normal file
View 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
View 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

View File

161
data/tmpls/tmpl.go Normal file
View 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
}

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
View File

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

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

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

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

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

View File

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

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

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

View File

@@ -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
View File

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

View File

@@ -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"
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

12
main.go
View File

@@ -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
View 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
View 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
}

View File

@@ -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
View 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...) }

View File

@@ -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)
}

View File

@@ -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
View File

@@ -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

BIN
sunhpc

Binary file not shown.

View File

@@ -1 +0,0 @@
Test GPG Sign

View File

@@ -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

View File

@@ -1,9 +0,0 @@
package tmpls
import (
"embed"
_ "embed"
)
//go:embed *.yaml
var FS embed.FS