Compare commits

..

2 Commits

Author SHA1 Message Date
3f5e333a4d 添加数据到数据库,但无法插入,临时提交保存 2026-03-06 00:29:34 +08:00
f7dcfa4e7d Tui fix focus 2026-02-28 19:29:17 +08:00
14 changed files with 1628 additions and 617 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
main
sunhpc
testgui

31
build-sunhpc.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
set -e # 出错立即退出,便于定位问题
# ========== 核心配置(根据你的项目修改) ==========
# 1. 必须和go.mod中的module名称一致关键
MODULE_NAME="sunhpc"
# 2. 编译的入口文件路径你的main.go在cmd/sunhpc下
ENTRY_FILE="./cmd/sunhpc/main.go"
# 3. 输出的可执行文件名称
APP_NAME="sunhpc"
# 4. 自定义版本号
APP_VERSION="v1.0.0"
# ========== 获取Git和编译信息兼容无Git环境 ==========
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
BUILD_TIME=$(date +"%Y-%m-%d-%H:%M:%S")
# ========== 编译核心修复ldflags格式 ==========
# 关键:去掉反斜杠,用单层双引号包裹,内部换行分隔参数
go build -ldflags "
-X ${MODULE_NAME}/pkg/info.Version=${APP_VERSION}
-X ${MODULE_NAME}/pkg/info.BuildTime=${BUILD_TIME}
-X ${MODULE_NAME}/pkg/info.GitCommit=${GIT_COMMIT}
-X ${MODULE_NAME}/pkg/info.GitBranch=${GIT_BRANCH}
" -o ${APP_NAME} ${ENTRY_FILE}
# ========== 验证提示 ==========
echo "- 执行文件:./${APP_NAME}"
echo "- 版本信息:${APP_VERSION} (Git: ${GIT_COMMIT} @ ${GIT_BRANCH})"
echo "- 编译时间:${BUILD_TIME}"

View File

@@ -1,66 +1,9 @@
package main
import (
"fmt"
tea "github.com/charmbracelet/bubbletea"
"sunhpc/pkg/info"
)
// 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)
}
info.PrintAllInfo()
}

1
go.mod
View File

@@ -20,6 +20,7 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect

2
go.sum
View File

@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=

View File

