mirror of
				https://github.com/TeaOSLab/EdgeAdmin.git
				synced 2025-11-04 05:00:25 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			417 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			417 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package setup
 | 
						||
 | 
						||
import (
 | 
						||
	"bytes"
 | 
						||
	"encoding/json"
 | 
						||
	"github.com/TeaOSLab/EdgeAdmin/internal/configs"
 | 
						||
	"github.com/TeaOSLab/EdgeAdmin/internal/nodes"
 | 
						||
	"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
 | 
						||
	"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
 | 
						||
	"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
 | 
						||
	"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
 | 
						||
	"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
 | 
						||
	"github.com/iwind/TeaGo/Tea"
 | 
						||
	"github.com/iwind/TeaGo/actions"
 | 
						||
	"github.com/iwind/TeaGo/dbs"
 | 
						||
	"github.com/iwind/TeaGo/logs"
 | 
						||
	"github.com/iwind/TeaGo/maps"
 | 
						||
	"github.com/iwind/gosock/pkg/gosock"
 | 
						||
	"gopkg.in/yaml.v3"
 | 
						||
	"io"
 | 
						||
	"os"
 | 
						||
	"os/exec"
 | 
						||
	"strings"
 | 
						||
	"time"
 | 
						||
)
 | 
						||
 | 
						||
type InstallAction struct {
 | 
						||
	actionutils.ParentAction
 | 
						||
 | 
						||
	apiSetupFinished bool
 | 
						||
}
 | 
						||
 | 
						||
