初步实现多集群共享节点

This commit is contained in:
GoEdgeLab
2021-07-31 22:23:07 +08:00
parent 15fb58581d
commit 3654c27c09
38 changed files with 610 additions and 201 deletions

View File

@@ -11,7 +11,7 @@ import (
"strconv"
)
// 创建节点
// CreateNodeAction 创建节点
type CreateNodeAction struct {
actionutils.ParentAction
}
@@ -57,8 +57,10 @@ func (this *CreateNodeAction) RunGet(params struct {
}
for _, route := range routesResp.Routes {
dnsRouteMaps = append(dnsRouteMaps, maps.Map{
"name": route.Name,
"code": route.Code,
"domainId": domainId,
"domainName": clusterDNSResp.Domain.Name,
"name": route.Name,
"code": route.Code,
})
}
}

View File

@@ -3,6 +3,7 @@ package node
import (
"encoding/json"
"fmt"
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/utils/numberutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/clusters/grants/grantutils"
@@ -37,6 +38,7 @@ func (this *DetailAction) RunGet(params struct {
return
}
// 主集群
var clusterMap maps.Map = nil
if node.NodeCluster != nil {
clusterId := node.NodeCluster.Id
@@ -55,6 +57,16 @@ func (this *DetailAction) RunGet(params struct {
}
}
// 从集群
var secondaryClustersMaps = []maps.Map{}
for _, cluster := range node.SecondaryNodeClusters {
secondaryClustersMaps = append(secondaryClustersMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"isOn": cluster.IsOn,
})
}
// IP地址
ipAddressesResp, err := this.RPC().NodeIPAddressRPC().FindAllEnabledIPAddressesWithNodeId(this.AdminContext(), &pb.FindAllEnabledIPAddressesWithNodeIdRequest{
NodeId: params.NodeId,
@@ -64,6 +76,7 @@ func (this *DetailAction) RunGet(params struct {
this.ErrorPage(err)
return
}
var ipAddresses = ipAddressesResp.Addresses
ipAddressMaps := []maps.Map{}
for _, addr := range ipAddressesResp.Addresses {
ipAddressMaps = append(ipAddressMaps, maps.Map{
@@ -75,33 +88,56 @@ func (this *DetailAction) RunGet(params struct {
}
// DNS相关
dnsInfoResp, err := this.RPC().NodeRPC().FindEnabledNodeDNS(this.AdminContext(), &pb.FindEnabledNodeDNSRequest{NodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
dnsRouteMaps := []maps.Map{}
recordName := ""
recordValue := ""
if dnsInfoResp.Node != nil {
recordName = dnsInfoResp.Node.NodeClusterDNSName + "." + dnsInfoResp.Node.DnsDomainName
recordValue = dnsInfoResp.Node.IpAddr
for _, dnsInfo := range dnsInfoResp.Node.Routes {
dnsRouteMaps = append(dnsRouteMaps, maps.Map{
"name": dnsInfo.Name,
"code": dnsInfo.Code,
})
var clusters = []*pb.NodeCluster{node.NodeCluster}
clusters = append(clusters, node.SecondaryNodeClusters...)
var recordMaps = []maps.Map{}
var routeMaps = []maps.Map{}
for _, cluster := range clusters {
dnsInfoResp, err := this.RPC().NodeRPC().FindEnabledNodeDNS(this.AdminContext(), &pb.FindEnabledNodeDNSRequest{
NodeId: params.NodeId,
NodeClusterId: cluster.Id,
})
if err != nil {
this.ErrorPage(err)
return
}
var dnsInfo = dnsInfoResp.Node
if len(dnsInfo.DnsDomainName) == 0 || len(dnsInfo.NodeClusterDNSName) == 0 {
continue
}
var domainName = dnsInfo.DnsDomainName
// 默认线路
if len(dnsInfo.Routes) == 0 {
dnsInfo.Routes = append(dnsInfo.Routes, &pb.DNSRoute{})
} else {
for _, route := range dnsInfo.Routes {
routeMaps = append(routeMaps, maps.Map{
"domainName": domainName,
"code": route.Code,
"name": route.Name,
})
}
}
for _, addr := range ipAddresses {
if !addr.CanAccess {
continue
}
for _, route := range dnsInfo.Routes {
var recordType = "A"
if utils.IsIPv6(addr.Ip) {
recordType = "AAAA"
}
recordMaps = append(recordMaps, maps.Map{
"name": dnsInfo.NodeClusterDNSName + "." + domainName,
"type": recordType,
"route": route.Name,
"value": addr.Ip,
})
}
}
}
if len(dnsRouteMaps) == 0 {
dnsRouteMaps = append(dnsRouteMaps, maps.Map{
"name": "",
"code": "",
})
}
this.Data["dnsRoutes"] = dnsRouteMaps
this.Data["dnsRecordName"] = recordName
this.Data["dnsRecordValue"] = recordValue
// 登录信息
var loginMap maps.Map = nil
@@ -217,17 +253,20 @@ func (this *DetailAction) RunGet(params struct {
}
this.Data["node"] = maps.Map{
"id": node.Id,
"name": node.Name,
"ipAddresses": ipAddressMaps,
"cluster": clusterMap,
"login": loginMap,
"installDir": node.InstallDir,
"isInstalled": node.IsInstalled,
"uniqueId": node.UniqueId,
"secret": node.Secret,
"maxCPU": node.MaxCPU,
"isOn": node.IsOn,
"id": node.Id,
"name": node.Name,
"ipAddresses": ipAddressMaps,
"cluster": clusterMap,
"secondaryClusters": secondaryClustersMaps,
"login": loginMap,
"installDir": node.InstallDir,
"isInstalled": node.IsInstalled,
"uniqueId": node.UniqueId,
"secret": node.Secret,
"maxCPU": node.MaxCPU,
"isOn": node.IsOn,
"records": recordMaps,
"routes": routeMaps,
"status": maps.Map{
"isActive": status.IsActive,

View File

@@ -66,44 +66,64 @@ func (this *UpdateAction) RunGet(params struct {
}
// DNS相关
dnsInfoResp, err := this.RPC().NodeRPC().FindEnabledNodeDNS(this.AdminContext(), &pb.FindEnabledNodeDNSRequest{NodeId: params.NodeId})
if err != nil {
this.ErrorPage(err)
return
}
nodeDNS := dnsInfoResp.Node
dnsRouteMaps := []maps.Map{}
if nodeDNS != nil {
for _, dnsInfo := range nodeDNS.Routes {
dnsRouteMaps = append(dnsRouteMaps, maps.Map{
"name": dnsInfo.Name,
"code": dnsInfo.Code,
})
var clusters = []*pb.NodeCluster{node.NodeCluster}
clusters = append(clusters, node.SecondaryNodeClusters...)
var allDNSRouteMaps = map[int64][]maps.Map{} // domain id => routes
var routeMaps = map[int64][]maps.Map{} // domain id => routes
for _, cluster := range clusters {
dnsInfoResp, err := this.RPC().NodeRPC().FindEnabledNodeDNS(this.AdminContext(), &pb.FindEnabledNodeDNSRequest{
NodeId: params.NodeId,
NodeClusterId: cluster.Id,
})
if err != nil {
this.ErrorPage(err)
return
}
}
this.Data["dnsRoutes"] = dnsRouteMaps
this.Data["allDNSRoutes"] = []maps.Map{}
if nodeDNS != nil {
this.Data["dnsDomainId"] = nodeDNS.DnsDomainId
} else {
this.Data["dnsDomainId"] = 0
}
if nodeDNS != nil && nodeDNS.DnsDomainId > 0 {
routesMaps := []maps.Map{}
var dnsInfo = dnsInfoResp.Node
if dnsInfo.DnsDomainId <= 0 || len(dnsInfo.DnsDomainName) == 0 {
continue
}
var domainId = dnsInfo.DnsDomainId
var domainName = dnsInfo.DnsDomainName
if len(dnsInfo.Routes) > 0 {
for _, route := range dnsInfo.Routes {
routeMaps[domainId] = append(routeMaps[domainId], maps.Map{
"domainId": domainId,
"domainName": domainName,
"code": route.Code,
"name": route.Name,
})
}
}
// 所有线路选项
routesResp, err := this.RPC().DNSDomainRPC().FindAllDNSDomainRoutes(this.AdminContext(), &pb.FindAllDNSDomainRoutesRequest{DnsDomainId: dnsInfoResp.Node.DnsDomainId})
if err != nil {
this.ErrorPage(err)
return
}
for _, route := range routesResp.Routes {
routesMaps = append(routesMaps, maps.Map{
"name": route.Name,
"code": route.Code,
allDNSRouteMaps[domainId] = append(allDNSRouteMaps[domainId], maps.Map{
"domainId": domainId,
"domainName": domainName,
"name": route.Name,
"code": route.Code,
})
}
this.Data["allDNSRoutes"] = routesMaps
}
var domainRoutes = []maps.Map{}
for _, m := range routeMaps {
domainRoutes = append(domainRoutes, m...)
}
this.Data["dnsRoutes"] = domainRoutes
var allDomainRoutes = []maps.Map{}
for _, m := range allDNSRouteMaps {
allDomainRoutes = append(allDomainRoutes, m...)
}
this.Data["allDNSRoutes"] = allDomainRoutes
// 登录信息
var loginMap maps.Map = nil
if node.Login != nil {
@@ -188,7 +208,7 @@ func (this *UpdateAction) RunGet(params struct {
}
}
this.Data["node"] = maps.Map{
var m = maps.Map{
"id": node.Id,
"name": node.Name,
"ipAddresses": ipAddressMaps,
@@ -202,23 +222,29 @@ func (this *UpdateAction) RunGet(params struct {
"maxCacheMemoryCapacity": maxCacheMemoryCapacity,
}
// 所有集群
resp, err := this.RPC().NodeClusterRPC().FindAllEnabledNodeClusters(this.AdminContext(), &pb.FindAllEnabledNodeClustersRequest{})
if err != nil {
this.ErrorPage(err)
if node.NodeCluster != nil {
m["primaryCluster"] = maps.Map{
"id": node.NodeCluster.Id,
"name": node.NodeCluster.Name,
}
} else {
m["primaryCluster"] = nil
}
if err != nil {
this.ErrorPage(err)
return
if len(node.SecondaryNodeClusters) > 0 {
var secondaryClusterMaps = []maps.Map{}
for _, cluster := range node.SecondaryNodeClusters {
secondaryClusterMaps = append(secondaryClusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
})
}
m["secondaryClusters"] = secondaryClusterMaps
} else {
m["secondaryClusters"] = []interface{}{}
}
clusterMaps := []maps.Map{}
for _, cluster := range resp.NodeClusters {
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
})
}
this.Data["clusters"] = clusterMaps
this.Data["node"] = m
this.Show()
}
@@ -230,7 +256,8 @@ func (this *UpdateAction) RunPost(params struct {
RegionId int64
Name string
IPAddressesJSON []byte `alias:"ipAddressesJSON"`
ClusterId int64
PrimaryClusterId int64
SecondaryClusterIds []byte
GrantId int64
SshHost string
SshPort int
@@ -256,8 +283,17 @@ func (this *UpdateAction) RunPost(params struct {
Require("请输入节点名称")
// TODO 检查cluster
if params.ClusterId <= 0 {
this.Fail("请选择所在集群")
if params.PrimaryClusterId <= 0 {
this.Fail("请选择节点所在集群")
}
var secondaryClusterIds = []int64{}
if len(params.SecondaryClusterIds) > 0 {
err := json.Unmarshal(params.SecondaryClusterIds, &secondaryClusterIds)
if err != nil {
this.ErrorPage(err)
return
}
}
// IP地址
@@ -325,18 +361,19 @@ func (this *UpdateAction) RunPost(params struct {
// 保存
_, err := this.RPC().NodeRPC().UpdateNode(this.AdminContext(), &pb.UpdateNodeRequest{
NodeId: params.NodeId,
NodeGroupId: params.GroupId,
NodeRegionId: params.RegionId,
Name: params.Name,
NodeClusterId: params.ClusterId,
NodeLogin: loginInfo,
MaxCPU: params.MaxCPU,
IsOn: params.IsOn,
DnsDomainId: params.DnsDomainId,
DnsRoutes: dnsRouteCodes,
MaxCacheDiskCapacity: pbMaxCacheDiskCapacity,
MaxCacheMemoryCapacity: pbMaxCacheMemoryCapacity,
NodeId: params.NodeId,
NodeGroupId: params.GroupId,
NodeRegionId: params.RegionId,
Name: params.Name,
NodeClusterId: params.PrimaryClusterId,
SecondaryNodeClusterIds: secondaryClusterIds,
NodeLogin: loginInfo,
MaxCPU: params.MaxCPU,
IsOn: params.IsOn,
DnsDomainId: params.DnsDomainId,
DnsRoutes: dnsRouteCodes,
MaxCacheDiskCapacity: pbMaxCacheDiskCapacity,
MaxCacheMemoryCapacity: pbMaxCacheMemoryCapacity,
})
if err != nil {
this.ErrorPage(err)

View File

@@ -156,6 +156,16 @@ func (this *NodesAction) RunGet(params struct {
dnsRouteNames = append(dnsRouteNames, route.Name)
}
// 从集群
var secondaryClusterMaps []maps.Map
for _, secondaryCluster := range node.SecondaryNodeClusters {
secondaryClusterMaps = append(secondaryClusterMaps, maps.Map{
"id": secondaryCluster.Id,
"name": secondaryCluster.Name,
"isOn": secondaryCluster.IsOn,
})
}
nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id,
"name": node.Name,
@@ -183,11 +193,12 @@ func (this *NodesAction) RunGet(params struct {
"id": node.NodeCluster.Id,
"name": node.NodeCluster.Name,
},
"isSynced": isSynced,
"ipAddresses": ipAddresses,
"group": groupMap,
"region": regionMap,
"dnsRouteNames": dnsRouteNames,
"secondaryClusters": secondaryClusterMaps,
"isSynced": isSynced,
"ipAddresses": ipAddresses,
"group": groupMap,
"region": regionMap,
"dnsRouteNames": dnsRouteNames,
})
}
this.Data["nodes"] = nodeMaps

View File

@@ -63,6 +63,14 @@ func (this *IndexAction) RunPost(params struct {
// 创建日志
defer this.CreateLog(oplogs.LevelInfo, "修改集群 %d DNS设置", params.ClusterId)
if params.DnsDomainId <= 0 {
this.Fail("请选择集群的主域名")
}
params.Must.
Field("dnsName", params.DnsName).
Require("请输入DNS子域名")
// 检查DNS名称
if len(params.DnsName) > 0 {
if !domainutils.ValidateDomainFormat(params.DnsName) {

View File

@@ -235,6 +235,16 @@ func (this *IndexAction) searchNodes(keyword string) {
dnsRouteNames = append(dnsRouteNames, route.Name)
}
// 从集群
var secondaryClusterMaps []maps.Map
for _, secondaryCluster := range node.SecondaryNodeClusters {
secondaryClusterMaps = append(secondaryClusterMaps, maps.Map{
"id": secondaryCluster.Id,
"name": secondaryCluster.Name,
"isOn": secondaryCluster.IsOn,
})
}
nodeMaps = append(nodeMaps, maps.Map{
"id": node.Id,
"name": node.Name,
@@ -260,11 +270,12 @@ func (this *IndexAction) searchNodes(keyword string) {
"id": node.NodeCluster.Id,
"name": node.NodeCluster.Name,
},
"isSynced": isSynced,
"ipAddresses": ipAddresses,
"group": groupMap,
"region": regionMap,
"dnsRouteNames": dnsRouteNames,
"secondaryClusters": secondaryClusterMaps,
"isSynced": isSynced,
"ipAddresses": ipAddresses,
"group": groupMap,
"region": regionMap,
"dnsRouteNames": dnsRouteNames,
})
}
this.Data["nodes"] = nodeMaps

View File

@@ -20,6 +20,7 @@ func init() {
EndHelpers().
Helper(helpers.NewUserMustAuth(configloaders.AdminModuleCodeCommon)).
Post("/options", new(OptionsAction)).
GetPost("/selectPopup", new(SelectPopupAction)).
EndAll()
})

View File

@@ -0,0 +1,64 @@
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
package clusters
import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/lists"
"github.com/iwind/TeaGo/maps"
)
type SelectPopupAction struct {
actionutils.ParentAction
}
func (this *SelectPopupAction) Init() {
this.Nav("", "", "")
}
func (this *SelectPopupAction) RunGet(params struct {
SelectedClusterIds string
}) {
var selectedIds = utils.SplitNumbers(params.SelectedClusterIds)
countResp, err := this.RPC().NodeClusterRPC().CountAllEnabledNodeClusters(this.AdminContext(), &pb.CountAllEnabledNodeClustersRequest{})
if err != nil {
this.ErrorPage(err)
return
}
var count = countResp.Count
var page = this.NewPage(count)
this.Data["page"] = page.AsHTML()
clustersResp, err := this.RPC().NodeClusterRPC().ListEnabledNodeClusters(this.AdminContext(), &pb.ListEnabledNodeClustersRequest{
Offset: page.Offset,
Size: page.Size,
})
if err != nil {
this.ErrorPage(err)
return
}
var clusterMaps = []maps.Map{}
for _, cluster := range clustersResp.NodeClusters {
// 节点数
countNodesResp, err := this.RPC().NodeRPC().CountAllEnabledNodesMatch(this.AdminContext(), &pb.CountAllEnabledNodesMatchRequest{NodeClusterId: cluster.Id})
if err != nil {
this.ErrorPage(err)
return
}
var countNodes = countNodesResp.Count
clusterMaps = append(clusterMaps, maps.Map{
"id": cluster.Id,
"name": cluster.Name,
"isOn": cluster.IsOn,
"countNodes": countNodes,
"isSelected": lists.ContainsInt64(selectedIds, cluster.Id),
})
}
this.Data["clusters"] = clusterMaps
this.Show()
}

View File

@@ -43,16 +43,21 @@ func (this *ClustersPopupAction) RunGet(params struct {
for _, cluster := range clustersResp.NodeClusters {
isOk := false
if len(cluster.Name) > 0 {
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.AdminContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: params.DomainId,
Name: cluster.DnsName,
Type: "A",
})
if err != nil {
this.ErrorPage(err)
return
for _, recordType := range []string{"A", "AAAA"} {
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.AdminContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: params.DomainId,
Name: cluster.DnsName,
Type: recordType,
})
if err != nil {
this.ErrorPage(err)
return
}
if checkResp.IsOk {
isOk = true
break
}
}
isOk = checkResp.IsOk
}
clusterMaps = append(clusterMaps, maps.Map{

View File

@@ -33,12 +33,17 @@ func ValidateDomainFormat(domain string) bool {
}
// ConvertRoutesToMaps 转换线路列表
func ConvertRoutesToMaps(routes []*pb.DNSRoute) []maps.Map {
func ConvertRoutesToMaps(info *pb.NodeDNSInfo) []maps.Map {
if info == nil {
return []maps.Map{}
}
result := []maps.Map{}
for _, route := range routes {
for _, route := range info.Routes {
result = append(result, maps.Map{
"name": route.Name,
"code": route.Code,
"name": route.Name,
"code": route.Code,
"domainId": info.DnsDomainId,
"domainName": info.DnsDomainName,
})
}
return result

View File

@@ -1,6 +1,7 @@
package domains
import (
"github.com/TeaOSLab/EdgeAdmin/internal/utils"
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
"github.com/iwind/TeaGo/maps"
@@ -55,10 +56,14 @@ func (this *NodesPopupAction) RunGet(params struct {
// 检查是否有域名解析记录
isOk := false
if len(route.Name) > 0 && len(node.IpAddr) > 0 && len(cluster.DnsName) > 0 {
var recordType = "A"
if utils.IsIPv6(node.IpAddr) {
recordType = "AAAA"
}
checkResp, err := this.RPC().DNSDomainRPC().ExistDNSDomainRecord(this.AdminContext(), &pb.ExistDNSDomainRecordRequest{
DnsDomainId: params.DomainId,
Name: cluster.DnsName,
Type: "A",
Type: recordType,
Route: route.Code,
Value: node.IpAddr,
})

View File

@@ -20,11 +20,15 @@ func (this *UpdateNodePopupAction) Init() {
}
func (this *UpdateNodePopupAction) RunGet(params struct {
NodeId int64
ClusterId int64
NodeId int64
}) {
this.Data["nodeId"] = params.NodeId
dnsInfoResp, err := this.RPC().NodeRPC().FindEnabledNodeDNS(this.AdminContext(), &pb.FindEnabledNodeDNSRequest{NodeId: params.NodeId})
dnsInfoResp, err := this.RPC().NodeRPC().FindEnabledNodeDNS(this.AdminContext(), &pb.FindEnabledNodeDNSRequest{
NodeId: params.NodeId,
NodeClusterId: params.ClusterId,
})
if err != nil {
this.ErrorPage(err)
return
@@ -35,7 +39,7 @@ func (this *UpdateNodePopupAction) RunGet(params struct {
return
}
this.Data["ipAddr"] = dnsInfo.IpAddr
this.Data["routes"] = domainutils.ConvertRoutesToMaps(dnsInfo.Routes)
this.Data["routes"] = domainutils.ConvertRoutesToMaps(dnsInfo)
this.Data["domainId"] = dnsInfo.DnsDomainId
this.Data["domainName"] = dnsInfo.DnsDomainName
@@ -50,13 +54,17 @@ func (this *UpdateNodePopupAction) RunGet(params struct {
if len(routesResp.Routes) > 0 {
for _, route := range routesResp.Routes {
allRouteMaps = append(allRouteMaps, maps.Map{
"name": route.Name,
"code": route.Code,
"name": route.Name,
"code": route.Code,
"domainName": dnsInfo.DnsDomainName,
"domainId": dnsInfo.DnsDomainId,
})
}
// 筛选
this.Data["routes"] = domainutils.ConvertRoutesToMaps(domainutils.FilterRoutes(dnsInfo.Routes, routesResp.Routes))
var routes = domainutils.FilterRoutes(dnsInfo.Routes, routesResp.Routes)
dnsInfo.Routes = routes
this.Data["routes"] = domainutils.ConvertRoutesToMaps(dnsInfo)
}
}
this.Data["allRoutes"] = allRouteMaps

View File

@@ -10,12 +10,16 @@ type DeleteAction struct {
}
func (this *DeleteAction) RunPost(params struct {
NodeId int64
ClusterId int64
NodeId int64
}) {
// 创建日志
defer this.CreateLogInfo("删除节点", params.NodeId)
defer this.CreateLogInfo("从集群 %d 中删除节点 %d", params.ClusterId, params.NodeId)
_, err := this.RPC().NodeRPC().DeleteNode(this.AdminContext(), &pb.DeleteNodeRequest{NodeId: params.NodeId})
_, err := this.RPC().NodeRPC().DeleteNodeFromNodeCluster(this.AdminContext(), &pb.DeleteNodeFromNodeClusterRequest{
NodeId: params.NodeId,
NodeClusterId: params.ClusterId,
})
if err != nil {
this.ErrorPage(err)
return

View File

@@ -1,3 +1,4 @@
// 单个集群选择
Vue.component("cluster-selector", {
mounted: function () {
let that = this

View File

@@ -0,0 +1,28 @@
// 显示节点的多个集群
Vue.component("node-clusters-labels", {
props: ["v-primary-cluster", "v-secondary-clusters", "size"],
data: function () {
let cluster = this.vPrimaryCluster
let secondaryClusters = this.vSecondaryClusters
if (secondaryClusters == null) {
secondaryClusters = []
}
let labelSize = this.size
if (labelSize == null) {
labelSize = "small"
}
if (labelSize == "tiny") {
labelSize += " olive"
}
return {
cluster: cluster,
secondaryClusters: secondaryClusters,
labelSize: labelSize
}
},
template: `<div>
<a v-if="cluster != null" :href="'/clusters/cluster?clusterId=' + cluster.id" class="ui label basic" :class="labelSize" title="主集群" style="margin-bottom: 0.3em;">{{cluster.name}}</a>
<a v-for="c in secondaryClusters" :href="'/clusters/cluster?clusterId=' + c.id" class="ui label basic" :class="labelSize" title="从集群" style="margin-bottom: 0.3em;"><span class="grey" style="text-decoration: none">{{c.name}}</span></a>
</div>`
})

View File

@@ -0,0 +1,98 @@
// 一个节点的多个集群选择器
Vue.component("node-clusters-selector", {
props: ["v-primary-cluster", "v-secondary-clusters"],
data: function () {
let primaryCluster = this.vPrimaryCluster
let secondaryClusters = this.vSecondaryClusters
if (secondaryClusters == null) {
secondaryClusters = []
}
return {
primaryClusterId: (primaryCluster == null) ? 0 : primaryCluster.id,
secondaryClusterIds: secondaryClusters.map(function (v) {
return v.id
}),
primaryCluster: primaryCluster,
secondaryClusters: secondaryClusters
}
},
methods: {
addPrimary: function () {
let that = this
let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds)
teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=single", {
height: "38em",
width: "50em",
callback: function (resp) {
if (resp.data.cluster != null) {
that.primaryCluster = resp.data.cluster
that.primaryClusterId = that.primaryCluster.id
that.notifyChange()
}
}
})
},
removePrimary: function () {
this.primaryClusterId = 0
this.primaryCluster = null
this.notifyChange()
},
addSecondary: function () {
let that = this
let selectedClusterIds = [this.primaryClusterId].concat(this.secondaryClusterIds)
teaweb.popup("/clusters/selectPopup?selectedClusterIds=" + selectedClusterIds.join(",") + "&mode=multiple", {
height: "38em",
width: "50em",
callback: function (resp) {
if (resp.data.cluster != null) {
that.secondaryClusterIds.push(resp.data.cluster.id)
that.secondaryClusters.push(resp.data.cluster)
that.notifyChange()
}
}
})
},
removeSecondary: function (index) {
this.secondaryClusterIds.$remove(index)
this.secondaryClusters.$remove(index)
this.notifyChange()
},
notifyChange: function () {
this.$emit("change", {
clusterId: this.primaryClusterId
})
}
},
template: `<div>
<input type="hidden" name="primaryClusterId" :value="primaryClusterId"/>
<input type="hidden" name="secondaryClusterIds" :value="JSON.stringify(secondaryClusterIds)"/>
<table class="ui table">
<tr>
<td class="title">主集群</td>
<td>
<div v-if="primaryCluster != null">
<div class="ui label basic small">{{primaryCluster.name}} &nbsp; <a href="" title="删除" @click.prevent="removePrimary"><i class="icon remove small"></i></a> </div>
</div>
<div style="margin-top: 0.6em" v-if="primaryClusterId == 0">
<button class="ui button tiny" type="button" @click.prevent="addPrimary">+</button>
</div>
<p class="comment">多个集群配置有冲突时,优先使用主集群配置。</p>
</td>
</tr>
<tr>
<td>从集群</td>
<td>
<div v-if="secondaryClusters.length > 0">
<div class="ui label basic small" v-for="(cluster, index) in secondaryClusters"><span class="grey">{{cluster.name}}</span> &nbsp; <a href="" title="删除" @click.prevent="removeSecondary(index)"><i class="icon remove small"></i></a> </div>
</div>
<div style="margin-top: 0.6em">
<button class="ui button tiny" type="button" @click.prevent="addSecondary">+</button>
</div>
</td>
</tr>
</table>
</div>`
})

View File

@@ -8,7 +8,7 @@ Vue.component("dns-route-selector", {
return {
routes: routes,
routeCodes: routes.$map(function (k, v) {
return v.code
return v.code + "@" + v.domainId
}),
isAdding: false,
routeCode: ""
@@ -31,7 +31,7 @@ Vue.component("dns-route-selector", {
}
let that = this
let route = this.vAllRoutes.$find(function (k, v) {
return v.code == that.routeCode
return v.code + "@" + v.domainId == that.routeCode
})
if (route == null) {
return
@@ -53,8 +53,8 @@ Vue.component("dns-route-selector", {
template: `<div>
<input type="hidden" name="dnsRoutesJSON" :value="JSON.stringify(routeCodes)"/>
<div v-if="routes.length > 0">
<tiny-basic-label v-for="route in routes" :key="route.code">
{{route.name}} <a href="" @click.prevent="remove(route)"><i class="icon remove"></i></a>
<tiny-basic-label v-for="route in routes" :key="route.code + '@' + route.domainId">
{{route.name}} <span class="grey small">{{route.domainName}}</span><a href="" @click.prevent="remove(route)"><i class="icon remove"></i></a>
</tiny-basic-label>
<div class="ui divider"></div>
</div>
@@ -64,7 +64,7 @@ Vue.component("dns-route-selector", {
<div class="ui field">
<select class="ui dropdown auto-width" v-model="routeCode">
<option value="">[请选择]</option>
<option v-for="route in vAllRoutes" :value="route.code">{{route.name}}</option>
<option v-for="route in vAllRoutes" :value="route.code + '@' + route.domainId">{{route.name}}{{route.domainName}}</option>
</select>
</div>
<div class="ui field">

View File

@@ -317,6 +317,27 @@ window.teaweb = {
Swal.fire(config);
},
toast: function (message, timeout, callback) {
if (timeout == null) {
timeout = 2000
}
var width = "20em";
if (message.length > 30) {
width = "30em";
}
Swal.fire({
text: message,
icon: "info",
width: width,
timer: timeout,
showConfirmButton: false,
onAfterClose: function () {
if (typeof callback == "function") {
callback()
}
}
});
},
successToast: function (message, timeout, callback) {
if (timeout == null) {
timeout = 2000

File diff suppressed because one or more lines are too long

View File

@@ -440,6 +440,7 @@ body.expanded .main {
.main-menu.theme1 {
background: #14539A !important;
.menu {
background: #14539A !important;
}
@@ -447,6 +448,7 @@ body.expanded .main {
.main-menu.theme2 {
background: #276AC6 !important;
.menu {
background: #276AC6 !important;
}
@@ -454,6 +456,7 @@ body.expanded .main {
.main-menu.theme3 {
background: #007D9C !important;
.menu {
background: #007D9C !important;
}

View File

@@ -1,5 +1,5 @@
<second-menu>
<menu-item :href="'/clusters/cluster/nodes?clusterId=' + clusterId">节点列表</menu-item>
<menu-item :href="'/clusters/cluster/nodes?clusterId=' + clusterId">节点列表</menu-item>
<span class="item disabled">|</span>
<menu-item :href="'/clusters/cluster/node?clusterId=' + clusterId + '&nodeId=' + node.id" code="node"
v-if="!teaIsPlus">"{{node.name}}"节点详情</menu-item>
@@ -7,6 +7,6 @@
<menu-item :href="'/clusters/cluster/node/detail?clusterId=' + clusterId + '&nodeId=' + node.id" code="node" v-if="teaIsPlus">节点详情</menu-item>
<menu-item :href="'/clusters/cluster/node/thresholds?clusterId=' + clusterId + '&nodeId=' + node.id" code="threshold" v-if="teaIsPlus">阈值设置</menu-item>
<menu-item :href="'/clusters/cluster/node/logs?clusterId=' + clusterId + '&nodeId=' + node.id" code="log">运行日志</menu-item>
<menu-item :href="'/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + node.id" code="install">安装节点</menu-item>
<menu-item :href="'/clusters/cluster/node/update?clusterId=' + clusterId + '&nodeId=' + node.id" code="update">修改设置</menu-item>
<menu-item :href="'/clusters/cluster/node/install?clusterId=' + clusterId + '&nodeId=' + node.id" code="install">安装节点</menu-item>
</second-menu>

View File

@@ -12,6 +12,12 @@
<td>状态</td>
<td><label-on :v-is-on="node.isOn"></label-on></td>
</tr>
<tr>
<td>所属集群</td>
<td>
<node-clusters-labels :v-primary-cluster="node.cluster" :v-secondary-clusters="node.secondaryClusters"></node-clusters-labels>
</td>
</tr>
<tr>
<td>IP地址</td>
<td>
@@ -29,38 +35,34 @@
</div>
</td>
</tr>
<tr v-if="dnsRoutes.length > 0 && dnsRoutes[0].name.length > 0">
<tr v-if="node.routes.length > 0">
<td>DNS线路</td>
<td>
<span class="ui label tiny basic" v-for="route in dnsRoutes">{{route.name}}</span>
<span class="ui label tiny basic" v-for="route in node.routes">{{route.name}} <span class="grey small">{{route.domainName}}</span></span>
</td>
</tr>
<tr v-if="dnsRecordName.length > 0 && dnsRecordValue.length > 0">
<tr v-if="node.records.length > 0">
<td>DNS记录</td>
<td>
<table class="ui table celled">
<thead class="full-width">
<tr>
<th>记录名</th>
<th>记录类型</th>
<th>线路</th>
<th>记录值</th>
</tr>
<tr>
<th>记录名</th>
<th>记录类型</th>
<th>线路</th>
<th>记录值</th>
</tr>
</thead>
<tbody v-for="address in node.ipAddresses" v-if="address.canAccess">
<tr v-for="route in dnsRoutes">
<td>{{dnsRecordName}}</td>
<td>
<span v-if="address.ip.indexOf(':') > -1">AAAA</span>
<span v-else>A</span>
</td>
<td>
<span v-if="route.name.length > 0">{{route.name}}</span>
<span v-else class="disabled">默认</span>
</td>
<td>{{address.ip}}</td>
</tr>
</tbody>
<tr v-for="record in node.records">
<td>{{record.name}}</td>
<td>{{record.type}}</td>
<td>
<span v-if="record.route.length > 0">{{record.route}}</span>
<span v-else class="disabled">默认</span>
</td>
<td>{{record.value}}</td>
</tr>
</table>
<p class="comment">通过设置A记录可以将集群上的服务请求转发到不同线路的节点上。</p>
</td>

View File

@@ -13,6 +13,12 @@
<input type="text" name="name" maxlength="50" ref="focus" v-model="node.name"/>
</td>
</tr>
<tr>
<td>所属集群</td>
<td>
<node-clusters-selector :v-primary-cluster="node.primaryCluster" :v-secondary-clusters="node.secondaryClusters" @change="changeClusters"></node-clusters-selector>
</td>
</tr>
<tr>
<td>IP地址 *</td>
<td>
@@ -20,22 +26,13 @@
<p class="comment">用于访问节点和域名解析等。</p>
</td>
</tr>
<tr v-if="allDNSRoutes.length > 0">
<tr v-if="allDNSRoutes.length > 0">
<td>DNS线路</td>
<td>
<input type="hidden" name="dnsDomainId" :value="dnsDomainId"/>
<dns-route-selector :v-all-routes="allDNSRoutes" :v-routes="dnsRoutes"></dns-route-selector>
<p class="comment">当前节点对应的DNS线路可用线路是根据集群设置的域名获取的注意DNS服务商可能对这些线路有其他限制。</p>
</td>
</tr>
<tr>
<td>所属集群</td>
<td>
<select class="ui dropdown" name="clusterId" style="width:10em" v-model="clusterId">
<option v-for="cluster in clusters" :value="cluster.id">{{cluster.name}}</option>
</select>
</td>
</tr>
<tr>
<td>所属区域</td>
<td>

View File

@@ -1,27 +1,32 @@
Tea.context(function () {
this.clusterId = 0;
this.clusterId = 0
if (this.node.cluster != null && this.node.cluster.id > 0) {
this.clusterId = this.node.cluster.id;
this.clusterId = this.node.cluster.id
}
this.success = NotifySuccess("保存成功", "/clusters/cluster/node?clusterId=" + this.clusterId + "&nodeId=" + this.node.id);
this.success = function () {
let that = this
teaweb.success("保存成功", function () {
window.location = "/clusters/cluster/node/detail?clusterId=" + that.clusterId + "&nodeId=" + that.node.id
})
}
// IP地址相关
this.ipAddresses = this.node.ipAddresses;
this.ipAddresses = this.node.ipAddresses
// 认证相关
this.grant = null;
this.grant = null
this.sshHost = "";
this.sshPort = "";
this.loginId = 0;
this.sshHost = ""
this.sshPort = ""
this.loginId = 0
if (this.node.login != null) {
this.loginId = this.node.login.id;
this.loginId = this.node.login.id
if (this.node.login.params != null) {
this.sshHost = this.node.login.params.host;
this.sshHost = this.node.login.params.host
if (this.node.login.params.port > 0) {
this.sshPort = this.node.login.params.port;
this.sshPort = this.node.login.params.port
}
}
@@ -31,7 +36,11 @@ Tea.context(function () {
name: this.node.login.grant.name,
method: this.node.login.grant.method,
methodName: this.node.login.grant.methodName
};
}
}
}
});
this.changeClusters = function (info) {
this.clusterId = info.clusterId
}
})

View File

@@ -68,10 +68,13 @@
<tr v-for="node in nodes">
<td>{{node.name}}
<div style="margin-top: 0.5em" v-if="node.region != null">
<tiny-basic-label>区域:{{node.region.name}}</tiny-basic-label>
<tiny-basic-label class="olive">区域:{{node.region.name}}</tiny-basic-label>
</div>
<div style="margin-top: 0.5em" v-if="node.group != null">
<tiny-basic-label>分组:{{node.group.name}}</tiny-basic-label>
<tiny-basic-label class="olive">分组:{{node.group.name}}</tiny-basic-label>
</div>
<div style="margin-top: 0.5em">
<node-clusters-labels :v-primary-cluster="node.cluster" :v-secondary-clusters="node.secondaryClusters" size="tiny"></node-clusters-labels>
</div>
</td>
<td>

View File

@@ -2,9 +2,10 @@ Tea.context(function () {
this.teaweb = teaweb
this.deleteNode = function (nodeId) {
teaweb.confirm("确定要删除这个节点吗?", function () {
teaweb.confirm("确定要从当前集群中删除这个节点吗?", function () {
this.$post("/nodes/delete")
.params({
clusterId: this.clusterId,
nodeId: nodeId
})
.refresh();

View File

@@ -9,14 +9,14 @@
<table class="ui table selectable definition">
<tr v-if="hasDomains">
<td>选择主域名</td>
<td>选择主域名 *</td>
<td>
<dns-domain-selector :v-domain-id="domainId" :v-domain-name="domainName"></dns-domain-selector>
<p class="comment">用于生成集群节点和网站服务的DNS解析记录。</p>
</td>
</tr>
<tr>
<td class="title">DNS子域名</td>
<td class="title">DNS子域名 *</td>
<td>
<div class="ui input right labeled">
<input type="text" name="dnsName" maxlength="64" style="width:10em" v-model="dnsName"/>

View File

@@ -0,0 +1,4 @@
h4 span {
font-size: 0.8em;
}
/*# sourceMappingURL=create.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["create.less"],"names":[],"mappings":"AAAA,EAAG;EACF,gBAAA","file":"create.css"}

View File

@@ -13,17 +13,19 @@
<td>默认缓存设置 *</td>
<td>
<http-cache-policy-selector></http-cache-policy-selector>
<p class="comment">此全局设置不会强制应用到每个网站服务。</p>
</td>
</tr>
<tr>
<td>默认WAF设置 *</td>
<td>
<http-firewall-policy-selector></http-firewall-policy-selector>
<p class="comment">此全局设置不会强制应用到每个网站服务。</p>
</td>
</tr>
</table>
<h4>节点安装选项</h4>
<h4>节点安装选项 &nbsp;<span class="grey small">(可选)</span></h4>
<table class="ui table selectable definition">
<tr>
<td class="title">默认SSH登录方式</td>
@@ -49,7 +51,7 @@
</tr>
</table>
<h4>DNS设置选项</h4>
<h4>DNS设置选项 &nbsp;<span class="grey small">(可选)</span></h4>
<table class="ui table selectable definition">
<tr v-if="hasDomains">
<td>选择主域名</td>

View File

@@ -0,0 +1,3 @@
h4 span {
font-size: 0.8em;
}

View File

@@ -93,7 +93,7 @@
<tr v-for="node in nodes">
<td><keyword :v-word="keyword">{{node.name}}</keyword>
<div style="margin-top: 0.5em">
<span class="ui label tiny">集群:{{node.cluster.name}}</span>
<node-clusters-labels :v-primary-cluster="node.cluster" :v-secondary-clusters="node.secondaryClusters" size="tiny"></node-clusters-labels>
</div>
</td>
<td>

View File

@@ -0,0 +1,25 @@
{$layout "layout_popup"}
<h3>选择集群</h3>
<table class="ui table celled selectable">
<thead>
<tr>
<th>集群名称</th>
<th>节点数</th>
<th class="two wide">状态</th>
<th class="two op">操作</th>
</tr>
</thead>
<tr v-for="cluster in clusters">
<td>{{cluster.name}}</td>
<td>{{cluster.countNodes}}</td>
<td><label-on :v-is-on="cluster.isOn"></label-on></td>
<td>
<span class="disabled" v-if="cluster.isSelected">已选择</span>
<a href="" @click.prevent="select(cluster)" v-if="!cluster.isSelected">选择</a>
</td>
</tr>
</table>
<div v-html="page"></div>

View File

@@ -0,0 +1,11 @@
Tea.context(function () {
this.select = function (cluster) {
NotifyPopup({
code: 200,
message: "",
data: {
cluster: cluster
}
})
}
})

View File

@@ -71,7 +71,7 @@
<link-red @click.prevent="updateCluster(issue.targetId)">修复</link-red>
</div>
<div v-if="issue.type == 'node'">
<link-red @click.prevent="updateNode(issue.targetId)">修复</link-red>
<link-red @click.prevent="updateNode(issue.params.clusterId, issue.targetId)">修复</link-red>
</div>
</td>
</tr>
@@ -108,18 +108,18 @@
</td>
<td>
<span v-if="node.ipAddr.length > 0">{{node.ipAddr}}</span>
<link-red title="点击设置" v-else @click.prevent="updateNode(node.id)">没有设置</link-red>
<link-red title="点击设置" v-else @click.prevent="updateNode(node.clusterId, node.id)">没有设置</link-red>
</td>
<td>
<span v-if="node.route.code.length > 0">{{node.route.name}}</span>
<link-red v-else title="点击设置" @click.prevent="updateNode(node.id)">没有设置</link-red>
<link-red v-else title="点击设置" @click.prevent="updateNode(node.clusterId, node.id)">没有设置</link-red>
</td>
<td>
<span class="green" v-if="node.isResolved">已解析</span>
<span v-else class="red">未解析</span>
</td>
<td>
<link-popup @click.prevent="updateNode(node.id)">修改</link-popup>
<link-popup @click.prevent="updateNode(node.clusterId, node.id)">修改</link-popup>
</td>
</tr>
</table>

View File

@@ -10,8 +10,8 @@ Tea.context(function () {
})
}
this.updateNode = function (nodeId) {
teaweb.popup("/dns/issues/updateNodePopup?nodeId=" + nodeId, {
this.updateNode = function (clusterId, nodeId) {
teaweb.popup("/dns/issues/updateNodePopup?clusterId=" + clusterId + "&nodeId=" + nodeId, {
height: "26em",
callback: function () {
teaweb.success("保存成功", function () {

View File

@@ -43,7 +43,7 @@
<link-red @click.prevent="updateCluster(issue.targetId)">修复</link-red>
</div>
<div v-if="issue.type == 'node'">
<link-red @click.prevent="updateNode(issue.targetId)">修复</link-red>
<link-red @click.prevent="updateNode(issue.params.clusterId, issue.targetId)">修复</link-red>
</div>
</td>
</tr>

View File

@@ -17,9 +17,9 @@ Tea.context(function () {
})
}
this.updateNode = function (nodeId) {
this.updateNode = function (clusterId, nodeId) {
let that = this
teaweb.popup("/dns/issues/updateNodePopup?nodeId=" + nodeId, {
teaweb.popup("/dns/issues/updateNodePopup?clusterId=" + clusterId + "&nodeId=" + nodeId, {
callback: function () {
teaweb.success("保存成功", function () {
that.reload()