@@ -35,7 +35,8 @@ func NewInitDBCmd() *cobra.Command {
defer db.Close()
if err := database.InitTables(db, force); err != nil {
return fmt.Errorf("数据库初始化失败: %w", err)
logger.Debug(err)
return err
}
// 测试数据库连接

View File

@@ -73,10 +73,28 @@ func InitConfigs() error {
return fmt.Errorf("创建默认配置文件失败: %w", err)
}
}
// 读取配置文件,根据配置文件内容初始化相关目录.
cfg, err := LoadConfig()
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
// 初始化日志
logger.Init(cfg.Log)
// 确保数据库目录存在
if err := os.MkdirAll(cfg.Database.Path, 0755); err != nil {
logger.Debugf("创建数据库目录 %s 失败: %v", cfg.Database.Path, err)
return err
}
logger.Debugf("创建数据库目录 %s 成功", cfg.Database.Path)
return nil
}
func createDefaultConfig(configPath string) error {
fmt.Printf("设置默认配置文件: %s\n", configPath)
defaultConfig := &Config{
Database: DatabaseConfig{
Path: utils.DefaultDBPath,
@@ -93,11 +111,6 @@ func createDefaultConfig(configPath string) error {
},
}
// 确保数据库目录存在
if err := os.MkdirAll(defaultConfig.Database.Path, 0755); err != nil {
return fmt.Errorf("创建数据库目录失败: %w", err)
}
// 序列号并写入
data, err := yaml.Marshal(defaultConfig)
if err != nil {
@@ -108,6 +121,7 @@ func createDefaultConfig(configPath string) error {
// ----------------------------------- 配置加载(只加载一次) -----------------------------------
func LoadConfig() (*Config, error) {
configMutex.RLock()
if GlobalConfig != nil {
// 如果已经加载过,直接返回

View File

@@ -1,11 +1,8 @@
package database
import (
"bufio"
"database/sql"
"fmt"
"os"
"strings"
"sync"
"sunhpc/pkg/config"
@@ -25,10 +22,114 @@ var (
dbErr error
)
// =========================================================
// 封装数据库函数使用Go实现
// =========================================================
// MapCategory - 根据类别名称查ID
// 查询方式: globalID, err := db.MapCategory(conn, "global")
func MapCategory(conn *sql.DB, catname string) (int, error) {
var id int
query := "select id from categories where name = ?"
logger.Debugf("查询SQL: %s", query)
logger.Debugf("查询类别ID: %s", catname)
err := conn.QueryRow(query, catname).Scan(&id)
if err == sql.ErrNoRows {
logger.Debugf("未找到类别 %s, 返回ID=0", catname)
return 0, nil // 无匹配返回0
}
logger.Debugf("查询到类别 %s, ID=%d", catname, id)
return id, nil
}
// MapCategoryIndex - 根据类别名称 + 索引名称查ID
// 调用方式: linuxOSID, err := db.MapCategoryIndex(conn, "os", "linux")
func MapCategoryIndex(conn *sql.DB, catindexName, categoryIndex string) (int, error) {
var id int
query := `
select index_id from vmapCategoryIndex
where categoryName = ? and categoryIndex = ?
`
logger.Debugf("查询SQL: %s", query)
logger.Debugf("查询索引ID: %s, 类别: %s", catindexName, categoryIndex)
err := conn.QueryRow(query, catindexName, categoryIndex).Scan(&id)
if err == sql.ErrNoRows {
logger.Debugf("未找到索引 %s, 返回ID=0", catindexName)
return 0, nil // 无匹配返回0
}
logger.Debugf("查询到索引 %s, ID=%d", catindexName, id)
return id, nil
}
// ResolveFirewalls - 解析指定主机的防火墙规则
// 返回解析后的防火墙规则(fwresolved表数据),临时表使用后自动清理
// 调用方式: rows, err := db.ResolveFirewalls(conn, "compute-0-1", "default")
func ResolveFirewalls(conn *sql.DB, hostname, chainname string) (*sql.Rows, error) {
// 步骤1 创建临时表 fresolved1
_, err := conn.Exec(`
DROP TABLE IF EXISTS fresolved1;
CREATE TEMPORARY TABLE fresolved1 AS
SELECT
? AS hostname,
? AS Resolver,
f.*,
r.precedence
FROM
resolvechain r
inner join hostselections hs on r.category = hs.category and r.name = ?
inner join firewalls f on hs.category = f.category and hs.selection = f.catindex
where hs.host = ?;
`, hostname, chainname, chainname, hostname)
if err != nil {
return nil, fmt.Errorf("Create temporary table fresolved1 failed: %w", err)
}
// 步骤2创建临时表 fresolved2
_, err = conn.Exec(`
DROP TABLE IF EXISTS fresolved2;
CREATE TEMPORARY TABLE fresolved2 AS
SELECT
*
FROM
fresolved1;
`)
if err != nil {
return nil, fmt.Errorf("Create temporary table fresolved2 failed: %w", err)
}
// 步骤3创建最终结果表 fwresolved
_, err = conn.Exec(`
DROP TABLE IF EXISTS fwresolved;
CREATE TEMPORARY TABLE fwresolved AS
SELECT
r1.*,
cat.name AS categoryName
FROM
fresolved1 r1
inner join (
select Rulename, MAX(precedence) as precedence
from fresolved2
group by Rulename
) AS r2 on r1.Rulename = r2.Rulename and r1.precedence = r2.precedence
inner join categories cat on r1.category = cat.id;
`)
if err != nil {
return nil, fmt.Errorf("Create temporary table fwresolved failed: %w", err)
}
// 步骤4查询结果并返回
rows, err := conn.Query("SELECT * FROM fwresolved")
if err != nil {
return nil, fmt.Errorf("Query fwresolved failed: %w", err)
}
return rows, nil
}
// =========================================================
// GetDB - 获取数据库连接(单例模式)
// =========================================================
func GetDB() (*sql.DB, error) {
logger.Debug("获取数据库连接...")
dbOnce.Do(func() {
if dbInstance != nil {
return
@@ -75,45 +176,11 @@ func GetDB() (*sql.DB, error) {
return dbInstance, 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 InitTables(db *sql.DB, force bool) error {
if force {
// 确认是否强制删除
if !confirmAction("确认强制删除所有表和触发器?") {
logger.Info("操作已取消")
db.Close()
os.Exit(0)
return nil
}
// 强制删除所有表和触发器
logger.Debug("强制删除所有表和触发器...")
if err := dropTables(db); err != nil {
return fmt.Errorf("删除表失败: %w", err)
}
logger.Debug("删除所有表和触发器成功")
if err := dropTriggers(db); err != nil {
return fmt.Errorf("删除触发器失败: %w", err)
}
logger.Debug("删除所有触发器成功")
}
// ✅ 调用 schema.go 中的函数
for _, ddl := range CreateTableStatements() {
//for _, ddl := range CreateTableStatements() {
for _, ddl := range BaseTables() {
logger.Debugf("执行: %s", ddl)
if _, err := db.Exec(ddl); err != nil {
return fmt.Errorf("数据表创建失败: %w", err)
@@ -128,26 +195,12 @@ func InitTables(db *sql.DB, force bool) error {
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
}
// 添加基础数据
if err := InitBaseData(db); err != nil {
return fmt.Errorf("初始化基础数据失败: %w", err)
}
logger.Info("基础数据初始化成功")
return nil
}
@@ -212,3 +265,64 @@ func TestNodeInsert(db *sql.DB) error {
return nil
})
}
// =========================================================
// 带事务执行 SQL 语句,自动提交/回滚
// =========================================================
// 执行单条SQL语句带事务管理
func ExecSingleWithTransaction(sqlStr string) error {
// 复用批量函数将单条SQL语句包装为数组执行
return ExecWithTransaction([]string{sqlStr})
}
// 批量执行 DDL 语句,带事务管理
func ExecWithTransaction(ddl []string) error {
conn, err := GetDB()
if err != nil {
logger.Errorf("获取数据库连接失败: %v", err)
return err
}
// 开始事务
tx, err := conn.Begin()
if err != nil {
logger.Errorf("开始事务失败: %v", err)
return err
}
// 延迟处理:如果函数异常,回滚事务
defer func() {
if r := recover(); r != nil {
// 捕获 panic 并回滚事务
tx.Rollback()
logger.Errorf("事务执行中发生 panic: %v", r)
}
}()
// 遍历执行 DDL 语句
for idx, sql := range ddl {
logger.Debugf("执行 DDL 语句 %d: %s", idx+1, sql)
_, err = tx.Exec(sql)
if err != nil {
// 执行失败时,回滚事务
rollbackErr := tx.Rollback()
if rollbackErr != nil {
logger.Errorf("执行失败: 回滚失败: %v (原错误: %v, SQL: %s)", rollbackErr, err, sql)
} else {
logger.Errorf("执行失败: 回滚事务: %v, SQL: %s", err, sql)
}
logger.Errorf("执行 %d 条, 失败: %w (SQL: %s)", idx+1, err, sql)
}
}
// 所有SQL语句执行成功提交事务
logger.Info("所有SQL语句执行成功,提交事务")
if err := tx.Commit(); err != nil {
logger.Errorf("提交事务失败: %w", err)
return err
}
logger.Debugf("成功执行 %d 条 SQL 语句, 事务已提交.", len(ddl))
return nil
}

View File

@@ -1,294 +1,571 @@
// Package db defines the database schema.
package database
// CurrentSchemaVersion returns the current schema version (for migrations)
func CurrentSchemaVersion() int {
return 1
import (
"database/sql"
"fmt"
)
func BaseTables() []string {
datalist := []string{}
Appliances := `
CREATE TABLE IF NOT EXISTS appliances (
ID integer primary key autoincrement,
Name varchar(32) not null default '',
Graph varchar(64) not null default 'default',
Node varchar(64) not null default '',
OS varchar(64) not null default 'linux'
);
`
datalist = append(datalist, Appliances)
Memberships := `
CREATE TABLE IF NOT EXISTS memberships (
ID integer primary key autoincrement,
Name varchar(64) not null default '',
Appliance integer(11) default '0',
Distribution integer(11) default '1',
Public varchar(64) not null default 'no'
);
`
datalist = append(datalist, Memberships)
Categories := `
CREATE TABLE IF NOT EXISTS categories (
ID integer primary key autoincrement,
Name varchar(64) not null unique default '0',
Description varchar(255) default null,
UNIQUE(Name)
);
`
datalist = append(datalist, Categories)
Catindex := `
CREATE TABLE IF NOT EXISTS catindex (
ID integer primary key autoincrement,
Name varchar(64) not null unique default '0',
Category integer not null,
Foreign key(Category) references categories(ID) on delete cascade
);
`
datalist = append(datalist, Catindex)
Resolvechain := `
CREATE TABLE IF NOT EXISTS resolvechain (
ID integer primary key autoincrement,
Name varchar(64) not null default '0',
Category integer(11) not null,
Precedence integer(11) not null default '10',
UNIQUE(Name, Category)
Foreign key(Category) references categories(ID) on delete cascade
);
`
datalist = append(datalist, Resolvechain)
Nodes := `
CREATE TABLE IF NOT EXISTS nodes (
ID integer primary key autoincrement,
Name varchar(128) default null,
Membership integer(11) default '2',
CPUs integer(11) not null default '1',
Rack varchar(11) default null,
Rank integer(11) default null,
Arch varchar(32) default null,
OS varchar(64) not null default 'linux',
RunAction varchar(64) default 'os',
InstallAction varchar(64) default 'install'
);
create index if not exists idx_nodes_name on nodes(Name);
`
datalist = append(datalist, Nodes)
Aliases := `
CREATE TABLE IF NOT EXISTS aliases (
ID integer primary key autoincrement,
Node integer(15) not null default '0',
Name varchar(32) default null,
Foreign key(Node) references nodes(ID) on delete cascade
);
`
datalist = append(datalist, Aliases)
Networks := `
CREATE TABLE IF NOT EXISTS networks (
ID integer primary key autoincrement,
Node integer(11) default null,
MAC varchar(64) default null,
IP varchar(64) default null,
Name varchar(128) default null,
Device varchar(32) default null,
Subnet integer(11) default null,
Module varchar(128) default null,
VlanID integer(11) default null,
Options varchar(128) default null,
Channel varchar(128) default null,
Foreign key(Node) references nodes(ID) on delete cascade,
Foreign key(Subnet) references subnets(ID) on delete cascade
);
`
datalist = append(datalist, Networks)
GlobalRoutes := `
CREATE TABLE IF NOT EXISTS globalroutes (
Network varchar(32) not null default '',
Netmask varchar(32) not null default '',
Gateway varchar(32) not null default '',
Subnet integer(11) default null,
Primary key(Network, Netmask)
Foreign key(Subnet) references subnets(ID) on delete cascade
);
`
datalist = append(datalist, GlobalRoutes)
OSRoutes := `
CREATE TABLE IF NOT EXISTS osroutes (
OS varchar(64) not null default 'linux',
Network varchar(32) not null default '',
Netmask varchar(32) not null default '',
Gateway varchar(32) not null default '',
Subnet integer(11) default null,
Primary key(OS, Network, Netmask)
Foreign key(Subnet) references subnets(ID) on delete cascade
);
`
datalist = append(datalist, OSRoutes)
ApplianceRoutes := `
CREATE TABLE IF NOT EXISTS applianceroutes (
Appliance varchar(11) not null default '0',
Network varchar(32) not null default '',
Netmask varchar(32) not null default '',
Gateway varchar(32) not null default '',
Subnet integer(11) default null,
Primary key(Appliance, Network, Netmask)
Foreign key(Subnet) references subnets(ID) on delete cascade
);
`
datalist = append(datalist, ApplianceRoutes)
NodeRoutes := `
CREATE TABLE IF NOT EXISTS noderoutes (
Node varchar(11) not null default '0',
Network varchar(32) not null default '',
Netmask varchar(32) not null default '',
Gateway varchar(32) not null default '',
Subnet integer(11) default null,
Primary key(Node, Network, Netmask)
Foreign key(Subnet) references subnets(ID) on delete cascade
);
`
datalist = append(datalist, NodeRoutes)
Subnets := `
CREATE TABLE IF NOT EXISTS subnets (
ID integer primary key autoincrement,
name varchar(32) unique not null,
dnszone varchar(64) unique not null,
subnet varchar(32) default null,
netmask varchar(32) default null,
mtu integer(11) default '1500',
servedns boolean default false
);
`
datalist = append(datalist, Subnets)
PublicKeys := `
CREATE TABLE IF NOT EXISTS publickeys (
ID integer primary key autoincrement,
Node integer(11) not null default '0',
Public_Key varchar(8192) default null,
Description varchar(8192) default null,
Foreign key(Node) references nodes(ID) on delete cascade
);
`
datalist = append(datalist, PublicKeys)
SecGlobal := `
CREATE TABLE IF NOT EXISTS secglobal (
Attr varchar(128) default null,
Value text,
Enc varchar(128) default null,
Primary key(Attr)
);
`
datalist = append(datalist, SecGlobal)
SecNodes := `
CREATE TABLE IF NOT EXISTS secnodes (
Attr varchar(128) default null,
Enc varchar(128) default null,
Value text,
Node integer(15) not null default '0',
Primary key(Attr, Node)
);
`
datalist = append(datalist, SecNodes)
Attributes := `
CREATE TABLE IF NOT EXISTS attributes (
ID integer primary key autoincrement,
Attr varchar(128) not null,
Value text,
Shadow text,
Category integer(11) not null,
Catindex integer(11) not null,
UNIQUE(Attr, Category, Catindex),
Foreign key(Catindex) references catindex(ID) on delete cascade
);
`
datalist = append(datalist, Attributes)
Partitions := `
CREATE TABLE IF NOT EXISTS partitions (
ID integer primary key autoincrement,
Node integer(15) not null default '0',
Device varchar(128) not null default '',
MountPoint varchar(128) not null default '',
SectorStart varchar(128) not null default '',
PartitionSize varchar(128) not null default '',
FsType varchar(128) not null default '',
PartitionFlags varchar(128) not null default '',
FormatFlags varchar(128) not null default ''
);
`
datalist = append(datalist, Partitions)
Firewalls := `
CREATE TABLE IF NOT EXISTS firewalls (
ID integer primary key autoincrement,
Rulename varchar(128) not null,
Rulesrc varchar(256) not null default 'custom',
InSubnet int(11),
OutSubnet int(11),
Service varchar(256),
Protocol varchar(256),
Action varchar(256),
Chain varchar(256),
Flags varchar(256),
Comment varchar(256),
Category integer(11) not null,
Catindex integer(11) not null,
Check(rulesrc IN ('system', 'custom'))
UNIQUE(Rulename, Category, Catindex),
Foreign key(Catindex) references catindex(ID) on delete cascade
);
`
datalist = append(datalist, Firewalls)
Rolls := `
CREATE TABLE IF NOT EXISTS rolls (
ID integer primary key autoincrement,
Name varchar(128) not null default '',
Version varchar(32) not null default '',
Arch varchar(32) not null default '',
OS varchar(64) not null default 'linux',
Enabled varchar(3) not null default 'yes',
Check(Enabled IN ('yes', 'no'))
Check(OS IN ('linux', 'other'))
);
`
datalist = append(datalist, Rolls)
NodeRolls := `
CREATE TABLE IF NOT EXISTS noderolls (
Node varchar(11) not null default '0',
RollID varchar(11) not null,
Primary key(Node, RollID)
);
`
datalist = append(datalist, NodeRolls)
Bootactions := `
CREATE TABLE IF NOT EXISTS bootactions (
ID integer primary key autoincrement,
Action varchar(256) default null,
Kernel varchar(256) default null,
Ramdisk varchar(256) default null,
Args varchar(1024) default null
);
`
datalist = append(datalist, Bootactions)
BootFlags := `
CREATE TABLE IF NOT EXISTS bootflags (
ID integer primary key autoincrement,
Node integer(11) not null default '0',
Flags varchar(256) default null
);
`
datalist = append(datalist, BootFlags)
Distributions := `
CREATE TABLE IF NOT EXISTS distributions (
ID integer primary key autoincrement,
Name varchar(32) not null default '',
OS varchar(32) default '',
Release varchar(32) default ''
);
`
datalist = append(datalist, Distributions)
View_vnet := `
DROP VIEW IF EXISTS vnet;
CREATE VIEW vnet AS
SELECT
n.name AS nodename, /* 查询nodes表中name字段,将字段改名为nodename */
m.name AS membership,
a.name AS appliance,
n.rack, n.rank, /* 查询nodes表中rack和rank字段,使用原始字段名 */
s.name AS subnet,
nt.ip, nt.device, nt.module,
nt.name AS hostname,
s.dnszone AS domainname,
s.netmask, s.mtu
FROM
nodes n /* 主表: 先查询nodes表,别名n */
inner join memberships m on n.membership=m.id /* 连接memberships表,on只保留满足条件的行 */
inner join appliances a on m.appliance=a.id /* 连接appliances表,on只保留满足条件的行 */
inner join networks nt on n.id=nt.node /* 连接networks表,on只保留满足条件的行 */
inner join subnets s on nt.subnet=s.id /* 连接subnets表,on只保留满足条件的行 */
;
`
datalist = append(datalist, View_vnet)
View_hostselections := `
DROP VIEW IF EXISTS hostselections;
CREATE VIEW hostselections AS
SELECT
n.name AS host,
c.id as category,
ci.id as selection
FROM
nodes n
inner join memberships m on n.membership=m.id -- 节点表关联所属分组
inner join appliances a on m.appliance=a.id -- 分组关联所属应用角色
inner join categories c on
-- 匹配4类分层配置的category(全局/OS/应用/主机)
c.name in ('global', 'os', 'appliance', 'host')
inner join catindex ci on
-- 核心匹配逻辑: category和catindex的name字段一一对应
(c.name = 'global' and ci.name = 'global') or
(c.name = 'os' and ci.name = n.os) or
(c.name = 'appliance' and ci.name = a.name) or
(c.name = 'host' and ci.name = n.name)
;
`
datalist = append(datalist, View_hostselections)
View_vcatindex := `
-- 视图vcatindex: 类别索引可读试图
DROP VIEW IF EXISTS vcatindex;
CREATE VIEW vcatindex AS
SELECT
c.id AS ID,
cat.Name AS Category,
ci.Name AS catindex
FROM
categories cat
inner join catindex ci on ci.category=cat.id
;
`
datalist = append(datalist, View_vcatindex)
View_vresolvechain := `
-- 视图vresolvechain: 解析链可读试图
DROP VIEW IF EXISTS vresolvechain;
CREATE VIEW vresolvechain AS
SELECT
r.name AS chain,
cat.name AS category,
precedence
FROM
resolvechain r
inner join categories cat on r.category=cat.id
order by chain, precedence
;
`
datalist = append(datalist, View_vresolvechain)
View_vattributes := `
-- 视图vattributes: 属性可读试图
DROP VIEW IF EXISTS vattributes;
CREATE VIEW vattributes AS
SELECT
a.id,
attr,
value,
shadow,
cat.name AS category,
ci.name AS catindex
FROM
attributes a
inner join catindex ci on a.catindex=ci.id
inner join categories cat on a.category=cat.id
order by attr, catindex, category
;
`
datalist = append(datalist, View_vattributes)
View_vfirewalls := `
-- 视图vfirewalls: 防火墙规则可读试图
DROP VIEW IF EXISTS vfirewalls;
CREATE VIEW vfirewalls AS
SELECT
f.id,
f.Rulename,
f.Rulesrc,
f.InSubnet,
f.OutSubnet,
f.Service,
f.Protocol,
f.Action,
f.Chain,
f.Flags,
f.Comment,
cat.name AS category,
ci.name AS catindex
FROM
firewalls f
inner join catindex ci on f.catindex=ci.id
inner join categories cat on f.category=cat.id
order by f.Rulename, catindex, category
;
`
datalist = append(datalist, View_vfirewalls)
View_vhostselections := `
-- 视图vhostselections: 主机选择可读试图
DROP VIEW IF EXISTS vhostselections;
CREATE VIEW vhostselections AS
SELECT
hs.host AS host,
cat.name AS category,
ci.name AS selection
FROM
hostselections hs
inner join categories cat on hs.category=cat.id
inner join catindex ci on hs.selection=ci.id
order by host, category, selection
;
`
datalist = append(datalist, View_vhostselections)
View_vmapCategoryIndex := `
-- 视图vmapcategoryindex: 类别索引映射可读试图
DROP VIEW IF EXISTS vmapcategoryindex;
CREATE VIEW vmapCategoryIndex AS
SELECT
cat.name AS categoryName,
ci.name AS categoryIndex,
ci.ID AS index_ID
FROM
cateindex ci
inner join categories cat on ci.category=cat.id
;
`
datalist = append(datalist, View_vmapCategoryIndex)
return datalist
}
// 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(),
func InitBaseData(conn *sql.DB) error {
// ========== 第一步:插入 categories 数据 ==========
categoryData := []struct {
Name string
Description string
}{
{"global", "Global Defaults"},
{"os", "OS Choice(Linux,Sunos)"},
{"appliance", "Logical Appliances"},
{"rack", "Machine Room Racks"},
{"host", "Hosts - Physical AND Virtual"},
}
}
// 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",
// 批量插入 categories (忽略重复)
for _, cd := range categoryData {
query := `
insert or ignore into categories (Name, Description)
values (?, ?)
`
_, err := conn.Exec(query, cd.Name, cd.Description)
if err != nil {
return fmt.Errorf("error inserting category %s: %w", cd.Name, err)
}
}
}
func DropTriggerStatements() []string {
return []string{
"trg_nodes_before_delete",
// ========== 第二步:插入 catindex 数据 ==========
catindexData := []struct {
Name string
Category string
}{
{"global", "global"},
{"linux", "os"},
{"sunos", "os"},
{"frontend", "appliance"},
{"compute", "appliance"},
{"nas", "appliance"},
{"network", "appliance"},
{"power", "appliance"},
{"devel-server", "appliance"},
{"login", "appliance"},
}
}
// --- 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;
`
// 批量插入 catindex (忽略重复)
for _, ci := range catindexData {
// 动态获取类别ID (复用MapCategory函数)
catID, err := MapCategory(conn, ci.Category)
if err != nil {
return fmt.Errorf("error mapping category %s: %w", ci.Category, err)
}
if catID == 0 {
return fmt.Errorf("category %s not found", ci.Category)
}
// 插入 catindex (忽略重复)
query := `
insert or ignore into catindex (Name, Category)
values (?, ?)
`
_, err = conn.Exec(query, ci.Name, catID)
if err != nil {
return fmt.Errorf("error inserting catindex %s: %w", ci.Name, err)
}
}
// ========== 第三步:插入 resolvechain 数据 ==========
resolveChainData := []struct {
Name string // 解析链名称,global/linux/sunos
Category string // 类别名称,linux/sunos
Precedence int // 优先级,数值越大优先级越高
}{
{"default", "global", 10},
{"default", "os", 20},
{"default", "appliance", 30},
{"default", "rack", 40},
{"default", "host", 50},
}
// 批量插入 resolvechain (忽略重复)
for _, rcd := range resolveChainData {
// 动态获取类别ID (复用MapCategory函数)
catID, err := MapCategory(conn, rcd.Category)
if err != nil {
return fmt.Errorf("error mapping category %s: %w", rcd.Category, err)
}
if catID == 0 {
return fmt.Errorf("category %s not found", rcd.Category)
}
// 插入 resolvechain (忽略重复)
query := `
insert or ignore into resolvechain (Name, Category, Precedence)
values (?, ?, ?)
`
_, err = conn.Exec(query, rcd.Name, catID, rcd.Precedence)
if err != nil {
return fmt.Errorf("error inserting resolvechain %s: %w", rcd.Name, err)
}
}
return nil
}

162
pkg/info/info.go Normal file
View File

@@ -0,0 +1,162 @@
package info
import (
"bufio"
"fmt"
"os"
"runtime"
"strings"
)
// -------------------------- 编译注入的静态信息 --------------------------
var (
Version = "dev" // 应用版本号
BuildTime = "unknown" // 编译时间
GitCommit = "unknown" // Git提交ID
GitBranch = "unknown" // Git分支
)
// -------------------------- 固定常量 --------------------------
const (
AppName = "sunhpc"
linuxProcVersion = "/proc/version"
linuxProcCpuinfo = "/proc/cpuinfo"
)
// -------------------------- 系统信息结构体 --------------------------
// SystemInfo 封装所有系统相关信息Linux专属
type SystemInfo struct {
OS string // 操作系统
Arch string // 系统架构
KernelVersion string // 内核版本
CPUModel string // CPU型号
NumCPU int // CPU核心数
MemTotal string // 总内存
Hostname string // 主机名
GoVersion string // Go运行时版本
}
// -------------------------- 通用工具函数(无错误返回) --------------------------
// readFileFirstLine 读取文件第一行,失败返回空字符串
func readFileFirstLine(path string) string {
file, err := os.Open(path)
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
if scanner.Scan() {
return strings.TrimSpace(scanner.Text())
}
return ""
}
// -------------------------- 核心函数(无错误返回) --------------------------
// readCPUModel 读取CPU型号失败返回 "unknown"
func readCPUModel() string {
file, err := os.Open(linuxProcCpuinfo)
if err != nil {
return "unknown"
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "model name") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
}
}
return "unknown"
}
// readMemTotal 读取总内存,失败返回 "unknown"
func readMemTotal() string {
file, err := os.Open("/proc/meminfo")
if err != nil {
return "unknown"
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "MemTotal") {
parts := strings.Fields(line)
if len(parts) >= 2 {
memKB := strings.TrimSuffix(parts[1], "kB")
var memTotalKB int
fmt.Sscanf(memKB, "%d", &memTotalKB)
memTotalMB := memTotalKB / 1024
return fmt.Sprintf("%d MB", memTotalMB)
}
}
}
return "unknown"
}
// GetSystemInfo 获取Linux系统信息无错误返回异常字段用默认值填充
func GetSystemInfo() SystemInfo {
// 初始化结构体,先填充基础默认值
sysInfo := SystemInfo{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
NumCPU: runtime.NumCPU(),
GoVersion: runtime.Version(),
// 以下字段先设为默认值,后续尝试覆盖
KernelVersion: "unknown",
CPUModel: "unknown",
MemTotal: "unknown",
Hostname: "unknown",
}
// 1. 获取主机名(失败保留默认值)
if hostname, err := os.Hostname(); err == nil {
sysInfo.Hostname = hostname
}
// 2. 获取内核版本(失败保留默认值)
procVersion := readFileFirstLine(linuxProcVersion)
if procVersion != "" {
versionParts := strings.Split(procVersion, " ")
if len(versionParts) >= 3 {
sysInfo.KernelVersion = versionParts[2]
} else {
sysInfo.KernelVersion = procVersion
}
}
// 3. 获取CPU型号失败已返回 "unknown"
sysInfo.CPUModel = readCPUModel()
// 4. 获取总内存(失败已返回 "unknown"
sysInfo.MemTotal = readMemTotal()
return sysInfo
}
// -------------------------- 辅助函数:打印所有信息 --------------------------
// PrintAllInfo 打印所有公共信息(调试用)
func PrintAllInfo() {
fmt.Println("=== 应用公共信息 ===")
fmt.Printf("应用名称 : %s\n", AppName)
fmt.Printf("版本号 : %s\n", Version)
fmt.Printf("编译时间 : %s\n", BuildTime)
fmt.Printf("Git提交ID : %s\n", GitCommit)
fmt.Printf("Git分支 : %s\n", GitBranch)
fmt.Println("\n=== 系统信息 ===")
sysInfo := GetSystemInfo()
fmt.Printf("操作系统 : %s\n", sysInfo.OS)
fmt.Printf("系统架构 : %s\n", sysInfo.Arch)
fmt.Printf("内核版本 : %s\n", sysInfo.KernelVersion)
fmt.Printf("CPU型号 : %s\n", sysInfo.CPUModel)
fmt.Printf("CPU核心数 : %d\n", sysInfo.NumCPU)
fmt.Printf("总内存 : %s\n", sysInfo.MemTotal)
fmt.Printf("主机名 : %s\n", sysInfo.Hostname)
fmt.Printf("Go版本 : %s\n", sysInfo.GoVersion)
}

View File

@@ -1,162 +1,324 @@
package wizard
import (
"bufio"
"database/sql"
"errors"
"fmt"
"net"
"os"
"strings"
"sunhpc/pkg/database"
"sunhpc/pkg/info"
"sunhpc/pkg/logger"
"sunhpc/pkg/utils"
"github.com/BurntSushi/toml"
)
// 配置项映射:定义每个配置项对应的表名、键名
var configMappings = []struct {
table string
key string
getVal func(m *model) interface{} // 动态获取配置值的函数
}{
// attributes 表
{"attributes", "license", func(m *model) any { return m.config.License }},
{"attributes", "accepted", func(m *model) any { return m.config.AgreementAccepted }},
{"attributes", "country", func(m *model) any { return m.config.Country }},
{"attributes", "region", func(m *model) any { return m.config.Region }},
{"attributes", "timezone", func(m *model) any { return m.config.Timezone }},
{"attributes", "homepage", func(m *model) any { return m.config.HomePage }},
{"attributes", "dbaddress", func(m *model) any { return m.config.DBAddress }},
{"attributes", "software", func(m *model) any { return m.config.Software }},
type ConfigMapping struct {
Title string `toml:"title"`
Base struct {
ClusterName string `toml:"cluster_name"`
Country string `toml:"country"`
State string `toml:"state"`
City string `toml:"city"`
HomePage string `toml:"homepage"`
Contact string `toml:"contact"`
License string `toml:"license"`
BaseDir string `toml:"base_dir"`
WorkDir string `toml:"work_dir"`
DistroDir string `toml:"distro_dir"`
Partition string `toml:"partition"`
Distribution string `toml:"distribution"`
Timezone string `toml:"timezone"`
SafePort string `toml:"safe_port"`
SafeDirs string `toml:"safe_dirs"`
SafeSecurity string `toml:"safe_security"`
PluginDirs string `toml:"plugin_dirs"`
PluginPort string `toml:"plugin_port"`
GangliaAddr string `toml:"ganglia_addr"`
} `toml:"base"`
Pxelinux struct {
NextServer string `toml:"next_server"`
PxeFilename string `toml:"pxe_filename"`
PxeLinuxDir string `toml:"pxelinux_dir"`
BootArgs string `toml:"boot_args"`
} `toml:"pxelinux"`
Public struct {
PublicHostname string `toml:"public_hostname"`
PublicInterface string `toml:"public_interface"`
PublicAddress string `toml:"public_address"`
PublicNetmask string `toml:"public_netmask"`
PublicGateway string `toml:"public_gateway"`
PublicNetwork string `toml:"public_network"`
PublicDomain string `toml:"public_domain"`
PublicCIDR string `toml:"public_cidr"`
PublicDNS string `toml:"public_dns"`
PublicMac string `toml:"public_mac"`
PublicMTU string `toml:"public_mtu"`
PublicNTP string `toml:"public_ntp"`
} `toml:"public"`
Private struct {
PrivateHostname string `toml:"private_hostname"`
PrivateInterface string `toml:"private_interface"`
PrivateAddress string `toml:"private_address"`
PrivateNetmask string `toml:"private_netmask"`
PrivateNetwork string `toml:"private_network"`
PrivateDomain string `toml:"private_domain"`
PrivateCIDR string `toml:"private_cidr"`
PrivateMac string `toml:"private_mac"`
PrivateMTU string `toml:"private_mtu"`
} `toml:"private"`
}
// nodes 表
{"nodes", "name", func(m *model) any { return m.config.Hostname }},
type IPMaskInfo struct {
NetworkAddress string // 网络地址 192.168.1.0
CIDR string // CIDR 格式 192.168.1.0/24
IPAddress string // IP 地址 192.168.1.100
MacAddress string // MAC 地址 00:11:22:33:44:55
Netmask string // 子网掩码 255.255.255.0
PrefixLength int // 前缀长度 24
}
// 公网设置表
{"public_network", "public_interface", func(m *model) any { return m.config.PublicInterface }},
{"public_network", "ip_address", func(m *model) any { return m.config.PublicIPAddress }},
{"public_network", "netmask", func(m *model) any { return m.config.PublicNetmask }},
{"public_network", "gateway", func(m *model) any { return m.config.PublicGateway }},
// 内网配置表
{"internal_network", "internal_interface", func(m *model) any { return m.config.InternalInterface }},
{"internal_network", "internal_ip", func(m *model) any { return m.config.InternalIPAddress }},
{"internal_network", "internal_mask", func(m *model) any { return m.config.InternalNetmask }},
// DNS配置表
{"dns_config", "dns_primary", func(m *model) any { return m.config.DNSPrimary }},
{"dns_config", "dns_secondary", func(m *model) any { return m.config.DNSSecondary }},
type AttrItem struct {
Key string
Value string
Shadow string
Category int
Catindex int
}
func NewConfigWithDefault() *ConfigMapping {
return &ConfigMapping{
Title: "Cluster Configuration",
Base: struct {
ClusterName string `toml:"cluster_name"`
Country string `toml:"country"`
State string `toml:"state"`
City string `toml:"city"`
HomePage string `toml:"homepage"`
Contact string `toml:"contact"`
License string `toml:"license"`
BaseDir string `toml:"base_dir"`
WorkDir string `toml:"work_dir"`
DistroDir string `toml:"distro_dir"`
Partition string `toml:"partition"`
Distribution string `toml:"distribution"`
Timezone string `toml:"timezone"`
SafePort string `toml:"safe_port"`
SafeDirs string `toml:"safe_dirs"`
SafeSecurity string `toml:"safe_security"`
PluginDirs string `toml:"plugin_dirs"`
PluginPort string `toml:"plugin_port"`
GangliaAddr string `toml:"ganglia_addr"`
}{
ClusterName: "SunHPC_Cluster",
Country: "CN",
State: "Beijing",
City: "Beijing",
HomePage: "https://www.sunhpc.com",
Contact: "admin@sunhpc.com",
License: "MIT",
BaseDir: "install",
WorkDir: "/export",
DistroDir: "/export/sunhpc",
Partition: "default",
Distribution: "sunhpc-dist",
Timezone: "Asia/Shanghai",
SafePort: "372",
SafeDirs: "safe.d",
SafeSecurity: "safe-security",
PluginDirs: "/etc/sunhpc/plugin",
PluginPort: "12123",
GangliaAddr: "224.0.0.3",
},
Pxelinux: struct {
NextServer string `toml:"next_server"`
PxeFilename string `toml:"pxe_filename"`
PxeLinuxDir string `toml:"pxelinux_dir"`
BootArgs string `toml:"boot_args"`
}{
NextServer: "192.168.1.1",
PxeFilename: "pxelinux.0",
PxeLinuxDir: "/tftpboot/pxelinux",
BootArgs: "net.ifnames=0 biosdevname=0",
},
Public: struct {
PublicHostname string `toml:"public_hostname"`
PublicInterface string `toml:"public_interface"`
PublicAddress string `toml:"public_address"`
PublicNetmask string `toml:"public_netmask"`
PublicGateway string `toml:"public_gateway"`
PublicNetwork string `toml:"public_network"`
PublicDomain string `toml:"public_domain"`
PublicCIDR string `toml:"public_cidr"`
PublicDNS string `toml:"public_dns"`
PublicMac string `toml:"public_mac"`
PublicMTU string `toml:"public_mtu"`
PublicNTP string `toml:"public_ntp"`
}{
PublicHostname: "cluster.hpc.org",
PublicInterface: "eth0",
PublicAddress: "",
PublicNetmask: "",
PublicGateway: "",
PublicNetwork: "",
PublicDomain: "hpc.org",
PublicCIDR: "",
PublicDNS: "",
PublicMac: "00:11:22:33:44:55",
PublicMTU: "1500",
PublicNTP: "pool.ntp.org",
},
Private: struct {
PrivateHostname string `toml:"private_hostname"`
PrivateInterface string `toml:"private_interface"`
PrivateAddress string `toml:"private_address"`
PrivateNetmask string `toml:"private_netmask"`
PrivateNetwork string `toml:"private_network"`
PrivateDomain string `toml:"private_domain"`
PrivateCIDR string `toml:"private_cidr"`
PrivateMac string `toml:"private_mac"`
PrivateMTU string `toml:"private_mtu"`
}{
PrivateHostname: "sunhpc",
PrivateInterface: "eth1",
PrivateAddress: "172.16.9.254",
PrivateNetmask: "255.255.255.0",
PrivateNetwork: "172.16.9.0",
PrivateDomain: "example.com",
PrivateCIDR: "172.16.9.0/24",
PrivateMac: "00:11:22:33:44:66",
PrivateMTU: "1500",
},
}
}
func loadConfig() (*ConfigMapping, error) {
configs := NewConfigWithDefault()
cfgfile := "/etc/sunhpc/config.toml"
// 尝试解析配置文件
if _, err := toml.DecodeFile(cfgfile, configs); err != nil {
if errors.Is(err, os.ErrNotExist) {
// 文件不存在,直接返回默认配置
logger.Infof("Config file %s not exist, use default config", cfgfile)
return configs, nil
}
// 其他错误,返回错误
logger.Debugf("[DEBUG] Parse config file %s failed: %v", cfgfile, err)
return nil, err
}
logger.Infof("Load config file %s success", cfgfile)
return configs, nil
}
// saveConfig 入口函数:保存所有配置到数据库
func (m *model) saveConfig() error {
conn, err := database.GetDB() // 假设database包已实现getDB()获取连接
if err != nil {
return fmt.Errorf("获取数据库连接失败: %w", err)
}
defer conn.Close()
m.force = false // 初始化全量覆盖标识
// 遍历所有配置项,逐个处理
for _, item := range configMappings {
val := item.getVal(m)
exists, err := m.checkExists(conn, item.table, item.key)
if err != nil {
return fmt.Errorf("检查%s.%s是否存在失败: %w", item.table, item.key, err)
}
// 根据存在性和用户选择处理
if !exists {
// 不存在则直接插入
if err := m.upsertConfig(conn, item.table, item.key, val, false); err != nil {
return fmt.Errorf("插入%s.%s失败: %w", item.table, item.key, err)
}
continue
}
// 已存在:判断是否全量覆盖
if m.force {
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
return fmt.Errorf("强制更新%s.%s失败: %w", item.table, item.key, err)
}
continue
}
// 询问用户操作
choice, err := m.askUserChoice(item.table, item.key)
if err != nil {
return fmt.Errorf("获取用户选择失败: %w", err)
}
switch strings.ToLower(choice) {
case "y", "yes":
// 单条覆盖
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
return fmt.Errorf("更新%s.%s失败: %w", item.table, item.key, err)
}
case "a", "all":
// 全量覆盖,后续不再询问
m.force = true
if err := m.upsertConfig(conn, item.table, item.key, val, true); err != nil {
return fmt.Errorf("全量更新%s.%s失败: %w", item.table, item.key, err)
}
case "n", "no":
// 跳过当前项
fmt.Printf("跳过%s.%s的更新\n", item.table, item.key)
default:
fmt.Printf("无效选择%s跳过%s.%s的更新\n", choice, item.table, item.key)
}
c, err := loadConfig()
if err != nil {
logger.Debugf("[DEBUG] Load config file failed: %v", err)
return err
}
// 合并配置项
result := make(map[string]string)
// base 配置
result["country"] = mergeValue(m.config.Country, c.Base.Country)
result["state"] = mergeValue(m.config.State, c.Base.State)
result["city"] = mergeValue(m.config.City, c.Base.City)
result["contact"] = mergeValue(m.config.Contact, c.Base.Contact)
result["homepage"] = mergeValue(m.config.HomePage, c.Base.HomePage)
result["cluster_name"] = mergeValue(m.config.ClusterName, c.Base.ClusterName)
result["license"] = c.Base.License
result["distribution"] = c.Base.Distribution
result["timezone"] = mergeValue(m.config.Timezone, c.Base.Timezone)
result["base_dir"] = c.Base.BaseDir
result["work_dir"] = c.Base.WorkDir
result["distro_dir"] = mergeValue(m.config.DistroDir, c.Base.DistroDir)
result["partition"] = c.Base.Partition
// safe 配置
result["safe_port"] = c.Base.SafePort
result["safe_dirs"] = c.Base.SafeDirs
result["safe_security"] = c.Base.SafeSecurity
// plugin 配置
result["plugin_dirs"] = c.Base.PluginDirs
result["plugin_port"] = c.Base.PluginPort
// monitor 配置
result["ganglia_addr"] = c.Base.GangliaAddr
// public 配置
result["public_hostname"] = mergeValue(m.config.PublicHostname, c.Public.PublicHostname)
result["public_interface"] = mergeValue(m.config.PublicInterface, c.Public.PublicInterface)
result["public_address"] = mergeValue(m.config.PublicIPAddress, c.Public.PublicAddress)
result["public_netmask"] = mergeValue(m.config.PublicNetmask, c.Public.PublicNetmask)
result["public_gateway"] = mergeValue(m.config.PublicGateway, c.Public.PublicGateway)
// 获取公网网络信息
publicIface := mergeValue(m.config.PublicInterface, c.Public.PublicInterface)
publicInfo, err := GetNetworkInfo(
publicIface, c.Public.PublicAddress, c.Public.PublicNetmask)
if err != nil {
logger.Debugf("[DEBUG] Get public interface %s IP mask info failed: %v",
publicIface, err)
}
result["public_network"] = publicInfo.NetworkAddress
result["public_domain"] = mergeValue(m.config.PublicDomain, c.Public.PublicDomain)
result["public_cidr"] = mergeValue(c.Public.PublicCIDR, publicInfo.CIDR)
result["public_dns"] = c.Public.PublicDNS
result["public_mac"] = publicInfo.MacAddress
result["public_mtu"] = mergeValue(m.config.PublicMTU, c.Public.PublicMTU)
result["public_ntp"] = c.Public.PublicNTP
// private 配置
// 获取内网网络信息
privateIface := mergeValue(m.config.PrivateInterface, c.Private.PrivateInterface)
privateInfo, err := GetNetworkInfo(
privateIface, c.Private.PrivateAddress, c.Private.PrivateNetmask)
if err != nil {
logger.Debugf("[DEBUG] Get private interface %s IP mask info failed: %v",
privateIface, err)
}
result["private_hostname"] = mergeValue(m.config.PrivateHostname, c.Private.PrivateHostname)
result["private_interface"] = mergeValue(m.config.PrivateInterface, c.Private.PrivateInterface)
result["private_address"] = mergeValue(m.config.PrivateIPAddress, c.Private.PrivateAddress)
result["private_netmask"] = mergeValue(m.config.PrivateNetmask, c.Private.PrivateNetmask)
result["private_network"] = privateInfo.NetworkAddress
result["private_domain"] = mergeValue(m.config.PrivateDomain, c.Private.PrivateDomain)
result["private_cidr"] = mergeValue(c.Private.PrivateCIDR, privateInfo.CIDR)
result["private_mac"] = privateInfo.MacAddress
result["private_mtu"] = mergeValue(m.config.PrivateMTU, c.Private.PrivateMTU)
// pxe 配置
result["next_server"] = mergeValue(privateInfo.IPAddress, c.Pxelinux.NextServer)
result["pxe_filename"] = c.Pxelinux.PxeFilename
result["pxelinux_dir"] = c.Pxelinux.PxeLinuxDir
result["boot_args"] = c.Pxelinux.BootArgs
// 插入数据到数据库
if err := insertDataToDB(result); err != nil {
logger.Debugf("[DEBUG] Insert config data to database failed: %v", err)
return err
}
logger.Debugf("Result: %v", result)
return nil
}
// checkExists 集中判断配置项是否存在(核心判断逻辑)
func (m *model) checkExists(conn *sql.DB, table, key string) (bool, error) {
var count int
// 通用存在性检查SQL假设所有表都有key字段作为主键
query := fmt.Sprintf("SELECT COUNT(1) FROM %s WHERE `key` = ?", table)
err := conn.QueryRow(query, key).Scan(&count)
if err != nil {
// 表不存在也视为"不存在"(可选:根据实际需求调整,比如先建表)
if strings.Contains(err.Error(), "table not found") {
return false, nil
}
return false, err
func mergeValue(tui_value, cfg_value string) string {
if tui_value == "" {
return cfg_value
}
return count > 0, nil
}
// upsertConfig 统一处理插入/更新逻辑
func (m *model) upsertConfig(conn *sql.DB, table, key string, val interface{}, update bool) error {
var query string
if !update {
// 插入:假设表结构为(key, value)
query = fmt.Sprintf("INSERT INTO %s (`key`, `value`) VALUES (?, ?)", table)
} else {
// 更新
query = fmt.Sprintf("UPDATE %s SET `value` = ? WHERE `key` = ?", table)
}
// 处理参数顺序(更新和插入的参数顺序不同)
var args []interface{}
if !update {
args = []interface{}{key, val}
} else {
args = []interface{}{val, key}
}
_, err := conn.Exec(query, args...)
return err
}
// askUserChoice 询问用户操作选择
func (m *model) askUserChoice(table, key string) (string, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("配置项%s.%s已存在选择操作(y/yes=覆盖, n/no=跳过, a/all=全量覆盖后续所有): ", table, key)
input, err := reader.ReadString('\n')
if err != nil {
return "", err
}
// 去除空格和换行
return strings.TrimSpace(input), nil
return tui_value
}
// 获取系统网络接口
@@ -183,3 +345,226 @@ func getNetworkInterfaces() []string {
}
return result
}
func GetNetworkInfo(iface, ip, mask string) (*IPMaskInfo, error) {
logger.Debugf("Get Network %s, IP %s, mask %s", iface, ip, mask)
// 解析IP
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
logger.Debugf("Invalid IP address: %s", ip)
return nil, fmt.Errorf("invalid IP address: %s", ip)
}
// 解析子网掩码
maskAddr := net.ParseIP(mask)
if maskAddr == nil {
logger.Debugf("Invalid subnet mask: %s", mask)
return nil, fmt.Errorf("invalid subnet mask: %s", mask)
}
// 确保是IPv4地址
ipv4 := ipAddr.To4()
maskv4 := maskAddr.To4()
if ipv4 == nil || maskv4 == nil {
logger.Debugf("Only support IPv4 address")
return nil, fmt.Errorf("only support IPv4 address")
}
// 计算网络地址 (IP & 子网掩码)
network := make([]byte, 4)
for i := 0; i < 4; i++ {
network[i] = ipv4[i] & maskv4[i]
}
networkAddr := fmt.Sprintf(
"%d.%d.%d.%d", network[0], network[1], network[2], network[3])
// 计算前缀长度
prefixLen := 0
for i := 0; i < 4; i++ {
for j := 7; j >= 0; j-- {
if maskv4[i]&(1<<uint(j)) != 0 {
prefixLen++
} else {
break
}
}
}
// 计算CIDR格式
cidr := fmt.Sprintf("%s/%d", networkAddr, prefixLen)
var mac string
// 获取Mac地址
ifaceName, err := net.InterfaceByName(iface)
if err == nil {
mac = ifaceName.HardwareAddr.String()
if mac == "" {
logger.Debugf("Network interface %s has no MAC address", iface)
}
} else {
logger.Debugf("Invalid network interface: %s", iface)
mac = ""
}
return &IPMaskInfo{
NetworkAddress: networkAddr,
CIDR: cidr,
IPAddress: ip,
MacAddress: mac,
Netmask: mask,
PrefixLength: prefixLen,
}, nil
}
func insertDataToDB(result map[string]string) error {
insertData := []string{}
infos := info.GetSystemInfo()
// initrd 配置
bootver := fmt.Sprintf("%s-%s", info.Version, infos.Arch)
vmlinuz := fmt.Sprintf("vmlinuz-%s", bootver)
initrds := fmt.Sprintf("initrd-%s", bootver)
insArgs := fmt.Sprintf("%s inst.ks.sendmac ksdevice=bootif", result["boot_args"])
resArgs := fmt.Sprintf("%s rescue", result["boot_args"])
lesArgs := fmt.Sprintf("%s vnc vncip=%s vncpassword=sunhpc", result["boot_args"], result["private_address"])
bootaction := []string{
fmt.Sprintf("insert into bootactions values (1, 'install', '%s', '%s', '%s');",
vmlinuz, initrds, insArgs),
"insert into bootactions values (2, 'os', 'localboot 0', '', '');",
"insert into bootactions values (3, 'memtest', 'kernel memtest', '', '');",
fmt.Sprintf("insert into bootactions values (4, 'install headless', '%s', '%s', '%s');",
vmlinuz, initrds, lesArgs),
fmt.Sprintf("insert into bootactions values (5, 'rescue', '%s', '%s', '%s');",
vmlinuz, initrds, resArgs),
"insert into bootactions values (6, 'pxeflash', 'kernel memdisk bigraw', 'pxeflash.img', 'keeppxe');",
}
insertData = append(insertData, bootaction...)
attrs := GetAttrs(result)
for _, item := range attrs {
key := item.Key
value := item.Value
shadow := item.Shadow
category := item.Category
catindex := item.Catindex
insertData = append(insertData,
fmt.Sprintf("insert into attributes values ('%s', '%s', '%s', %d, %d);",
key, value, shadow, category, catindex))
}
nodes := []string{
fmt.Sprintf(
"insert into nodes values (1, '%s', '%d', 0, 0, '%s', '%s', '', 'install');",
result["private_hostname"],
info.GetSystemInfo().NumCPU,
info.GetSystemInfo().Arch,
info.GetSystemInfo().OS),
fmt.Sprintf(
"insert into networks values (1, 1, '%s', '%s', '%s', '%s', '2');",
result["public_mac"],
result["public_address"],
result["private_hostname"],
result["public_interface"]),
fmt.Sprintf(
"insert into networks values (2, 1, '%s', '%s', '%s', '%s', '1');",
result["private_mac"],
result["private_address"],
result["private_hostname"],
result["private_interface"]),
fmt.Sprintf(
"insert into subnets values (1, 'private', '%s', '%s', '%s', '%s', '1');",
result["private_domain"],
result["private_network"],
result["private_netmask"],
result["private_mtu"]),
fmt.Sprintf(
"insert into subnets values (2, 'public', '%s', '%s', '%s', '%s', '0');",
result["public_domain"],
result["public_network"],
result["public_netmask"],
result["public_mtu"]),
}
insertData = append(insertData, nodes...)
if err := database.ExecWithTransaction(insertData); err != nil {
logger.Debugf("[DEBUG] Insert config data to database failed: %v", err)
return err
}
return nil
}
func GetAttrs(results map[string]string) []AttrItem {
attrs := []AttrItem{
{Key: "Info_CertificateCountry", Value: results["country"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Info_CertificateState", Value: results["state"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Info_CertificateCity", Value: results["city"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Info_CertificateOrganization", Value: "DLHP", Shadow: "false", Category: 1, Catindex: 1},
{Key: "Info_Contact", Value: results["contact"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Info_ClusterHostname", Value: results["ClusterHostname"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_WorkDir", Value: results["work_dir"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_DistroDir", Value: results["distro_dir"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_Partition", Value: results["partition"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicHostname", Value: results["public_hostname"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicInterface", Value: results["public_interface"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicAddress", Value: results["public_address"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicMacAddr", Value: results["public_mac"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicNetmask", Value: results["public_netmask"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicGateway", Value: results["public_gateway"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicNetwork", Value: results["public_network"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicDomain", Value: results["public_domain"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicCIDR", Value: results["public_cidr"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicDNS", Value: results["public_dns"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicMTU", Value: results["public_mtu"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PublicNTP", Value: results["public_ntp"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateHostname", Value: results["private_hostname"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateInterface", Value: results["private_interface"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateAddress", Value: results["private_address"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateMacAddr", Value: results["private_mac"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateNetmask", Value: results["private_netmask"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateGateway", Value: results["private_gateway"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateNetwork", Value: results["private_network"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateDomain", Value: results["private_domain"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateCIDR", Value: results["private_cidr"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_PrivateMTU", Value: results["private_mtu"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_Timezone", Value: results["timezone"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_Bootargs", Value: results["boot_args"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_Distribution", Value: results["distribution"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstart_BaseDir", Value: results["base_dir"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "SafePort", Value: results["safe_port"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "SafeDirs", Value: results["safe_dirs"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "SafeSecurity", Value: results["safe_security"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Plugin_dirs", Value: results["plugin_dirs"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Plugin_port", Value: results["plugin_port"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Ganglia_addr", Value: results["ganglia_addr"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Sunhpc_version", Value: results["sunhpc_version"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "DHCP_filename", Value: results["pxe_filename"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "DHCP_nextserver", Value: results["next_server"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Pxelinuxdir", Value: results["pxelinux_dir"], Shadow: "false", Category: 1, Catindex: 1},
{Key: "Kickstartable", Value: "yes", Shadow: "false", Category: 3, Catindex: 4},
{Key: "Kickstartable", Value: "yes", Shadow: "false", Category: 3, Catindex: 5},
{Key: "Kickstartable", Value: "yes", Shadow: "false", Category: 3, Catindex: 6},
{Key: "Kickstartable", Value: "no", Shadow: "false", Category: 3, Catindex: 7},
{Key: "Kickstartable", Value: "no", Shadow: "false", Category: 3, Catindex: 8},
{Key: "Kickstartable", Value: "yes", Shadow: "false", Category: 3, Catindex: 9},
{Key: "Kickstartable", Value: "yes", Shadow: "false", Category: 3, Catindex: 10},
{Key: "Managed", Value: "true", Shadow: "false", Category: 1, Catindex: 1},
{Key: "OS", Value: "linux", Shadow: "false", Category: 1, Catindex: 1},
}
return attrs
}

View File

@@ -31,8 +31,8 @@ func NewTextInput(placeholder string, defaultValue string) *TextInput {
ti := textinput.New()
ti.Placeholder = placeholder
ti.SetValue(defaultValue)
ti.Focus()
return &TextInput{Model: ti, focused: true}
ti.Blur()
return &TextInput{Model: ti, focused: false}
}
func (t *TextInput) Focus() tea.Cmd {
@@ -135,9 +135,14 @@ func NewFocusManager(loop bool) *FocusManager {
}
}
// Register 注册可聚焦组件(指定标识和切换顺序)
/*
Register 注册可聚焦组件(指定标识和切换顺序)
ID 组件的唯一标识,用于后续切换和获取焦点
例如 "form1.ip_input"、"form1.next_btn"
*/
func (fm *FocusManager) Register(id string, comp Focusable) {
// 防御性检查:避免 components 为空导致 panic
// 防御性检查:避免 components 未初始化为nil导致 panic
if fm.components == nil {
fm.components = make(map[string]Focusable)
}
@@ -147,6 +152,8 @@ func (fm *FocusManager) Register(id string, comp Focusable) {
return
}
// id : accept_btn, form1.reject_btn
// comp: 接受协议按钮, 拒绝协议按钮
fm.components[id] = comp
fm.order = append(fm.order, id)
@@ -211,6 +218,7 @@ func (fm *FocusManager) Prev() tea.Cmd {
return nil
}
//fm.components[fm.currentFocusID].Blur()
fm.components[fm.currentFocusID].Blur()
prevID := fm.order[prevIdx]
fm.currentFocusID = prevID
@@ -259,48 +267,57 @@ func (m *model) initPageFocus(page PageType) {
m.focusManager = NewFocusManager(true)
// 获取当前页面的组件
pageComps, exists := m.pageComponents[page]
if !exists {
return
}
// 按 [业务逻辑顺序] 注册组件 (决定Tab切换的顺序)
var componentOrder []string
var defaultFocusID string
// 按页面类型定义不同的注册顺序
switch page {
case PageAgreement:
componentOrder = []string{"accept_btn", "reject_btn"}
defaultFocusID = "accept_btn"
case PageData:
componentOrder = []string{
"Homepage_input",
"Hostname_input",
"ClusterName_input",
"Country_input",
"Region_input",
"State_input",
"City_input",
"Contact_input",
"Timezone_input",
"DBPath_input",
"Software_input",
"DistroDir_input",
"next_btn",
"prev_btn",
}
defaultFocusID = "next_btn"
case PagePublicNetwork:
componentOrder = []string{
"PublicHostname_input",
"PublicInterface_input",
"PublicIPAddress_input",
"PublicNetmask_input",
"PublicGateway_input",
"PublicDomain_input",
"PublicMTU_input",
"next_btn",
"prev_btn",
}
defaultFocusID = "next_btn"
case PageInternalNetwork:
componentOrder = []string{
"InternalInterface_input",
"InternalIPAddress_input",
"InternalNetmask_input",
"PrivateHostname_input",
"PrivateInterface_input",
"PrivateIPAddress_input",
"PrivateNetmask_input",
"PrivateDomain_input",
"PrivateMTU_input",
"next_btn",
"prev_btn",
}
defaultFocusID = "next_btn"
case PageDNS:
componentOrder = []string{
"Pri_DNS_input",
@@ -308,14 +325,25 @@ func (m *model) initPageFocus(page PageType) {
"next_btn",
"prev_btn",
}
defaultFocusID = "next_btn"
case PageSummary:
componentOrder = []string{"confirm_btn", "cancel_btn"}
defaultFocusID = "confirm_btn"
}
// 注册组件到焦点管理器(按顺序)
for _, compID := range componentOrder {
if comp, exists := pageComps[compID]; exists {
m.focusManager.Register(compID, comp)
}
}
if defaultFocusID != "" {
if currentComp, exists := m.focusManager.GetCurrent(); exists {
currentComp.Blur()
}
if targetComp, exists := pageComps[defaultFocusID]; exists {
m.focusManager.currentFocusID = defaultFocusID
targetComp.Focus()
}
}
}

View File

@@ -20,24 +20,32 @@ type Config struct {
AgreementAccepted bool `json:"agreement_accepted"`
// 数据接收
Hostname string `json:"hostname"`
Country string `json:"country"`
Region string `json:"region"`
Timezone string `json:"timezone"`
HomePage string `json:"homepage"`
DBAddress string `json:"db_address"`
Software string `json:"software"`
ClusterName string `json:"cluster_name"`
Country string `json:"country"`
State string `json:"state"`
City string `json:"city"`
Contact string `json:"contact"`
Timezone string `json:"timezone"`
HomePage string `json:"homepage"`
DBAddress string `json:"db_address"`
DistroDir string `json:"distro_dir"`
// 公网设置
PublicHostname string `json:"public_hostname"`
PublicInterface string `json:"public_interface"`
PublicIPAddress string `json:"ip_address"`
PublicNetmask string `json:"netmask"`
PublicGateway string `json:"gateway"`
PublicDomain string `json:"public_domain"`
PublicMTU string `json:"public_mtu"`
// 内网配置
InternalInterface string `json:"internal_interface"`
InternalIPAddress string `json:"internal_ip"`
InternalNetmask string `json:"internal_mask"`
PrivateHostname string `json:"private_hostname"`
PrivateInterface string `json:"private_interface"`
PrivateIPAddress string `json:"private_ip"`
PrivateNetmask string `json:"private_mask"`
PrivateDomain string `json:"private_domain"`
PrivateMTU string `json:"private_mtu"`
// DNS 配置
DNSPrimary string `json:"dns_primary"`
@@ -96,23 +104,31 @@ func defaultConfig() Config {
}
return Config{
License: "This test license is for testing purposes only. Do not use it in production.",
Hostname: "cluster.hpc.org",
Country: "China",
Region: "Beijing",
Timezone: "Asia/Shanghai",
HomePage: "www.sunhpc.com",
DBAddress: "/var/lib/sunhpc/sunhpc.db",
Software: "/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",
License: "This test license is for testing purposes only. Do not use it in production.",
ClusterName: "cluster.hpc.org",
Country: "China",
State: "Beijing",
City: "Beijing",
Contact: "admin@sunhpc.com",
Timezone: "Asia/Shanghai",
HomePage: "www.sunhpc.com",
DBAddress: "/var/lib/sunhpc/sunhpc.db",
DistroDir: "/export/sunhpc",
PublicHostname: "cluster.hpc.org",
PublicInterface: defaultPublicInterface,
PublicIPAddress: "",
PublicNetmask: "",
PublicGateway: "",
PublicDomain: "hpc.org",
PublicMTU: "1500",
PrivateHostname: "cluster",
PrivateInterface: defaultInternalInterface,
PrivateIPAddress: "172.16.9.254",
PrivateNetmask: "255.255.255.0",
PrivateDomain: "local",
PrivateMTU: "1500",
DNSPrimary: "8.8.8.8",
DNSSecondary: "8.8.4.4",
}
}
@@ -132,31 +148,38 @@ func initialModel() model {
// ------------------ 页面2基础信息页面 --------------------
page2Comps := make(map[string]Focusable)
page2Comps["Homepage_input"] = NewTextInput("Homepage", cfg.HomePage)
page2Comps["Hostname_input"] = NewTextInput("Hostname", cfg.Hostname)
page2Comps["ClusterName_input"] = NewTextInput("ClusterName", cfg.ClusterName)
page2Comps["Country_input"] = NewTextInput("Country", cfg.Country)
page2Comps["Region_input"] = NewTextInput("Region", cfg.Region)
page2Comps["State_input"] = NewTextInput("State", cfg.State)
page2Comps["City_input"] = NewTextInput("City", cfg.City)
page2Comps["Contact_input"] = NewTextInput("Contact", cfg.Contact)
page2Comps["Timezone_input"] = NewTextInput("Timezone", cfg.Timezone)
page2Comps["DBPath_input"] = NewTextInput("DBPath", cfg.DBAddress)
page2Comps["Software_input"] = NewTextInput("Software", cfg.Software)
page2Comps["DistroDir_input"] = NewTextInput("DistroDir", cfg.DistroDir)
page2Comps["next_btn"] = NewButton("下一步")
page2Comps["prev_btn"] = NewButton("上一步")
pageComponents[PageData] = page2Comps
// ------------------ 页面3公网网络页面 --------------------
page3Comps := make(map[string]Focusable)
page3Comps["PublicHostname_input"] = NewTextInput("PublicHostname", cfg.PublicHostname)
page3Comps["PublicInterface_input"] = NewTextInput("PublicInterface", cfg.PublicInterface)
page3Comps["PublicIPAddress_input"] = NewTextInput("PublicIPAddress", cfg.PublicIPAddress)
page3Comps["PublicNetmask_input"] = NewTextInput("PublicNetmask", cfg.PublicNetmask)
page3Comps["PublicGateway_input"] = NewTextInput("PublicGateway", cfg.PublicGateway)
page3Comps["PublicDomain_input"] = NewTextInput("PublicDomain", cfg.PublicDomain)
page3Comps["PublicMTU_input"] = NewTextInput("PublicMTU", cfg.PublicMTU)
page3Comps["next_btn"] = NewButton("下一步")
page3Comps["prev_btn"] = NewButton("上一步")
pageComponents[PagePublicNetwork] = page3Comps
// ------------------ 页面4内网网络页面 --------------------
page4Comps := make(map[string]Focusable)
page4Comps["InternalInterface_input"] = NewTextInput("InternalInterface", cfg.InternalInterface)
page4Comps["InternalIPAddress_input"] = NewTextInput("InternalIPAddress", cfg.InternalIPAddress)
page4Comps["InternalNetmask_input"] = NewTextInput("InternalNetmask", cfg.InternalNetmask)
page4Comps["PrivateHostname_input"] = NewTextInput("PrivateHostname", cfg.PrivateHostname)
page4Comps["PrivateInterface_input"] = NewTextInput("PrivateInterface", cfg.PrivateInterface)
page4Comps["PrivateIPAddress_input"] = NewTextInput("PrivateIPAddress", cfg.PrivateIPAddress)
page4Comps["PrivateNetmask_input"] = NewTextInput("PrivateNetmask", cfg.PrivateNetmask)
page4Comps["PrivateDomain_input"] = NewTextInput("PrivateDomain", cfg.PrivateDomain)
page4Comps["PrivateMTU_input"] = NewTextInput("PrivateMTU", cfg.PrivateMTU)
page4Comps["next_btn"] = NewButton("下一步")
page4Comps["prev_btn"] = NewButton("上一步")
pageComponents[PageInternalNetwork] = page4Comps
@@ -199,11 +222,13 @@ func (m model) Init() tea.Cmd {
// Update 处理消息更新
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
m.quitting = true
m.saveConfig()
//m.quitting = true
return m, tea.Quit
// 1. 焦点切换Tab/Shift+Tab交给管理器处理
@@ -213,6 +238,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 2. 回车键:处理当前焦点组件的点击/确认
case "enter":
currentCompID := m.focusManager.currentFocusID
switch currentCompID {
// 页1accept → 进入页2
@@ -232,7 +258,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 页6确认配置 → 退出并保存
case "confirm_btn":
m.done = true
m.quitting = true
//m.quitting = true
return m, tea.Quit
case "cancel_btn":
@@ -258,18 +284,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 页2基础信息
case "Homepage_input":
m.config.HomePage = comp.Value()
case "Hostname_input":
m.config.Hostname = comp.Value()
case "ClusterName_input":
m.config.ClusterName = comp.Value()
case "Country_input":
m.config.Country = comp.Value()
case "Region_input":
m.config.Region = comp.Value()
case "State_input":
m.config.State = comp.Value()
case "City_input":
m.config.City = comp.Value()
case "Contact_input":
m.config.Contact = comp.Value()
case "Timezone_input":
m.config.Timezone = comp.Value()
case "DBPath_input":
m.config.DBAddress = comp.Value()
case "Software_input":
m.config.Software = comp.Value()
case "DistroDir_input":
m.config.DistroDir = comp.Value()
// 页3公网网络
case "PublicInterface_input":
@@ -280,14 +308,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.config.PublicNetmask = comp.Value()
case "PublicGateway_input":
m.config.PublicGateway = comp.Value()
case "PublicMTU_input":
m.config.PublicMTU = comp.Value()
// 页4内网网络
case "InternalInterface_input":
m.config.InternalInterface = comp.Value()
case "InternalIPAddress_input":
m.config.InternalIPAddress = comp.Value()
case "InternalNetmask_input":
m.config.InternalNetmask = comp.Value()
case "PrivateInterface_input":
m.config.PrivateInterface = comp.Value()
case "PrivateIPAddress_input":
m.config.PrivateIPAddress = comp.Value()
case "PrivateNetmask_input":
m.config.PrivateNetmask = comp.Value()
case "PrivateMTU_input":
m.config.PrivateMTU = comp.Value()
// 页5DNS
case "Pri_DNS_input":

View File

@@ -103,17 +103,19 @@ func renderDataInfoPage(m model) string {
split_line,
makeRow("Homepage", pageComps["Homepage_input"].View()),
split_line,
makeRow("Hostname", pageComps["Hostname_input"].View()),
makeRow("ClusterName", pageComps["ClusterName_input"].View()),
split_line,
makeRow("Country", pageComps["Country_input"].View()),
split_line,
makeRow("Region", pageComps["Region_input"].View()),
makeRow("State", pageComps["State_input"].View()),
split_line,
makeRow("City", pageComps["City_input"].View()),
split_line,
makeRow("Contact", pageComps["Contact_input"].View()),
split_line,
makeRow("Timezone", pageComps["Timezone_input"].View()),
split_line,
makeRow("DBPath", pageComps["DBPath_input"].View()),
split_line,
makeRow("Software", pageComps["Software_input"].View()),
makeRow("DistroDir", pageComps["DistroDir_input"].View()),
split_line,
)
@@ -143,6 +145,8 @@ func renderPublicNetworkPage(m model) string {
// 拼接公网网络表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("PublicHostname", pageComps["PublicHostname_input"].View()),
split_line,
makeRow("PublicInterface", pageComps["PublicInterface_input"].View()),
split_line,
@@ -152,6 +156,10 @@ func renderPublicNetworkPage(m model) string {
split_line,
makeRow("PublicGateway", pageComps["PublicGateway_input"].View()),
split_line,
makeRow("PublicDomain", pageComps["PublicDomain_input"].View()),
split_line,
makeRow("PublicMTU", pageComps["PublicMTU_input"].View()),
split_line,
)
// 按钮区域
@@ -186,11 +194,17 @@ func renderInternalNetworkPage(m model) string {
// 拼接内网网络表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("InternalInterface", pageComps["InternalInterface_input"].View()),
makeRow("PrivateHostname", pageComps["PrivateHostname_input"].View()),
split_line,
makeRow("InternalIPAddress", pageComps["InternalIPAddress_input"].View()),
makeRow("PrivateInterface", pageComps["PrivateInterface_input"].View()),
split_line,
makeRow("InternalNetmask", pageComps["InternalNetmask_input"].View()),
makeRow("PrivateIPAddress", pageComps["PrivateIPAddress_input"].View()),
split_line,
makeRow("PrivateNetmask", pageComps["PrivateNetmask_input"].View()),
split_line,
makeRow("PrivateDomain", pageComps["PrivateDomain_input"].View()),
split_line,
makeRow("PrivateMTU", pageComps["PrivateMTU_input"].View()),
split_line,
)
@@ -254,11 +268,15 @@ func renderSummaryPage(m model) string {
// 拼接 Summary 表单
formContent := lipgloss.JoinVertical(lipgloss.Center,
split_line,
makeRow("Hostname", m.config.Hostname),
makeRow("ClusterName", m.config.ClusterName),
split_line,
makeRow("Country", m.config.Country),
split_line,
makeRow("Region", m.config.Region),
makeRow("State", m.config.State),
split_line,
makeRow("City", m.config.City),
split_line,
makeRow("Contact", m.config.Contact),
split_line,
makeRow("Timezone", m.config.Timezone),
split_line,
@@ -266,7 +284,7 @@ func renderSummaryPage(m model) string {
split_line,
makeRow("DBPath", m.config.DBAddress),
split_line,
makeRow("Software", m.config.Software),
makeRow("DistroDir", m.config.DistroDir),
split_line,
makeRow("PublicInterface", m.config.PublicInterface),
split_line,
@@ -276,11 +294,13 @@ func renderSummaryPage(m model) string {
split_line,
makeRow("PublicGateway", m.config.PublicGateway),
split_line,
makeRow("InternalInterface", m.config.InternalInterface),
makeRow("PrivateInterface", m.config.PrivateInterface),
split_line,
makeRow("InternalIPAddress", m.config.InternalIPAddress),
makeRow("PrivateIPAddress", m.config.PrivateIPAddress),
split_line,
makeRow("InternalNetmask", m.config.InternalNetmask),
makeRow("PrivateNetmask", m.config.PrivateNetmask),
split_line,
makeRow("PrivateMTU", m.config.PrivateMTU),
split_line,
makeRow("Pri DNS", m.config.DNSPrimary),
split_line,