diff --git a/pkg/database/database.go b/pkg/database/database.go index ccfd4bf..f8efa70 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -3,6 +3,7 @@ package database import ( "database/sql" "fmt" + "strings" "sync" "sunhpc/pkg/config" @@ -22,22 +23,20 @@ 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) + fullSQL := ReplaceSQLQuery(query, 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) + logger.Debugf("查询语句: %s , CatName=%s, ID=%d", fullSQL, catname, id) return id, nil } @@ -49,14 +48,15 @@ func MapCategoryIndex(conn *sql.DB, catindexName, categoryIndex string) (int, er select index_id from vmapCategoryIndex where categoryName = ? and categoryIndex = ? ` - logger.Debugf("查询SQL: %s", query) - logger.Debugf("查询索引ID: %s, 类别: %s", catindexName, categoryIndex) + fullSQL := ReplaceSQLQuery(query, 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) + logger.Debugf("查询语句: %s , CatIndexName=%s, CategoryIndex=%s, ID=%d", + fullSQL, catindexName, categoryIndex, id) return id, nil } @@ -165,6 +165,13 @@ func GetDB() (*sql.DB, error) { return } + var version string + err = sqlDB.QueryRow("select sqlite_version()").Scan(&version) + if err != nil { + version = "unknown" + } + logger.Debugf("数据库版本: %s", version) + logger.Debug("数据库连接成功") dbInstance = sqlDB }) @@ -178,10 +185,44 @@ func GetDB() (*sql.DB, error) { func InitTables(db *sql.DB, force bool) error { + // 临时关闭外键约束(解决外键依赖删除报错问题) + _, err := db.Exec("PRAGMA foreign_keys = OFF;") + if err != nil { + logger.Errorf("关闭外键约束失败: %v", err) + return err + } + defer func() { + // 延迟恢复外键约束(确保在函数退出时恢复) + _, err := db.Exec("PRAGMA foreign_keys = ON;") + if err != nil { + logger.Errorf("恢复外键约束失败: %v", err) + } + }() + // ✅ 调用 schema.go 中的函数 - //for _, ddl := range CreateTableStatements() { - for _, ddl := range BaseTables() { - logger.Debugf("执行: %s", ddl) + for name, ddl := range BaseTables() { + // 删除表或者试图(如果存在) + logger.Debugf("执行删除 - %s", name) + + // 先尝试作为表进行删除 + query := fmt.Sprintf("DROP TABLE IF EXISTS %s;", name) + logger.Debugf("执行语句: %s", query) + + _, err := db.Exec(query) + if err != nil { + // 如果作为表删除失败,尝试作为试图删除 + logger.Debugf("删除表失败: %v", err) + query = fmt.Sprintf("DROP VIEW IF EXISTS %s;", name) + logger.Debugf("执行语句: %s", query) + + _, err = db.Exec(query) + if err != nil { + return fmt.Errorf("删除失败: %w", err) + } + } + + logger.Debugf("执行图表 - %s", name) + logger.Debugf("执行语句: %s", ddl) if _, err := db.Exec(ddl); err != nil { return fmt.Errorf("数据表创建失败: %w", err) } @@ -291,28 +332,36 @@ func ExecWithTransaction(ddl []string) error { return err } + var finished bool + // 延迟处理:如果函数异常,回滚事务 defer func() { if r := recover(); r != nil { - // 捕获 panic 并回滚事务 - tx.Rollback() - logger.Errorf("事务执行中发生 panic: %v", r) + if !finished { + // 捕获 panic 并回滚事务 + tx.Rollback() + logger.Errorf("事务执行中发生 panic: %v", r) + } + panic(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() + finished = true // 标记事务已完成 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) + return fmt.Errorf("执行 %d 条, 失败: %w (SQL: %s)", idx+1, err, sql) } } @@ -323,6 +372,21 @@ func ExecWithTransaction(ddl []string) error { return err } + finished = true // 标记事务已完成 logger.Debugf("成功执行 %d 条 SQL 语句, 事务已提交.", len(ddl)) return nil } + +func ReplaceSQLQuery(query string, args ...interface{}) string { + for _, arg := range args { + switch v := arg.(type) { + case string: + query = strings.Replace(query, "?", fmt.Sprintf("'%s'", v), 1) + case int, int64, float64: + query = strings.Replace(query, "?", fmt.Sprintf("%v", v), 1) + default: + query = strings.Replace(query, "?", fmt.Sprintf("%v", v), 1) + } + } + return strings.TrimSpace(strings.ReplaceAll(query, "\n", " ")) +} diff --git a/pkg/database/schema.go b/pkg/database/schema.go index aeea9e6..ce8c582 100644 --- a/pkg/database/schema.go +++ b/pkg/database/schema.go @@ -4,473 +4,412 @@ package database import ( "database/sql" "fmt" + "sunhpc/pkg/logger" ) -func BaseTables() []string { +func BaseTables() map[string]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 + return map[string]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' + ); + `, + "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' + ); + `, + "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) + ); + `, + "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 + ); + `, + "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 + ); + `, + "nodes": ` + CREATE TABLE IF NOT EXISTS nodes ( + ID integer primary key autoincrement, + Name varchar default null, + Membership integer default '2', + CPUs integer not null default '1', + Rack varchar default null, + Rank integer default null, + Arch varchar default null, + OS varchar default null, + RunAction varchar(64) default 'os', + InstallAction varchar(64) default 'install' + ); + create index if not exists idx_nodes_name on nodes(Name); + `, + "aliases": ` + CREATE TABLE IF NOT EXISTS aliases ( + ID integer primary key autoincrement, + Node integer not null default '0', + Name varchar default null, + Foreign key(Node) references nodes(ID) on delete cascade + ); + create index if not exists idx_aliases_name on aliases(Name); + `, + "networks": ` + CREATE TABLE IF NOT EXISTS networks ( + ID integer primary key autoincrement, + Node integer not null default '0', + MAC varchar default null, + IP varchar default null, + Name varchar default null, + Device varchar default null, + Subnet integer default null, + Module varchar default null, + VlanID integer default null, + Options varchar default null, + Channel varchar default null, + Foreign key(Node) references nodes(ID) on delete cascade, + Foreign key(Subnet) references subnets(ID) on delete cascade + ); + create index if not exists idx_networks_name on networks(Name); + `, + "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 default null, + Primary key(Network, Netmask) + Foreign key(Subnet) references subnets(ID) on delete cascade + ); + `, + "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 default null, + Primary key(OS, Network, Netmask) + Foreign key(Subnet) references subnets(ID) on delete cascade + ); + `, + "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 default null, + Primary key(Appliance, Network, Netmask) + Foreign key(Subnet) references subnets(ID) on delete cascade + ); + `, + "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 default null, + Primary key(Node, Network, Netmask) + Foreign key(Subnet) references subnets(ID) on delete cascade + ); + `, + "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 + ); + `, + "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 + ); + `, + "secglobal": ` + CREATE TABLE IF NOT EXISTS secglobal ( + Attr varchar(128) default null, + Value text, + Enc varchar(128) default null, + Primary key(Attr) + ); + `, + "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) + ); + `, + "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 + ); + `, + "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 '' + ); + `, + "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 + ); + `, + "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')) + ); + `, + "noderolls": ` + CREATE TABLE IF NOT EXISTS noderolls ( + Node varchar(11) not null default '0', + RollID varchar(11) not null, + Primary key(Node, RollID) + ); + `, + "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 + ); + `, + "bootflags": ` + CREATE TABLE IF NOT EXISTS bootflags ( + ID integer primary key autoincrement, + Node integer(11) not null default '0', + Flags varchar(256) default null + ); + `, + "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 '' + ); + `, + "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只保留满足条件的行 */ + ; + `, + "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) + ; + `, + "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 + ; + `, + "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 + ; + `, + "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 + ; + `, + "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 + ; + `, + "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 + ; + `, + "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 + ; + `, + } } func InitBaseData(conn *sql.DB) error { + logger.Debug("初始化基础数据...") // ========== 第一步:插入 categories 数据 ========== categoryData := []struct { Name string @@ -484,15 +423,26 @@ func InitBaseData(conn *sql.DB) error { } // 批量插入 categories (忽略重复) + logger.Debug("插入 categories 数据...") for _, cd := range categoryData { query := ` insert or ignore into categories (Name, Description) values (?, ?) ` - _, err := conn.Exec(query, cd.Name, cd.Description) + fullSQL := ReplaceSQLQuery(query, cd.Name, cd.Description) + logger.Debugf("执行语句: %s", fullSQL) + + // 执行 SQL 语句(仍用占位符,避免SQL注入) + result, err := conn.Exec(query, cd.Name, cd.Description) if err != nil { return fmt.Errorf("error inserting category %s: %w", cd.Name, err) } + + id, err := result.LastInsertId() + if err != nil { + return fmt.Errorf("error getting last insert ID: %w", err) + } + logger.Debugf("执行语句: %s, 插入ID: %d", fullSQL, id) } // ========== 第二步:插入 catindex 数据 ========== @@ -511,7 +461,9 @@ func InitBaseData(conn *sql.DB) error { {"devel-server", "appliance"}, {"login", "appliance"}, } + // 批量插入 catindex (忽略重复) + logger.Debug("插入 Catindex 数据...") for _, ci := range catindexData { // 动态获取类别ID (复用MapCategory函数) catID, err := MapCategory(conn, ci.Category) @@ -527,10 +479,19 @@ func InitBaseData(conn *sql.DB) error { insert or ignore into catindex (Name, Category) values (?, ?) ` - _, err = conn.Exec(query, ci.Name, catID) + fullSQL := ReplaceSQLQuery(query, ci.Name, catID) + + // 执行 SQL 语句(仍用占位符,避免SQL注入) + result, err := conn.Exec(query, ci.Name, catID) if err != nil { return fmt.Errorf("error inserting catindex %s: %w", ci.Name, err) } + + id, err := result.LastInsertId() + if err != nil { + return fmt.Errorf("error getting last insert ID: %w", err) + } + logger.Debugf("执行语句: %s, 插入ID: %d", fullSQL, id) } // ========== 第三步:插入 resolvechain 数据 ========== @@ -546,6 +507,7 @@ func InitBaseData(conn *sql.DB) error { {"default", "host", 50}, } // 批量插入 resolvechain (忽略重复) + logger.Debugf("插入 resolvechain 数据...") for _, rcd := range resolveChainData { // 动态获取类别ID (复用MapCategory函数) catID, err := MapCategory(conn, rcd.Category) @@ -561,10 +523,19 @@ func InitBaseData(conn *sql.DB) error { insert or ignore into resolvechain (Name, Category, Precedence) values (?, ?, ?) ` - _, err = conn.Exec(query, rcd.Name, catID, rcd.Precedence) + fullSQL := ReplaceSQLQuery(query, rcd.Name, catID, rcd.Precedence) + + // 执行 SQL 语句(仍用占位符,避免SQL注入) + result, err := conn.Exec(query, rcd.Name, catID, rcd.Precedence) if err != nil { return fmt.Errorf("error inserting resolvechain %s: %w", rcd.Name, err) } + + id, err := result.LastInsertId() + if err != nil { + return fmt.Errorf("error getting last insert ID: %w", err) + } + logger.Debugf("执行语句: %s, 插入ID: %d", fullSQL, id) } return nil diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 98681fb..b95cae5 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -80,6 +80,72 @@ type LogConfig struct { ShowColor bool `mapstructure:"show_color" yaml:"show_color"` } +type LevelFilterWriter struct { + writer io.Writer + maxLevel logrus.Level // 控制台: 只输出 <= 该级别 + minLevel logrus.Level // 文件: 只输出 >= 该级别 + isConsole bool // 是否是控制台输出 +} + +// Write 实现io.Writer接口,核心过滤逻辑 +func (f *LevelFilterWriter) Write(p []byte) (n int, err error) { + // 解析日志级别(适配logrus默认格式和CustomFormatter) + logLevel := parseLogLevelFromContent(p) + + // 控制台:只输出 Info 及以下级别(Trace/Debug/Info) + if f.isConsole { + if logLevel <= f.maxLevel { + return f.writer.Write(p) + } + return len(p), nil // 过滤掉,返回长度避免Writer报错 + } + + // 文件:只输出 Warn 及以上级别(Warn/Error/Fatal/Panic) + if logLevel >= f.minLevel { + return f.writer.Write(p) + } + return len(p), nil +} + +// parseLogLevelFromContent 解析日志内容中的级别(兼容自定义格式) +func parseLogLevelFromContent(p []byte) logrus.Level { + content := string(p) + // 适配常见的级别关键字(兼容你的CustomFormatter) + switch { + case contains(content, "TRACE"): + return logrus.TraceLevel + case contains(content, "DEBUG"): + return logrus.DebugLevel + case contains(content, "INFO"): + return logrus.InfoLevel + case contains(content, "WARN") || contains(content, "WARNING"): + return logrus.WarnLevel + case contains(content, "ERROR"): + return logrus.ErrorLevel + case contains(content, "FATAL"): + return logrus.FatalLevel + case contains(content, "PANIC"): + return logrus.PanicLevel + default: + return logrus.InfoLevel // 解析失败默认Info级别 + } +} + +// contains 辅助函数:判断字符串是否包含子串 +func contains(s, substr string) bool { + return len(s) >= len(substr) && indexOf(s, substr) != -1 +} + +// indexOf 简易字符串查找(避免依赖额外库) +func indexOf(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} + // 默认配置 var defaultConfig = LogConfig{ Level: "info", @@ -216,34 +282,7 @@ func Init(cfg LogConfig) { // 1. 创建logrus实例 logrusInst := logrus.New() - // 2. 配置输出(控制台 + 文件,可选) - var outputs []io.Writer - outputs = append(outputs, os.Stdout) // 控制台输出 - - // 如果配置了日志文件,添加文件输出 - if cfg.LogFile != "" { - // 确保日志目录存在 - dir := filepath.Dir(cfg.LogFile) - if err := os.MkdirAll(dir, 0755); err != nil { - // 目录创建失败,只输出警告,不影响程序运行 - logrusInst.Warnf("创建日志目录失败: %v,仅输出到控制台", err) - } else { - file, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err == nil { - outputs = append(outputs, file) - } else { - logrusInst.Warnf("打开日志文件失败: %v,仅输出到控制台", err) - } - } - } - logrusInst.SetOutput(io.MultiWriter(outputs...)) - - // 3. 配置格式 - logrusInst.SetFormatter(&CustomFormatter{ - ShowColor: cfg.ShowColor, - }) - - // 4. 配置日志级别 + // 2. 先配置日志级别(总开关,必须在输出配置前) lvl, err := logrus.ParseLevel(cfg.Level) if err != nil { lvl = logrus.InfoLevel // 解析失败默认Info级别 @@ -257,6 +296,45 @@ func Init(cfg LogConfig) { // 启用文件行号(必须开启,否则getCallerInfo拿不到数据) logrusInst.SetReportCaller(true) + // 3. 配置输出(控制台 + 文件,可选) + var outputs []io.Writer + + // 控制台输出: 只输出 Info 及以下级别 + consoleWriter := &LevelFilterWriter{ + writer: os.Stdout, + minLevel: logrus.InfoLevel, + isConsole: true, + } + outputs = append(outputs, consoleWriter) + + // 如果配置了日志文件,添加文件输出 + if cfg.LogFile != "" { + // 确保日志目录存在 + dir := filepath.Dir(cfg.LogFile) + if err := os.MkdirAll(dir, 0755); err != nil { + // 目录创建失败,只输出警告,不影响程序运行 + logrusInst.Warnf("创建日志目录失败: %v,仅输出到控制台", err) + } else { + file, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err == nil { + fileWriter := &LevelFilterWriter{ + writer: file, + minLevel: logrus.WarnLevel, + isConsole: false, + } + outputs = append(outputs, fileWriter) + } else { + logrusInst.Warnf("打开日志文件失败: %v,仅输出到控制台", err) + } + } + } + logrusInst.SetOutput(io.MultiWriter(outputs...)) + + // 4. 配置格式 + logrusInst.SetFormatter(&CustomFormatter{ + ShowColor: cfg.ShowColor, + }) + // 5. 赋值给全局默认实例 DefaultLogger = &logrusLogger{logrusInst} }) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 95b4f42..576d883 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "crypto/rand" "encoding/hex" + "fmt" "os" "os/exec" "time" @@ -32,6 +33,29 @@ func GetTimestamp() string { return time.Now().Format("2006-01-02 15:04:05") } +func OutputMaps(maps map[string]string) []string { + + output := []string{} + maxLen := 0 + for key := range maps { + if len(key) > maxLen { + maxLen = len(key) + } + } + + // 使用动态宽度的格式化字符串输出 + // %-*s 的含义 + // %: 格式化开始 + // -: 左对齐,默认是右对齐 + // *: 表示宽度由后续参数指定(maxLen) + // s: 表示字符串类型 + + for key, value := range maps { + output = append(output, fmt.Sprintf("%-*s: %s", maxLen, key, value)) + } + return output +} + // 定义短语 const ( NoAvailableNetworkInterfaces = "No available network interfaces" diff --git a/pkg/wizard/config.go b/pkg/wizard/config.go index 094153b..b2924fd 100644 --- a/pkg/wizard/config.go +++ b/pkg/wizard/config.go @@ -201,7 +201,7 @@ func loadConfig() (*ConfigMapping, error) { 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) + logger.Debugf("Config file %s not exist, use default config", cfgfile) return configs, nil } // 其他错误,返回错误 @@ -209,7 +209,7 @@ func loadConfig() (*ConfigMapping, error) { return nil, err } - logger.Infof("Load config file %s success", cfgfile) + logger.Debugf("Load config file %s success", cfgfile) return configs, nil } @@ -310,7 +310,9 @@ func (m *model) saveConfig() error { return err } - logger.Debugf("Result: %v", result) + for _, value := range utils.OutputMaps(result) { + logger.Debugf("%s", value) + } return nil } @@ -433,15 +435,15 @@ func insertDataToDB(result map[string]string) error { 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');", + fmt.Sprintf("insert or replace 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');", + "insert or replace into bootactions values (2, 'os', 'localboot 0', '', '');", + "insert or replace into bootactions values (3, 'memtest', 'kernel memtest', '', '');", + fmt.Sprintf("insert or replace into bootactions values (4, 'install headless', '%s', '%s', '%s');", vmlinuz, initrds, lesArgs), - fmt.Sprintf("insert into bootactions values (5, 'rescue', '%s', '%s', '%s');", + fmt.Sprintf("insert or replace into bootactions values (5, 'rescue', '%s', '%s', '%s');", vmlinuz, initrds, resArgs), - "insert into bootactions values (6, 'pxeflash', 'kernel memdisk bigraw', 'pxeflash.img', 'keeppxe');", + "insert or replace into bootactions values (6, 'pxeflash', 'kernel memdisk bigraw', 'pxeflash.img', 'keeppxe');", } insertData = append(insertData, bootaction...) @@ -453,42 +455,41 @@ func insertDataToDB(result map[string]string) error { category := item.Category catindex := item.Catindex insertData = append(insertData, - fmt.Sprintf("insert into attributes values ('%s', '%s', '%s', %d, %d);", + fmt.Sprintf("insert or replace into attributes values (NULL, '%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');", + "insert or replace into nodes values (1, '%s', '2', '%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');", + `insert or replace 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');", + `insert or replace into subnets values (2, 'public', '%s', '%s', '%s', '%s', '0');`, result["public_domain"], result["public_network"], result["public_netmask"], result["public_mtu"]), + fmt.Sprintf( + `insert or replace into networks values (1, 1, '%s', '%s', '%s', '%s', '2', NULL, NULL,NULL,NULL);`, + result["public_mac"], + result["public_address"], + result["private_hostname"], + result["public_interface"]), + fmt.Sprintf( + `insert or replace into networks values (2, 1, '%s', '%s', '%s', '%s', '1', NULL, NULL, NULL, NULL);`, + result["private_mac"], + result["private_address"], + result["private_hostname"], + result["private_interface"]), } insertData = append(insertData, nodes...) diff --git a/pkg/wizard/model.go b/pkg/wizard/model.go index dc5c5a9..1f17814 100644 --- a/pkg/wizard/model.go +++ b/pkg/wizard/model.go @@ -227,7 +227,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": - m.saveConfig() //m.quitting = true return m, tea.Quit @@ -257,6 +256,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // 页6:确认配置 → 退出并保存 case "confirm_btn": + m.saveConfig() m.done = true //m.quitting = true return m, tea.Quit diff --git a/pkg/wizard/styles.go b/pkg/wizard/styles.go index 8dd2def..5ff24b6 100644 --- a/pkg/wizard/styles.go +++ b/pkg/wizard/styles.go @@ -24,11 +24,12 @@ var ( // 基础布局样式 appStyle = lipgloss.NewStyle(). Padding(1, 1). + MarginBottom(1). BorderStyle(lipgloss.RoundedBorder()). BorderForeground(primaryColor). Foreground(textColor). - Align(lipgloss.Center). - Height(40) + Align(lipgloss.Center) + //Height(40) // 标题样式 titleStyle = lipgloss.NewStyle().