重构架构

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

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
}

294
pkg/database/schema.go Normal file
View File

@@ -0,0 +1,294 @@
// Package db defines the database schema.
package database
// CurrentSchemaVersion returns the current schema version (for migrations)
func CurrentSchemaVersion() int {
return 1
}
// CreateTableStatements returns a list of CREATE TABLE statements.
func CreateTableStatements() []string {
return []string{
createAliasesTable(),
createAttributesTable(),
createBootactionTable(),
createDistributionsTable(),
createFirewallsTable(),
createNetworksTable(),
createPartitionsTable(),
createPublicKeysTable(),
createSoftwareTable(),
createNodesTable(),
createSubnetsTable(),
createTrg_nodes_before_delete(),
}
}
// DropTableOrder returns table names in reverse dependency order for safe DROP.
func DropTableOrder() []string {
return []string{
"aliases",
"attributes",
"bootactions",
"distributions",
"firewalls",
"networks",
"partitions",
"publickeys",
"software",
"nodes",
"subnets",
}
}
func DropTriggerStatements() []string {
return []string{
"trg_nodes_before_delete",
}
}
// --- Private DDL Functions ---
func createAliasesTable() string {
return `
CREATE TABLE IF NOT EXISTS aliases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
alias TEXT NOT NULL,
CONSTRAINT fk_aliases_node FOREIGN KEY(node_id) REFERENCES nodes(id),
UNIQUE(node_id, alias)
);
create index if not exists idx_aliases_node on aliases (node_id);
`
}
func createAttributesTable() string {
return `
CREATE TABLE IF NOT EXISTS attributes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
attr TEXT NOT NULL,
value TEXT,
shadow TEXT,
CONSTRAINT fk_attributes_node FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_attributes_node on attributes (node_id);
`
}
func createBootactionTable() string {
return `
CREATE TABLE IF NOT EXISTS bootactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
action TEXT,
kernel TEXT,
initrd TEXT,
cmdline TEXT,
CONSTRAINT fk_bootactions_node FOREIGN KEY(node_id) REFERENCES nodes(id),
UNIQUE(node_id)
);
create index if not exists idx_bootactions_node on bootactions (node_id);
`
}
func createDistributionsTable() string {
return `
CREATE TABLE IF NOT EXISTS distributions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
name TEXT NOT NULL,
version TEXT,
lang TEXT,
os_release TEXT,
constraint distributions_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_distributions_node on distributions (node_id);
`
}
func createFirewallsTable() string {
return `
CREATE TABLE IF NOT EXISTS firewalls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
rulename TEXT NOT NULL,
rulesrc TEXT NOT NULL,
insubnet INTEGER,
outsubnet INTEGER,
service TEXT,
protocol TEXT,
action TEXT,
chain TEXT,
flags TEXT,
comment TEXT,
constraint firewalls_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_firewalls_node on firewalls (node_id);
`
}
func createNetworksTable() string {
return `
CREATE TABLE IF NOT EXISTS networks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
subnet_id INTEGER,
mac TEXT,
ip TEXT,
name TEXT,
device TEXT,
module TEXT,
vlanid INTEGER,
options TEXT,
channel TEXT,
disable_kvm INTEGER NOT NULL DEFAULT 0 CHECK (disable_kvm IN (0, 1)),
constraint networks_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id),
constraint networks_subnets_fk FOREIGN KEY(subnet_id) REFERENCES subnets(id)
);
create index if not exists idx_networks_node on networks (node_id);
`
}
func createNodesTable() string {
return `
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cpus INTEGER NOT NULL,
rack INTEGER NOT NULL,
rank INTEGER NOT NULL,
arch TEXT,
os TEXT,
runaction TEXT,
installaction TEXT
);
create index if not exists idx_nodes_name on nodes (name);
`
}
func createPartitionsTable() string {
return `
CREATE TABLE IF NOT EXISTS partitions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
device TEXT NOT NULL,
formatflags TEXT NOT NULL,
fstype TEXT NOT NULL,
mountpoint TEXT NOT NULL,
partitionflags TEXT NOT NULL,
partitionid TEXT NOT NULL,
partitionsize TEXT NOT NULL,
sectorstart TEXT NOT NULL,
constraint partitions_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_partitions_node on partitions (node_id);
`
}
func createPublicKeysTable() string {
return `
CREATE TABLE IF NOT EXISTS publickeys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER,
publickey TEXT NOT NULL,
description TEXT,
constraint publickeys_nodes_fk FOREIGN KEY(node_id) REFERENCES nodes(id)
);
create index if not exists idx_publickeys_node on publickeys (node_id);
`
}
func createSubnetsTable() string {
return `
CREATE TABLE IF NOT EXISTS subnets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
dnszone TEXT NOT NULL,
subnet TEXT NOT NULL,
netmask TEXT NOT NULL,
mtu INTEGER NOT NULL DEFAULT 1500,
servedns INTEGER NOT NULL DEFAULT 0 CHECK (servedns IN (0, 1)),
UNIQUE(name, dnszone)
);`
}
func createSoftwareTable() string {
return `
CREATE TABLE IF NOT EXISTS software (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
pathversion TEXT,
fullversion TEXT,
description TEXT,
website TEXT,
license TEXT,
install_method TEXT NOT NULL CHECK (install_method IN (
'source',
'binary',
'rpm',
'docker',
'apptainer',
'conda',
'mamba',
'spack',
'tarball',
'zipball',
'pip',
'npm',
'custom'
)),
-- 源码编译相关参数
source_url TEXT, -- 源码下载地址
source_checksum TEXT, -- 源码校验和
source_checksum_type TEXT NOT NULL CHECK (source_checksum_type IN (
'md5',
'sha1',
'sha256',
'sha512'
)),
build_dependencies TEXT, -- 编译依赖(JSON格式)
configure_params TEXT, -- 配置参数(JSON格式)
make_params TEXT, -- make参数(JSON格式)
make_install_params TEXT, -- make install参数(JSON格式)
-- 安装路径参数
install_path TEXT NOT NULL, -- 安装路径
env_vars TEXT, -- 环境变量(JSON格式)
-- 状态信息
is_installed INTEGER NOT NULL DEFAULT 0 CHECK (is_installed IN (0, 1)), -- 是否安装
install_date TEXT, -- 安装日期
updated_date TEXT, -- 更新日期
install_user TEXT, -- 安装用户
notes TEXT, -- 安装备注
-- 元数据
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, -- 更新时间
UNIQUE(name)
);
create index if not exists idx_software_name on software (name);
create index if not exists idx_software_install_method on software (install_method);
create index if not exists idx_software_is_installed on software (is_installed);
`
}
func createTrg_nodes_before_delete() string {
return `
CREATE TRIGGER IF NOT EXISTS trg_nodes_before_delete
BEFORE DELETE ON nodes
FOR EACH ROW
BEGIN
-- 先删除子表的关联记录
DELETE FROM aliases WHERE node_id = OLD.id;
DELETE FROM attributes WHERE node_id = OLD.id;
DELETE FROM bootactions WHERE node_id = OLD.id;
DELETE FROM distributions WHERE node_id = OLD.id;
DELETE FROM firewalls WHERE node_id = OLD.id;
DELETE FROM networks WHERE node_id = OLD.id;
DELETE FROM partitions WHERE node_id = OLD.id;
DELETE FROM publickeys WHERE node_id = OLD.id;
END;
`
}

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

