diff --git a/cmd/init/database.go b/cmd/init/database.go deleted file mode 100644 index a114371..0000000 --- a/cmd/init/database.go +++ /dev/null @@ -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 -} diff --git a/cmd/init/init.go b/cmd/init/init.go deleted file mode 100644 index f659242..0000000 --- a/cmd/init/init.go +++ /dev/null @@ -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()) -} diff --git a/cmd/insert/main.go b/cmd/insert/main.go new file mode 100644 index 0000000..8a92094 --- /dev/null +++ b/cmd/insert/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("✓ 计算节点添加成功") +} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 1c31c40..0000000 --- a/cmd/root.go +++ /dev/null @@ -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) -} diff --git a/cmd/soft/install.go b/cmd/soft/install.go deleted file mode 100644 index fa378cd..0000000 --- a/cmd/soft/install.go +++ /dev/null @@ -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 ", - 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") -} diff --git a/cmd/soft/soft.go b/cmd/soft/soft.go deleted file mode 100644 index fb16736..0000000 --- a/cmd/soft/soft.go +++ /dev/null @@ -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 等子命令 -} diff --git a/cmd/sunhpc/main.go b/cmd/sunhpc/main.go new file mode 100644 index 0000000..19e7221 --- /dev/null +++ b/cmd/sunhpc/main.go @@ -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) + } +} diff --git a/cmd/sunhpc/sunhpc b/cmd/sunhpc/sunhpc new file mode 100755 index 0000000..e04b362 Binary files /dev/null and b/cmd/sunhpc/sunhpc differ diff --git a/cmd/tmpl/init.go b/cmd/tmpl/init.go deleted file mode 100644 index fa9ad5d..0000000 --- a/cmd/tmpl/init.go +++ /dev/null @@ -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()) -} diff --git a/configs/config.yaml b/configs/config.yaml new file mode 100644 index 0000000..e69de29 diff --git a/data/confs/confs.go b/data/confs/confs.go new file mode 100644 index 0000000..0843546 --- /dev/null +++ b/data/confs/confs.go @@ -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 +} diff --git a/data/confs/db/base.yaml b/data/confs/db/base.yaml new file mode 100644 index 0000000..179f285 --- /dev/null +++ b/data/confs/db/base.yaml @@ -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 \ No newline at end of file diff --git a/data/confs/firewall/rule1.yaml b/data/confs/firewall/rule1.yaml new file mode 100644 index 0000000..e69de29 diff --git a/autofs.yaml b/data/tmpls/services/autofs.yaml similarity index 100% rename from autofs.yaml rename to data/tmpls/services/autofs.yaml diff --git a/data/tmpls/tmpl.go b/data/tmpls/tmpl.go new file mode 100644 index 0000000..56d82fd --- /dev/null +++ b/data/tmpls/tmpl.go @@ -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 +} diff --git a/docs/database_usage.md b/docs/database_usage.md deleted file mode 100644 index 4175e02..0000000 --- a/docs/database_usage.md +++ /dev/null @@ -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()` 时会自动创建数据库文件和表结构。 diff --git a/go.mod b/go.mod index 5ee5dad..2aa9afa 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 59a83a2..ea82af4 100644 --- a/go.sum +++ b/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= diff --git a/cmd/init/config.go b/internal/cli/init/cfg.go similarity index 59% rename from cmd/init/config.go rename to internal/cli/init/cfg.go index c4d3bc3..18b17ca 100644 --- a/cmd/init/config.go +++ b/internal/cli/init/cfg.go @@ -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 }, } diff --git a/internal/cli/init/db.go b/internal/cli/init/db.go new file mode 100644 index 0000000..6802a7a --- /dev/null +++ b/internal/cli/init/db.go @@ -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 +} diff --git a/internal/cli/init/init.go b/internal/cli/init/init.go new file mode 100644 index 0000000..6793ae4 --- /dev/null +++ b/internal/cli/init/init.go @@ -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 +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 0000000..e1c9905 --- /dev/null +++ b/internal/cli/root.go @@ -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() +} diff --git a/internal/cli/soft/install.go b/internal/cli/soft/install.go new file mode 100644 index 0000000..41e1893 --- /dev/null +++ b/internal/cli/soft/install.go @@ -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 +} diff --git a/internal/cli/soft/soft.go b/internal/cli/soft/soft.go new file mode 100644 index 0000000..cef2084 --- /dev/null +++ b/internal/cli/soft/soft.go @@ -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 +} diff --git a/cmd/tmpl/dump.go b/internal/cli/tmpl/dump.go similarity index 96% rename from cmd/tmpl/dump.go rename to internal/cli/tmpl/dump.go index 7ece475..6661dfe 100644 --- a/cmd/tmpl/dump.go +++ b/internal/cli/tmpl/dump.go @@ -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" ) diff --git a/internal/cli/tmpl/init.go b/internal/cli/tmpl/init.go new file mode 100644 index 0000000..3e5b5b3 --- /dev/null +++ b/internal/cli/tmpl/init.go @@ -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 +} diff --git a/cmd/tmpl/render.go b/internal/cli/tmpl/render.go similarity index 97% rename from cmd/tmpl/render.go rename to internal/cli/tmpl/render.go index 5e18910..67184d9 100644 --- a/cmd/tmpl/render.go +++ b/internal/cli/tmpl/render.go @@ -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" ) diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 9c89bb7..0000000 --- a/internal/config/config.go +++ /dev/null @@ -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 -} diff --git a/internal/config/defaults.go b/internal/config/defaults.go deleted file mode 100644 index 221bac5..0000000 --- a/internal/config/defaults.go +++ /dev/null @@ -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) -} diff --git a/internal/db/db.go b/internal/db/db.go deleted file mode 100644 index af4640b..0000000 --- a/internal/db/db.go +++ /dev/null @@ -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 -} diff --git a/internal/db/db.txt b/internal/db/db.txt deleted file mode 100644 index c1182ef..0000000 --- a/internal/db/db.txt +++ /dev/null @@ -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 -) diff --git a/internal/soft/binary.go b/internal/handler/soft/binary.go similarity index 100% rename from internal/soft/binary.go rename to internal/handler/soft/binary.go diff --git a/internal/soft/manager.go b/internal/handler/soft/manager.go similarity index 98% rename from internal/soft/manager.go rename to internal/handler/soft/manager.go index 1e0ba21..77101d4 100644 --- a/internal/soft/manager.go +++ b/internal/handler/soft/manager.go @@ -1,7 +1,7 @@ package soft import ( - "sunhpc/internal/log" + log "sunhpc/pkg/logger" ) // InstallContext 安装上下文,包含所有命令行参数 diff --git a/internal/soft/package.go b/internal/handler/soft/package.go similarity index 97% rename from internal/soft/package.go rename to internal/handler/soft/package.go index ed3aa2f..57bc19b 100644 --- a/internal/soft/package.go +++ b/internal/handler/soft/package.go @@ -4,7 +4,7 @@ import ( "fmt" "os" "os/exec" - "sunhpc/internal/log" + log "sunhpc/pkg/logger" "sunhpc/pkg/utils" ) diff --git a/internal/soft/source.go b/internal/handler/soft/source.go similarity index 97% rename from internal/soft/source.go rename to internal/handler/soft/source.go index fc381db..9a506f0 100644 --- a/internal/soft/source.go +++ b/internal/handler/soft/source.go @@ -4,7 +4,7 @@ import ( "fmt" "os" "os/exec" - "sunhpc/internal/log" + log "sunhpc/pkg/logger" "sunhpc/pkg/utils" ) diff --git a/internal/log/logger.go b/internal/log/logger.go deleted file mode 100644 index 51391bf..0000000 --- a/internal/log/logger.go +++ /dev/null @@ -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 -} diff --git a/internal/auth/auth.go b/internal/middler/auth/auth.go similarity index 100% rename from internal/auth/auth.go rename to internal/middler/auth/auth.go diff --git a/internal/model/db.go b/internal/model/db.go deleted file mode 100644 index 21c82ed..0000000 --- a/internal/model/db.go +++ /dev/null @@ -1,5 +0,0 @@ -package model - -type DBConfig struct { - ForceDB bool -} diff --git a/main.go b/main.go deleted file mode 100644 index 62c1aeb..0000000 --- a/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "os" - "sunhpc/cmd" -) - -func main() { - if err := cmd.Execute(); err != nil { - os.Exit(1) - } -} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..25196f0 --- /dev/null +++ b/pkg/config/config.go @@ -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 + }{} +} diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..8ffa09f --- /dev/null +++ b/pkg/database/database.go @@ -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 +} diff --git a/internal/db/schema.go b/pkg/database/schema.go similarity index 99% rename from internal/db/schema.go rename to pkg/database/schema.go index 768dc1a..10befb9 100644 --- a/internal/db/schema.go +++ b/pkg/database/schema.go @@ -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 { diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..008a51d --- /dev/null +++ b/pkg/logger/logger.go @@ -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...) } diff --git a/internal/templating/embedded.go b/pkg/templating/embedded.go similarity index 87% rename from internal/templating/embedded.go rename to pkg/templating/embedded.go index fa5447c..2346c9e 100644 --- a/internal/templating/embedded.go +++ b/pkg/templating/embedded.go @@ -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) } diff --git a/internal/templating/engine.go b/pkg/templating/engine.go similarity index 100% rename from internal/templating/engine.go rename to pkg/templating/engine.go diff --git a/internal/templating/types.go b/pkg/templating/types.go similarity index 100% rename from internal/templating/types.go rename to pkg/templating/types.go diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index cd8119c..9a0852e 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -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") +} diff --git a/readme.md b/readme.md index 8de9a1e..7b1a569 100644 --- a/readme.md +++ b/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() -} -``` \ No newline at end of file +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 \ No newline at end of file diff --git a/sunhpc b/sunhpc index bf2e519..40eb5db 100755 Binary files a/sunhpc and b/sunhpc differ diff --git a/test.txt b/test.txt deleted file mode 100644 index 05aa6ba..0000000 --- a/test.txt +++ /dev/null @@ -1 +0,0 @@ -Test GPG Sign diff --git a/tmpls/autofs.yaml b/tmpls/autofs.yaml deleted file mode 100644 index 4ab87e0..0000000 --- a/tmpls/autofs.yaml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/tmpls/tmpls.go b/tmpls/tmpls.go deleted file mode 100644 index 44f769c..0000000 --- a/tmpls/tmpls.go +++ /dev/null @@ -1,9 +0,0 @@ -package tmpls - -import ( - "embed" - _ "embed" -) - -//go:embed *.yaml -var FS embed.FS