优化节点创建流程

This commit is contained in:
GoEdgeLab
2021-08-14 18:06:24 +08:00
parent 2affb5de3e
commit 7ffdfc62f9
13 changed files with 550 additions and 17 deletions

View File

@@ -71,6 +71,10 @@ func (this *RPCClient) NodeGrantRPC() pb.NodeGrantServiceClient {
return pb.NewNodeGrantServiceClient(this.pickConn())
}
func (this *RPCClient) NodeLoginRPC() pb.NodeLoginServiceClient {
return pb.NewNodeLoginServiceClient(this.pickConn())
}
func (this *RPCClient) NodeClusterRPC() pb.NodeClusterServiceClient {
return pb.NewNodeClusterServiceClient(this.pickConn())
}

View File

@@ -4,11 +4,13 @@ import (
"encoding/json"
"github.com/TeaOSLab/EdgeAdmin/internal/oplogs"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants/grantutils"
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/actions"
"github.com/iwind/TeaGo/maps"
"strconv"
"strings"
)
// CreateNodeAction 创建节点
@@ -67,6 +69,22 @@ func (this *CreateNodeAction) RunGet(params struct {
}
this.Data["dnsRoutes"] = dnsRouteMaps
// API节点列表
apiNodesResp, err := this.RPC().APINodeRPC().FindAllEnabledAPINodes(this.AdminContext(), &pb.FindAllEnabledAPINodesRequest{})
if err != nil {
this.ErrorPage(err)
return
}
apiNodes := apiNodesResp.Nodes
apiEndpoints := []string{}
for _, apiNode := range apiNodes {
if !apiNode.IsOn {
continue
}
apiEndpoints = append(apiEndpoints, apiNode.AccessAddrs...)
}
this.Data["apiEndpoints"] = "\"" + strings.Join(apiEndpoints, "\", \"") + "\""
this.Show()
}
@@ -174,5 +192,56 @@ func (this *CreateNodeAction) RunPost(params struct {
// 创建日志
defer this.CreateLog(oplogs.LevelInfo, "创建节点 %d", nodeId)
// 响应数据
this.Data["nodeId"] = nodeId
nodeResp, err := this.RPC().NodeRPC().FindEnabledNode(this.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: nodeId})
if err != nil {
this.ErrorPage(err)
return
}
if nodeResp.Node != nil {
var addresses = []string{}
for _, addrMap := range ipAddresses {
addresses = append(addresses, addrMap.GetString("ip"))
}
var grantMap maps.Map = nil
grantId := params.GrantId
if grantId > 0 {
grantResp, err := this.RPC().NodeGrantRPC().FindEnabledNodeGrant(this.AdminContext(), &pb.FindEnabledNodeGrantRequest{NodeGrantId: grantId})
if err != nil {
this.ErrorPage(err)
return
}
if grantResp.NodeGrant != nil && grantResp.NodeGrant.Id > 0 {
grantMap = maps.Map{
"id": grantResp.NodeGrant.Id,
"name": grantResp.NodeGrant.Name,
"method": grantResp.NodeGrant.Method,
"methodName": grantutils.FindGrantMethodName(grantResp.NodeGrant.Method),
}
}
}
this.Data["node"] = maps.Map{
"id": nodeResp.Node.Id,
"name": nodeResp.Node.Name,
"uniqueId": nodeResp.Node.UniqueId,
"secret": nodeResp.Node.Secret,
"addresses": addresses,
"login": maps.Map{
"id": 0,
"name": "SSH",
"type": "ssh",
"params": maps.Map{
"grantId": params.GrantId,
"host": params.SshHost,
"port": params.SshPort,
},
},
"grant": grantMap,
}
}
this.Success()
}

View File