@@ -0,0 +1,53 @@
// internal/templating/embedded.go
package templating
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sunhpc/data/tmpls"
"gopkg.in/yaml.v3"
)
// ListEmbeddedTemplates 返回所有内置模板名称(不含路径和扩展名)
func ListEmbeddedTemplates() ([]string, error) {
entries, err := fs.ReadDir(tmpls.ConfigFS, ".")
if err != nil {
return nil, err
}
var names []string
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".yaml" {
continue
}
names = append(names, entry.Name()[:len(entry.Name())-5]) // 去掉 .yaml
}
return names, nil
}
// LoadEmbeddedTemplate 从二进制加载内置模板
func LoadEmbeddedTemplate(name string) (*Template, error) {
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("内置模板 '%s' 不存在", name)
}
return nil, err
}
var tmpl Template
if err := yaml.Unmarshal(data, &tmpl); err != nil {
return nil, fmt.Errorf("解析内置模板失败: %w", err)
}
return &tmpl, nil
}
// DumpEmbeddedTemplateToFile 将内置模板写入文件
func DumpEmbeddedTemplateToFile(name, outputPath string) error {
data, err := tmpls.ConfigFS.ReadFile(name + ".yaml")
if err != nil {
return fmt.Errorf("找不到内置模板 '%s': %w", name, err)
}
return os.WriteFile(outputPath, data, 0644)
}

