diff --git a/internal/web/actions/default/settings/settingutils/advanced_helper.go b/internal/web/actions/default/settings/settingutils/advanced_helper.go index 6902afc0..21338cf3 100644 --- a/internal/web/actions/default/settings/settingutils/advanced_helper.go +++ b/internal/web/actions/default/settings/settingutils/advanced_helper.go @@ -40,6 +40,7 @@ func (this *AdvancedHelper) BeforeAction(actionPtr actions.ActionWrapper) (goNex if teaconst.IsPlus { tabbar.Add("监控节点", "", "/settings/monitorNodes", "", this.tab == "monitorNodes") } + tabbar.Add("迁移", "", "/settings/transfer", "", this.tab == "transfer") if teaconst.BuildPlus { tabbar.Add("商业版认证", "", "/settings/authority", "", this.tab == "authority") } diff --git a/internal/web/actions/default/settings/transfer/index.go b/internal/web/actions/default/settings/transfer/index.go new file mode 100644 index 00000000..31943f6a --- /dev/null +++ b/internal/web/actions/default/settings/transfer/index.go @@ -0,0 +1,17 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package transfer + +import "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + +type IndexAction struct { + actionutils.ParentAction +} + +func (this *IndexAction) Init() { + this.Nav("", "transfer", "") +} + +func (this *IndexAction) RunGet(params struct{}) { + this.Show() +} diff --git a/internal/web/actions/default/settings/transfer/init.go b/internal/web/actions/default/settings/transfer/init.go new file mode 100644 index 00000000..0477e468 --- /dev/null +++ b/internal/web/actions/default/settings/transfer/init.go @@ -0,0 +1,23 @@ +package transfer + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/configloaders" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/settingutils" + "github.com/TeaOSLab/EdgeAdmin/internal/web/helpers" + "github.com/iwind/TeaGo" +) + +func init() { + TeaGo.BeforeStart(func(server *TeaGo.Server) { + server. + Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeSetting)). + Helper(settingutils.NewAdvancedHelper("transfer")). + Prefix("/settings/transfer"). + Get("", new(IndexAction)). + Post("/validateAPI", new(ValidateAPIAction)). + Post("/updateHosts", new(UpdateHostsAction)). + Post("/upgradeNodes", new(UpgradeNodesAction)). + Post("/statNodes", new(StatNodesAction)). + EndAll() + }) +} diff --git a/internal/web/actions/default/settings/transfer/statNodes.go b/internal/web/actions/default/settings/transfer/statNodes.go new file mode 100644 index 00000000..d77effd7 --- /dev/null +++ b/internal/web/actions/default/settings/transfer/statNodes.go @@ -0,0 +1,23 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package transfer + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" +) + +type StatNodesAction struct { + actionutils.ParentAction +} + +func (this *StatNodesAction) RunPost(params struct{}) { + countNodesResp, err := this.RPC().NodeRPC().CountAllEnabledNodesMatch(this.AdminContext(), &pb.CountAllEnabledNodesMatchRequest{ActiveState: 1}) + if err != nil { + this.ErrorPage(err) + return + } + this.Data["countNodes"] = countNodesResp.Count + + this.Success() +} diff --git a/internal/web/actions/default/settings/transfer/updateHosts.go b/internal/web/actions/default/settings/transfer/updateHosts.go new file mode 100644 index 00000000..cf4d7ce7 --- /dev/null +++ b/internal/web/actions/default/settings/transfer/updateHosts.go @@ -0,0 +1,158 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package transfer + +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/configutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" + "github.com/iwind/TeaGo/lists" +) + +type UpdateHostsAction struct { + actionutils.ParentAction +} + +func (this *UpdateHostsAction) RunPost(params struct { + Protocol string + Host string + Port string + + OldHosts []string + NewHosts []string +}) { + if len(params.OldHosts) != len(params.NewHosts) { + this.Fail("参数配置错误,请刷新页面后重试") + } + + // 检查端口 + config, err := configs.LoadAPIConfig() + if err != nil { + this.Fail("加载当前平台的API配置失败:" + err.Error()) + } + var apiURL = params.Protocol + "://" + configutils.QuoteIP(params.Host) + ":" + params.Port + config.RPC.Endpoints = []string{apiURL} + client, err := rpc.NewRPCClient(config, false) + if err != nil { + this.Fail("检查API节点地址出错:" + err.Error()) + } + defer func() { + _ = client.Close() + }() + + if err != nil { + this.FailField("host", "测试API节点时出错,请检查配置,错误信息:"+err.Error()) + } + _, err = client.APINodeRPC().FindCurrentAPINodeVersion(client.APIContext(0), &pb.FindCurrentAPINodeVersionRequest{}) + if err != nil { + this.FailField("host", "无法连接此API节点,错误信息:"+err.Error()) + } + + defer func() { + _ = client.Close() + }() + + // API节点列表 + nodesResp, err := client.APINodeRPC().FindAllEnabledAPINodes(client.Context(0), &pb.FindAllEnabledAPINodesRequest{}) + if err != nil { + this.Fail("获取API节点列表失败,错误信息:" + err.Error()) + } + var endpoints = []string{} + for _, node := range nodesResp.ApiNodes { + if !node.IsOn { + continue + } + + // http + if len(node.HttpJSON) > 0 { + for index, oldHost := range params.OldHosts { + if len(params.NewHosts[index]) == 0 { + continue + } + node.HttpJSON = bytes.ReplaceAll(node.HttpJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\"")) + } + } + + // https + if len(node.HttpsJSON) > 0 { + for index, oldHost := range params.OldHosts { + if len(params.NewHosts[index]) == 0 { + continue + } + node.HttpsJSON = bytes.ReplaceAll(node.HttpsJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\"")) + } + } + + // restHTTP + if len(node.RestHTTPJSON) > 0 { + for index, oldHost := range params.OldHosts { + if len(params.NewHosts[index]) == 0 { + continue + } + node.RestHTTPJSON = bytes.ReplaceAll(node.RestHTTPJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\"")) + } + } + + // restHTTPS + if len(node.RestHTTPSJSON) > 0 { + for index, oldHost := range params.OldHosts { + if len(params.NewHosts[index]) == 0 { + continue + } + node.RestHTTPSJSON = bytes.ReplaceAll(node.RestHTTPSJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\"")) + } + } + + // access addrs + if len(node.AccessAddrsJSON) > 0 { + for index, oldHost := range params.OldHosts { + if len(params.NewHosts[index]) == 0 { + continue + } + node.AccessAddrsJSON = bytes.ReplaceAll(node.AccessAddrsJSON, []byte("\""+oldHost+"\""), []byte("\""+params.NewHosts[index]+"\"")) + } + + var addrs []*serverconfigs.NetworkAddressConfig + err = json.Unmarshal(node.AccessAddrsJSON, &addrs) + if err != nil { + this.Fail("读取节点访问地址失败:" + err.Error()) + } + for _, addr := range addrs { + err = addr.Init() + if err != nil { + // 暂时不提示错误 + continue + } + for _, a := range addr.FullAddresses() { + if !lists.ContainsString(endpoints, a) { + endpoints = append(endpoints, a) + } + } + } + } + + // 保存 + _, err = client.APINodeRPC().UpdateAPINode(client.Context(0), &pb.UpdateAPINodeRequest{ + ApiNodeId: node.Id, + Name: node.Name, + Description: node.Description, + HttpJSON: node.HttpJSON, + HttpsJSON: node.HttpsJSON, + AccessAddrsJSON: node.AccessAddrsJSON, + IsOn: node.IsOn, + RestIsOn: node.RestIsOn, + RestHTTPJSON: node.RestHTTPJSON, + RestHTTPSJSON: node.RestHTTPSJSON, + }) + if err != nil { + this.Fail("保存API节点信息失败:" + err.Error()) + } + } + + this.Success() +} diff --git a/internal/web/actions/default/settings/transfer/upgradeNodes.go b/internal/web/actions/default/settings/transfer/upgradeNodes.go new file mode 100644 index 00000000..c2b92469 --- /dev/null +++ b/internal/web/actions/default/settings/transfer/upgradeNodes.go @@ -0,0 +1,65 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package transfer + +import ( + "encoding/json" + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/configutils" + "github.com/TeaOSLab/EdgeCommon/pkg/messageconfigs" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/types" +) + +type UpgradeNodesAction struct { + actionutils.ParentAction +} + +func (this *UpgradeNodesAction) RunPost(params struct { + ApiNodeProtocol string + ApiNodeHost string + ApiNodePort int +}) { + nodesResp, err := this.RPC().NodeRPC().ListEnabledNodesMatch(this.AdminContext(), &pb.ListEnabledNodesMatchRequest{ + ActiveState: 1, + Size: 100, + }) + if err != nil { + this.ErrorPage(err) + return + } + + var nodes = nodesResp.Nodes + this.Data["hasNext"] = len(nodes) > 0 + this.Data["count"] = len(nodes) + + if len(nodes) > 0 { + var message = &messageconfigs.ChangeAPINodeMessage{ + Addr: params.ApiNodeProtocol + "://" + configutils.QuoteIP(params.ApiNodeHost) + ":" + types.String(params.ApiNodePort), + } + messageJSON, err := json.Marshal(message) + if err != nil { + this.ErrorPage(err) + return + } + + for _, node := range nodesResp.Nodes { + resp, err := this.RPC().NodeRPC().SendCommandToNode(this.AdminContext(), &pb.NodeStreamMessage{ + NodeId: node.Id, + TimeoutSeconds: 3, + Code: messageconfigs.MessageCodeChangeAPINode, + DataJSON: messageJSON, + }) + if err != nil { + this.ErrorPage(err) + return + } + if !resp.IsOk { + this.Fail(resp.Message) + return + } + } + } + + this.Success() +} diff --git a/internal/web/actions/default/settings/transfer/validateAPI.go b/internal/web/actions/default/settings/transfer/validateAPI.go new file mode 100644 index 00000000..e3e2ec67 --- /dev/null +++ b/internal/web/actions/default/settings/transfer/validateAPI.go @@ -0,0 +1,152 @@ +// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved. + +package transfer + +import ( + "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/configutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" + "github.com/iwind/TeaGo/actions" + "github.com/iwind/TeaGo/lists" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "regexp" +) + +type ValidateAPIAction struct { + actionutils.ParentAction +} + +func (this *ValidateAPIAction) RunPost(params struct { + Host string + Port string + Protocol string + + Must *actions.Must +}) { + params.Must. + Field("newAPINodeHost", params.Host). + Require("请输入新的API节点IP或域名"). + Field("newAPINodePort", params.Port). + Require("请输入新的API节点端口") + + if !regexp.MustCompile(`^\d{1,5}$`).MatchString(params.Port) { + this.FailField("newAPINodePort", "请输入正确的端口") + } + + // 检查端口 + config, err := configs.LoadAPIConfig() + if err != nil { + this.Fail("加载当前平台的API配置失败:" + err.Error()) + } + config.RPC.Endpoints = []string{params.Protocol + "://" + configutils.QuoteIP(params.Host) + ":" + params.Port} + client, err := rpc.NewRPCClient(config, false) + if err != nil { + this.Fail("检查API节点地址出错:" + err.Error()) + } + defer func() { + _ = client.Close() + }() + + _, err = client.AdminRPC().FindAdminFullname(this.AdminContext(), &pb.FindAdminFullnameRequest{AdminId: this.AdminId()}) + if err != nil { + statusErr, ok := status.FromError(err) + if ok { + if statusErr.Code() == codes.Unavailable { + this.Fail("测试新API节点失败:无法连接新的API节点:请检查:1、API节点地址和端口是否正确;2、防火墙或安全策略是否已正确设置。详细原因:" + err.Error()) + } + } + this.Fail("测试新API节点失败:" + err.Error()) + } + + // 所有API节点 + apiNodesResp, err := client.APINodeRPC().FindAllEnabledAPINodes(this.AdminContext(), &pb.FindAllEnabledAPINodesRequest{}) + if err != nil { + this.ErrorPage(err) + return + } + var apiNodes = apiNodesResp.ApiNodes + var hosts = []string{} + for _, node := range apiNodes { + if !node.IsOn { + continue + } + + // http + if len(node.HttpJSON) > 0 { + var config = &serverconfigs.HTTPProtocolConfig{} + err = json.Unmarshal(node.HttpJSON, config) + if err != nil { + this.Fail("读取节点HTTP信息失败:" + err.Error()) + } + for _, listen := range config.Listen { + if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) { + hosts = append(hosts, listen.Host) + } + } + } + + // https + if len(node.HttpsJSON) > 0 { + var config = &serverconfigs.HTTPSProtocolConfig{} + err = json.Unmarshal(node.HttpsJSON, config) + if err != nil { + this.Fail("读取节点HTTPS信息失败:" + err.Error()) + } + for _, listen := range config.Listen { + if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) { + hosts = append(hosts, listen.Host) + } + } + } + + // restHTTP + if len(node.RestHTTPJSON) > 0 { + var config = &serverconfigs.HTTPProtocolConfig{} + err = json.Unmarshal(node.RestHTTPJSON, config) + if err != nil { + this.Fail("读取节点REST HTTP信息失败:" + err.Error()) + } + for _, listen := range config.Listen { + if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) { + hosts = append(hosts, listen.Host) + } + } + } + + // restHTTPS + if len(node.RestHTTPSJSON) > 0 { + var config = &serverconfigs.HTTPSProtocolConfig{} + err = json.Unmarshal(node.RestHTTPSJSON, config) + if err != nil { + this.Fail("读取节点REST HTTPS信息失败:" + err.Error()) + } + for _, listen := range config.Listen { + if len(listen.Host) > 0 && !lists.ContainsString(hosts, listen.Host) { + hosts = append(hosts, listen.Host) + } + } + } + + // access addrs + if len(node.AccessAddrsJSON) > 0 { + var addrs []*serverconfigs.NetworkAddressConfig + err = json.Unmarshal(node.AccessAddrsJSON, &addrs) + if err != nil { + this.Fail("读取节点访问地址失败:" + err.Error()) + } + for _, addr := range addrs { + if len(addr.Host) > 0 && !lists.ContainsString(hosts, addr.Host) { + hosts = append(hosts, addr.Host) + } + } + } + } + this.Data["hosts"] = hosts + + this.Success() +} diff --git a/internal/web/import.go b/internal/web/import.go index 75bb8eeb..23be79ad 100644 --- a/internal/web/import.go +++ b/internal/web/import.go @@ -117,6 +117,7 @@ import ( _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/profile" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/security" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/server" + _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/transfer" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/ui" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/upgrade" _ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/settings/user-ui" diff --git a/web/public/js/vue.tea.js b/web/public/js/vue.tea.js index c65fb034..f208a7e5 100644 --- a/web/public/js/vue.tea.js +++ b/web/public/js/vue.tea.js @@ -2306,7 +2306,7 @@ window.Tea.Action = function (action, params) { console.log(error); if (typeof (_errorFn) === "function") { - _errorFn.call(Tea.Vue, {}); + _errorFn.call(Tea.Vue, { message: error.message }); } }) .then(function () { diff --git a/web/views/@default/settings/transfer/index.css b/web/views/@default/settings/transfer/index.css new file mode 100644 index 00000000..4f3109b2 --- /dev/null +++ b/web/views/@default/settings/transfer/index.css @@ -0,0 +1,13 @@ +.steps .step.active .content { + font-weight: bold; +} +.step-box .button-group { + margin-top: 1em; +} +.step-box .button-group .next { + float: right; +} +.step-box .content { + min-height: 6em; +} +/*# sourceMappingURL=index.css.map */ \ No newline at end of file diff --git a/web/views/@default/settings/transfer/index.css.map b/web/views/@default/settings/transfer/index.css.map new file mode 100644 index 00000000..6dc86a27 --- /dev/null +++ b/web/views/@default/settings/transfer/index.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA,MACC,MAAK,OACJ;EACC,iBAAA;;AAKH,SACC;EACC,eAAA;;AAFF,SACC,cAGC;EACC,YAAA;;AALH,SASC;EACC,eAAA","file":"index.css"} \ No newline at end of file diff --git a/web/views/@default/settings/transfer/index.html b/web/views/@default/settings/transfer/index.html new file mode 100644 index 00000000..c53b81f3 --- /dev/null +++ b/web/views/@default/settings/transfer/index.html @@ -0,0 +1,304 @@ +{$layout} + +
+ +通过此引导程序可以帮助你将当前平台配置迁移到新的平台中。
+每个步骤请小心操作,一旦配置错误可能需要手工还原。
+所有迁移任务已完成,请登录新的管理平台进行管理。
+所有迁移任务已完成。
+