@@ -0,0 +1,76 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cluster
import (
"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"
)
type CreateNodeInstallAction struct {
actionutils.ParentAction
}
func (this *CreateNodeInstallAction) RunPost(params struct {
NodeId int64
SshHost string
SshPort int
GrantId int64
Must *actions.Must
}) {
defer this.CreateLogInfo("安装节点 %d", params.NodeId)
params.Must.
Field("sshHost2", params.SshHost).
Require("请填写SSH主机地址").
Field("sshPort2", params.SshPort).
Gt(0, "请填写SSH主机端口").
Lt(65535, "SSH主机端口需要小于65535").
Field("grantId", params.GrantId).
Gt(0, "请选择SSH登录认证")
// 查询login
nodeResp, err := this.RPC().NodeRPC().FindEnabledNode(this.AdminContext(), &pb.FindEnabledNodeRequest{NodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
var node = nodeResp.Node
if node == nil {
this.Fail("找不到要修改的节点")
}
var loginId int64
if node.NodeLogin != nil {
loginId = node.NodeLogin.Id
}
// 修改节点信息
_, err = this.RPC().NodeRPC().UpdateNodeLogin(this.AdminContext(), &pb.UpdateNodeLoginRequest{
NodeId: params.NodeId,
NodeLogin: &pb.NodeLogin{
Id: loginId,
Name: "SSH",
Type: "ssh",
Params: maps.Map{
"grantId": params.GrantId,
"host": params.SshHost,
"port": params.SshPort,
}.AsJSON(),
},
})
if err != nil {
this.ErrorPage(err)
return
}
// 开始安装
_, err = this.RPC().NodeRPC().InstallNode(this.AdminContext(), &pb.InstallNodeRequest{NodeId: params.NodeId})
if err != nil {
this.Fail("安装失败:" + err.Error())
}
this.Success()
}

View File

@@ -27,9 +27,11 @@ func init() {
Post("/upgradeStatus", new(UpgradeStatusAction)).
GetPost("/delete", new(DeleteAction)).
GetPost("/createNode", new(CreateNodeAction)).
Post("/createNodeInstall", new(CreateNodeInstallAction)).
GetPost("/createBatch", new(CreateBatchAction)).
GetPost("/updateNodeSSH", new(UpdateNodeSSHAction)).
GetPost("/installManual", new(InstallManualAction)).
Post("/suggestLoginPorts", new(SuggestLoginPortsAction)).
// 节点相关
Prefix("/clusters/cluster/node").
@@ -58,7 +60,6 @@ func init() {
// 看板相关
Prefix("/clusters/cluster/boards").
Get("", new(boards.IndexAction)).
EndAll()
})
}

View File

@@ -9,7 +9,7 @@ import (
"strings"
)
// 安装节点
// InstallAction 安装节点
type InstallAction struct {
actionutils.ParentAction
}
@@ -97,7 +97,7 @@ func (this *InstallAction) RunGet(params struct {
this.Show()
}
// 开始安装
// RunPost 开始安装
func (this *InstallAction) RunPost(params struct {
NodeId int64

View File

@@ -0,0 +1,36 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package cluster
import (
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
)
type SuggestLoginPortsAction struct {
actionutils.ParentAction
}
func (this *SuggestLoginPortsAction) RunPost(params struct {
Host string
}) {
portsResp, err := this.RPC().NodeLoginRPC().FindNodeLoginSuggestPorts(this.AdminContext(), &pb.FindNodeLoginSuggestPortsRequest{Host: params.Host})
if err != nil {
this.ErrorPage(err)
return
}
if len(portsResp.Ports) == 0 {
this.Data["ports"] = []int32{}
} else {
this.Data["ports"] = portsResp.Ports
}
if len(portsResp.AvailablePorts) == 0 {
this.Data["availablePorts"] = []int32{}
} else {
this.Data["availablePorts"] = portsResp.AvailablePorts
}
this.Success()
}

View File

@@ -16,21 +16,24 @@ Vue.component("grant-selector", {
if (that.grantId > 0) {
that.grant = resp.data.grant;
}
that.notifyUpdate()
}
});
})
},
// 创建授权
create: function () {
let that = this
teaweb.popup("/clusters/grants/createPopup", {
height: "26em",
callback: (resp) => {
this.grantId = resp.data.grant.id;
if (this.grantId > 0) {
this.grant = resp.data.grant;
that.grantId = resp.data.grant.id;
if (that.grantId > 0) {
that.grant = resp.data.grant;
}
that.notifyUpdate()
}
});
})
},
// 修改授权
@@ -39,18 +42,24 @@ Vue.component("grant-selector", {
window.location.reload();
return;
}
let that = this
teaweb.popup("/clusters/grants/updatePopup?grantId=" + this.grant.id, {
height: "26em",
callback: (resp) => {
this.grant = resp.data.grant;
that.grant = resp.data.grant
that.notifyUpdate()
}
})
},
// 删除已选择授权
remove: function () {
this.grant = null;
this.grantId = 0;
this.grant = null
this.grantId = 0
this.notifyUpdate()
},
notifyUpdate: function () {
this.$emit("change", this.grant)
}
},
template: `<div>

View File

@@ -0,0 +1,60 @@
// 节点登录推荐端口
Vue.component("node-login-suggest-ports", {
data: function () {
return {
ports: [],
availablePorts: [],
autoSelected: false,
isLoading: false
}
},
methods: {
reload: function (host) {
let that = this
this.autoSelected = false
this.isLoading = true
Tea.action("/clusters/cluster/suggestLoginPorts")
.params({
host: host
})
.success(function (resp) {
if (resp.data.availablePorts != null) {
that.availablePorts = resp.data.availablePorts
if (that.availablePorts.length > 0) {
that.autoSelectPort(that.availablePorts[0])
that.autoSelected = true
}
}
if (resp.data.ports != null) {
that.ports = resp.data.ports
if (that.ports.length > 0 && !that.autoSelected) {
that.autoSelectPort(that.ports[0])
that.autoSelected = true
}
}
})
.done(function () {
that.isLoading = false
})
.post()
},
selectPort: function (port) {
this.$emit("select", port)
},
autoSelectPort: function (port) {
this.$emit("auto-select", port)
}
},
template: `<span>
<span v-if="isLoading">正在检查端口...</span>
<span v-if="availablePorts.length > 0">
推荐端口:<a href="" v-for="port in availablePorts" @click.prevent="selectPort(port)" class="ui label tiny basic blue" style="border: 1px #2185d0 dashed; font-weight: normal">{{port}}</a>
&nbsp; &nbsp;
</span>
<span v-if="ports.length > 0">
常用端口:<a href="" v-for="port in ports" @click.prevent="selectPort(port)" class="ui label tiny basic blue" style="border: 1px #2185d0 dashed; font-weight: normal">{{port}}</a>
</span>
<span v-if="ports.length == 0">常用端口有22等。</span>
<span v-if="ports.length > 0" class="grey small">(可以点击要使用的端口)</span>
</span>`
})

View File

@@ -4,4 +4,10 @@
.right-box {
top: 10em;
}
.row {
line-height: 4;
}
.step.active {
font-weight: bold;
}
/*# sourceMappingURL=createNode.css.map */

View File

@@ -1 +1 @@
{"version":3,"sources":["createNode.less"],"names":[],"mappings":"AAAA;EACC,SAAA;;AAGD;EACC,SAAA","file":"createNode.css"}
{"version":3,"sources":["createNode.less"],"names":[],"mappings":"AAAA;EACC,SAAA;;AAGD;EACC,SAAA;;AAGD;EACC,cAAA;;AAGD,KAAK;EACJ,iBAAA","file":"createNode.css"}

View File

@@ -1,9 +1,23 @@
{$layout}
{$template "/clusters/cluster/menu"}
{$template "/left_menu"}
{$template "/code_editor"}
<div class="right-box">
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success">
<div class="ui steps small fluid">
<div class="ui step" :class="{active: step == 'info'}">
<div class="content">1. 填写节点信息</div>
</div>
<div class="ui step" :class="{active: step == 'install'}">
<div class="content">2. 安装节点</div>
</div>
<div class="ui step" :class="{active: step == 'finish'}">
<div class="content">3. 完成</div>
</div>
</div>
<!-- 填写信息 -->
<form method="post" class="ui form" data-tea-action="$" data-tea-success="success" v-show="step == 'info'">
<input type="hidden" name="clusterId" :value="clusterId"/>
<table class="ui table definition selectable">
<tr>
@@ -51,7 +65,7 @@
<td>SSH主机地址</td>
<td>
<input type="text" name="sshHost" maxlength="64"/>
<p class="comment">比如192.168.1.100</p>
<p class="comment">比如192.168.1.100,用于远程安装节点应用程序。</p>
</td>
</tr>
<tr>
@@ -69,6 +83,86 @@
</tr>
</tbody>
</table>
<submit-btn></submit-btn>
<submit-btn>下一步</submit-btn>
</form>
<!-- 安装节点 -->
<div v-if="step == 'install'">
<div class="ui tabular menu">
<a href="" class="item" :class="{active: installMethod == 'remote'}" @click.prevent="switchInstallMethod('remote')">远程安装</a>
<a href="" class="item" :class="{active: installMethod == 'manual'}" @click.prevent="switchInstallMethod('manual')">手动安装</a>
</div>
<!-- 远程安装 -->
<div v-if="installMethod == 'remote'">
<form class="ui form">
<table class="ui table definition selectable">
<tr>
<td class="title">SSH主机地址 *</td>
<td>
<input type="text" name="sshHost2" maxlength="64" v-model="sshHost" ref="installSSHHostRef" style="width: 16em" @change="changeSSHHost"/>
<div v-if="node.addresses != null && node.addresses.length > 1" style="margin-top: 1em">
<a href="" class="ui label small basic" v-for="addr in node.addresses" title="点击使用" @click.prevent="selectSSHHost(addr)">{{addr}}</a>
</div>
<p class="comment">比如192.168.1.100,用于远程安装节点应用程序。</p>
</td>
</tr>
<tr>
<td>SSH主机端口 *</td>
<td>
<input type="text" name="sshPort2" maxlength="5" v-model="sshPort" style="width:5em"/>
<p class="comment"><node-login-suggest-ports ref="nodeLoginSuggestPortsRef" @select="selectLoginPort" @auto-select="autoSelectLoginPort"></node-login-suggest-ports></p>
</td>
</tr>
<tr>
<td>SSH登录认证 *</td>
<td>
<grant-selector :v-grant="node.grant" @change="changeGrant"></grant-selector>
</td>
</tr>
</table>
<div v-if="installStatus != null && (installStatus.isRunning || installStatus.isFinished)"
class="ui segment installing-box">
<div v-if="installStatus.isRunning" class="blue">安装中...</div>
<div v-if="installStatus.isFinished">
<span v-if="installStatus.isOk" class="green">已安装成功</span>
<span v-if="!installStatus.isOk" class="red">安装过程中发生错误:{{installStatus.error}}</span>
</div>
</div>
<button class="ui primary button" type="button" @click.prevent="install" v-if="!isInstalling">远程安装</button>
<button class="ui button disabled" v-if="isInstalling">正在安装</button>
<a href="" @click.prevent="finish" v-if="!isInstalling" style="margin-left: 1em; float: right">跳过安装</a>
</form>
</div>
<!-- 手动安装 -->
<div v-if="installMethod == 'manual'">
<div class="row">在边缘节点安装目录下,复制<code-label>configs/api.template.yaml</code-label><code-label>configs/api.yaml</code-label>,然后修改文件里面的内容为以下内容:</div>
<source-code-box id="rpc-code" type="text/yaml">rpc:
endpoints: [ {{apiEndpoints}} ]
nodeId: "{{node.uniqueId}}"
secret: "{{node.secret}}"</source-code-box>
<div class="row">然后再使用<code-label>bin/edge-node start</code-label>命令启动节点。</div>
<div>
<a href="" @click.prevent="finish" style="float: right">跳过安装</a>
</div>
</div>
</div>
<!-- 完成 -->
<div v-show="step == 'finish'">
<div>
<div style="text-align: center; font-size: 1.4em; margin-top: 2.4em" v-if="isInstalled"><span class="green">"{{node.name}}"节点已被创建并安装成功。</span></div>
<div style="text-align: center; font-size: 1.4em; margin-top: 2.4em" v-if="!isInstalled"><span class="green">"{{node.name}}"节点已创建成功。</span></div>
<div style="text-align: center; margin-top: 3em">
<a :href="'/clusters/cluster/node?nodeId=' + nodeId + '&clusterId=' + clusterId" class="ui button primary" type="button">现在进入节点详情<i class="ui icon long arrow alternate right"></i></a>
</div>
<div style="text-align: center; margin-top: 1em">
<a href="" @click.prevent="createNext">继续创建下一个节点</a>
</div>
</div>
</div>
</div>

View File

@@ -1,3 +1,173 @@
Tea.context(function () {
this.success = NotifySuccess("保存成功", "/clusters/cluster/nodes?clusterId=" + this.clusterId);
});
this.nodeId = 0
this.node = {}
this.sshHost = ""
this.sshPort = ""
this.grantId = 0
this.step = "info"
this.success = function (resp) {
this.node = resp.data.node
this.nodeId = this.node.id
this.sshHost = this.node.login.params.host
if (this.node.login.params.port > 0) {
this.sshPort = this.node.login.params.port
}
if (this.node.addresses.length > 0) {
this.sshHost = this.node.addresses[0]
}
this.step = "install"
this.$delay(function () {
this.$refs.installSSHHostRef.focus()
this.$refs.nodeLoginSuggestPortsRef.reload(this.sshHost)
})
}
/**
* 安装
*/
this.isInstalled = false
this.installMethod = "remote" // remote | manual
this.isInstalling = false
this.switchInstallMethod = function (method) {
this.installMethod = method
}
this.selectSSHHost = function (host) {
this.sshHost = host
this.changeSSHHost()
}
this.changeSSHHost = function () {
if (this.$refs.nodeLoginSuggestPortsRef != null) {
this.$refs.nodeLoginSuggestPortsRef.reload(this.sshHost)
}
}
this.selectLoginPort = function (port) {
this.sshPort = port
}
this.autoSelectLoginPort = function (port) {
if (this.sshPort == null || this.sshPort <= 0) {
this.sshPort = port
}
}
this.install = function () {
if (this.node.grant != null) {
this.grantId = this.node.grant.id
}
this.isInstalling = true
this.$post(".createNodeInstall")
.params({
nodeId: this.node.id,
sshHost: this.sshHost,
sshPort: this.sshPort,
grantId: this.grantId
})
.timeout(30)
.success(function () {
this.$delay(function () {
this.isInstalling = true
this.reloadStatus(this.node.id)
})
})
.done(function () {
this.isInstalling = false
})
}
this.changeGrant = function (grant) {
if (grant != null) {
this.grantId = grant.id
} else {
this.grantId = 0
}
}
// 刷新状态
this.installStatus = null
this.reloadStatus = function (nodeId) {
let that = this
this.$post("/clusters/cluster/node/status")
.params({
nodeId: nodeId
})
.success(function (resp) {
this.installStatus = resp.data.installStatus
this.node.isInstalled = resp.data.isInstalled
if (this.node.isInstalled) {
this.isInstalling = false
this.isInstalled = true
this.finish()
}
if (!this.isInstalling) {
return
}
let nodeId = this.node.id
let errMsg = this.installStatus.error
if (this.installStatus.errorCode.length > 0 || errMsg.length > 0) {
this.isInstalling = false
}
switch (this.installStatus.errorCode) {
case "EMPTY_LOGIN":
case "EMPTY_SSH_HOST":
case "EMPTY_SSH_PORT":
case "EMPTY_GRANT":
teaweb.warn("需要填写SSH登录信息", function () {
teaweb.popup("/clusters/cluster/updateNodeSSH?nodeId=" + nodeId, {
height: "30em",
callback: function () {
that.install()
}
})
})
return
case "SSH_LOGIN_FAILED":
teaweb.warn("SSH登录失败请检查设置")
return
case "CREATE_ROOT_DIRECTORY_FAILED":
teaweb.warn("创建根目录失败,请检查目录权限或者手工创建:" + errMsg)
return
case "INSTALL_HELPER_FAILED":
teaweb.warn("安装助手失败:" + errMsg)
return
case "TEST_FAILED":
teaweb.warn("环境测试失败:" + errMsg)
return
case "RPC_TEST_FAILED":
teaweb.confirm("html:要安装的节点到API服务之间的RPC通讯测试失败具体错误" + errMsg + "<br/>现在修改API信息", function () {
window.location = "/api"
})
return
default:
shouldReload = true
//teaweb.warn("安装失败:" + errMsg)
}
})
.done(function () {
this.$delay(function () {
this.reloadStatus(nodeId)
}, 1000)
});
}
/**
* 完成
*/
this.finish = function () {
this.step = "finish"
}
this.createNext = function () {
teaweb.reload()
}
})

View File

@@ -4,4 +4,12 @@
.right-box {
top: 10em;
}
.row {
line-height: 4;
}
.step.active {
font-weight: bold;
}