This commit is contained in:
2026-02-15 07:18:14 +08:00
parent d7cd899983
commit 8a7bf8a39c
39 changed files with 1578 additions and 2868 deletions

View File

@@ -11,31 +11,58 @@ import (
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Short: "生成基础配置文件",
Long: "创建 /etc/sunhpc 目录并生成所有默认配置文件(若目录已存在则跳过)",
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.RequireRoot(); err != nil {
return err
}
// NewConfigCmd 创建 "init config" 命令
func NewConfigCmd() *cobra.Command {
var (
force bool
path string
verbose bool
)
// 检查目录是否已存在
if _, err := os.Stat(config.BaseDir); err == nil {
log.Warnf("配置目录 %s 已存在,跳过初始化", config.BaseDir)
cmd := &cobra.Command{
Use: "config",
Short: "生成默认配置文件",
Long: `
在指定路径生成 SunHPC 默认配置文件 (sunhpc.yaml)
示例:
sunhpc init config # 生成默认配置文件
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)
return nil
}
},
}
log.Info("初始化 SunHPC 配置目录...")
if err := config.InitDirs(); err != nil {
return fmt.Errorf("创建目录失败: %v", err)
}
// 定义局部 flags
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制覆盖已有配置文件")
cmd.Flags().StringVarP(&path, "path", "p", "", "指定配置文件路径")
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "显示详细日志")
if err := config.CreateDefaultConfigs(); err != nil {
return fmt.Errorf("生成默认配置文件失败: %v", err)
}
log.Info("配置文件已生成,请根据需要编辑 /etc/sunhpc/ 下的 YAML 文件")
return nil
},
return cmd
}

View File

@@ -1,193 +1,47 @@
package initcmd
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"sunhpc/internal/auth"
"sunhpc/internal/db"
"sunhpc/internal/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
appName string = "sunhpc"
defaultDBPath string = "/var/lib/sunhpc"
defaultDBName string = "sunhpc.db"
)
func NewDatabaseCmd() *cobra.Command {
var force bool
var (
forceDB bool
dbPath string
dbName string
dbFullPath string
)
func initDBPathWithViper() error {
/*
从 Viper 配置文件获取数据库路径
配置文件里的键要和 Viper.GetXXX 的键对应
配置文件格式:
db:
path: "/tmp/sunhpc" # 自定义数据库路径
name: "my_sunhpc.db" # 自定义数据库名
*/
log.Infof("从 Viper 配置文件获取数据库路径...")
// ========== 第一步:设置 Viper 配置文件规则(核心) ==========
// 1. 设置Viper基础规则
viper.SetConfigType("yaml") // 配置文件类型
viper.SetConfigName("config") // 配置文件名(不带后缀)
viper.SetEnvPrefix(appName) // 环境变量前缀SUNHPC_
viper.AutomaticEnv() // 自动读取环境变量(可选,增强兼容性)
// 2. 添加配置文件搜索目录Viper 会按顺序查找,找到第一个就停止)
// 优先级:当前目录 → 用户级目录 → 系统级目录
// ① 当前目录(开发/测试常用)
viper.AddConfigPath(".")
// ② Linux/macOS 用户级目录(~/.config/sunhpc/
if homeDir, err := os.UserHomeDir(); err == nil {
viper.AddConfigPath(filepath.Join(homeDir, ".config", appName))
}
// ③ Linux/macOS 系统级目录(/etc/sunhpc/
viper.AddConfigPath(filepath.Join("/etc", appName))
// ========== 第二步:设置默认值(最低优先级) ==========
viper.SetDefault("db.path", defaultDBPath)
viper.SetDefault("db.name", defaultDBName)
// ========== 第三步:绑定环境变量(优先级高于默认值,低于配置文件) ==========
viper.BindEnv("db.path", "DB_PATH") // 绑定 SUNHPC_DB_PATH → db.path
viper.BindEnv("db.name", "DB_NAME") // 绑定 SUNHPC_DB_NAME → db.name
// ========== 第四步:读取配置文件(优先级高于环境变量,低于默认值) ==========
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
log.Info("未找到配置文件,将使用环境变量/默认值")
return nil // 配置文件存在但格式错误,返回错误
}
log.Warnf("读取配置文件失败: %v", err)
return fmt.Errorf("读取配置文件失败: %w", err)
}
log.Infof("成功加载配置文件: %s", viper.ConfigFileUsed())
return nil
}
func initDBPath() error {
// 1. 从 Viper 配置文件获取数据库路径(加载配置文件->环境变量->默认值)
if err := initDBPathWithViper(); err != nil {
return fmt.Errorf("Viper初始化数据库失败: %v", err)
}
// 2. 从Viper获取数据库路径
dbPath = viper.GetString("db.path")
dbName = viper.GetString("db.name")
// 3. 拼接数据库路径
dbFullPath = filepath.Join(dbPath, dbName)
log.Infof("数据库完整路径: %s", dbFullPath)
// 3. 检查数据库文件是否存在
dir := filepath.Dir(dbFullPath)
// 4. 检查数据库目录是否存在
if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Infof("数据库目录不存在,创建目录: %s", dir)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建数据库目录失败: %v", err)
}
}
return nil
}
var databaseCmd = &cobra.Command{
Use: "database",
Short: "初始化数据库",
Long: `初始化SQLite数据库,创建所有表结构和默认数据。
cmd := &cobra.Command{
Use: "database",
Short: "初始化数据库",
Long: `初始化SQLite数据库,创建所有表结构和默认数据。
示例:
sunhpc init database # 初始化数据库
sunhpc init database --force # 强制重新初始化`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.RequireRoot(); err != nil {
return err
}
log.Debug("初始化数据库...")
if err := initDBPath(); err != nil {
return fmt.Errorf("初始化数据库路径失败: %w", err)
}
log.Debugf("数据库目录存在: %s", dbPath)
// 强制模式:用户确认
if forceDB {
log.Warn("⚠️ 警告:强制重新初始化将清空数据库中的所有数据!")
fmt.Printf("数据库路径: %s\n", dbFullPath)
fmt.Print("确认要重新初始化数据库吗?这将删除所有现有数据。(Y/yes): ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("读取用户输入失败: %v", err)
Annotations: map[string]string{
"skip-db-check": "true", // 标记此命令跳过数据库检查
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.RequireRoot(); err != nil {
return err
}
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" && input != "yes" {
log.Info("操作已取消")
return nil
log.Info("初始化数据库...")
if force {
log.Warn("⚠️ 警告:强制重新初始化将清空数据库中的所有数据!")
}
log.Info("用户确认重新初始化数据库")
}
// 数据库存在且不是强制模式则跳过初始化
if _, err := os.Stat(dbFullPath); err == nil && !forceDB {
log.Infof("数据库文件已存在: %s", dbFullPath)
dbInst := db.MustGetDB() // panic if fail (ok for CLI tool)
if err := dbInst.InitSchema(force); err != nil {
return err
}
log.Info("数据库初始化完成")
return nil
}
},
}
// 初始化数据库(使用配置的路径)
database, err := db.GetInstanceWithConfig(dbPath, dbName)
if err != nil {
return fmt.Errorf("初始化数据库失败: %v", err)
}
defer database.Close()
// 如果是强制模式,设置强制重新初始化标志
if forceDB {
database.SetForceInit(true)
log.Info("强制重新初始化数据库表...")
// 关闭现有连接以触发重新连接
if err := database.CloseConnection(); err != nil {
return fmt.Errorf("关闭现有数据库连接失败: %v", err)
}
// 重新连接并初始化
if err := database.Connect(); err != nil {
return fmt.Errorf("强制重新初始化数据库失败: %v", err)
}
}
log.Infof("数据库初始化成功: %s", dbFullPath)
return nil
},
}
func init() {
databaseCmd.Flags().BoolVarP(&forceDB, "force", "f", false, "强制重新初始化,删除现有数据库")
Cmd.AddCommand(databaseCmd)
cmd.Flags().BoolVarP(&force, "force", "f", false, "强制重新初始化")
return cmd
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"
)
// 仅定义 Cmd 注册子命令,只负责组装命令树,尽量不包含业务逻辑
var Cmd = &cobra.Command{
Use: "init",
Short: "初始化集群配置",
@@ -11,7 +12,7 @@ var Cmd = &cobra.Command{
}
func init() {
Cmd.AddCommand(configCmd)
Cmd.AddCommand(systemCmd)
Cmd.AddCommand(serviceCmd)
// 注册所有子命令(通过工厂函数创建, 例如 DatabaseCmd()
Cmd.AddCommand(NewDatabaseCmd())
Cmd.AddCommand(NewConfigCmd())
}

View File

@@ -1,37 +0,0 @@
package initcmd
import (
"fmt"
"sunhpc/internal/auth"
"sunhpc/internal/config"
"sunhpc/internal/log"
"sunhpc/internal/service"
"github.com/spf13/cobra"
)
var serviceCmd = &cobra.Command{
Use: "service",
Short: "根据配置文件初始化服务",
Long: `读取 /etc/sunhpc/services.yaml 并部署/配置相关服务。
支持 HTTPD、TFTPD、DHCPD 等。`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := auth.RequireRoot(); err != nil {
return err
}
svcCfg, err := config.LoadServices()
if err != nil {
return fmt.Errorf("加载 services.yaml 失败: %v", err)
}
log.Info("开始部署服务...")
if err := service.Deploy(svcCfg); err != nil {
return fmt.Errorf("服务部署失败: %v", err)
}
log.Info("服务初始化完成")
return nil
},
}

View File

@@ -1,49 +0,0 @@
package initcmd
import (
"fmt"
"sunhpc/internal/auth"
"sunhpc/internal/config"
"sunhpc/internal/system"
"github.com/spf13/cobra"
)
var (
dryRun bool // --dry-run -n: 仅模拟执行,不实际应用
verbose bool // --verbose -v: 启用详细日志输出
)
var systemCmd = &cobra.Command{
Use: "system [flags]",
Short: "根据配置文件初始化系统",
Long: `读取 /etc/sunhpc/sunhpc.yaml 中的系统配置项并应用到当前节点。
示例:
sunhpc init system # 应用所有配置项
sunhpc init system --dry-run # 仅模拟执行,不实际应用
sunhpc init system --verbose # 启用详细日志输出
`,
RunE: func(cmd *cobra.Command, args []string) error {
// 权限检查:必须以 root 或 sudo 运行
if err := auth.RequireRoot(); err != nil {
return err
}
// 加载主配置
cfg, err := config.LoadSunHPC()
if err != nil {
return fmt.Errorf("加载 sunhpc.yaml 失败: %v", err)
}
// 统一应用所有配置
return system.ApplyAll(cfg)
},
}
// init 初始化 systemCmd 的标志,添加长参数和段参数.
func init() {
// 注册长参数, 布尔参数
systemCmd.Flags().BoolVarP(&dryRun, "dry-run", "n", false, "仅模拟执行,不实际应用")
systemCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "启用详细日志输出")
}

View File

@@ -1,113 +0,0 @@
package cmd
import (
"fmt"
"sunhpc/internal/db"
"sunhpc/internal/log"
"github.com/spf13/cobra"
)
var nodeCmd = &cobra.Command{
Use: "node",
Short: "节点管理",
Long: "管理集群节点,包括添加、删除、查询等操作",
}
var nodeListCmd = &cobra.Command{
Use: "list",
Short: "列出所有节点",
RunE: func(cmd *cobra.Command, args []string) error {
log.Info("查询节点列表...")
// 获取数据库实例(自动使用之前配置的路径)
database, err := db.GetInstance()
if err != nil {
return fmt.Errorf("获取数据库连接失败: %v", err)
}
defer database.Close()
// 执行查询
_, err = database.Execute("SELECT id, name, rack, rank, cpus, memory, disk, os, kernel FROM nodes ORDER BY name")
if err != nil {
return fmt.Errorf("查询节点失败: %v", err)
}
// 获取所有结果
rows, err := database.FetchAll()
if err != nil {
return fmt.Errorf("获取结果失败: %v", err)
}
if len(rows) == 0 {
log.Info("暂无节点数据")
return nil
}
// 打印结果
fmt.Printf("%-5s %-20s %-8s %-8s %-8s %-10s %-10s %-10s\n",
"ID", "名称", "机架", "排名", "CPU", "内存", "磁盘", "操作系统")
fmt.Println("----------------------------------------------------------------------------------")
for _, row := range rows {
fmt.Printf("%-5v %-20s %-8v %-8v %-8v %-10v %-10v %-10s\n",
row["id"], row["name"], row["rack"], row["rank"],
row["cpus"], row["memory"], row["disk"], row["os"])
}
return nil
},
}
var nodeAddCmd = &cobra.Command{
Use: "add <name>",
Short: "添加节点",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
nodeName := args[0]
log.Infof("添加节点: %s", nodeName)
database, err := db.GetInstance()
if err != nil {
return fmt.Errorf("获取数据库连接失败: %v", err)
}
defer database.Close()
// 插入节点
_, err = database.Execute(
"INSERT INTO nodes (name, rack, rank, cpus, memory, disk, os, kernel) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
nodeName, rack, rank, cpus, memory, disk, nodeOS, kernel,
)
if err != nil {
return fmt.Errorf("添加节点失败: %v", err)
}
log.Infof("节点 %s 添加成功", nodeName)
return nil
},
}
var (
rack int
rank int
cpus int
memory int
disk int
nodeOS string
kernel string
)
func init() {
// 添加子命令
nodeCmd.AddCommand(nodeListCmd)
nodeCmd.AddCommand(nodeAddCmd)
// 添加参数
nodeAddCmd.Flags().IntVar(&rack, "rack", 0, "机架号")
nodeAddCmd.Flags().IntVar(&rank, "rank", 0, "排名")
nodeAddCmd.Flags().IntVar(&cpus, "cpus", 0, "CPU核心数")
nodeAddCmd.Flags().IntVar(&memory, "memory", 0, "内存大小(GB)")
nodeAddCmd.Flags().IntVar(&disk, "disk", 0, "磁盘大小(GB)")
nodeAddCmd.Flags().StringVar(&nodeOS, "os", "", "操作系统")
nodeAddCmd.Flags().StringVar(&kernel, "kernel", "", "内核版本")
}

View File

@@ -1,12 +1,16 @@
package cmd
import (
"os"
"strings"
initcmd "sunhpc/cmd/init"
"sunhpc/cmd/soft"
"sunhpc/cmd/tmpl"
"sunhpc/internal/auth"
"sunhpc/internal/config"
"sunhpc/internal/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
@@ -15,26 +19,85 @@ var (
noColor bool
)
func checkDB() error {
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal("加载配置失败: ", err)
}
// 统一转为小写,避免用户输入错误
dbType := strings.ToLower(cfg.DB.Type)
// 打印配置(调试用)
log.Debugf("数据库类型: %s", dbType)
log.Debugf("数据库名称: %s", cfg.DB.Name)
log.Debugf("数据库路径: %s", cfg.DB.Path)
log.Debugf("数据库用户: %s", cfg.DB.User)
log.Debugf("数据库主机: %s", cfg.DB.Host)
log.Debugf("数据库套接字: %s", cfg.DB.Socket)
log.Debugf("数据库详细日志: %v", cfg.DB.Verbose)
// 支持 sqlitemysql的常见别名
isSQLite := dbType == "sqlite" || dbType == "sqlite3"
isMySQL := dbType == "mysql"
// 检查数据库类型,只允许 sqlite 和 mysql
if !isSQLite && !isMySQL {
log.Fatalf("不支持的数据库类型: %s(仅支持 sqlite、sqlite3、mysql)", dbType)
}
// 检查数据库路径是否存在
if isSQLite {
if _, err := os.Stat(cfg.DB.Path); os.IsNotExist(err) {
log.Warnf("SQLite 数据库路径 %s 不存在", cfg.DB.Path)
log.Fatalf("必须先执行 'sunhpc init database' 初始化数据库")
}
}
return nil
}
var rootCmd = &cobra.Command{
Use: "sunhpc",
Short: "SunHPC - HPC集群一体化运维工具",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// 初始化日志
// 初始化日志verbose=false 不显示调试信息)
log.Init(verbose)
// 是否禁用颜色
if noColor {
log.EnableColor(false)
log.EnableColor(!noColor)
log.Debugf("当前命令 Annotations: %+v", cmd.Annotations)
// 检查当前命令是否标记为跳过 DB 检查
if cmd.Annotations["skip-db-check"] == "true" {
log.Debugf("当前命令 %s 标记为跳过 DB 检查", cmd.Name())
return
} else {
// 检查数据库
if err := checkDB(); err != nil {
log.Fatalf("数据库检查失败: %v", err)
}
}
log.Debugf("命令: %s", cmd.Name())
// 需要 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 {
@@ -42,32 +105,12 @@ func Execute() error {
}
func init() {
cobra.OnInitialize(initConfig)
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(nodeCmd)
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.AddConfigPath("/etc/sunhpc")
viper.SetConfigType("yaml")
viper.SetConfigName("sunhpc")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
log.Infof("使用配置文件: %s", viper.ConfigFileUsed())
} else {
log.Debugf("未找到配置文件: %v", err)
}
rootCmd.AddCommand(tmpl.Cmd)
}

58
cmd/tmpl/dump.go Normal file
View File

@@ -0,0 +1,58 @@
package tmpl
import (
"fmt"
"sunhpc/internal/log"
"sunhpc/internal/templating"
"github.com/spf13/cobra"
)
func newDumpCmd() *cobra.Command {
var output string
cmd := &cobra.Command{
Use: "dump <template-name>",
Short: "导出内置模板到文件",
Long: `
将内置的 YAML 模板导出为可编辑的文件。
示例:
sunhpc tmpl dump autofs --output ./my-autofs.yaml`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
// 检查模板是否存在
available, _ := templating.ListEmbeddedTemplates()
found := false
for _, n := range available {
if n == name {
found = true
break
}
}
if !found {
return fmt.Errorf("内置模板 '%s' 不存在。可用模板: %v", name, available)
}
outPath := output
if outPath == "" {
outPath = name + ".yaml"
}
if err := templating.DumpEmbeddedTemplateToFile(name, outPath); err != nil {
return err
}
log.Infof("内置模板 '%s' 已导出到: %s", name, outPath)
log.Infof("你可以编辑此文件,然后用以下命令使用它:")
log.Infof(" sunhpc tmpl render %s -f %s [flags]", name, outPath)
return nil
},
}
cmd.Flags().StringVarP(&output, "output", "o", "", "输出文件路径(默认: <name>.yaml")
return cmd
}

16
cmd/tmpl/init.go Normal file
View File

@@ -0,0 +1,16 @@
// 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())
}

