mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2026-02-07 21:45:37 +08:00
初步实现安装界面
This commit is contained in:
17
internal/web/actions/default/setup/helper.go
Normal file
17
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
|
||||
}
|
||||
46
internal/web/actions/default/setup/index.go
Normal file
46
internal/web/actions/default/setup/index.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct{}) {
|
||||
// 当前服务器的IP
|
||||
serverIPs := []string{}
|
||||
addrs, _ := net.InterfaceAddrs()
|
||||
for _, addr := range addrs {
|
||||
netAddr, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
serverIPs = append(serverIPs, netAddr.IP.String())
|
||||
}
|
||||
|
||||
// 对IP进行排序,我们希望IPv4排在前面,而且希望127.0.0.1排在IPv4里的最后
|
||||
sort.Slice(serverIPs, func(i, j int) bool {
|
||||
ip1 := serverIPs[i]
|
||||
|
||||
if ip1 == "127.0.0.1" {
|
||||
return false
|
||||
}
|
||||
if regexp.MustCompile(`^\d+\.\d+\.\d+.\d+$`).MatchString(ip1) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
this.Data["serverIPs"] = serverIPs
|
||||
|
||||
this.Show()
|
||||
}
|
||||
17
internal/web/actions/default/setup/init.go
Normal file
17
internal/web/actions/default/setup/init.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package setup
|
||||
|
||||
import "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)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
203
internal/web/actions/default/setup/install.go
Normal file
203
internal/web/actions/default/setup/install.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"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/go-yaml/yaml"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InstallAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *InstallAction) RunPost(params struct {
|
||||
ApiNodeJSON []byte
|
||||
DbJSON []byte
|
||||
AdminJSON []byte
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
// API节点配置
|
||||
apiNodeMap := maps.Map{}
|
||||
err := json.Unmarshal(params.ApiNodeJSON, &apiNodeMap)
|
||||
if err != nil {
|
||||
this.Fail("API节点配置数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
|
||||
}
|
||||
|
||||
// 数据库
|
||||
dbMap := maps.Map{}
|
||||
err = json.Unmarshal(params.DbJSON, &dbMap)
|
||||
if err != nil {
|
||||
this.Fail("数据库配置数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
|
||||
}
|
||||
|
||||
// 管理员
|
||||
adminMap := maps.Map{}
|
||||
err = json.Unmarshal(params.AdminJSON, &adminMap)
|
||||
if err != nil {
|
||||
this.Fail("管理员数据解析错误,请刷新页面后重新尝试安装,错误信息:" + err.Error())
|
||||
}
|
||||
|
||||
// 安装API节点
|
||||
mode := apiNodeMap.GetString("mode")
|
||||
if mode == "new" {
|
||||
// 整个系统目录结构为:
|
||||
// edge-admin/
|
||||
// edge-api/
|
||||
// bin/
|
||||
// ...
|
||||
|
||||
// 检查环境
|
||||
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("在当前目录下找不到" + dir + "目录,请将" + dir + "目录上传或者重新下载解压")
|
||||
}
|
||||
this.Fail("无法检查" + dir + "目录,发生错误:" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 保存数据库配置
|
||||
dsn := dbMap.GetString("username") + ":" + dbMap.GetString("password") + "@tcp(" + 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",
|
||||
}},
|
||||
}
|
||||
dbConfigData, err := yaml.Marshal(dbConfig)
|
||||
if err != nil {
|
||||
this.Fail("生成数据库配置失败:" + err.Error())
|
||||
}
|
||||
err = ioutil.WriteFile(apiNodeDir+"/configs/db.yaml", dbConfigData, 0666)
|
||||
if err != nil {
|
||||
this.Fail("保存数据库配置失败:" + err.Error())
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(Tea.ConfigFile("/api_db.yaml"), dbConfigData, 0666)
|
||||
if err != nil {
|
||||
this.Fail("保存数据库配置失败:" + err.Error())
|
||||
}
|
||||
|
||||
// 开始安装
|
||||
var resultMap = maps.Map{}
|
||||
{
|
||||
cmd := exec.Command(apiNodeDir+"/bin/edge-api", "setup", "-api-node-protocol=http", "-api-node-host=\""+apiNodeMap.GetString("newHost")+"\"", "-api-node-port=\""+apiNodeMap.GetString("newPort")+"\"")
|
||||
output := bytes.NewBuffer([]byte{})
|
||||
cmd.Stdout = output
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
this.Fail("安装失败:" + err.Error())
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
// 启动API节点
|
||||
{
|
||||
cmd := exec.Command(apiNodeDir + "/bin/edge-api")
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
this.Fail("API节点启动失败:" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 写入API节点配置,完成安装
|
||||
apiConfig := &configs.APIConfig{
|
||||
RPC: struct {
|
||||
Endpoints []string `yaml:"endpoints"`
|
||||
}{
|
||||
Endpoints: []string{"http://" + apiNodeMap.GetString("newHost") + ":" + apiNodeMap.GetString("newPort")},
|
||||
},
|
||||
NodeId: resultMap.GetString("adminNodeId"),
|
||||
Secret: resultMap.GetString("adminNodeSecret"),
|
||||
}
|
||||
|
||||
// 设置管理员
|
||||
client, err := rpc.NewRPCClient(apiConfig)
|
||||
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)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
this.Fail("设置管理员账号出错:" + err.Error())
|
||||
}
|
||||
|
||||
err = apiConfig.WriteFile(Tea.ConfigFile("api.yaml"))
|
||||
if err != nil {
|
||||
this.Fail("保存配置失败,原因:" + err.Error())
|
||||
}
|
||||
|
||||
this.Success()
|
||||
} else if mode == "old" {
|
||||
// 构造RPC
|
||||
apiConfig := &configs.APIConfig{
|
||||
RPC: struct {
|
||||
Endpoints []string `yaml:"endpoints"`
|
||||
}{
|
||||
Endpoints: []string{apiNodeMap.GetString("oldProtocol") + "://" + apiNodeMap.GetString("oldHost") + ":" + apiNodeMap.GetString("oldPort")},
|
||||
},
|
||||
NodeId: apiNodeMap.GetString("oldNodeId"),
|
||||
Secret: apiNodeMap.GetString("oldNodeSecret"),
|
||||
}
|
||||
client, err := rpc.NewRPCClient(apiConfig)
|
||||
if err != nil {
|
||||
this.FailField("oldHost", "测试API节点时出错,请检查配置,错误信息:"+err.Error())
|
||||
}
|
||||
|
||||
// 设置管理员
|
||||
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())
|
||||
}
|
||||
|
||||
// 写入API节点配置,完成安装
|
||||
err = apiConfig.WriteFile(Tea.ConfigFile("api.yaml"))
|
||||
if err != nil {
|
||||
this.Fail("保存配置失败,原因:" + err.Error())
|
||||
}
|
||||
|
||||
// 成功
|
||||
this.Success()
|
||||
} else {
|
||||
this.Fail("错误的API节点模式:'" + mode + "'")
|
||||
}
|
||||
}
|
||||
38
internal/web/actions/default/setup/validateAdmin.go
Normal file
38
internal/web/actions/default/setup/validateAdmin.go
Normal file
@@ -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()
|
||||
}
|
||||
105
internal/web/actions/default/setup/validateApi.go
Normal file
105
internal/web/actions/default/setup/validateApi.go
Normal file
@@ -0,0 +1,105 @@
|
||||
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/rpc/pb"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"net"
|
||||
"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("请输入节点端口").
|
||||
Match(`^\d+$`, "节点端口只能是数字").
|
||||
MinLength(4, "请输入4位以上的数字").
|
||||
MaxLength(5, "请输入5位以下的数字")
|
||||
newPort := types.Int(params.NewPort)
|
||||
if newPort < 1024 {
|
||||
this.FailField("newPort", "端口号不能小于1024")
|
||||
}
|
||||
if newPort > 65534 {
|
||||
this.FailField("newPort", "端口号不能大于65534")
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:"+params.NewPort)
|
||||
if err == nil {
|
||||
_ = conn.Close()
|
||||
this.FailField("newPort", "此端口已经被别的服务占用,请换一个")
|
||||
}
|
||||
|
||||
params.Must.
|
||||
Field("newHost", params.NewHost).
|
||||
Require("请输入节点主机地址")
|
||||
|
||||
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{
|
||||
RPC: struct {
|
||||
Endpoints []string `yaml:"endpoints"`
|
||||
}{
|
||||
Endpoints: []string{params.OldProtocol + "://" + params.OldHost + ":" + params.OldPort},
|
||||
},
|
||||
NodeId: params.OldNodeId,
|
||||
Secret: params.OldNodeSecret,
|
||||
})
|
||||
if err != nil {
|
||||
this.FailField("oldHost", "测试API节点时出错,请检查配置,错误信息:"+err.Error())
|
||||
}
|
||||
_, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.APIContext(0), &pb.FindCurrentAPINodeVersionRequest{})
|
||||
if err != nil {
|
||||
this.FailField("oldHost", "无法连接此API节点,错误信息:"+err.Error())
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
78
internal/web/actions/default/setup/validateDb.go
Normal file
78
internal/web/actions/default/setup/validateDb.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package setup
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/iwind/TeaGo/actions"
|
||||
"github.com/iwind/TeaGo/dbs"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ValidateDbAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *ValidateDbAction) RunPost(params struct {
|
||||
Host string
|
||||
Port string
|
||||
Database string
|
||||
Username string
|
||||
Password string
|
||||
|
||||
Must *actions.Must
|
||||
}) {
|
||||
params.Must.
|
||||
Field("host", params.Host).
|
||||
Require("请输入主机地址").
|
||||
Match(`^[\w\.-]+$`, "主机地址中不能包含特殊字符").
|
||||
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: params.Username + ":" + params.Password + "@tcp(" + params.Host + ":" + params.Port + ")/" + params.Database,
|
||||
Prefix: "",
|
||||
})
|
||||
if err != nil {
|
||||
this.Fail("数据库信息错误:" + err.Error())
|
||||
}
|
||||
|
||||
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(" + params.Host + ":" + params.Port + ")/",
|
||||
Prefix: "",
|
||||
})
|
||||
|
||||
_, err = db.Exec("CREATE DATABASE `" + params.Database + "`")
|
||||
if err != nil {
|
||||
this.Fail("尝试创建数据库失败:" + err.Error())
|
||||
}
|
||||
} else {
|
||||
this.Fail("无法连接到数据库,请检查配置:" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
}
|
||||
|
||||
this.Success()
|
||||
}
|
||||
Reference in New Issue
Block a user