func (this *InstallAction) RunPost(params struct {
 | 
						||
	ApiNodeJSON []byte
 | 
						||
	DbJSON      []byte
 | 
						||
	AdminJSON   []byte
 | 
						||
 | 
						||
	Must *actions.Must
 | 
						||
}) {
 | 
						||
	currentStatusText = ""
 | 
						||
	defer func() {
 | 
						||
		currentStatusText = ""
 | 
						||
	}()
 | 
						||
 | 
						||
	// API节点配置
 | 
						||
	currentStatusText = "正在检查API节点配置"
 | 
						||
	var apiNodeMap = maps.Map{}
 | 
						||
	err := json.Unmarshal(params.ApiNodeJSON, &apiNodeMap)
 | 
						||
	if err != nil {
 | 
						||
		this.Fail("API节点配置数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
 | 
						||
	}
 | 
						||
 | 
						||
	// 数据库
 | 
						||
	currentStatusText = "正在检查数据库配置"
 | 
						||
	var dbMap = maps.Map{}
 | 
						||
	err = json.Unmarshal(params.DbJSON, &dbMap)
 | 
						||
	if err != nil {
 | 
						||
		this.Fail("数据库配置数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
 | 
						||
	}
 | 
						||
 | 
						||
	// 管理员
 | 
						||
	currentStatusText = "正在检查管理员配置"
 | 
						||
	var adminMap = maps.Map{}
 | 
						||
	err = json.Unmarshal(params.AdminJSON, &adminMap)
 | 
						||
	if err != nil {
 | 
						||
		this.Fail("管理员数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
 | 
						||
	}
 | 
						||
 | 
						||
	// 安装API节点
 | 
						||
	var mode = apiNodeMap.GetString("mode")
 | 
						||
	if mode == "new" {
 | 
						||
		currentStatusText = "准备启动新API节点"
 | 
						||
 | 
						||
		// 整个系统目录结构为:
 | 
						||
		//  edge-admin/
 | 
						||
		//    edge-api/
 | 
						||
		//    bin/
 | 
						||
		//    ...
 | 
						||
 | 
						||
		// 检查环境
 | 
						||
		var apiNodeDir = Tea.Root + "/edge-api"
 | 
						||
		for _, dir := range []string{"edge-api", "edge-api/configs", "edge-api/bin"} {
 | 
						||
			apiNodeDir := Tea.Root + "/" + dir
 | 
						||
			_, err = os.Stat(apiNodeDir)
 | 
						||
			if err != nil {
 | 
						||
				if os.IsNotExist(err) {
 | 
						||
					this.Fail("在当前目录(" + Tea.Root + ")下找不到" + dir + "目录,请将" + dir + "目录上传或者重新下载解压")
 | 
						||
				}
 | 
						||
				this.Fail("无法检查" + dir + "目录,发生错误:" + err.Error())
 | 
						||
			}
 | 
						||
		}
 | 
						||
 | 
						||
		// 保存数据库配置
 | 
						||
		var dsn = dbMap.GetString("username") + ":" + dbMap.GetString("password") + "@tcp(" + configutils.QuoteIP(dbMap.GetString("host")) + ":" + dbMap.GetString("port") + ")/" + dbMap.GetString("database") + "?charset=utf8mb4&timeout=30s"
 | 
						||
		dbConfig := &dbs.Config{
 | 
						||
			DBs: map[string]*dbs.DBConfig{
 | 
						||
				"prod": {
 | 
						||
					Driver: "mysql",
 | 
						||
					Dsn:    dsn,
 | 
						||
					Prefix: "edge",
 | 
						||
				}},
 | 
						||
		}
 | 
						||
		dbConfig.Default.DB = "prod"
 | 
						||
		dbConfigData, err := yaml.Marshal(dbConfig)
 | 
						||
		if err != nil {
 | 
						||
			this.Fail("生成数据库配置失败:" + err.Error())
 | 
						||
		}
 | 
						||
		err = os.WriteFile(apiNodeDir+"/configs/db.yaml", dbConfigData, 0666)
 | 
						||
		if err != nil {
 | 
						||
			this.Fail("保存数据库配置失败:" + err.Error())
 | 
						||
		}
 | 
						||
 | 
						||
		// 生成备份文件
 | 
						||
		homeDir, _ := os.UserHomeDir()
 | 
						||
		var backupDirs = []string{"/etc/edge-api"}
 | 
						||
		if len(homeDir) > 0 {
 | 
						||
			backupDirs = append(backupDirs, homeDir+"/.edge-api")
 | 
						||
		}
 | 
						||
		for _, backupDir := range backupDirs {
 | 
						||
			stat, err := os.Stat(backupDir)
 | 
						||
			if err == nil && stat.IsDir() {
 | 
						||
				_ = os.WriteFile(backupDir+"/db.yaml", dbConfigData, 0666)
 | 
						||
			} else if err != nil && os.IsNotExist(err) {
 | 
						||
				err = os.Mkdir(backupDir, 0777)
 | 
						||
				if err == nil {
 | 
						||
					_ = os.WriteFile(backupDir+"/db.yaml", dbConfigData, 0666)
 | 
						||
				}
 | 
						||
			}
 | 
						||
		}
 | 
						||
 | 
						||
		err = os.WriteFile(Tea.ConfigFile("/api_db.yaml"), dbConfigData, 0666)
 | 
						||
		if err != nil {
 | 
						||
			this.Fail("保存数据库配置失败:" + err.Error())
 | 
						||
		}
 | 
						||
 | 
						||
		// 生成备份文件
 | 
						||
		backupDirs = []string{"/etc/edge-admin"}
 | 
						||
		if len(homeDir) > 0 {
 | 
						||
			backupDirs = append(backupDirs, homeDir+"/.edge-admin")
 | 
						||
		}
 | 
						||
		for _, backupDir := range backupDirs {
 | 
						||
			stat, err := os.Stat(backupDir)
 | 
						||
			if err == nil && stat.IsDir() {
 | 
						||
				_ = os.WriteFile(backupDir+"/api_db.yaml", dbConfigData, 0666)
 | 
						||
			} else if err != nil && os.IsNotExist(err) {
 | 
						||
				err = os.Mkdir(backupDir, 0777)
 | 
						||
				if err == nil {
 | 
						||
					_ = os.WriteFile(backupDir+"/api_db.yaml", dbConfigData, 0666)
 | 
						||
				}
 | 
						||
			}
 | 
						||
		}
 | 
						||
 | 
						||
		// 开始安装
 | 
						||
		currentStatusText = "正在安装数据库表结构并写入数据"
 | 
						||
		var resultMap = maps.Map{}
 | 
						||
		logs.Println("[INSTALL]setup edge-api")
 | 
						||
		{
 | 
						||
			this.apiSetupFinished = false
 | 
						||
			var cmd = exec.Command(apiNodeDir+"/bin/edge-api", "setup", "-api-node-protocol=http", "-api-node-host=\""+apiNodeMap.GetString("newHost")+"\"", "-api-node-port=\""+apiNodeMap.GetString("newPort")+"\"")
 | 
						||
			var output = bytes.NewBuffer([]byte{})
 | 
						||
			cmd.Stdout = output
 | 
						||
 | 
						||
			// 试图读取执行日志
 | 
						||
			go this.startReadingAPIInstallLog()
 | 
						||
 | 
						||
			err = cmd.Run()
 | 
						||
			this.apiSetupFinished = true
 | 
						||
			if err != nil {
 | 
						||
				this.Fail("安装失败:" + err.Error())
 | 
						||
			}
 | 
						||
 | 
						||
			var resultData = output.Bytes()
 | 
						||
			err = json.Unmarshal(resultData, &resultMap)
 | 
						||
			if err != nil {
 | 
						||
				this.Fail("安装节点时返回数据错误:" + err.Error() + "(" + string(resultData) + ")")
 | 
						||
			}
 | 
						||
			if !resultMap.GetBool("isOk") {
 | 
						||
				this.Fail("节点安装错误:" + resultMap.GetString("error"))
 | 
						||
			}
 | 
						||
 | 
						||
			// 等数据完全写入
 | 
						||
			time.Sleep(1 * time.Second)
 | 
						||
		}
 | 
						||
 | 
						||
		// 关闭正在运行的API节点,防止冲突
 | 
						||
		logs.Println("[INSTALL]stop edge-api")
 | 
						||
		{
 | 
						||
			var cmd = exec.Command(apiNodeDir+"/bin/edge-api", "stop")
 | 
						||
			_ = cmd.Run()
 | 
						||
		}
 | 
						||
 | 
						||
		// 启动API节点
 | 
						||
		currentStatusText = "正在启动API节点"
 | 
						||
		logs.Println("[INSTALL]start edge-api")
 | 
						||
		{
 | 
						||
			var cmd = exec.Command(apiNodeDir + "/bin/edge-api")
 | 
						||
			err = cmd.Start()
 | 
						||
			if err != nil {
 | 
						||
				this.Fail("API节点启动失败:" + err.Error())
 | 
						||
			}
 | 
						||
 | 
						||
			// 记录子PID方便退出的时候一起退出
 | 
						||
			nodes.SharedAdminNode.AddSubPID(cmd.Process.Pid)
 | 
						||
 | 
						||
			// 等待API节点初始化完成
 | 
						||
			currentStatusText = "正在等待API节点启动完毕"
 | 
						||
			var apiNodeSock = gosock.NewTmpSock("edge-api")
 | 
						||
			var maxRetries = 5
 | 
						||
			for {
 | 
						||
				reply, err := apiNodeSock.SendTimeout(&gosock.Command{
 | 
						||
					Code: "starting",
 | 
						||
				}, 3*time.Second)
 | 
						||
				if err != nil {
 | 
						||
					if maxRetries < 0 {
 | 
						||
						this.Fail("API节点启动失败,请查看运行日志检查是否正常")
 | 
						||
					} else {
 | 
						||
						time.Sleep(3 * time.Second)
 | 
						||
						maxRetries--
 | 
						||
					}
 | 
						||
				} else {
 | 
						||
					if !maps.NewMap(reply.Params).GetBool("isStarting") {
 | 
						||
						currentStatusText = "API节点启动完毕"
 | 
						||
						break
 | 
						||
					}
 | 
						||
 | 
						||
					// 继续等待完成
 | 
						||
					time.Sleep(3 * time.Second)
 | 
						||
				}
 | 
						||
			}
 | 
						||
		}
 | 
						||
 | 
						||
		// 写入API节点配置,完成安装
 | 
						||
		var apiConfig = &configs.APIConfig{
 | 
						||
			RPCEndpoints: []string{"http://" + configutils.QuoteIP(apiNodeMap.GetString("newHost")) + ":" + apiNodeMap.GetString("newPort")},
 | 
						||
			NodeId:       resultMap.GetString("adminNodeId"),
 | 
						||
			Secret:       resultMap.GetString("adminNodeSecret"),
 | 
						||
		}
 | 
						||
 | 
						||
		// 设置管理员
 | 
						||
		currentStatusText = "正在设置管理员"
 | 
						||
		client, err := rpc.NewRPCClient(apiConfig, false)
 | 
						||
		if err != nil {
 | 
						||
			this.FailField("oldHost", "测试API节点时出错,请检查配置,错误信息:"+err.Error())
 | 
						||
		}
 | 
						||
		ctx := client.Context(0)
 | 
						||
		for i := 0; i < 3; i++ {
 | 
						||
			_, err = client.AdminRPC().CreateOrUpdateAdmin(ctx, &pb.CreateOrUpdateAdminRequest{
 | 
						||
				Username: adminMap.GetString("username"),
 | 
						||
				Password: adminMap.GetString("password"),
 | 
						||
			})
 | 
						||
			// 这里我们尝试多次是为了等待API节点启动完毕
 | 
						||
			if err != nil {
 | 
						||
				time.Sleep(1 * time.Second)
 | 
						||
			} else {
 | 
						||
				break
 | 
						||
			}
 | 
						||
		}
 | 
						||
		if err != nil {
 | 
						||
			this.Fail("设置管理员账号出错:" + err.Error())
 | 
						||
		}
 | 
						||
 | 
						||
		// 设置访问日志保留天数
 | 
						||
		currentStatusText = "正在配置访问日志保留天数"
 | 
						||
		var accessLogKeepDays = dbMap.GetInt("accessLogKeepDays")
 | 
						||
		if accessLogKeepDays > 0 {
 | 
						||
			var config = systemconfigs.NewDatabaseConfig()
 | 
						||
			config.ServerAccessLog.Clean.Days = accessLogKeepDays
 | 
						||
			configJSON, err := json.Marshal(config)
 | 
						||
			if err != nil {
 | 
						||
				this.Fail("配置设置访问日志保留天数出错:" + err.Error())
 | 
						||
				return
 | 
						||
			}
 | 
						||
			_, err = client.SysSettingRPC().UpdateSysSetting(ctx, &pb.UpdateSysSettingRequest{
 | 
						||
				Code:      systemconfigs.SettingCodeDatabaseConfigSetting,
 | 
						||
				ValueJSON: configJSON,
 | 
						||
			})
 | 
						||
			if err != nil {
 | 
						||
				this.Fail("配置设置访问日志保留天数出错:" + err.Error())
 | 
						||
				return
 | 
						||
			}
 | 
						||
		}
 | 
						||
 | 
						||
		err = apiConfig.WriteFile(Tea.ConfigFile(configs.ConfigFileName))
 | 
						||
		if err != nil {
 | 
						||
			this.Fail("保存配置失败,原因:" + err.Error())
 | 
						||
		}
 | 
						||
 | 
						||
		this.Success()
 | 
						||
	} else if mode == "old" {
 | 
						||
		// 构造RPC
 | 
						||
		var apiConfig = &configs.APIConfig{
 | 
						||
			RPCEndpoints: []string{apiNodeMap.GetString("oldProtocol") + "://" + configutils.QuoteIP(apiNodeMap.GetString("oldHost")) + ":" + apiNodeMap.GetString("oldPort")},
 | 
						||
			NodeId:       apiNodeMap.GetString("oldNodeId"),
 | 
						||
			Secret:       apiNodeMap.GetString("oldNodeSecret"),
 | 
						||
		}
 | 
						||
		client, err := rpc.NewRPCClient(apiConfig, false)
 | 
						||
		if err != nil {
 | 
						||
			this.FailField("oldHost", "测试API节点时出错,请检查配置,错误信息:"+err.Error())
 | 
						||
		}
 | 
						||
 | 
						||
		defer func() {
 | 
						||
			_ = client.Close()
 | 
						||
		}()
 | 
						||
 | 
						||
		// 设置管理员
 | 
						||
		var ctx = client.APIContext(0)
 | 
						||
		_, err = client.AdminRPC().CreateOrUpdateAdmin(ctx, &pb.CreateOrUpdateAdminRequest{
 | 
						||
			Username: adminMap.GetString("username"),
 | 
						||
			Password: adminMap.GetString("password"),
 | 
						||
		})
 | 
						||
		if err != nil {
 | 
						||
			this.Fail("设置管理员账号出错:" + err.Error())
 | 
						||
		}
 | 
						||
 | 
						||
		// 设置访问日志保留天数
 | 
						||
		var accessLogKeepDays = dbMap.GetInt("accessLogKeepDays")
 | 
						||
		if accessLogKeepDays > 0 {
 | 
						||
			var config = systemconfigs.NewDatabaseConfig()
 | 
						||
			config.ServerAccessLog.Clean.Days = accessLogKeepDays
 | 
						||
			configJSON, err := json.Marshal(config)
 | 
						||
			if err != nil {
 | 
						||
				this.Fail("配置设置访问日志保留天数出错:" + err.Error())
 | 
						||
				return
 | 
						||
			}
 | 
						||
			_, err = client.SysSettingRPC().UpdateSysSetting(ctx, &pb.UpdateSysSettingRequest{
 | 
						||
				Code:      systemconfigs.SettingCodeDatabaseConfigSetting,
 | 
						||
				ValueJSON: configJSON,
 | 
						||
			})
 | 
						||
			if err != nil {
 | 
						||
				this.Fail("配置设置访问日志保留天数出错:" + err.Error())
 | 
						||
				return
 | 
						||
			}
 | 
						||
		}
 | 
						||
 | 
						||
		// 写入API节点配置,完成安装
 | 
						||
		err = apiConfig.WriteFile(Tea.ConfigFile(configs.ConfigFileName))
 | 
						||
		if err != nil {
 | 
						||
			this.Fail("保存配置失败,原因:" + err.Error())
 | 
						||
		}
 | 
						||
 | 
						||
		// 成功
 | 
						||
		this.Success()
 | 
						||
	} else {
 | 
						||
		this.Fail("错误的API节点模式:'" + mode + "'")
 | 
						||
	}
 | 
						||
}
 | 
						||
 | 
						||
// 读取API安装时的日志,以便于显示当前正在执行的任务
 | 
						||
func (this *InstallAction) startReadingAPIInstallLog() {
 | 
						||
	var tmpDir = os.TempDir()
 | 
						||
	if len(tmpDir) == 0 {
 | 
						||
		return
 | 
						||
	}
 | 
						||
	var logFile = tmpDir + "/edge-install.log"
 | 
						||
 | 
						||
	var logFp *os.File
 | 
						||
	var err error
 | 
						||
 | 
						||
	// 尝试5秒钟
 | 
						||
	for i := 0; i < 10; i++ {
 | 
						||
		logFp, err = os.Open(logFile)
 | 
						||
		if err != nil {
 | 
						||
			time.Sleep(1 * time.Second)
 | 
						||
			continue
 | 
						||
		} else {
 | 
						||
			break
 | 
						||
		}
 | 
						||
	}
 | 
						||
	if err != nil {
 | 
						||
		return
 | 
						||
	}
 | 
						||
 | 
						||
	if this.apiSetupFinished {
 | 
						||
		_ = logFp.Close()
 | 
						||
		return
 | 
						||
	}
 | 
						||
 | 
						||
	go func() {
 | 
						||
		defer func() {
 | 
						||
			_ = logFp.Close()
 | 
						||
		}()
 | 
						||
 | 
						||
		var ticker = time.NewTicker(1 * time.Second)
 | 
						||
		var logBuf = make([]byte, 256)
 | 
						||
		for range ticker.C {
 | 
						||
			if this.apiSetupFinished {
 | 
						||
				return
 | 
						||
			}
 | 
						||
 | 
						||
			_, err = logFp.Seek(-256, io.SeekEnd)
 | 
						||
			if err != nil {
 | 
						||
				currentStatusText = ""
 | 
						||
				return
 | 
						||
			}
 | 
						||
 | 
						||
			n, err := logFp.Read(logBuf)
 | 
						||
			if err != nil {
 | 
						||
				currentStatusText = ""
 | 
						||
				return
 | 
						||
			}
 | 
						||
 | 
						||
			if n > 0 {
 | 
						||
				var logData = string(logBuf[:n])
 | 
						||
				var lines = strings.Split(logData, "\n")
 | 
						||
				if len(lines) >= 3 {
 | 
						||
					var line = strings.TrimSpace(lines[len(lines)-2])
 | 
						||
					if len(line) > 0 {
 | 
						||
						if !this.apiSetupFinished {
 | 
						||
							currentStatusText = "正在执行 " + line + " ..."
 | 
						||
						}
 | 
						||
					}
 | 
						||
				}
 | 
						||
			}
 | 
						||
		}
 | 
						||
	}()
 | 
						||
}
 |