104
pkg/templating/engine.go Normal file
View File

@@ -0,0 +1,104 @@
// internal/templating/engine.go
package templating
import (
"bytes"
"fmt"
"os"
"path/filepath"
"text/template"
"gopkg.in/yaml.v3"
)
// LoadTemplate 从文件加载 YAML 模板
func LoadTemplate(path string) (*Template, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("无法读取模板文件 %s: %w", path, err)
}
var tmpl Template
if err := yaml.Unmarshal(data, &tmpl); err != nil {
return nil, fmt.Errorf("YAML 解析失败: %w", err)
}
return &tmpl, nil
}
// Render 渲染模板为具体操作
func (t *Template) Render(ctx Context) (map[string][]RenderedStep, error) {
result := make(map[string][]RenderedStep)
for stageName, steps := range t.Stages {
var renderedSteps []RenderedStep
for _, step := range steps {
// 处理 condition
if step.Condition != "" {
condTmpl, err := template.New("condition").Parse(step.Condition)
if err != nil {
return nil, fmt.Errorf("条件模板语法错误: %w", err)
}
var buf bytes.Buffer
if err := condTmpl.Execute(&buf, ctx); err != nil {
return nil, fmt.Errorf("执行条件模板失败: %w", err)
}
if buf.String() == "" {
continue // 条件不满足,跳过
}
}
// 渲染 content
contentTmpl, err := template.New("content").Parse(step.Content)
if err != nil {
return nil, fmt.Errorf("内容模板语法错误: %w", err)
}
var buf bytes.Buffer
if err := contentTmpl.Execute(&buf, ctx); err != nil {
return nil, fmt.Errorf("执行内容模板失败: %w", err)
}
renderedSteps = append(renderedSteps, RenderedStep{
Type: step.Type,
Path: step.Path,
Content: buf.String(),
})
}
result[stageName] = renderedSteps
}
return result, nil
}
// RenderedStep 是渲染后的步骤
type RenderedStep struct {
Type string
Path string
Content string
}
// WriteFiles 将 file 类型步骤写入磁盘
func WriteFiles(steps []RenderedStep, rootDir string) error {
for _, s := range steps {
if s.Type != "file" {
continue
}
fullPath := s.Path
if !filepath.IsAbs(s.Path) {
fullPath = filepath.Join(rootDir, s.Path)
}
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return err
}
if err := os.WriteFile(fullPath, []byte(s.Content), 0644); err != nil {
return fmt.Errorf("写入文件 %s 失败: %w", fullPath, err)
}
}
return nil
}
// PrintScripts 打印 script 内容(安全起见,先不自动执行)
func PrintScripts(steps []RenderedStep) {
for _, s := range steps {
if s.Type == "script" {
fmt.Printf("# --- 脚本开始 ---\n%s\n# --- 脚本结束 ---\n", s.Content)
}
}
}

38
pkg/templating/types.go Normal file
View File

@@ -0,0 +1,38 @@
package templating
// Template 是 YAML 模板的顶层结构
type Template struct {
Description string `yaml:"description,omitempty"`
Copyright string `yaml:"copyright,omitempty"`
Stages map[string][]Step `yaml:"stages"`
}
// Step 表示一个操作步骤
type Step struct {
Type string `yaml:"type"` // "file" 或 "script"
Path string `yaml:"path,omitempty"` // 文件路径(仅 type=file
Content string `yaml:"content"` // 多行内容
Condition string `yaml:"condition,omitempty"` // 条件表达式Go template
}
// Context 是渲染模板时的上下文数据
type Context struct {
Node NodeInfo `json:"node"`
Cluster ClusterInfo `json:"cluster"`
}
// NodeInfo 节点信息
type NodeInfo struct {
Hostname string `json:"hostname"`
OldHostname string `json:"old_hostname,omitempty"`
Domain string `json:"domain"`
IP string `json:"ip"`
}
// ClusterInfo 集群信息
type ClusterInfo struct {
Name string `json:"name"`
Domain string `json:"domain"`
AdminEmail string `json:"admin_email"`
TimeZone string `json:"time_zone"`
}

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