Initial commit (code only without large binaries)
This commit is contained in:
39
EdgeAdmin/internal/web/actions/default/setup/checkLocalIP.go
Normal file
39
EdgeAdmin/internal/web/actions/default/setup/checkLocalIP.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"net"
|
||||
)
|
||||
|
||||
// CheckLocalIPAction 检查IP是否为局域网IP
|
||||
type CheckLocalIPAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *CheckLocalIPAction) RunPost(params struct {
|
||||
Host string
|
||||
}) {
|
||||
var ip = net.ParseIP(params.Host)
|
||||
if ip == nil {
|
||||
// 默认为true
|
||||
this.Data["isLocal"] = true
|
||||
this.Success()
|
||||
}
|
||||
|
||||
var ipObj = ip.To4()
|
||||
if ipObj == nil {
|
||||
ipObj = ip.To16()
|
||||
}
|
||||
if ipObj == nil {
|
||||
// 默认为true
|
||||
this.Data["isLocal"] = true
|
||||
this.Success()
|
||||
}
|
||||
|
||||
this.Data["isLocal"] = utils.IsLocalIP(ipObj)
|
||||
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package confirm
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type Helper struct {
|
||||
}
|
||||
|
||||
func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) {
|
||||
if !setup.IsNewInstalled() {
|
||||
actionPtr.Object().RedirectURL("/")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
144
EdgeAdmin/internal/web/actions/default/setup/confirm/index.go
Normal file
144
EdgeAdmin/internal/web/actions/default/setup/confirm/index.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package confirm
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/rpc"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
var endpoints = []string{}
|
||||
|
||||
config, err := configs.LoadAPIConfig()
|
||||
if err == nil {
|
||||
endpoints = config.RPCEndpoints
|
||||
this.Data["nodeId"] = config.NodeId
|
||||
this.Data["secret"] = config.Secret
|
||||
this.Data["canInstall"] = false
|
||||
} else {
|
||||
this.Data["nodeId"] = ""
|
||||
this.Data["secret"] = ""
|
||||
this.Data["canInstall"] = true
|
||||
}
|
||||
|
||||
if len(endpoints) == 0 {
|
||||
endpoints = []string{""} // 初始化一个空的
|
||||
}
|
||||
|
||||
this.Data["endpoints"] = endpoints
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunPost(params struct {
|
||||
Endpoints []string
|
||||
NodeId string
|
||||
Secret string
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
var endpoints = []string{}
|
||||
for _, endpoint := range params.Endpoints {
|
||||
if len(endpoint) > 0 {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
this.Fail("API节点地址'" + endpoint + "'格式错误")
|
||||
}
|
||||
endpoint = u.Scheme + "://" + u.Host
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
this.Fail("API节点地址'" + endpoint + "'中的协议错误,目前只支持http或者https")
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "http":
|
||||
if len(u.Port()) == 0 {
|
||||
endpoint += ":80"
|
||||
}
|
||||
case "https":
|
||||
if len(u.Port()) == 0 {
|
||||
endpoint += ":443"
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否连接
|
||||
var config = &configs.APIConfig{}
|
||||
config.NodeId = params.NodeId
|
||||
config.Secret = params.Secret
|
||||
config.RPCEndpoints = []string{endpoint}
|
||||
client, err := rpc.NewRPCClient(config, false)
|
||||
if err != nil {
|
||||
this.Fail("尝试配置RPC发生错误:" + err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := client.APINodeRPC().FindCurrentAPINodeVersion(client.Context(0), &pb.FindCurrentAPINodeVersionRequest{})
|
||||
if err != nil {
|
||||
_ = client.Close()
|
||||
|
||||
if strings.Contains(err.Error(), "wrong token role") {
|
||||
this.Fail("你输入的NodeId和Secret为其他节点的配置信息,不是管理系统的配置信息,所以无法使用;请从管理系统的配置目录下找到管理系统的配置信息并填入。如果你不知道如何查找,请刷新当前页面,使用默认填写的NodeId和Secret提交。")
|
||||
} else {
|
||||
this.Fail("无法连接你填入的API节点地址,请检查协议、IP和端口是否正确,错误信息:" + err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if resp != nil && resp.Role != "admin" {
|
||||
this.Fail("你输入的NodeId和Secret为API节点的配置信息,不是管理系统的配置信息,所以无法使用;请从管理系统的配置目录下找到管理系统的配置信息并填入")
|
||||
return
|
||||
}
|
||||
_ = client.Close()
|
||||
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
if len(endpoints) == 0 {
|
||||
this.Fail("请输入至少一个API节点地址")
|
||||
}
|
||||
|
||||
if len(params.NodeId) == 0 {
|
||||
this.Fail("请输入NodeId")
|
||||
}
|
||||
if len(params.Secret) == 0 {
|
||||
this.Fail("请输入Secret")
|
||||
}
|
||||
|
||||
// 创建配置文件
|
||||
config, err := configs.LoadAPIConfig()
|
||||
if err != nil {
|
||||
config = &configs.APIConfig{}
|
||||
}
|
||||
config.NodeId = params.NodeId
|
||||
config.Secret = params.Secret
|
||||
config.RPCEndpoints = endpoints
|
||||
config.RPCDisableUpdate = true
|
||||
err = config.WriteFile(Tea.ConfigFile(configs.ConfigFileName))
|
||||
if err != nil {
|
||||
this.Fail("配置保存失败:" + err.Error())
|
||||
}
|
||||
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
this.Fail("RPC配置无法读取:" + err.Error())
|
||||
}
|
||||
err = rpcClient.UpdateConfig(config)
|
||||
if err != nil {
|
||||
this.Fail("重载RPC配置失败:" + err.Error())
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
13
EdgeAdmin/internal/web/actions/default/setup/confirm/init.go
Normal file
13
EdgeAdmin/internal/web/actions/default/setup/confirm/init.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package confirm
|
||||
|
||||
import "github.com/iwind/TeaGo"
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(new(Helper)).
|
||||
Prefix("/setup/confirm").
|
||||
GetPost("", new(IndexAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
80
EdgeAdmin/internal/web/actions/default/setup/detectDB.go
Normal file
80
EdgeAdmin/internal/web/actions/default/setup/detectDB.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DetectDBAction 尝试从本地服务器中发现MySQL
|
||||
type DetectDBAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *DetectDBAction) RunPost(params struct{}) {
|
||||
var localHost = ""
|
||||
var localPort = ""
|
||||
var localUsername = ""
|
||||
var localPassword = ""
|
||||
|
||||
// 本地的3306端口是否可以连接
|
||||
for _, tryingHost := range []string{"127.0.0.1", "localhost", "172.20.0.2"} {
|
||||
conn, dialErr := net.DialTimeout("tcp", tryingHost+":3306", 3*time.Second)
|
||||
if dialErr == nil {
|
||||
_ = conn.Close()
|
||||
localHost = tryingHost
|
||||
localPort = "3306"
|
||||
|
||||
var username = "root"
|
||||
var passwords = []string{"", "123456", "654321", "Aa_123456", "111111"}
|
||||
|
||||
// 使用 foolish-mysql 安装的MySQL
|
||||
localGeneratedPasswordData, err := os.ReadFile("/usr/local/mysql/generated-password.txt")
|
||||
if err == nil {
|
||||
var localGeneratedPassword = strings.TrimSpace(string(localGeneratedPasswordData))
|
||||
if len(localGeneratedPassword) > 0 {
|
||||
passwords = append(passwords, localGeneratedPassword)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pass := range passwords {
|
||||
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
|
||||
Driver: "mysql",
|
||||
Dsn: url.QueryEscape(username) + ":" + url.QueryEscape(pass) + "@tcp(" + configutils.QuoteIP(localHost) + ":" + localPort + ")/edges",
|
||||
Prefix: "",
|
||||
})
|
||||
if err == nil {
|
||||
err = db.Raw().Ping()
|
||||
_ = db.Close()
|
||||
|
||||
if err == nil || strings.Contains(err.Error(), "Error 1049") {
|
||||
localUsername = username
|
||||
localPassword = pass
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.Data["localDB"] = maps.Map{
|
||||
"host": localHost,
|
||||
"port": localPort,
|
||||
"username": localUsername,
|
||||
"password": localPassword,
|
||||
"canInstall": runtime.GOOS == "linux" && runtime.GOARCH == "amd64" && os.Getgid() == 0,
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
17
EdgeAdmin/internal/web/actions/default/setup/helper.go
Normal file
17
EdgeAdmin/internal/web/actions/default/setup/helper.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/setup"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
)
|
||||
|
||||
type Helper struct {
|
||||
}
|
||||
|
||||
func (this *Helper) BeforeAction(actionPtr actions.ActionWrapper) (goNext bool) {
|
||||
if setup.IsConfigured() {
|
||||
actionPtr.Object().RedirectURL("/")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
32
EdgeAdmin/internal/web/actions/default/setup/index.go
Normal file
32
EdgeAdmin/internal/web/actions/default/setup/index.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
var currentHost = this.Request.Host
|
||||
if strings.Contains(this.Request.Host, ":") {
|
||||
host, _, err := net.SplitHostPort(this.Request.Host)
|
||||
if err == nil {
|
||||
currentHost = host
|
||||
}
|
||||
}
|
||||
if net.ParseIP(currentHost) != nil && currentHost != "localhost" && currentHost != "127.0.0.1" {
|
||||
this.Data["currentHost"] = currentHost
|
||||
} else {
|
||||
this.Data["currentHost"] = ""
|
||||
}
|
||||
|
||||
this.Show()
|
||||
}
|
||||
25
EdgeAdmin/internal/web/actions/default/setup/init.go
Normal file
25
EdgeAdmin/internal/web/actions/default/setup/init.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Helper(new(Helper)).
|
||||
Prefix("/setup").
|
||||
Get("", new(IndexAction)).
|
||||
Post("/validateApi", new(ValidateApiAction)).
|
||||
Post("/validateDb", new(ValidateDbAction)).
|
||||
Post("/validateAdmin", new(ValidateAdminAction)).
|
||||
Post("/install", new(InstallAction)).
|
||||
Post("/status", new(StatusAction)).
|
||||
Post("/detectDB", new(DetectDBAction)).
|
||||
Post("/checkLocalIP", new(CheckLocalIPAction)).
|
||||
GetPost("/mysql/installPopup", new(mysql.InstallPopupAction)).
|
||||
Post("/mysql/installLogs", new(mysql.InstallLogsAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
425
EdgeAdmin/internal/web/actions/default/setup/install.go
Normal file
425
EdgeAdmin/internal/web/actions/default/setup/install.go
Normal file
@@ -0,0 +1,425 @@
|
||||
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/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"} {
|
||||
var searchDir = Tea.Root + "/" + dir
|
||||
_, err = os.Stat(searchDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
this.Fail("在当前目录(" + Tea.Root + ")下找不到" + dir + "目录,请将" + dir + "目录上传或者重新下载解压")
|
||||
}
|
||||
this.Fail("无法检查" + dir + "目录,发生错误:" + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 保存数据库配置
|
||||
var dbConfig = &configs.SimpleDBConfig{
|
||||
User: dbMap.GetString("username"),
|
||||
Password: dbMap.GetString("password"),
|
||||
Database: dbMap.GetString("database"),
|
||||
Host: configutils.QuoteIP(dbMap.GetString("host")) + ":" + dbMap.GetString("port"),
|
||||
}
|
||||
dbConfigData, err := yaml.Marshal(dbConfig)
|
||||
if err != nil {
|
||||
this.Fail("生成数据库配置失败:" + err.Error())
|
||||
return
|
||||
}
|
||||
err = os.WriteFile(apiNodeDir+"/configs/db.yaml", dbConfigData, 0666)
|
||||
if err != nil {
|
||||
this.Fail("保存数据库配置失败(db.yaml):" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = dbConfig.GenerateOldConfig(apiNodeDir + "/configs/.db.yaml")
|
||||
if err != nil {
|
||||
this.Fail("保存数据库配置失败(.db.yaml):" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 生成备份文件
|
||||
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("保存数据库配置失败(api_db.yaml):" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 生成备份文件
|
||||
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(nil)
|
||||
cmd.Stdout = output
|
||||
|
||||
var stderr = bytes.NewBuffer(nil)
|
||||
cmd.Stderr = stderr
|
||||
|
||||
// 试图读取执行日志
|
||||
go this.startReadingAPIInstallLog()
|
||||
|
||||
err = cmd.Run()
|
||||
this.apiSetupFinished = true
|
||||
if err != nil {
|
||||
this.Fail("安装失败:" + err.Error() + ": " + string(append(output.Bytes(), stderr.Bytes()...)))
|
||||
}
|
||||
|
||||
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 + " ..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||
)
|
||||
|
||||
type InstallLogsAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *InstallLogsAction) RunPost(params struct{}) {
|
||||
this.Data["logs"] = utils.SharedLogger.ReadAll()
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||
)
|
||||
|
||||
type InstallPopupAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *InstallPopupAction) RunGet(params struct{}) {
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *InstallPopupAction) RunPost(params struct{}) {
|
||||
// 清空日志
|
||||
utils.SharedLogger.Reset()
|
||||
|
||||
this.Data["isOk"] = false
|
||||
|
||||
var installer = mysqlinstallers.NewMySQLInstaller()
|
||||
var targetDir = "/usr/local/mysql"
|
||||
xzFile, err := installer.Download()
|
||||
if err != nil {
|
||||
this.Data["err"] = "download failed: " + err.Error()
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
err = installer.InstallFromFile(xzFile, targetDir)
|
||||
if err != nil {
|
||||
this.Data["err"] = "install from '" + xzFile + "' failed: " + err.Error()
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
this.Data["user"] = "root"
|
||||
this.Data["password"] = installer.Password()
|
||||
this.Data["dir"] = targetDir
|
||||
this.Data["isOk"] = true
|
||||
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package mysqlinstallers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
executils "github.com/TeaOSLab/EdgeAdmin/internal/utils/exec"
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MySQLInstaller struct {
|
||||
password string
|
||||
}
|
||||
|
||||
func NewMySQLInstaller() *MySQLInstaller {
|
||||
return &MySQLInstaller{}
|
||||
}
|
||||
|
||||
func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string) error {
|
||||
// check whether mysql already running
|
||||
this.log("checking mysqld ...")
|
||||
var oldPid = utils.FindPidWithName("mysqld")
|
||||
if oldPid > 0 {
|
||||
return errors.New("there is already a running mysql server process, pid: '" + strconv.Itoa(oldPid) + "'")
|
||||
}
|
||||
|
||||
// check target dir
|
||||
this.log("checking target dir '" + targetDir + "' ...")
|
||||
_, err := os.Stat(targetDir)
|
||||
if err == nil {
|
||||
// check target dir
|
||||
matches, _ := filepath.Glob(targetDir + "/*")
|
||||
if len(matches) > 0 {
|
||||
return errors.New("target dir '" + targetDir + "' already exists and not empty")
|
||||
} else {
|
||||
err = os.Remove(targetDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clean target dir '%s' failed: %w", targetDir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check 'tar' command
|
||||
this.log("checking 'tar' command ...")
|
||||
var tarExe, _ = executils.LookPath("tar")
|
||||
if len(tarExe) == 0 {
|
||||
this.log("installing 'tar' command ...")
|
||||
err = this.installTarCommand()
|
||||
if err != nil {
|
||||
this.log("WARN: failed to install 'tar' ...")
|
||||
}
|
||||
}
|
||||
|
||||
// check commands
|
||||
this.log("checking system commands ...")
|
||||
var cmdList = []string{"tar" /** again **/, "chown", "sh"}
|
||||
for _, cmd := range cmdList {
|
||||
cmdPath, err := executils.LookPath(cmd)
|
||||
if err != nil || len(cmdPath) == 0 {
|
||||
return errors.New("could not find '" + cmd + "' command in this system")
|
||||
}
|
||||
}
|
||||
|
||||
groupAddExe, err := this.lookupGroupAdd()
|
||||
if err != nil {
|
||||
return errors.New("could not find 'groupadd' command in this system")
|
||||
}
|
||||
|
||||
userAddExe, err := this.lookupUserAdd()
|
||||
if err != nil {
|
||||
return errors.New("could not find 'useradd' command in this system")
|
||||
}
|
||||
|
||||
// ubuntu apt
|
||||
aptGetExe, err := exec.LookPath("apt-get")
|
||||
if err == nil && len(aptGetExe) > 0 {
|
||||
for _, lib := range []string{"libaio1", "libncurses5", "libnuma1"} {
|
||||
this.log("checking " + lib + " ...")
|
||||
var cmd = utils.NewCmd(aptGetExe, "-y", "install", lib)
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
// try apt
|
||||
aptExe, aptErr := exec.LookPath("apt")
|
||||
if aptErr == nil && len(aptExe) > 0 {
|
||||
cmd = utils.NewCmd(aptExe, "-y", "install", lib)
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if lib == "libnuma1" {
|
||||
err = nil
|
||||
} else {
|
||||
return errors.New("install " + lib + " failed: " + cmd.Stderr())
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
} else { // yum
|
||||
yumExe, err := executils.LookPath("yum")
|
||||
if err == nil && len(yumExe) > 0 {
|
||||
for _, lib := range []string{"libaio", "ncurses-libs", "ncurses-compat-libs", "numactl-libs"} {
|
||||
var cmd = utils.NewCmd("yum", "-y", "install", lib)
|
||||
_ = cmd.Run()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// create symbolic links
|
||||
{
|
||||
var libFile = "/usr/lib64/libncurses.so.5"
|
||||
_, err = os.Stat(libFile)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
var latestLibFile = this.findLatestVersionFile("/usr/lib64", "libncurses.so.")
|
||||
if len(latestLibFile) > 0 {
|
||||
this.log("link '" + latestLibFile + "' to '" + libFile + "'")
|
||||
_ = os.Symlink(latestLibFile, libFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var libFile = "/usr/lib64/libtinfo.so.5"
|
||||
_, err = os.Stat(libFile)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
var latestLibFile = this.findLatestVersionFile("/usr/lib64", "libtinfo.so.")
|
||||
if len(latestLibFile) > 0 {
|
||||
this.log("link '" + latestLibFile + "' to '" + libFile + "'")
|
||||
_ = os.Symlink(latestLibFile, libFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create 'mysql' user group
|
||||
this.log("checking 'mysql' user group ...")
|
||||
{
|
||||
data, err := os.ReadFile("/etc/group")
|
||||
if err != nil {
|
||||
return fmt.Errorf("check user group failed: %w", err)
|
||||
}
|
||||
if !bytes.Contains(data, []byte("\nmysql:")) {
|
||||
var cmd = utils.NewCmd(groupAddExe, "mysql")
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("add 'mysql' user group failed: " + cmd.Stderr())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create 'mysql' user
|
||||
this.log("checking 'mysql' user ...")
|
||||
{
|
||||
data, err := os.ReadFile("/etc/passwd")
|
||||
if err != nil {
|
||||
return fmt.Errorf("check user failed: %w", err)
|
||||
}
|
||||
if !bytes.Contains(data, []byte("\nmysql:")) {
|
||||
var cmd *utils.Cmd
|
||||
if strings.HasSuffix(userAddExe, "useradd") {
|
||||
cmd = utils.NewCmd(userAddExe, "mysql", "-g", "mysql")
|
||||
} else { // adduser
|
||||
cmd = utils.NewCmd(userAddExe, "-S", "-G", "mysql", "mysql")
|
||||
}
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("add 'mysql' user failed: " + cmd.Stderr())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mkdir
|
||||
{
|
||||
var parentDir = filepath.Dir(targetDir)
|
||||
stat, err := os.Stat(parentDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(parentDir, 0777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("try to create dir '%s' failed: %w", parentDir, err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("check dir '%s' failed: %w", parentDir, err)
|
||||
}
|
||||
} else {
|
||||
if !stat.IsDir() {
|
||||
return errors.New("'" + parentDir + "' should be a directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check installer file .xz
|
||||
this.log("checking installer file ...")
|
||||
{
|
||||
stat, err := os.Stat(xzFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open the installer file: %w", err)
|
||||
}
|
||||
if stat.IsDir() {
|
||||
return errors.New("'" + xzFilePath + "' not a valid file")
|
||||
}
|
||||
|
||||
var basename = filepath.Base(xzFilePath)
|
||||
if !strings.HasSuffix(basename, ".xz") {
|
||||
return errors.New("installer file should has '.xz' extension")
|
||||
}
|
||||
}
|
||||
|
||||
// extract
|
||||
this.log("extracting installer file ...")
|
||||
var tmpDir = os.TempDir() + "/goedge-mysql-tmp"
|
||||
{
|
||||
_, err := os.Stat(tmpDir)
|
||||
if err == nil {
|
||||
err = os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clean temporary directory '%s' failed: %w", tmpDir, err)
|
||||
}
|
||||
}
|
||||
err = os.Mkdir(tmpDir, 0777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary directory '%s' failed: %w", tmpDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var cmd = utils.NewCmd("tar", "-xJvf", xzFilePath, "-C", tmpDir)
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("extract installer file '" + xzFilePath + "' failed: " + cmd.Stderr())
|
||||
}
|
||||
}
|
||||
|
||||
// create datadir
|
||||
matches, err := filepath.Glob(tmpDir + "/mysql-*")
|
||||
if err != nil || len(matches) == 0 {
|
||||
return errors.New("could not find mysql installer directory from '" + tmpDir + "'")
|
||||
}
|
||||
var baseDir = matches[0]
|
||||
var dataDir = baseDir + "/data"
|
||||
_, err = os.Stat(dataDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Mkdir(dataDir, 0777)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create data dir '%s' failed: %w", dataDir, err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("check data dir '%s' failed: %w", dataDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// chown datadir
|
||||
{
|
||||
var cmd = utils.NewCmd("chown", "mysql:mysql", dataDir)
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("chown data dir '%s' failed: %w", dataDir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// create my.cnf
|
||||
var myCnfFile = "/etc/my.cnf"
|
||||
_, err = os.Stat(myCnfFile)
|
||||
if err == nil {
|
||||
// backup it
|
||||
err = os.Rename(myCnfFile, "/etc/my.cnf."+timeutil.Format("YmdHis"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("backup '/etc/my.cnf' failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// mysql server options https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html
|
||||
var myCnfTemplate = this.createMyCnf(baseDir, dataDir)
|
||||
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write '%s' failed: %w", myCnfFile, err)
|
||||
}
|
||||
|
||||
// initialize
|
||||
this.log("initializing mysql ...")
|
||||
var generatedPassword string
|
||||
{
|
||||
var cmd = utils.NewCmd(baseDir+"/bin/mysqld", "--initialize", "--user=mysql")
|
||||
cmd.WithStderr()
|
||||
cmd.WithStdout()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("initialize failed: " + cmd.Stderr())
|
||||
}
|
||||
|
||||
// read from stdout
|
||||
var match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stdout())
|
||||
if len(match) == 0 {
|
||||
// read from stderr
|
||||
match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stderr())
|
||||
|
||||
if len(match) == 0 {
|
||||
return errors.New("initialize successfully, but could not find generated password, please report to developer")
|
||||
}
|
||||
}
|
||||
generatedPassword = strings.TrimSpace(match[1])
|
||||
|
||||
// write password to file
|
||||
var passwordFile = baseDir + "/generated-password.txt"
|
||||
err = os.WriteFile(passwordFile, []byte(generatedPassword), 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write password failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// move to right place
|
||||
this.log("moving files to target dir ...")
|
||||
err = os.Rename(baseDir, targetDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("move '%s' to '%s' failed: %w", baseDir, targetDir, err)
|
||||
}
|
||||
baseDir = targetDir
|
||||
|
||||
// change my.cnf
|
||||
myCnfTemplate = this.createMyCnf(baseDir, baseDir+"/data")
|
||||
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create new '%s' failed: %w", myCnfFile, err)
|
||||
}
|
||||
|
||||
// start mysql
|
||||
this.log("starting mysql ...")
|
||||
{
|
||||
var cmd = utils.NewCmd(baseDir+"/bin/mysqld_safe", "--user=mysql")
|
||||
cmd.WithStderr()
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return errors.New("start failed '" + cmd.String() + "': " + cmd.Stderr())
|
||||
}
|
||||
|
||||
// waiting for startup
|
||||
for i := 0; i < 30; i++ {
|
||||
var conn net.Conn
|
||||
conn, err = net.Dial("tcp", "127.0.0.1:3306")
|
||||
if err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
} else {
|
||||
_ = conn.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// change password
|
||||
newPassword, err := this.generatePassword()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate new password failed: %w", err)
|
||||
}
|
||||
|
||||
this.log("changing mysql password ...")
|
||||
var passwordSQL = "ALTER USER 'root'@'localhost' IDENTIFIED BY '" + newPassword + "';"
|
||||
{
|
||||
var cmd = utils.NewCmd("sh", "-c", baseDir+"/bin/mysql --host=\"127.0.0.1\" --user=root --password=\""+generatedPassword+"\" --execute=\""+passwordSQL+"\" --connect-expired-password")
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("change password failed: " + cmd.String() + ": " + cmd.Stderr())
|
||||
}
|
||||
}
|
||||
this.password = newPassword
|
||||
var passwordFile = baseDir + "/generated-password.txt"
|
||||
err = os.WriteFile(passwordFile, []byte(this.password), 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write generated file failed: %w", err)
|
||||
}
|
||||
|
||||
// remove temporary directory
|
||||
_ = os.Remove(tmpDir)
|
||||
|
||||
// create link to 'mysql' client command
|
||||
var clientExe = "/usr/local/bin/mysql"
|
||||
_, err = os.Stat(clientExe)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = os.Symlink(baseDir+"/bin/mysql", clientExe)
|
||||
if err == nil {
|
||||
this.log("created symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql'")
|
||||
} else {
|
||||
this.log("WARN: failed to create symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql': " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// install service
|
||||
// this is not required, so we ignore all errors
|
||||
err = this.installService(baseDir)
|
||||
if err != nil {
|
||||
this.log("WARN: install service failed: " + err.Error())
|
||||
}
|
||||
|
||||
this.log("finished")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *MySQLInstaller) Download() (path string, err error) {
|
||||
var client = &http.Client{}
|
||||
|
||||
// check latest version
|
||||
this.log("checking mysql latest version ...")
|
||||
var latestVersion = "8.2.0" // default version
|
||||
var majorVersion = "8.2"
|
||||
{
|
||||
req, err := http.NewRequest(http.MethodGet, "https://dev.mysql.com/downloads/mysql/", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/78.0.3904.108 Chrome/78.0.3904.108 Safari/537.36")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.New("check latest version failed: " + err.Error())
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", errors.New("read latest version failed: " + err.Error())
|
||||
}
|
||||
|
||||
var reg = regexp.MustCompile(`<h1>MySQL Community Server ([\d.]+) `)
|
||||
var matches = reg.FindSubmatch(data)
|
||||
if len(matches) > 0 {
|
||||
latestVersion = string(matches[1])
|
||||
var matchPieces = strings.Split(latestVersion, ".")
|
||||
if len(matchPieces) >= 2 {
|
||||
majorVersion = strings.Join(matchPieces[:2], ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.log("found version: v" + latestVersion)
|
||||
|
||||
// download
|
||||
this.log("start downloading ...")
|
||||
var downloadURL = "https://cdn.mysql.com/Downloads/MySQL-" + majorVersion + "/mysql-" + latestVersion + "-linux-glibc2.17-x86_64-minimal.tar.xz"
|
||||
|
||||
{
|
||||
this.log("downloading from url '" + downloadURL + "' ...")
|
||||
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.New("check latest version failed: " + err.Error())
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New("check latest version failed: invalid response code: " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
path = filepath.Base(downloadURL)
|
||||
fp, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
|
||||
if err != nil {
|
||||
return "", errors.New("create download file '" + path + "' failed: " + err.Error())
|
||||
}
|
||||
var writer = utils.NewProgressWriter(fp, resp.ContentLength)
|
||||
var ticker = time.NewTicker(1 * time.Second)
|
||||
var done = make(chan bool, 1)
|
||||
go func() {
|
||||
var lastProgress float32 = -1
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
var progress = writer.Progress()
|
||||
if lastProgress < 0 || progress-lastProgress > 0.1 || progress == 1 {
|
||||
lastProgress = progress
|
||||
this.log(fmt.Sprintf("%.2f%%", progress*100))
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(writer, resp.Body)
|
||||
if err != nil {
|
||||
_ = fp.Close()
|
||||
done <- true
|
||||
return "", errors.New("download failed: " + err.Error())
|
||||
}
|
||||
|
||||
err = fp.Close()
|
||||
if err != nil {
|
||||
done <- true
|
||||
return "", errors.New("download failed: " + err.Error())
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second) // waiting for progress printing
|
||||
done <- true
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Password get generated password
|
||||
func (this *MySQLInstaller) Password() string {
|
||||
return this.password
|
||||
}
|
||||
|
||||
// create my.cnf content
|
||||
func (this *MySQLInstaller) createMyCnf(baseDir string, dataDir string) string {
|
||||
var memoryTotalG = 1
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
memoryTotalG = this.sysMemoryGB() / 2
|
||||
if memoryTotalG <= 0 {
|
||||
memoryTotalG = 1
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
[mysqld]
|
||||
port=3306
|
||||
basedir="` + baseDir + `"
|
||||
datadir="` + dataDir + `"
|
||||
|
||||
max_connections=256
|
||||
innodb_flush_log_at_trx_commit=2
|
||||
max_prepared_stmt_count=65535
|
||||
binlog_cache_size=1M
|
||||
binlog_stmt_cache_size=1M
|
||||
thread_cache_size=32
|
||||
binlog_expire_logs_seconds=604800
|
||||
innodb_sort_buffer_size=8M
|
||||
innodb_buffer_pool_size=` + strconv.Itoa(memoryTotalG) + "G"
|
||||
}
|
||||
|
||||
// generate random password
|
||||
func (this *MySQLInstaller) generatePassword() (string, error) {
|
||||
var p = make([]byte, 16)
|
||||
n, err := rand.Read(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", p[:n]), nil
|
||||
}
|
||||
|
||||
// print log
|
||||
func (this *MySQLInstaller) log(message string) {
|
||||
utils.SharedLogger.Push("[" + timeutil.Format("H:i:s") + "]" + message)
|
||||
}
|
||||
|
||||
// copy file
|
||||
func (this *MySQLInstaller) installService(baseDir string) error {
|
||||
_, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
this.log("registering systemd service ...")
|
||||
|
||||
var startCmd = "${BASE_DIR}/support-files/mysql.server start"
|
||||
bashPath, _ := exec.LookPath("bash")
|
||||
if len(bashPath) > 0 {
|
||||
startCmd = bashPath + " -c \"" + startCmd + "\""
|
||||
}
|
||||
|
||||
var desc = `### BEGIN INIT INFO
|
||||
# Provides: mysql
|
||||
# Required-Start: $local_fs $network $remote_fs
|
||||
# Should-Start: ypbind nscd ldap ntpd xntpd
|
||||
# Required-Stop: $local_fs $network $remote_fs
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: start and stop MySQL
|
||||
# Description: MySQL is a very fast and reliable SQL database engine.
|
||||
### END INIT INFO
|
||||
|
||||
[Unit]
|
||||
Description=MySQL Service
|
||||
Before=shutdown.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
RemainAfterExit=yes
|
||||
ExecStart=` + startCmd + `
|
||||
ExecStop=${BASE_DIR}/support-files/mysql.server stop
|
||||
ExecRestart=${BASE_DIR}/support-files/mysql.server restart
|
||||
ExecStatus=${BASE_DIR}/support-files/mysql.server status
|
||||
ExecReload=${BASE_DIR}/support-files/mysql.server reload
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`
|
||||
|
||||
desc = strings.ReplaceAll(desc, "${BASE_DIR}", baseDir)
|
||||
|
||||
err = os.WriteFile("/etc/systemd/system/mysqld.service", []byte(desc), 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cmd = utils.NewTimeoutCmd(5*time.Second, "systemctl", "enable", "mysqld.service")
|
||||
cmd.WithStderr()
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return errors.New("enable mysqld.service failed: " + cmd.Stderr())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// install 'tar' command automatically
|
||||
func (this *MySQLInstaller) installTarCommand() error {
|
||||
// dnf
|
||||
dnfExe, err := exec.LookPath("dnf")
|
||||
if err == nil && len(dnfExe) > 0 {
|
||||
var cmd = utils.NewTimeoutCmd(10*time.Second, dnfExe, "-y", "install", "tar")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// yum
|
||||
yumExe, err := executils.LookPath("yum")
|
||||
if err == nil && len(yumExe) > 0 {
|
||||
var cmd = utils.NewTimeoutCmd(10*time.Second, yumExe, "-y", "install", "tar")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// apt-get
|
||||
aptGetExe, err := exec.LookPath("apt-get")
|
||||
if err == nil && len(aptGetExe) > 0 {
|
||||
var cmd = utils.NewTimeoutCmd(10*time.Second, aptGetExe, "-y", "install", "tar")
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
aptExe, aptErr := exec.LookPath("apt")
|
||||
if aptErr == nil {
|
||||
cmd = utils.NewTimeoutCmd(10*time.Second, aptExe, "-y", "install", "tar")
|
||||
err = cmd.Run()
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *MySQLInstaller) lookupGroupAdd() (string, error) {
|
||||
for _, cmd := range []string{"groupadd", "addgroup"} {
|
||||
path, err := executils.LookPath(cmd)
|
||||
if err == nil && len(path) > 0 {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
func (this *MySQLInstaller) lookupUserAdd() (string, error) {
|
||||
for _, cmd := range []string{"useradd", "adduser"} {
|
||||
path, err := executils.LookPath(cmd)
|
||||
if err == nil && len(path) > 0 {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
}
|
||||
|
||||
func (this *MySQLInstaller) findLatestVersionFile(dir string, prefix string) string {
|
||||
files, err := filepath.Glob(filepath.Clean(dir + "/" + prefix + "*"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var resultFile = ""
|
||||
var lastVersion = ""
|
||||
var reg = regexp.MustCompile(`\.([\d.]+)`)
|
||||
for _, file := range files {
|
||||
var filename = filepath.Base(file)
|
||||
var matches = reg.FindStringSubmatch(filename)
|
||||
if len(matches) > 1 {
|
||||
var version = matches[1]
|
||||
if len(lastVersion) == 0 || stringutil.VersionCompare(lastVersion, version) < 0 {
|
||||
lastVersion = version
|
||||
resultFile = file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultFile
|
||||
}
|
||||
|
||||
func (this *MySQLInstaller) sysMemoryGB() int {
|
||||
if runtime.GOOS != "linux" {
|
||||
return 0
|
||||
}
|
||||
meminfoData, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
for _, line := range bytes.Split(meminfoData, []byte{'\n'}) {
|
||||
line = bytes.TrimSpace(line)
|
||||
if bytes.Contains(line, []byte{':'}) {
|
||||
name, value, found := bytes.Cut(line, []byte{':'})
|
||||
if found {
|
||||
name = bytes.TrimSpace(name)
|
||||
if bytes.Equal(name, []byte("MemTotal")) {
|
||||
for _, unit := range []string{"gB", "mB", "kB"} {
|
||||
if bytes.Contains(value, []byte(unit)) {
|
||||
value = bytes.TrimSpace(bytes.ReplaceAll(value, []byte(unit), nil))
|
||||
valueInt, err := strconv.Atoi(string(value))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
switch unit {
|
||||
case "gB":
|
||||
return valueInt
|
||||
case "mB":
|
||||
return valueInt / 1024
|
||||
case "kB":
|
||||
return valueInt / 1024 / 1024
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cmd struct {
|
||||
name string
|
||||
args []string
|
||||
env []string
|
||||
dir string
|
||||
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
cancelFunc func()
|
||||
|
||||
captureStdout bool
|
||||
captureStderr bool
|
||||
|
||||
stdout *bytes.Buffer
|
||||
stderr *bytes.Buffer
|
||||
|
||||
rawCmd *exec.Cmd
|
||||
}
|
||||
|
||||
func NewCmd(name string, args ...string) *Cmd {
|
||||
return &Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
|
||||
return (&Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}).WithTimeout(timeout)
|
||||
}
|
||||
|
||||
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
|
||||
this.timeout = timeout
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
this.ctx = ctx
|
||||
this.cancelFunc = cancelFunc
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStdout() *Cmd {
|
||||
this.captureStdout = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStderr() *Cmd {
|
||||
this.captureStderr = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithEnv(env []string) *Cmd {
|
||||
this.env = env
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithDir(dir string) *Cmd {
|
||||
this.dir = dir
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) Start() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func (this *Cmd) Wait() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
func (this *Cmd) Run() error {
|
||||
if this.cancelFunc != nil {
|
||||
defer this.cancelFunc()
|
||||
}
|
||||
|
||||
var cmd = this.compose()
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStdout() string {
|
||||
if this.stdout != nil {
|
||||
return this.stdout.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stdout() string {
|
||||
return strings.TrimSpace(this.RawStdout())
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStderr() string {
|
||||
if this.stderr != nil {
|
||||
return this.stderr.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stderr() string {
|
||||
return strings.TrimSpace(this.RawStderr())
|
||||
}
|
||||
|
||||
func (this *Cmd) String() string {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.String()
|
||||
}
|
||||
var newCmd = exec.Command(this.name, this.args...)
|
||||
return newCmd.String()
|
||||
}
|
||||
|
||||
func (this *Cmd) Process() *os.Process {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.Process
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Cmd) compose() *exec.Cmd {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd
|
||||
}
|
||||
|
||||
if this.ctx != nil {
|
||||
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
|
||||
} else {
|
||||
this.rawCmd = exec.Command(this.name, this.args...)
|
||||
}
|
||||
|
||||
if this.env != nil {
|
||||
this.rawCmd.Env = this.env
|
||||
}
|
||||
|
||||
if len(this.dir) > 0 {
|
||||
this.rawCmd.Dir = this.dir
|
||||
}
|
||||
|
||||
if this.captureStdout {
|
||||
this.stdout = &bytes.Buffer{}
|
||||
this.rawCmd.Stdout = this.stdout
|
||||
}
|
||||
if this.captureStderr {
|
||||
this.stderr = &bytes.Buffer{}
|
||||
this.rawCmd.Stderr = this.stderr
|
||||
}
|
||||
|
||||
return this.rawCmd
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
var SharedLogger = NewLogger()
|
||||
|
||||
type Logger struct {
|
||||
c chan string
|
||||
}
|
||||
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
c: make(chan string, 1024),
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Logger) Push(msg string) {
|
||||
select {
|
||||
case this.c <- msg:
|
||||
default:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Logger) ReadAll() (msgList []string) {
|
||||
msgList = []string{}
|
||||
|
||||
for {
|
||||
select {
|
||||
case msg := <-this.c:
|
||||
msgList = append(msgList, msg)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Logger) Reset() {
|
||||
for {
|
||||
select {
|
||||
case <-this.c:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ProcDir = "/proc"
|
||||
)
|
||||
|
||||
func FindPidWithName(name string) int {
|
||||
// process name
|
||||
commFiles, err := filepath.Glob(ProcDir + "/*/comm")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
for _, commFile := range commFiles {
|
||||
data, err := os.ReadFile(commFile)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(string(data)) == name {
|
||||
var pieces = strings.Split(commFile, "/")
|
||||
var pid = pieces[len(pieces)-2]
|
||||
pidInt, _ := strconv.Atoi(pid)
|
||||
return pidInt
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package utils
|
||||
|
||||
import "io"
|
||||
|
||||
type ProgressWriter struct {
|
||||
rawWriter io.Writer
|
||||
total int64
|
||||
written int64
|
||||
}
|
||||
|
||||
func NewProgressWriter(rawWriter io.Writer, total int64) *ProgressWriter {
|
||||
return &ProgressWriter{
|
||||
rawWriter: rawWriter,
|
||||
total: total,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *ProgressWriter) Write(p []byte) (n int, err error) {
|
||||
n, err = this.rawWriter.Write(p)
|
||||
this.written += int64(n)
|
||||
return
|
||||
}
|
||||
|
||||
func (this *ProgressWriter) Progress() float32 {
|
||||
if this.total <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float32(float64(this.written) / float64(this.total))
|
||||
}
|
||||
16
EdgeAdmin/internal/web/actions/default/setup/status.go
Normal file
16
EdgeAdmin/internal/web/actions/default/setup/status.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package setup
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
|
||||
var currentStatusText = ""
|
||||
|
||||
type StatusAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *StatusAction) RunPost(params struct{}) {
|
||||
this.Data["statusText"] = currentStatusText
|
||||
this.Success()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ValidateAdminAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *ValidateAdminAction) RunPost(params struct {
|
||||
AdminUsername string
|
||||
AdminPassword string
|
||||
AdminPassword2 string
|
||||
Must *actions.Must
|
||||
}) {
|
||||
params.Must.
|
||||
Field("adminUsername", params.AdminUsername).
|
||||
Require("请输入管理员登录用户名").
|
||||
Match(`^[a-zA-Z0-9_]+$`, "用户名中只能包含英文、数字或下划线").
|
||||
Field("adminPassword", params.AdminPassword).
|
||||
Require("请输入管理员登录密码").
|
||||
Match(`^[a-zA-Z0-9_]+$`, "密码中只能包含英文、数字或下划线").
|
||||
Field("adminPassword2", params.AdminPassword2).
|
||||
Require("请输入确认密码").
|
||||
Equal(params.AdminPassword, "两次输入的密码不一致")
|
||||
|
||||
this.Data["admin"] = maps.Map{
|
||||
"username": params.AdminUsername,
|
||||
"password": params.AdminPassword,
|
||||
"passwordMask": strings.Repeat("*", len(params.AdminPassword)),
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
123
EdgeAdmin/internal/web/actions/default/setup/validateApi.go
Normal file
123
EdgeAdmin/internal/web/actions/default/setup/validateApi.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/configs"
|
||||
"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/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ValidateApiAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *ValidateApiAction) RunPost(params struct {
|
||||
Mode string
|
||||
NewPort string
|
||||
NewHost string
|
||||
OldProtocol string
|
||||
OldHost string
|
||||
OldPort string
|
||||
OldNodeId string
|
||||
OldNodeSecret string
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
params.OldNodeId = strings.Trim(params.OldNodeId, "\"' ")
|
||||
params.OldNodeSecret = strings.Trim(params.OldNodeSecret, "\"' ")
|
||||
|
||||
this.Data["apiNode"] = maps.Map{
|
||||
"mode": params.Mode,
|
||||
|
||||
"newPort": params.NewPort,
|
||||
"newHost": params.NewHost,
|
||||
|
||||
"oldProtocol": params.OldProtocol,
|
||||
"oldHost": params.OldHost,
|
||||
"oldPort": params.OldPort,
|
||||
"oldNodeId": params.OldNodeId,
|
||||
"oldNodeSecret": params.OldNodeSecret,
|
||||
}
|
||||
|
||||
if params.Mode == "new" {
|
||||
params.Must.
|
||||
Field("newPort", params.NewPort).
|
||||
Require("请输入API节点端口").
|
||||
Match(`^\d+$`, "API节点端口只能是数字").
|
||||
MinLength(4, "请输入4位以上的数字端口号").
|
||||
MaxLength(5, "请输入5位以下的数字端口号")
|
||||
var newPort = types.Int(params.NewPort)
|
||||
if newPort < 1024 {
|
||||
this.FailField("newPort", "API端口号不能小于1024")
|
||||
}
|
||||
if newPort > 65534 {
|
||||
this.FailField("newPort", "API端口号不能大于65534")
|
||||
}
|
||||
|
||||
if net.ParseIP(params.NewHost) == nil {
|
||||
// 检查是否为域名
|
||||
var domainReg = regexp.MustCompile(`^[\w-]+$`) // 这里暂不支持 unicode 域名
|
||||
var digitReg = regexp.MustCompile(`^\d+$`)
|
||||
var isIP = true
|
||||
for _, piece := range strings.Split(params.NewHost, ".") {
|
||||
if !domainReg.MatchString(piece) {
|
||||
this.FailField("newHost", "请输入正确的API节点主机地址")
|
||||
return
|
||||
}
|
||||
if !digitReg.MatchString(piece) {
|
||||
isIP = false
|
||||
}
|
||||
}
|
||||
|
||||
if isIP {
|
||||
this.FailField("newHost", "请输入正确的API节点主机地址")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
params.Must.
|
||||
Field("newHost", params.NewHost).
|
||||
Require("请输入API节点主机地址")
|
||||
|
||||
this.Success()
|
||||
return
|
||||
}
|
||||
|
||||
// 使用已有的API节点
|
||||
params.Must.
|
||||
Field("oldHost", params.OldHost).
|
||||
Require("请输入主机地址").
|
||||
Field("oldPort", params.OldPort).
|
||||
Require("请输入服务端口").
|
||||
Match(`^\d+$`, "服务端口只能是数字").
|
||||
Field("oldNodeId", params.OldNodeId).
|
||||
Require("请输入节点nodeId").
|
||||
Field("oldNodeSecret", params.OldNodeSecret).
|
||||
Require("请输入节点secret")
|
||||
client, err := rpc.NewRPCClient(&configs.APIConfig{
|
||||
RPCEndpoints: []string{params.OldProtocol + "://" + configutils.QuoteIP(params.OldHost) + ":" + params.OldPort},
|
||||
NodeId: params.OldNodeId,
|
||||
Secret: params.OldNodeSecret,
|
||||
}, false)
|
||||
if err != nil {
|
||||
this.FailField("oldHost", "测试API节点时出错,请检查配置,错误信息:"+err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = client.Close()
|
||||
}()
|
||||
|
||||
_, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.APIContext(0), &pb.FindCurrentAPINodeVersionRequest{})
|
||||
if err != nil {
|
||||
this.FailField("oldHost", "无法连接此API节点,错误信息:"+err.Error())
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
141
EdgeAdmin/internal/web/actions/default/setup/validateDb.go
Normal file
141
EdgeAdmin/internal/web/actions/default/setup/validateDb.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ValidateDbAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *ValidateDbAction) RunPost(params struct {
|
||||
Host string
|
||||
Port string
|
||||
Database string
|
||||
Username string
|
||||
Password string
|
||||
AccessLogKeepDays int
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
params.Must.
|
||||
Field("host", params.Host).
|
||||
Require("请输入主机地址").
|
||||
Expect(func() (message string, success bool) {
|
||||
// 是否为IP
|
||||
if net.ParseIP(params.Host) != nil {
|
||||
success = true
|
||||
return
|
||||
}
|
||||
if !regexp.MustCompile(`^[\w.-]+$`).MatchString(params.Host) {
|
||||
message = "主机地址中不能包含特殊字符"
|
||||
success = false
|
||||
return
|
||||
}
|
||||
success = true
|
||||
return
|
||||
}).
|
||||
Field("port", params.Port).
|
||||
Require("请输入端口").
|
||||
Match(`^\d+$`, "端口中只能包含数字").
|
||||
Field("database", params.Database).
|
||||
Require("请输入数据库名称").
|
||||
Match(`^[\w\.-]+$`, "数据库名称中不能包含特殊字符").
|
||||
Field("username", params.Username).
|
||||
Require("请输入连接数据库的用户名").
|
||||
Match(`^[\w\.-]+$`, "用户名中不能包含特殊字符")
|
||||
|
||||
// 测试连接
|
||||
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
|
||||
Driver: "mysql",
|
||||
Dsn: url.QueryEscape(params.Username) + ":" + url.QueryEscape(params.Password) + "@tcp(" + configutils.QuoteIP(params.Host) + ":" + params.Port + ")/" + params.Database,
|
||||
Prefix: "",
|
||||
})
|
||||
if err != nil {
|
||||
this.Fail("数据库信息错误:" + err.Error())
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = db.Close()
|
||||
}()
|
||||
|
||||
err = db.Raw().Ping()
|
||||
if err != nil {
|
||||
// 是否是数据库不存在
|
||||
if strings.Contains(err.Error(), "Error 1049") {
|
||||
db, err := dbs.NewInstanceFromConfig(&dbs.DBConfig{
|
||||
Driver: "mysql",
|
||||
Dsn: params.Username + ":" + params.Password + "@tcp(" + configutils.QuoteIP(params.Host) + ":" + params.Port + ")/",
|
||||
Prefix: "",
|
||||
})
|
||||
if err != nil {
|
||||
this.Fail("尝试创建数据库失败:" + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Exec("CREATE DATABASE `" + params.Database + "`")
|
||||
if err != nil {
|
||||
this.Fail("尝试创建数据库失败:" + err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(err.Error(), "Error 1044:") {
|
||||
this.Fail("无法连接到数据库,权限检查失败:" + err.Error())
|
||||
return
|
||||
}
|
||||
this.Fail("无法连接到数据库,请检查配置:" + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
// edgeTest表名需要根据表结构的变更而变更,防止升级时冲突
|
||||
var testTable = "edgeTest1"
|
||||
_, err = db.Exec("CREATE TABLE IF NOT EXISTS `" + testTable + "` (\n `id` int(11) NOT NULL AUTO_INCREMENT,\n PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;")
|
||||
if err != nil {
|
||||
this.Fail("当前连接的用户无法创建新表,请检查CREATE权限设置:" + err.Error())
|
||||
}
|
||||
|
||||
_, err = db.Exec("ALTER TABLE `" + testTable + "` CHANGE `id` `id` int(11) NOT NULL AUTO_INCREMENT")
|
||||
if err != nil {
|
||||
this.Fail("当前连接的用户无法修改表结构,请检查ALTER权限设置:" + err.Error())
|
||||
}
|
||||
|
||||
// 删除edgeTest,忽略可能的错误,因为我们不需要DROP权限
|
||||
_, _ = db.Exec("DROP TABLE `" + testTable + "`")
|
||||
|
||||
// 检查数据库版本
|
||||
one, err := db.FindOne("SELECT VERSION() AS v")
|
||||
if err != nil {
|
||||
this.Fail("检查数据库版本时出错:" + err.Error())
|
||||
}
|
||||
if one == nil {
|
||||
this.Fail("检查数据库版本时出错:无法获取数据库版本")
|
||||
}
|
||||
version := one.GetString("v")
|
||||
if stringutil.VersionCompare(version, "5.7.8") < 0 {
|
||||
this.Fail("数据库版本至少在v5.7.8以上,你现在使用的是v" + version)
|
||||
}
|
||||
|
||||
this.Data["db"] = maps.Map{
|
||||
"host": params.Host,
|
||||
"port": params.Port,
|
||||
"database": params.Database,
|
||||
"username": params.Username,
|
||||
"password": params.Password,
|
||||
"passwordMask": strings.Repeat("*", len(params.Password)),
|
||||
"accessLogKeepDays": params.AccessLogKeepDays,
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
Reference in New Issue
Block a user