Compare commits
2 Commits
ce9af9f7d0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
3a5f5ddd5d
|
|||
|
47a2dfeda1
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
sunhpc
|
||||
testgui
|
||||
|
||||
66
cmd/test/main.go
Normal file
66
cmd/test/main.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// model 定义应用的状态
|
||||
type model struct {
|
||||
items []string // 列表数据
|
||||
selectedIdx int // 当前选中的索引
|
||||
}
|
||||
|
||||
// Init 初始化模型,返回初始命令(这里不需要,返回 nil)
|
||||
func (m model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update 处理用户输入和状态更新
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
// 处理键盘输入
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
// Tab 键:切换选中项(循环切换)
|
||||
case "tab":
|
||||
m.selectedIdx = (m.selectedIdx + 1) % len(m.items)
|
||||
// Ctrl+C 或 q 键:退出程序
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// View 渲染界面
|
||||
func (m model) View() string {
|
||||
s := "网络接口列表(按 Tab 切换选择,按 q 退出)\n\n"
|
||||
|
||||
// 遍历列表项,渲染每一项
|
||||
for i, item := range m.items {
|
||||
// 标记当前选中的项
|
||||
if i == m.selectedIdx {
|
||||
s += fmt.Sprintf("→ %s (选中)\n", item)
|
||||
} else {
|
||||
s += fmt.Sprintf(" %s\n", item)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 初始化模型,设置列表数据
|
||||
initialModel := model{
|
||||
items: []string{"eth0", "eth1", "eth2", "eth3"},
|
||||
selectedIdx: 0, // 默认选中第一个项
|
||||
}
|
||||
|
||||
// 启动 Bubble Tea 程序
|
||||
p := tea.NewProgram(initialModel)
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Printf("程序运行出错: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package initcmd
|
||||
import (
|
||||
"fmt"
|
||||
"sunhpc/internal/middler/auth"
|
||||
"sunhpc/pkg/config"
|
||||
"sunhpc/pkg/database"
|
||||
"sunhpc/pkg/logger"
|
||||
|
||||
@@ -28,26 +27,29 @@ func NewInitDBCmd() *cobra.Command {
|
||||
|
||||
logger.Debug("执行数据库初始化...")
|
||||
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
db, err := database.GetInstance(&cfg.Database, nil)
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("数据库连接失败: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if err := db.InitTables(force); err != nil {
|
||||
if err := database.InitTables(db, force); err != nil {
|
||||
return fmt.Errorf("数据库初始化失败: %w", err)
|
||||
}
|
||||
|
||||
// 测试数据库连接
|
||||
if err := database.TestNodeInsert(db); err != nil {
|
||||
return fmt.Errorf("数据库测试失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制重新初始化")
|
||||
cmd.Flags().BoolVarP(
|
||||
&force, "force", "f", false,
|
||||
"强制重新初始化")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
initcmd "sunhpc/internal/cli/init"
|
||||
"sunhpc/pkg/config"
|
||||
"sunhpc/pkg/logger"
|
||||
@@ -20,21 +23,49 @@ func NewRootCmd() *cobra.Command {
|
||||
Use: "sunhpc",
|
||||
Short: "SunHPC - HPC集群一体化运维工具",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// 加载全局配置(只加载一次)
|
||||
// 设置 CLI 参数
|
||||
config.CLIParams.Verbose = verbose
|
||||
config.CLIParams.NoColor = noColor
|
||||
config.CLIParams.Config = cfgFile
|
||||
|
||||
// 初始化配置目录和默认文件
|
||||
if err := config.InitConfigs(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "初始化配置目录失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 加载配置(后续调用直接返回缓存)
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
// 配置加载失败,使用默认日志配置初始化
|
||||
logger.Warnf("加载配置失败,使用默认日志配置: %v", err)
|
||||
logger.Init(logger.LogConfig{})
|
||||
fmt.Fprintf(os.Stderr, "加载配置失败: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 初始化日志
|
||||
logger.Init(cfg.Log)
|
||||
|
||||
// 记录启动信息
|
||||
logger.Debugf("SunHPC 启动中...")
|
||||
logger.Debugf("配置文件: %s", viper.ConfigFileUsed())
|
||||
logger.Debugf("日志级别: %s", cfg.Log.Level)
|
||||
logger.Debugf("日志格式: %s", cfg.Log.Format)
|
||||
logger.Debugf("日志输出: %s", cfg.Log.Output)
|
||||
logger.Debugf("日志文件: %s", cfg.Log.LogFile)
|
||||
logger.Debugf("显示颜色: %v", cfg.Log.ShowColor)
|
||||
|
||||
// 跳过数据库检查,仅在 init db 时检查
|
||||
if isInitDbCommand(cmd) {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 初始化全局日志(全局只执行一次)
|
||||
logger.Init(logger.LogConfig{
|
||||
Verbose: cfg.Log.Verbose,
|
||||
ShowColor: !cfg.Log.ShowColor,
|
||||
LogFile: cfg.Log.LogFile,
|
||||
})
|
||||
// 检查数据库文件是否存在
|
||||
dbPath := filepath.Join(cfg.Database.Path, cfg.Database.Name)
|
||||
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
||||
logger.Warnf("数据库文件不存在: %s", dbPath)
|
||||
logger.Errorf("请先运行 sunhpc init db 初始化数据库")
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.Debugf("数据库文件存在: %s", dbPath)
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@@ -42,23 +73,17 @@ func NewRootCmd() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&config.CLIParams.Config,
|
||||
"config", "c",
|
||||
"", "配置文件路径 (默认:/etc/sunhpc/config.yaml)")
|
||||
|
||||
cmd.PersistentFlags().BoolVarP(
|
||||
&config.CLIParams.Verbose,
|
||||
"verbose", "v", false, "启用详细日志输出")
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&verbose, "verbose", false,
|
||||
"启用详细日志输出")
|
||||
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&config.CLIParams.NoColor,
|
||||
"no-color", false, "禁用彩色输出")
|
||||
&noColor, "no-color", false,
|
||||
"禁用彩色输出")
|
||||
|
||||
// 如果指定了 --config 参数,优先使用该配置文件
|
||||
if config.CLIParams.Config != "" {
|
||||
viper.SetConfigFile(config.CLIParams.Config)
|
||||
}
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&cfgFile, "config", "",
|
||||
"配置文件路径 (默认:/etc/sunhpc/config.yaml)")
|
||||
|
||||
cmd.AddCommand(initcmd.NewInitCmd())
|
||||
return cmd
|
||||
@@ -67,3 +92,14 @@ func NewRootCmd() *cobra.Command {
|
||||
func Execute() error {
|
||||
return NewRootCmd().Execute()
|
||||
}
|
||||
|
||||
func isInitDbCommand(cmd *cobra.Command) bool {
|
||||
// 检查当前命令是否是 db 子命令
|
||||
if cmd.Name() == "db" {
|
||||
// 检查父命令是否是 init
|
||||
if parent := cmd.Parent(); parent != nil {
|
||||
return parent.Name() == "init"
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -4,70 +4,145 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sunhpc/pkg/logger"
|
||||
"sunhpc/pkg/utils"
|
||||
"sync"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// ==============================================================
|
||||
// 全局变量
|
||||
// ==============================================================
|
||||
var (
|
||||
GlobalConfig *Config
|
||||
configOnce sync.Once // 确保配置只加载一次
|
||||
configMutex sync.RWMutex // 读写锁保护 GlobalConfig
|
||||
)
|
||||
|
||||
// ==============================================================
|
||||
// 目录常量 (可在 main.go 中通过 flag 覆盖默认值)
|
||||
// ==============================================================
|
||||
var (
|
||||
BaseDir string = utils.DefaultBaseDir
|
||||
TmplDir string = utils.DefaultTmplDir
|
||||
LogDir string = utils.DefaultLogDir
|
||||
)
|
||||
|
||||
// ==============================================================
|
||||
// 配置结构体
|
||||
// ==============================================================
|
||||
type Config struct {
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
Log logger.LogConfig `mapstructure:"log" yaml:"log"`
|
||||
Database DatabaseConfig `mapstructure:"database" yaml:"database"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
DSN string `yaml:"dsn"` // 数据库连接字符串
|
||||
Path string `yaml:"path"` // SQLite: 目录路径
|
||||
Name string `yaml:"name"` // SQLite: 文件名
|
||||
DSN string `mapstructure:"dsn" yaml:"dsn"` // 数据库连接字符串
|
||||
Path string `mapstructure:"path" yaml:"path"` // SQLite: 目录路径
|
||||
Name string `mapstructure:"name" yaml:"name"` // SQLite: 文件名
|
||||
Args string `mapstructure:"args" yaml:"args"` // 数据库连接参数
|
||||
}
|
||||
|
||||
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"`
|
||||
type CLIParamsType struct {
|
||||
Verbose bool // -v/--verbose
|
||||
NoColor bool // --no-color
|
||||
Config string // -c/--config
|
||||
}
|
||||
|
||||
// --------------------------------- 全局单例配置(核心) ---------------------------------
|
||||
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"
|
||||
)
|
||||
var CLIParams CLIParamsType // 命令行参数
|
||||
|
||||
// InitConfigs 初始化所有配置目录等数据
|
||||
func InitConfigs() 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查配置文件是否存在,不存在则创建默认配置
|
||||
configPath := filepath.Join(BaseDir, "sunhpc.yaml")
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
if err := createDefaultConfig(configPath); err != nil {
|
||||
return fmt.Errorf("创建默认配置文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDefaultConfig(configPath string) error {
|
||||
defaultConfig := &Config{
|
||||
Database: DatabaseConfig{
|
||||
Path: utils.DefaultDBPath,
|
||||
Name: utils.DefaultDBName,
|
||||
Args: utils.DefaultDBArgs,
|
||||
},
|
||||
Log: logger.LogConfig{
|
||||
Level: "info",
|
||||
Format: "text",
|
||||
Output: "stdout",
|
||||
Verbose: false,
|
||||
LogFile: utils.DefaultLogFile,
|
||||
ShowColor: true,
|
||||
},
|
||||
}
|
||||
|
||||
// 确保数据库目录存在
|
||||
if err := os.MkdirAll(defaultConfig.Database.Path, 0755); err != nil {
|
||||
return fmt.Errorf("创建数据库目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 序列号并写入
|
||||
data, err := yaml.Marshal(defaultConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化配置失败: %w", err)
|
||||
}
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
// ----------------------------------- 配置加载(只加载一次) -----------------------------------
|
||||
func LoadConfig() (*Config, error) {
|
||||
// 如果已经加载过,直接返回
|
||||
configMutex.RLock()
|
||||
if GlobalConfig != nil {
|
||||
// 如果已经加载过,直接返回
|
||||
configMutex.RUnlock()
|
||||
return GlobalConfig, nil
|
||||
}
|
||||
configMutex.RUnlock()
|
||||
configMutex.Lock()
|
||||
defer configMutex.Unlock()
|
||||
|
||||
// 双重检查
|
||||
if GlobalConfig != nil {
|
||||
// 如果已经加载过,直接返回
|
||||
return GlobalConfig, nil
|
||||
}
|
||||
|
||||
viper.SetConfigName("sunhpc")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(BaseDir)
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath(filepath.Join(os.Getenv("HOME"), "."))
|
||||
// 配置文件路径
|
||||
if CLIParams.Config != "" {
|
||||
viper.SetConfigFile(CLIParams.Config)
|
||||
} else {
|
||||
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")
|
||||
viper.SetDefault("log.log_file", utils.DefaultLogFile)
|
||||
viper.SetDefault("database.name", utils.DefaultDBName)
|
||||
viper.SetDefault("database.path", utils.DefaultDBPath)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
// 配置文件不存在时,使用默认值
|
||||
@@ -87,10 +162,12 @@ func LoadConfig() (*Config, error) {
|
||||
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)
|
||||
// 计算派生配置 (如数据库 DSN)
|
||||
dbPath := viper.GetString("database.path")
|
||||
dbName := viper.GetString("database.name")
|
||||
dbArgs := viper.GetString("database.args")
|
||||
fullPath := filepath.Join(dbPath, dbName)
|
||||
dsn := fmt.Sprintf("%s?%s", fullPath, dbArgs)
|
||||
viper.Set("database.dsn", dsn)
|
||||
|
||||
// 解码到结构体
|
||||
@@ -104,57 +181,23 @@ func LoadConfig() (*Config, error) {
|
||||
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
|
||||
}
|
||||
// ==============================================================
|
||||
// SaveConfig - 保存全局配置到文件、运行时配置
|
||||
// ==============================================================
|
||||
func SaveConfig() error {
|
||||
configMutex.RLock()
|
||||
defer configMutex.RUnlock()
|
||||
|
||||
func (c *Config) WriteDefaultConfig(path string) error {
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("创建目录失败: %w", err)
|
||||
if GlobalConfig == nil {
|
||||
return fmt.Errorf("全局配置为空")
|
||||
}
|
||||
|
||||
// 生成默认配置
|
||||
cfg := DefaultConfig(path)
|
||||
|
||||
// 序列化为 YAML
|
||||
data, err := yaml.Marshal(cfg)
|
||||
configPath := filepath.Join(BaseDir, "config.yaml")
|
||||
data, err := yaml.Marshal(GlobalConfig)
|
||||
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,
|
||||
},
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
// ResetConfig 重置全局配置为默认值
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -16,65 +15,57 @@ import (
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
db *sql.DB
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 全局变量
|
||||
// =========================================================
|
||||
var (
|
||||
dbInstance *DB
|
||||
dbInstance *sql.DB
|
||||
dbOnce sync.Once
|
||||
dbMutex sync.RWMutex
|
||||
dbErr error
|
||||
)
|
||||
|
||||
func GetInstance(dbConfig *config.DatabaseConfig, log logger.Logger) (*DB, error) {
|
||||
// =========================================================
|
||||
// GetDB - 获取数据库连接(单例模式)
|
||||
// =========================================================
|
||||
func GetDB() (*sql.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
|
||||
if dbInstance != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(dbConfig.Path, dbConfig.Name)
|
||||
log.Debugf("数据库路径: %s", fullPath)
|
||||
// 确保配置已加载
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
dbErr = fmt.Errorf("加载配置失败: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建DSN
|
||||
dsn := fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL&_timeout=5000&cache=shared",
|
||||
fullPath)
|
||||
log.Debugf("DSN: %s", dsn)
|
||||
logger.Debugf("DSN: %s", cfg.Database.DSN)
|
||||
|
||||
// 打开SQLite 连接
|
||||
sqlDB, err := sql.Open("sqlite3", dsn)
|
||||
sqlDB, err := sql.Open("sqlite3", cfg.Database.DSN)
|
||||
if err != nil {
|
||||
log.Errorf("数据库打开失败: %v", err)
|
||||
dbErr = err
|
||||
dbErr = fmt.Errorf("数据库打开失败: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
sqlDB.SetMaxOpenConns(1) // SQLite 只支持单连接
|
||||
sqlDB.SetMaxIdleConns(1) // 保持一个空闲连接
|
||||
sqlDB.SetMaxOpenConns(10) // 最大打开连接数
|
||||
sqlDB.SetMaxIdleConns(5) // 保持空闲连接
|
||||
sqlDB.SetConnMaxLifetime(0) // 禁用连接生命周期超时
|
||||
sqlDB.SetConnMaxIdleTime(0) // 禁用空闲连接超时
|
||||
|
||||
// 测试数据库连接
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
sqlDB.Close()
|
||||
log.Errorf("数据库连接失败: %v", err)
|
||||
dbErr = err
|
||||
dbErr = fmt.Errorf("数据库连接失败: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 赋值 *DB 类型的单例(而非直接复制 *sql.DB)
|
||||
log.Info("数据库连接成功")
|
||||
dbInstance = &DB{sqlDB, log}
|
||||
logger.Debug("数据库连接成功")
|
||||
dbInstance = sqlDB
|
||||
})
|
||||
|
||||
if dbErr != nil {
|
||||
@@ -84,21 +75,6 @@ func GetInstance(dbConfig *config.DatabaseConfig, log logger.Logger) (*DB, error
|
||||
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)
|
||||
|
||||
@@ -112,38 +88,38 @@ func confirmAction(prompt string) bool {
|
||||
return response == "y" || response == "yes"
|
||||
}
|
||||
|
||||
func (d *DB) InitTables(force bool) error {
|
||||
d.logger.Info("开始初始化数据库表...")
|
||||
func InitTables(db *sql.DB, force bool) error {
|
||||
|
||||
if force {
|
||||
// 确认是否强制删除
|
||||
if !confirmAction("确认强制删除所有表和触发器?") {
|
||||
d.logger.Info("操作已取消")
|
||||
logger.Info("操作已取消")
|
||||
db.Close()
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 强制删除所有表和触发器
|
||||
d.logger.Debug("强制删除所有表和触发器...")
|
||||
if err := dropTables(d.db); err != nil {
|
||||
logger.Debug("强制删除所有表和触发器...")
|
||||
if err := dropTables(db); err != nil {
|
||||
return fmt.Errorf("删除表失败: %w", err)
|
||||
}
|
||||
d.logger.Debug("删除所有表和触发器成功")
|
||||
logger.Debug("删除所有表和触发器成功")
|
||||
|
||||
if err := dropTriggers(d.db); err != nil {
|
||||
if err := dropTriggers(db); err != nil {
|
||||
return fmt.Errorf("删除触发器失败: %w", err)
|
||||
}
|
||||
d.logger.Debug("删除所有触发器成功")
|
||||
logger.Debug("删除所有触发器成功")
|
||||
}
|
||||
|
||||
// ✅ 调用 schema.go 中的函数
|
||||
for _, ddl := range CreateTableStatements() {
|
||||
d.logger.Debugf("执行: %s", ddl)
|
||||
if _, err := d.db.Exec(ddl); err != nil {
|
||||
logger.Debugf("执行: %s", ddl)
|
||||
if _, err := db.Exec(ddl); err != nil {
|
||||
return fmt.Errorf("数据表创建失败: %w", err)
|
||||
}
|
||||
}
|
||||
d.logger.Info("数据库表创建成功")
|
||||
logger.Info("数据库表创建成功")
|
||||
/*
|
||||
使用sqlite3命令 测试数据库是否存在表
|
||||
✅ 查询所有表
|
||||
@@ -174,3 +150,65 @@ func dropTriggers(db *sql.DB) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CloseDB() error {
|
||||
dbMutex.Lock()
|
||||
defer dbMutex.Unlock()
|
||||
|
||||
if dbInstance == nil {
|
||||
if err := dbInstance.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
dbInstance = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用事务回滚测试
|
||||
func RunTestWithRollback(db *sql.DB, testFunc func(*sql.Tx) error) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 执行测试
|
||||
if err := testFunc(tx); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// 回滚事务,所有更改(包括 ID 递增)都会撤销
|
||||
return tx.Rollback()
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
func TestNodeInsert(db *sql.DB) error {
|
||||
logger.Debug("测试数据插入...")
|
||||
return RunTestWithRollback(db, func(tx *sql.Tx) error {
|
||||
// 插入测试数据
|
||||
logger.Debug("执行插入测试数据...")
|
||||
|
||||
_, err := tx.Exec(`
|
||||
INSERT INTO nodes (name, cpus, rack, rank)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, "test-node", 64, 1, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证插入
|
||||
var count int
|
||||
logger.Debug("执行查询测试数据...")
|
||||
err = tx.QueryRow(`
|
||||
SELECT COUNT(*) FROM nodes WHERE name = ?
|
||||
`, "test-node").Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Infof("测试数据插入成功,共 %d 条", count)
|
||||
|
||||
// 不需要手动删除,回滚会自动撤销
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,21 +67,25 @@ var (
|
||||
// DefaultLogger 全局默认日志实例(所有模块可直接用,也可注入自定义实现)
|
||||
DefaultLogger Logger
|
||||
// once 保证日志只初始化一次
|
||||
once sync.Once
|
||||
initOnce sync.Once
|
||||
)
|
||||
|
||||
// LogConfig 日志配置结构体(和项目的config包对齐)
|
||||
type LogConfig struct {
|
||||
Verbose bool // 是否开启详细模式(Debug级别)
|
||||
Level string // 日志级别:debug/info/warn/error
|
||||
ShowColor bool // 是否显示彩色输出
|
||||
LogFile string // 日志文件路径(可选,空则只输出到控制台)
|
||||
Level string `mapstructure:"level" yaml:"level"`
|
||||
Format string `mapstructure:"format" yaml:"format"`
|
||||
Output string `mapstructure:"output" yaml:"output"`
|
||||
Verbose bool `mapstructure:"verbose" yaml:"verbose"`
|
||||
LogFile string `mapstructure:"log_file" yaml:"log_file"`
|
||||
ShowColor bool `mapstructure:"show_color" yaml:"show_color"`
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var defaultConfig = LogConfig{
|
||||
Verbose: false,
|
||||
Level: "info",
|
||||
Format: "text",
|
||||
Output: "stdout",
|
||||
Verbose: false,
|
||||
ShowColor: true,
|
||||
LogFile: "/var/log/sunhpc/sunhpc.log",
|
||||
}
|
||||
@@ -113,6 +117,9 @@ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
colorReset = ""
|
||||
}
|
||||
|
||||
// Debug,打印原始字节,用于调试
|
||||
// fmt.Printf("%q\n", entry.Message)
|
||||
|
||||
// 拼接格式:
|
||||
// 灰色日期 + 空格 + 带颜色的[级别] + 空格 + 日志内容 + 空格 + 灰色文件行号 + 重置
|
||||
fmt.Fprintf(&buf, "%s%s%s %s[%s]%s %s %s%s:%d%s\n",
|
||||
@@ -150,27 +157,6 @@ func getLevelInfo(level logrus.Level) (string, string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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自身)
|
||||
@@ -203,8 +189,7 @@ func shouldSkipPackage(funcName, file string) bool {
|
||||
}
|
||||
|
||||
// 跳过logger包(你自己的包装包)
|
||||
if strings.Contains(funcName, "your/package/logger") ||
|
||||
strings.Contains(file, "logger") {
|
||||
if strings.Contains(file, "/logger/") {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -227,21 +212,14 @@ func getCallerInfoWithSkip(skip int) (string, int) {
|
||||
|
||||
// Init 初始化全局默认日志实例(全局只执行一次)
|
||||
func Init(cfg LogConfig) {
|
||||
once.Do(func() {
|
||||
// 合并配置:传入的配置为空则用默认值
|
||||
if cfg.Level == "" {
|
||||
cfg.Level = defaultConfig.Level
|
||||
}
|
||||
if cfg.LogFile == "" {
|
||||
cfg.LogFile = defaultConfig.LogFile
|
||||
}
|
||||
|
||||
initOnce.Do(func() {
|
||||
// 1. 创建logrus实例
|
||||
logrusInst := logrus.New()
|
||||
|
||||
// 2. 配置输出(控制台 + 文件,可选)
|
||||
var outputs []io.Writer
|
||||
outputs = append(outputs, os.Stdout) // 控制台输出
|
||||
|
||||
// 如果配置了日志文件,添加文件输出
|
||||
if cfg.LogFile != "" {
|
||||
// 确保日志目录存在
|
||||
|
||||
@@ -31,3 +31,23 @@ func GenerateID() (string, error) {
|
||||
func GetTimestamp() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// 定义短语
|
||||
const (
|
||||
NoAvailableNetworkInterfaces = "No available network interfaces"
|
||||
)
|
||||
|
||||
// 定义目录
|
||||
const (
|
||||
DefaultBaseDir string = "/etc/sunhpc"
|
||||
DefaultTmplDir string = DefaultBaseDir + "/tmpl.d"
|
||||
DefaultLogDir string = "/var/log/sunhpc"
|
||||
DefaultLogFile string = DefaultLogDir + "/sunhpc.log"
|
||||
)
|
||||
|
||||
// 定义数据库
|
||||
const (
|
||||
DefaultDBName string = "sunhpc.db"
|
||||
DefaultDBPath string = "/var/lib/sunhpc"
|
||||
DefaultDBArgs string = "_foreign_keys=on&_journal_mode=WAL&_timeout=5000"
|
||||
)
|
||||
|
||||
@@ -3,8 +3,10 @@ package wizard
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sunhpc/pkg/utils"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
)
|
||||
@@ -66,31 +68,30 @@ func (m *model) saveCurrentPage() {
|
||||
|
||||
func (m *model) saveDataPage() {
|
||||
if len(m.textInputs) >= 8 {
|
||||
m.config.Hostname = m.textInputs[0].Value()
|
||||
m.config.Country = m.textInputs[1].Value()
|
||||
m.config.Region = m.textInputs[2].Value()
|
||||
m.config.Timezone = m.textInputs[3].Value()
|
||||
m.config.HomePage = m.textInputs[4].Value()
|
||||
m.config.HomePage = m.textInputs[0].Value()
|
||||
m.config.Hostname = m.textInputs[1].Value()
|
||||
m.config.Country = m.textInputs[2].Value()
|
||||
m.config.Region = m.textInputs[3].Value()
|
||||
m.config.Timezone = m.textInputs[4].Value()
|
||||
m.config.DBAddress = m.textInputs[5].Value()
|
||||
m.config.DBName = m.textInputs[6].Value()
|
||||
m.config.DataAddress = m.textInputs[7].Value()
|
||||
m.config.DataAddress = m.textInputs[6].Value()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) savePublicNetworkPage() {
|
||||
if len(m.textInputs) >= 4 {
|
||||
m.config.PublicInterface = m.textInputs[0].Value()
|
||||
m.config.IPAddress = m.textInputs[1].Value()
|
||||
m.config.Netmask = m.textInputs[2].Value()
|
||||
m.config.Gateway = m.textInputs[3].Value()
|
||||
m.config.PublicIPAddress = m.textInputs[1].Value()
|
||||
m.config.PublicNetmask = m.textInputs[2].Value()
|
||||
m.config.PublicGateway = m.textInputs[3].Value()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) saveInternalNetworkPage() {
|
||||
if len(m.textInputs) >= 3 {
|
||||
m.config.InternalInterface = m.textInputs[0].Value()
|
||||
m.config.InternalIP = m.textInputs[1].Value()
|
||||
m.config.InternalMask = m.textInputs[2].Value()
|
||||
m.config.InternalIPAddress = m.textInputs[1].Value()
|
||||
m.config.InternalNetmask = m.textInputs[2].Value()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,20 +109,18 @@ func (m *model) initPageInputs() {
|
||||
switch m.currentPage {
|
||||
case PageData:
|
||||
fields := []struct{ label, value string }{
|
||||
{"主机名:", m.config.Hostname},
|
||||
{"国家:", m.config.Country},
|
||||
{"地区:", m.config.Region},
|
||||
{"时区:", m.config.Timezone},
|
||||
{"主页:", m.config.HomePage},
|
||||
{"数据库地址:", m.config.DBAddress},
|
||||
{"数据库名称:", m.config.DBName},
|
||||
{"Data 地址:", m.config.DataAddress},
|
||||
{"Homepage", m.config.HomePage},
|
||||
{"Hostname", m.config.Hostname},
|
||||
{"Country", m.config.Country},
|
||||
{"Region", m.config.Region},
|
||||
{"Timezone", m.config.Timezone},
|
||||
{"DB Path", m.config.DBAddress},
|
||||
{"Software", m.config.DataAddress},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.Placeholder = f.label
|
||||
//ti.Placeholder = "请输入" + f.label[:len(f.label)-1]
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
@@ -130,18 +129,19 @@ func (m *model) initPageInputs() {
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"Hostname", "Country", "Region", "Timezone", "Homepage", "DBPath", "DBName", "Software"}
|
||||
m.inputLabels = []string{"Homepage", "Hostname", "Country", "Region", "Timezone", "DBPath", "Software"}
|
||||
|
||||
case PagePublicNetwork:
|
||||
fields := []struct{ label, value string }{
|
||||
{"公网接口:", m.config.PublicInterface},
|
||||
{"IP 地址:", m.config.IPAddress},
|
||||
{"子网掩码:", m.config.Netmask},
|
||||
{"网关:", m.config.Gateway},
|
||||
{"Public Interface", m.config.PublicInterface},
|
||||
{"Public IP Address", m.config.PublicIPAddress},
|
||||
{"Public Netmask", m.config.PublicNetmask},
|
||||
{"Public Gateway", m.config.PublicGateway},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.Placeholder = f.label
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
@@ -150,17 +150,17 @@ func (m *model) initPageInputs() {
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"Wan iface", "IPAddress", "Netmask", "Gateway"}
|
||||
m.inputLabels = []string{"Public Interface", "Public IP Address", "Public Netmask", "Public Gateway"}
|
||||
|
||||
case PageInternalNetwork:
|
||||
fields := []struct{ label, value string }{
|
||||
{"内网接口:", m.config.InternalInterface},
|
||||
{"内网 IP:", m.config.InternalIP},
|
||||
{"内网掩码:", m.config.InternalMask},
|
||||
{"Internal Interface", m.config.InternalInterface},
|
||||
{"Internal IP Address", m.config.InternalIPAddress},
|
||||
{"Internal Netmask", m.config.InternalNetmask},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.Placeholder = f.label
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
@@ -169,16 +169,16 @@ func (m *model) initPageInputs() {
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"Lan iface", "IPAddress", "Netmask"}
|
||||
m.inputLabels = []string{"Internal Interface", "Internal IP", "Internal Mask"}
|
||||
|
||||
case PageDNS:
|
||||
fields := []struct{ label, value string }{
|
||||
{"主 DNS:", m.config.DNSPrimary},
|
||||
{"备 DNS:", m.config.DNSSecondary},
|
||||
{"Primary DNS", m.config.DNSPrimary},
|
||||
{"Secondary DNS", m.config.DNSSecondary},
|
||||
}
|
||||
for _, f := range fields {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.Placeholder = f.label
|
||||
ti.SetValue(f.value)
|
||||
ti.Width = 50
|
||||
m.textInputs = append(m.textInputs, ti)
|
||||
@@ -187,6 +187,31 @@ func (m *model) initPageInputs() {
|
||||
if len(m.textInputs) > 0 {
|
||||
m.textInputs[0].Focus()
|
||||
}
|
||||
m.inputLabels = []string{"DNSPrimary", "DNSSecondary"}
|
||||
m.inputLabels = []string{"Pri DNS", "Sec DNS"}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取系统网络接口
|
||||
func getNetworkInterfaces() []string {
|
||||
// 实现获取系统网络接口的逻辑
|
||||
// 例如:使用 net.Interface() 函数获取系统网络接口
|
||||
// 返回一个字符串切片,包含系统网络接口的名称
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return []string{utils.NoAvailableNetworkInterfaces}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, iface := range interfaces {
|
||||
// 跳过 loopback 接口
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, iface.Name)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return []string{utils.NoAvailableNetworkInterfaces}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package wizard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
@@ -17,19 +19,18 @@ type Config struct {
|
||||
Timezone string `json:"timezone"`
|
||||
HomePage string `json:"homepage"`
|
||||
DBAddress string `json:"db_address"`
|
||||
DBName string `json:"db_name"`
|
||||
DataAddress string `json:"data_address"`
|
||||
|
||||
// 公网设置
|
||||
PublicInterface string `json:"public_interface"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Netmask string `json:"netmask"`
|
||||
Gateway string `json:"gateway"`
|
||||
PublicIPAddress string `json:"ip_address"`
|
||||
PublicNetmask string `json:"netmask"`
|
||||
PublicGateway string `json:"gateway"`
|
||||
|
||||
// 内网配置
|
||||
InternalInterface string `json:"internal_interface"`
|
||||
InternalIP string `json:"internal_ip"`
|
||||
InternalMask string `json:"internal_mask"`
|
||||
InternalIPAddress string `json:"internal_ip"`
|
||||
InternalNetmask string `json:"internal_mask"`
|
||||
|
||||
// DNS 配置
|
||||
DNSPrimary string `json:"dns_primary"`
|
||||
@@ -56,40 +57,60 @@ const (
|
||||
|
||||
// model TUI 主模型
|
||||
type model struct {
|
||||
config Config
|
||||
currentPage PageType
|
||||
totalPages int
|
||||
textInputs []textinput.Model
|
||||
inputLabels []string // 存储标签
|
||||
focusIndex int
|
||||
focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮
|
||||
agreementIdx int // 0=拒绝,1=接受
|
||||
width int
|
||||
height int
|
||||
err error
|
||||
quitting bool
|
||||
done bool
|
||||
force bool
|
||||
config Config
|
||||
currentPage PageType
|
||||
totalPages int
|
||||
networkInterfaces []string // 所有系统网络接口
|
||||
textInputs []textinput.Model
|
||||
inputLabels []string // 存储标签
|
||||
focusIndex int
|
||||
focusType int // 0=输入框, 1=上一步按钮, 2=下一步按钮
|
||||
agreementIdx int // 0=拒绝,1=接受
|
||||
width int
|
||||
height int
|
||||
err error
|
||||
quitting bool
|
||||
done bool
|
||||
force bool
|
||||
}
|
||||
|
||||
// defaultConfig 返回默认配置
|
||||
func defaultConfig() Config {
|
||||
var (
|
||||
defaultPublicInterface string
|
||||
defaultInternalInterface string
|
||||
)
|
||||
interfaces := getNetworkInterfaces()
|
||||
switch len(interfaces) {
|
||||
case 0:
|
||||
defaultPublicInterface = ""
|
||||
defaultInternalInterface = ""
|
||||
case 1:
|
||||
defaultPublicInterface = interfaces[0]
|
||||
defaultInternalInterface = fmt.Sprintf("%s:0", interfaces[0])
|
||||
case 2:
|
||||
defaultPublicInterface = interfaces[0]
|
||||
defaultInternalInterface = interfaces[1]
|
||||
default:
|
||||
defaultPublicInterface = interfaces[0]
|
||||
defaultInternalInterface = interfaces[1]
|
||||
}
|
||||
|
||||
return Config{
|
||||
Hostname: "sunhpc01",
|
||||
Hostname: "cluster.hpc.org",
|
||||
Country: "China",
|
||||
Region: "Beijing",
|
||||
Timezone: "Asia/Shanghai",
|
||||
HomePage: "https://sunhpc.example.com",
|
||||
DBAddress: "127.0.0.1",
|
||||
DBName: "sunhpc_db",
|
||||
DataAddress: "/data/sunhpc",
|
||||
PublicInterface: "eth0",
|
||||
InternalInterface: "eth1",
|
||||
IPAddress: "192.168.1.100",
|
||||
Netmask: "255.255.255.0",
|
||||
Gateway: "192.168.1.1",
|
||||
InternalIP: "10.0.0.100",
|
||||
InternalMask: "255.255.255.0",
|
||||
HomePage: "www.sunhpc.com",
|
||||
DBAddress: "/var/lib/sunhpc/sunhpc.db",
|
||||
DataAddress: "/export/sunhpc",
|
||||
PublicInterface: defaultPublicInterface,
|
||||
PublicIPAddress: "",
|
||||
PublicNetmask: "",
|
||||
PublicGateway: "",
|
||||
InternalInterface: defaultInternalInterface,
|
||||
InternalIPAddress: "172.16.9.254",
|
||||
InternalNetmask: "255.255.255.0",
|
||||
DNSPrimary: "8.8.8.8",
|
||||
DNSSecondary: "8.8.4.4",
|
||||
}
|
||||
|
||||
@@ -45,43 +45,39 @@ func (m model) View() string {
|
||||
|
||||
// agreementView 协议页面
|
||||
func (m model) agreementView() string {
|
||||
title := titleStyle.Render("SunHPC 系统初始化向导")
|
||||
subtitle := subTitleStyle.Render("请先阅读并同意以下协议")
|
||||
title := titleStyle.Render("SunHPC Software License Agreement")
|
||||
|
||||
agreement := agreementBox.Render(`
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SunHPC 软件许可协议 │
|
||||
│ SunHPC License Agreement │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
1. 许可授予
|
||||
本软件授予您非独占、不可转让的使用许可。
|
||||
|
||||
2. 使用限制
|
||||
- 不得用于非法目的
|
||||
- 不得反向工程或反编译
|
||||
- 不得移除版权标识
|
||||
|
||||
3. 免责声明
|
||||
本软件按"原样"提供,不提供任何明示或暗示的保证。
|
||||
|
||||
4. 责任限制
|
||||
在任何情况下,作者不对因使用本软件造成的任何损失负责。
|
||||
|
||||
5. 协议终止
|
||||
如违反本协议条款,许可将自动终止。
|
||||
|
||||
1. License Grant
|
||||
This software grants you a non-exclusive, non-transferable
|
||||
license to use it.
|
||||
2. Disclaimer of Warranties
|
||||
This software is provided "as is" without any warranties,
|
||||
whether express or implied.
|
||||
3. Limitation of Liability
|
||||
This software is provided without warranty of any kind,
|
||||
either express or implied.
|
||||
In no event shall the author be liable for any damages
|
||||
arising out of the use of this software.
|
||||
4. Termination of Agreement
|
||||
If you violate any of the terms of this agreement,
|
||||
the license will automatically terminate.
|
||||
───────────────────────────────────────────────────────────────
|
||||
请仔细阅读以上条款,点击"接受"表示您同意并遵守本协议。
|
||||
PLEASE READ THE ABOVE TERMS CAREFULLY AND CLICK "ACCEPT"
|
||||
TO AGREE AND FOLLOW THIS AGREEMENT.
|
||||
───────────────────────────────────────────────────────────────
|
||||
`)
|
||||
|
||||
var acceptBtn, rejectBtn string
|
||||
if m.agreementIdx == 0 {
|
||||
rejectBtn = selectedButton.Render(">> 拒绝 <<")
|
||||
acceptBtn = selectedButton.Render(" 同意 ")
|
||||
rejectBtn = selectedButton.Render(">> Reject <<")
|
||||
acceptBtn = " Accept "
|
||||
} else {
|
||||
rejectBtn = selectedButton.Render(" 拒绝 ")
|
||||
acceptBtn = selectedButton.Render(">> 同意 <<")
|
||||
rejectBtn = " Reject "
|
||||
acceptBtn = selectedButton.Render(">> Accept <<")
|
||||
}
|
||||
|
||||
buttonGroup := lipgloss.JoinHorizontal(
|
||||
@@ -92,11 +88,9 @@ func (m model) agreementView() string {
|
||||
// debugInfo := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).
|
||||
// Render(fmt.Sprintf("[DEBUG: idx=%d]", m.agreementIdx),)
|
||||
|
||||
hint := hintStyle.Render("使用 <- -> 或 Tab 选择,Enter 确认")
|
||||
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
subtitle, "",
|
||||
agreement, "",
|
||||
buttonGroup, "",
|
||||
// debugInfo, "", // ✅ 显示调试信息
|
||||
@@ -106,8 +100,7 @@ func (m model) agreementView() string {
|
||||
|
||||
// dataView 数据接收页面
|
||||
func (m model) dataView() string {
|
||||
title := titleStyle.Render("集群基础配置")
|
||||
subtitle := subTitleStyle.Render("请填写系统基本信息")
|
||||
title := titleStyle.Render("Cluster Information")
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
@@ -117,11 +110,9 @@ func (m model) dataView() string {
|
||||
}
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
|
||||
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
subtitle, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
hint,
|
||||
@@ -130,25 +121,24 @@ func (m model) dataView() string {
|
||||
|
||||
// publicNetworkView 公网设置页面
|
||||
func (m model) publicNetworkView() string {
|
||||
title := titleStyle.Render("公网配置")
|
||||
subtitle := subTitleStyle.Render("请配置网络接口信息")
|
||||
title := titleStyle.Render("Public Network Configuration")
|
||||
|
||||
autoDetect := infoStyle.Render("[*] 自动检测网络接口: eth0, eth1, ens33")
|
||||
networkInterfaces := getNetworkInterfaces()
|
||||
autoDetect := infoStyle.Render(
|
||||
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
|
||||
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
|
||||
input := inputBox.Render(info + ti.View())
|
||||
inputs.WriteString(input + "\n")
|
||||
}
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
|
||||
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
subtitle, "",
|
||||
autoDetect, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
@@ -158,22 +148,25 @@ func (m model) publicNetworkView() string {
|
||||
|
||||
// internalNetworkView 内网配置页面
|
||||
func (m model) internalNetworkView() string {
|
||||
title := titleStyle.Render("内网配置")
|
||||
subtitle := subTitleStyle.Render("请配置内网信息")
|
||||
title := titleStyle.Render("Internal Network Configuration")
|
||||
|
||||
networkInterfaces := getNetworkInterfaces()
|
||||
autoDetect := infoStyle.Render(
|
||||
"[*] Auto Detect Network Interfaces: " + strings.Join(networkInterfaces, ", "))
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
info := fmt.Sprintf("%-10s|", m.inputLabels[i])
|
||||
info := fmt.Sprintf("%-20s|", m.inputLabels[i])
|
||||
input := inputBox.Render(info + ti.View())
|
||||
inputs.WriteString(input + "\n")
|
||||
}
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
subtitle, "",
|
||||
autoDetect, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
hint,
|
||||
@@ -182,8 +175,7 @@ func (m model) internalNetworkView() string {
|
||||
|
||||
// dnsView DNS 配置页面
|
||||
func (m model) dnsView() string {
|
||||
title := titleStyle.Render("DNS 配置")
|
||||
subtitle := subTitleStyle.Render("请配置 DNS 服务器")
|
||||
title := titleStyle.Render("DNS Configuration")
|
||||
|
||||
var inputs strings.Builder
|
||||
for i, ti := range m.textInputs {
|
||||
@@ -193,12 +185,10 @@ func (m model) dnsView() string {
|
||||
}
|
||||
|
||||
buttons := m.renderNavButtons()
|
||||
|
||||
hint := hintStyle.Render("使用 Up/Down 或 Tab 切换、Enter 确认")
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
subtitle, "",
|
||||
inputs.String(), "",
|
||||
buttons, "",
|
||||
hint,
|
||||
@@ -207,71 +197,71 @@ func (m model) dnsView() string {
|
||||
|
||||
// summaryView 总结页面
|
||||
func (m model) summaryView() string {
|
||||
title := titleStyle.Render("配置总结")
|
||||
subtitle := subTitleStyle.Render("请确认以下配置信息")
|
||||
title := titleStyle.Render("Summary")
|
||||
subtitle := subTitleStyle.Render("Please confirm the following configuration information")
|
||||
|
||||
summary := summaryBox.Render(fmt.Sprintf(`
|
||||
+---------------------------------------------+
|
||||
基本信息
|
||||
+---------------------------------------------+
|
||||
主机名:%-35s
|
||||
国 家: %-31s
|
||||
地 区:%-31s
|
||||
时 区:%-38s
|
||||
主 页:%-38s
|
||||
+---------------------------------------------+
|
||||
数据库
|
||||
+---------------------------------------------+
|
||||
地 址:%-38s
|
||||
名 称:%-38s
|
||||
软 件:%-33s
|
||||
+---------------------------------------------+
|
||||
公网配置
|
||||
+---------------------------------------------+
|
||||
接 口:%-38s
|
||||
地 址: %-41s
|
||||
掩 码:%-38s
|
||||
网 关:%-38s
|
||||
+---------------------------------------------+
|
||||
内网配置
|
||||
+---------------------------------------------+
|
||||
接 口:%-38s
|
||||
地 址: %-41s
|
||||
掩 码:%-38s
|
||||
+---------------------------------------------+
|
||||
DNS
|
||||
+---------------------------------------------+
|
||||
主 DNS: %-37s
|
||||
备 DNS: %-37s
|
||||
+---------------------------------------------+
|
||||
+----------------------------------------------------+
|
||||
Basic Information
|
||||
+----------------------------------------------------+
|
||||
Homepage : %-38s
|
||||
Hostname : %-35s
|
||||
Country : %-31s
|
||||
Region : %-31s
|
||||
Timezone : %-38s
|
||||
Homepage : %-38s
|
||||
+----------------------------------------------------+
|
||||
Database Configuration
|
||||
+----------------------------------------------------+
|
||||
Database Path : %-38s
|
||||
Software : %-33s
|
||||
+----------------------------------------------------+
|
||||
Public Network Configuration
|
||||
+----------------------------------------------------+
|
||||
Public Interface : %-38s
|
||||
Public IP : %-41s
|
||||
Public Netmask : %-38s
|
||||
Public Gateway : %-38s
|
||||
+----------------------------------------------------+
|
||||
Internal Network Configuration
|
||||
+----------------------------------------------------+
|
||||
Internal Interface: %-38s
|
||||
Internal IP : %-41s
|
||||
Internal Netmask : %-38s
|
||||
+----------------------------------------------------+
|
||||
DNS Configuration
|
||||
+----------------------------------------------------+
|
||||
Primary DNS : %-37s
|
||||
Secondary DNS : %-37s
|
||||
+----------------------------------------------------+
|
||||
`,
|
||||
m.config.HomePage,
|
||||
m.config.Hostname,
|
||||
m.config.Country,
|
||||
m.config.Region,
|
||||
m.config.Timezone,
|
||||
m.config.HomePage,
|
||||
m.config.DBAddress,
|
||||
m.config.DBName,
|
||||
m.config.DataAddress,
|
||||
m.config.PublicInterface,
|
||||
m.config.IPAddress,
|
||||
m.config.Netmask,
|
||||
m.config.Gateway,
|
||||
m.config.PublicIPAddress,
|
||||
m.config.PublicNetmask,
|
||||
m.config.PublicGateway,
|
||||
m.config.InternalInterface,
|
||||
m.config.InternalIP,
|
||||
m.config.InternalMask,
|
||||
m.config.InternalIPAddress,
|
||||
m.config.InternalNetmask,
|
||||
m.config.DNSPrimary,
|
||||
m.config.DNSSecondary,
|
||||
))
|
||||
|
||||
var buttons string
|
||||
if m.focusIndex == 0 {
|
||||
buttons = selectedButton.Render("[>] 执行初始化") + " " + normalButton.Render("[ ] 取消")
|
||||
buttons = selectedButton.Render("[>] Start Initialization") + " " + normalButton.Render("[ ] Cancel")
|
||||
} else {
|
||||
buttons = normalButton.Render("[>] 执行初始化") + " " + selectedButton.Render("[ ] 取消")
|
||||
buttons = normalButton.Render("[>] Start Initialization") + " " + selectedButton.Render("[ ] Cancel")
|
||||
}
|
||||
|
||||
hint := hintStyle.Render("使用 <- -> 或 Tab 选择,Enter 确认")
|
||||
hint := hintStyle.Render("Use Up/Down OR Tab Change,Enter Confirm")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Center,
|
||||
title, "",
|
||||
@@ -297,7 +287,7 @@ func progressView(current PageType, total int) string {
|
||||
progress += " "
|
||||
}
|
||||
}
|
||||
labels := []string{"协议", "数据", "公网", "内网", "DNS", "总结"}
|
||||
labels := []string{"License", "Data", "Network", "Network", "DNS", "Summary"}
|
||||
label := labelStyle.Render(labels[current])
|
||||
return progressStyle.Render(progress) + " " + label
|
||||
}
|
||||
@@ -305,26 +295,26 @@ func progressView(current PageType, total int) string {
|
||||
// successView 成功视图
|
||||
func successView() string {
|
||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||
successTitle.Render("初始化完成!"), "",
|
||||
successMsg.Render("系统配置已保存,正在初始化..."), "",
|
||||
hintStyle.Render("按任意键退出"),
|
||||
successTitle.Render("Initialization Completed!"), "",
|
||||
successMsg.Render("System configuration has been saved, and the system is initializing..."), "",
|
||||
hintStyle.Render("Press any key to exit"),
|
||||
))
|
||||
}
|
||||
|
||||
// quitView 退出视图
|
||||
func quitView() string {
|
||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||
errorTitle.Render("已取消"), "",
|
||||
errorMsg.Render("初始化已取消,未保存任何配置"),
|
||||
errorTitle.Render("Canceled"), "",
|
||||
errorMsg.Render("Initialization canceled, no configuration saved"),
|
||||
))
|
||||
}
|
||||
|
||||
// errorView 错误视图
|
||||
func errorView(err error) string {
|
||||
return containerStyle.Render(lipgloss.JoinVertical(lipgloss.Center,
|
||||
errorTitle.Render("错误"), "",
|
||||
errorTitle.Render("Error"), "",
|
||||
errorMsg.Render(err.Error()), "",
|
||||
hintStyle.Render("按 Ctrl+C 退出"),
|
||||
hintStyle.Render("Press Ctrl+C to exit"),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -332,9 +322,9 @@ func errorView(err error) string {
|
||||
func navButtons(m model, prev, next string) string {
|
||||
var btns string
|
||||
if m.currentPage == 0 {
|
||||
btns = normalButton.Render(prev) + " " + selectedButton.Render(next)
|
||||
btns = normalButton.Render(next) + " " + selectedButton.Render(prev)
|
||||
} else {
|
||||
btns = selectedButton.Render(prev) + " " + normalButton.Render(next)
|
||||
btns = selectedButton.Render(next) + " " + normalButton.Render(prev)
|
||||
}
|
||||
return btns
|
||||
}
|
||||
@@ -345,22 +335,22 @@ func (m model) renderNavButtons() string {
|
||||
switch m.focusType {
|
||||
case FocusTypePrev:
|
||||
// 焦点在"上一步"
|
||||
prevBtn = selectedButton.Render("<< 上一步 >>")
|
||||
nextBtn = normalButton.Render("下一步 >>")
|
||||
nextBtn = normalButton.Render(" Next ")
|
||||
prevBtn = selectedButton.Render(" << Prev >>")
|
||||
case FocusTypeNext:
|
||||
// 焦点在"下一步"
|
||||
prevBtn = normalButton.Render("<< 上一步")
|
||||
nextBtn = selectedButton.Render("<< 下一步 >>")
|
||||
nextBtn = selectedButton.Render(" << Next >>")
|
||||
prevBtn = normalButton.Render(" Prev ")
|
||||
default:
|
||||
// 焦点在输入框
|
||||
prevBtn = normalButton.Render("<< 上一步")
|
||||
nextBtn = normalButton.Render("下一步 >>")
|
||||
nextBtn = normalButton.Render(" Next ")
|
||||
prevBtn = normalButton.Render(" Prev ")
|
||||
}
|
||||
|
||||
return lipgloss.JoinHorizontal(
|
||||
lipgloss.Center,
|
||||
prevBtn,
|
||||
" ",
|
||||
nextBtn,
|
||||
" ",
|
||||
prevBtn,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,8 +41,6 @@ var normalButton = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#666666")) // 深灰色,更暗
|
||||
|
||||
var selectedButton = lipgloss.NewStyle().
|
||||
Padding(0, 2).
|
||||
Foreground(lipgloss.Color("#3d4747ff")). // 亮绿色
|
||||
Bold(true)
|
||||
|
||||
// 输入框样式
|
||||
|
||||
Reference in New Issue
Block a user