96
cmd/tmpl/render.go Normal file
View File

@@ -0,0 +1,96 @@
package tmpl
import (
"fmt"
"sunhpc/internal/log"
"sunhpc/internal/templating"
"github.com/spf13/cobra"
)
var (
tmplFile string
hostname string
domain string
oldHostname string
ip string
clusterName string
outputRoot string
)
func newRenderCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "render <template-name>",
Short: "渲染配置模板",
Long: "根据 YAML 模板和上下文变量生成配置文件或脚本",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
tmplName := args[0]
var template *templating.Template
var err error
// 优先使用 -f 指定的外部模版文件
if tmplFile != "" {
template, err = templating.LoadTemplate(tmplFile)
if err != nil {
return fmt.Errorf("加载外部模板失败: %w", err)
}
log.Infof("✅ 外部模板 '%s' 已加载\n", tmplFile)
} else {
// 否则从内置模板加载
template, err = templating.LoadEmbeddedTemplate(tmplName)
if err != nil {
return err
}
log.Infof("✅ 内置模板 '%s' 已加载\n", tmplName)
}
ctx := templating.Context{
Node: templating.NodeInfo{
Hostname: hostname,
OldHostname: oldHostname,
Domain: domain,
IP: ip,
},
Cluster: templating.ClusterInfo{
Name: clusterName,
},
}
rendered, err := template.Render(ctx)
if err != nil {
return fmt.Errorf("模板渲染失败: %w", err)
}
// 处理 post 阶段
if steps, ok := rendered["post"]; ok {
fmt.Println(">>> 执行 post 阶段")
if err := templating.WriteFiles(steps, outputRoot); err != nil {
return err
}
templating.PrintScripts(steps)
}
// 处理 configure 阶段
if steps, ok := rendered["configure"]; ok {
fmt.Println(">>> 执行 configure 阶段")
templating.PrintScripts(steps)
}
fmt.Println("✅ 模板渲染完成")
return nil
},
}
cmd.Flags().StringVarP(&tmplFile, "file", "f", "", "指定模板文件路径(覆盖默认查找)")
cmd.Flags().StringVar(&hostname, "hostname", "", "节点主机名")
cmd.Flags().StringVar(&domain, "domain", "cluster.local", "DNS 域名")
cmd.Flags().StringVar(&oldHostname, "old-hostname", "", "旧主机名(用于迁移)")
cmd.Flags().StringVar(&ip, "ip", "", "节点 IP 地址")
cmd.Flags().StringVar(&clusterName, "cluster", "default", "集群名称")
cmd.Flags().StringVarP(&outputRoot, "output", "o", "/", "文件输出根目录")
_ = cmd.MarkFlagRequired("hostname")
return cmd
}