Initial commit (code only without large binaries)
This commit is contained in:
12
EdgeAPI/internal/installers/credentials.go
Normal file
12
EdgeAPI/internal/installers/credentials.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package installers
|
||||
|
||||
type Credentials struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
PrivateKey string
|
||||
Passphrase string
|
||||
Method string
|
||||
Sudo bool
|
||||
}
|
||||
79
EdgeAPI/internal/installers/deploy_file.go
Normal file
79
EdgeAPI/internal/installers/deploy_file.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package installers
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// DeployFile 部署文件描述
|
||||
type DeployFile struct {
|
||||
OS string
|
||||
Arch string
|
||||
Version string
|
||||
Path string
|
||||
}
|
||||
|
||||
// Sum 计算概要
|
||||
func (this *DeployFile) Sum() (string, error) {
|
||||
fp, err := os.Open(this.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
m := md5.New()
|
||||
buffer := make([]byte, 128*1024)
|
||||
for {
|
||||
n, err := fp.Read(buffer)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
_, err = m.Write(buffer[:n])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
sum := m.Sum(nil)
|
||||
return fmt.Sprintf("%x", sum), nil
|
||||
}
|
||||
|
||||
// Read 读取一个片段数据
|
||||
func (this *DeployFile) Read(offset int64) (data []byte, newOffset int64, err error) {
|
||||
fp, err := os.Open(this.Path)
|
||||
if err != nil {
|
||||
return nil, offset, err
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
stat, err := fp.Stat()
|
||||
if err != nil {
|
||||
return nil, offset, err
|
||||
}
|
||||
if offset >= stat.Size() {
|
||||
return nil, offset, io.EOF
|
||||
}
|
||||
|
||||
_, err = fp.Seek(offset, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, offset, err
|
||||
}
|
||||
|
||||
buffer := make([]byte, 128*1024)
|
||||
n, err := fp.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, offset, err
|
||||
}
|
||||
|
||||
return buffer[:n], offset + int64(n), nil
|
||||
}
|
||||
36
EdgeAPI/internal/installers/deploy_file_test.go
Normal file
36
EdgeAPI/internal/installers/deploy_file_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package installers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeployFile_Sum(t *testing.T) {
|
||||
d := &DeployFile{Path: "deploy_test.txt"}
|
||||
sum, err := d.Sum()
|
||||
if err != nil {
|
||||
t.Log("err:", err)
|
||||
return
|
||||
}
|
||||
t.Log("sum:", sum)
|
||||
}
|
||||
|
||||
func TestDeployFile_Read(t *testing.T) {
|
||||
d := &DeployFile{Path: "deploy_test.txt"}
|
||||
|
||||
var offset int64
|
||||
for i := 0; i < 3; i++ {
|
||||
data, newOffset, err := d.Read(offset)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
t.Log("err: ", err)
|
||||
return
|
||||
}
|
||||
t.Log("offset:", newOffset, "data:", string(data))
|
||||
offset = newOffset
|
||||
}
|
||||
}
|
||||
151
EdgeAPI/internal/installers/deploy_manager.go
Normal file
151
EdgeAPI/internal/installers/deploy_manager.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/files"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var SharedDeployManager = NewDeployManager()
|
||||
|
||||
// DeployManager 节点部署文件管理器
|
||||
// 如果节点部署文件有变化,需要重启API节点以便于生效
|
||||
type DeployManager struct {
|
||||
dir string
|
||||
|
||||
nodeFiles []*DeployFile
|
||||
nsNodeFiles []*DeployFile
|
||||
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
// NewDeployManager 获取新节点部署文件管理器
|
||||
func NewDeployManager() *DeployManager {
|
||||
var manager = &DeployManager{
|
||||
dir: Tea.Root + "/deploy",
|
||||
}
|
||||
manager.LoadNodeFiles()
|
||||
manager.LoadNSNodeFiles()
|
||||
return manager
|
||||
}
|
||||
|
||||
// LoadNodeFiles 加载所有边缘节点文件
|
||||
func (this *DeployManager) LoadNodeFiles() []*DeployFile {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
if len(this.nodeFiles) > 0 {
|
||||
return this.nodeFiles
|
||||
}
|
||||
|
||||
var keyMap = map[string]*DeployFile{} // key => File
|
||||
|
||||
var reg = regexp.MustCompile(`^edge-node-(\w+)-(\w+)-v([0-9.]+)\.zip$`)
|
||||
for _, file := range files.NewFile(this.dir).List() {
|
||||
var name = file.Name()
|
||||
if !reg.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
var matches = reg.FindStringSubmatch(name)
|
||||
var osName = matches[1]
|
||||
var arch = matches[2]
|
||||
var version = matches[3]
|
||||
|
||||
var key = osName + "_" + arch
|
||||
oldFile, ok := keyMap[key]
|
||||
if ok && stringutil.VersionCompare(oldFile.Version, version) > 0 {
|
||||
continue
|
||||
}
|
||||
keyMap[key] = &DeployFile{
|
||||
OS: osName,
|
||||
Arch: arch,
|
||||
Version: version,
|
||||
Path: file.Path(),
|
||||
}
|
||||
}
|
||||
|
||||
var result = []*DeployFile{}
|
||||
for _, v := range keyMap {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
this.nodeFiles = result
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindNodeFile 查找特别平台的节点文件
|
||||
func (this *DeployManager) FindNodeFile(os string, arch string) *DeployFile {
|
||||
for _, file := range this.LoadNodeFiles() {
|
||||
if file.OS == os && file.Arch == arch {
|
||||
return file
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadNSNodeFiles 加载所有NS节点安装文件
|
||||
func (this *DeployManager) LoadNSNodeFiles() []*DeployFile {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
if len(this.nsNodeFiles) > 0 {
|
||||
return this.nsNodeFiles
|
||||
}
|
||||
|
||||
var keyMap = map[string]*DeployFile{} // key => File
|
||||
|
||||
var reg = regexp.MustCompile(`^edge-dns-(\w+)-(\w+)-v([0-9.]+)\.zip$`)
|
||||
for _, file := range files.NewFile(this.dir).List() {
|
||||
var name = file.Name()
|
||||
if !reg.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
var matches = reg.FindStringSubmatch(name)
|
||||
var osName = matches[1]
|
||||
var arch = matches[2]
|
||||
var version = matches[3]
|
||||
|
||||
var key = osName + "_" + arch
|
||||
oldFile, ok := keyMap[key]
|
||||
if ok && stringutil.VersionCompare(oldFile.Version, version) > 0 {
|
||||
continue
|
||||
}
|
||||
keyMap[key] = &DeployFile{
|
||||
OS: osName,
|
||||
Arch: arch,
|
||||
Version: version,
|
||||
Path: file.Path(),
|
||||
}
|
||||
}
|
||||
|
||||
var result = []*DeployFile{}
|
||||
for _, v := range keyMap {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
this.nsNodeFiles = result
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindNSNodeFile 查找特别平台的NS节点安装文件
|
||||
func (this *DeployManager) FindNSNodeFile(os string, arch string) *DeployFile {
|
||||
for _, file := range this.LoadNSNodeFiles() {
|
||||
if file.OS == os && file.Arch == arch {
|
||||
return file
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload 重置缓存
|
||||
func (this *DeployManager) Reload() {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
this.nodeFiles = nil
|
||||
this.nsNodeFiles = nil
|
||||
}
|
||||
23
EdgeAPI/internal/installers/deploy_manager_test.go
Normal file
23
EdgeAPI/internal/installers/deploy_manager_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package installers
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDeployManager_LoadNodeFiles(t *testing.T) {
|
||||
files := NewDeployManager().LoadNodeFiles()
|
||||
for _, file := range files {
|
||||
t.Logf("%#v", file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployManager_LoadNSNodeFiles(t *testing.T) {
|
||||
files := NewDeployManager().LoadNSNodeFiles()
|
||||
for _, file := range files {
|
||||
t.Logf("%#v", file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployManager_FindNSNodeFile(t *testing.T) {
|
||||
file := NewDeployManager().FindNSNodeFile("linux", "amd64")
|
||||
t.Log(file)
|
||||
|
||||
}
|
||||
7
EdgeAPI/internal/installers/env.go
Normal file
7
EdgeAPI/internal/installers/env.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package installers
|
||||
|
||||
type Env struct {
|
||||
OS string
|
||||
Arch string
|
||||
HelperPath string
|
||||
}
|
||||
27
EdgeAPI/internal/installers/errors.go
Normal file
27
EdgeAPI/internal/installers/errors.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package installers
|
||||
|
||||
type GrantError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func newGrantError(err string) *GrantError {
|
||||
return &GrantError{err: err}
|
||||
}
|
||||
|
||||
func (this *GrantError) Error() string {
|
||||
return this.err
|
||||
}
|
||||
|
||||
func (this *GrantError) String() string {
|
||||
return this.err
|
||||
}
|
||||
|
||||
func IsGrantError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := err.(*GrantError)
|
||||
return ok
|
||||
}
|
||||
832
EdgeAPI/internal/installers/fluent_bit.go
Normal file
832
EdgeAPI/internal/installers/fluent_bit.go
Normal file
@@ -0,0 +1,832 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
slashpath "path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/systemconfigs"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
)
|
||||
|
||||
const (
|
||||
fluentBitConfigDir = "/etc/fluent-bit"
|
||||
fluentBitStorageDir = "/var/lib/fluent-bit/storage"
|
||||
fluentBitMainConfigFile = "/etc/fluent-bit/fluent-bit.conf"
|
||||
fluentBitParsersFile = "/etc/fluent-bit/parsers.conf"
|
||||
fluentBitManagedMetaFile = "/etc/fluent-bit/.edge-managed.json"
|
||||
fluentBitManagedEnvFile = "/etc/fluent-bit/.edge-managed.env"
|
||||
fluentBitDropInDir = "/etc/systemd/system/fluent-bit.service.d"
|
||||
fluentBitDropInFile = "/etc/systemd/system/fluent-bit.service.d/edge-managed.conf"
|
||||
fluentBitServiceName = "fluent-bit"
|
||||
fluentBitDefaultBinPath = "/opt/fluent-bit/bin/fluent-bit"
|
||||
fluentBitLocalPackagesRoot = "packages"
|
||||
fluentBitHTTPPathPattern = "/var/log/edge/edge-node/*.log"
|
||||
fluentBitDNSPathPattern = "/var/log/edge/edge-dns/*.log"
|
||||
fluentBitManagedMarker = "managed-by-edgeapi"
|
||||
fluentBitRoleNode = "node"
|
||||
fluentBitRoleDNS = "dns"
|
||||
)
|
||||
|
||||
var errFluentBitLocalPackageNotFound = errors.New("fluent-bit local package not found")
|
||||
|
||||
var fluentBitPackageFileMapping = map[string]string{
|
||||
"ubuntu22.04-amd64": "fluent-bit_4.2.2_amd64.deb",
|
||||
"ubuntu22.04-arm64": "fluent-bit_4.2.2_arm64.deb",
|
||||
"amzn2023-amd64": "fluent-bit-4.2.2-1.x86_64.rpm",
|
||||
"amzn2023-arm64": "fluent-bit-4.2.2-1.aarch64.rpm",
|
||||
}
|
||||
|
||||
type fluentBitManagedMeta struct {
|
||||
Roles []string `json:"roles"`
|
||||
Hash string `json:"hash"`
|
||||
UpdatedAt int64 `json:"updatedAt"`
|
||||
SourceVersion string `json:"sourceVersion"`
|
||||
}
|
||||
|
||||
type fluentBitDesiredConfig struct {
|
||||
Roles []string
|
||||
ClickHouse *systemconfigs.ClickHouseSetting
|
||||
HTTPPathPattern string
|
||||
DNSPathPattern string
|
||||
}
|
||||
|
||||
// SetupFluentBit 安装并托管 Fluent Bit 配置(离线包 + 平台渲染配置)。
|
||||
func (this *BaseInstaller) SetupFluentBit(role nodeconfigs.NodeRole) error {
|
||||
if this.client == nil {
|
||||
return errors.New("ssh client is nil")
|
||||
}
|
||||
|
||||
uname := this.uname()
|
||||
if !strings.Contains(uname, "Linux") {
|
||||
return nil
|
||||
}
|
||||
|
||||
tempDir := strings.TrimRight(this.client.UserHome(), "/") + "/.edge-fluent-bit"
|
||||
_, _, _ = this.client.Exec("mkdir -p " + shQuote(tempDir))
|
||||
defer func() {
|
||||
_, _, _ = this.client.Exec("rm -rf " + shQuote(tempDir))
|
||||
}()
|
||||
|
||||
if err := this.ensureFluentBitInstalled(tempDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, stderr, err := this.client.Exec("mkdir -p " + shQuote(fluentBitConfigDir) + " " + shQuote(fluentBitStorageDir))
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare fluent-bit directories failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
|
||||
parserContent, err := this.readLocalParsersContent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingMeta, err := this.readManagedMeta()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mergedRoles, err := mergeManagedRoles(existingMeta, role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
desiredConfig, err := this.buildDesiredFluentBitConfig(mergedRoles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configChanged, err := this.applyManagedConfig(tempDir, desiredConfig, parserContent, existingMeta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
binPath, err := this.lookupFluentBitBinPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := this.ensureFluentBitService(tempDir, binPath, configChanged); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) ensureFluentBitInstalled(tempDir string) error {
|
||||
binPath, _ := this.lookupFluentBitBinPath()
|
||||
if binPath != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
platformKey, packageName, arch, err := this.detectRemotePlatformAndPackage()
|
||||
if err != nil {
|
||||
return fmt.Errorf("detect fluent-bit platform failed: %w", err)
|
||||
}
|
||||
|
||||
if err := this.installFluentBitFromLocalPackage(tempDir, arch, packageName); err != nil {
|
||||
if errors.Is(err, errFluentBitLocalPackageNotFound) {
|
||||
expectedPath := filepath.Join("deploy", "fluent-bit", fluentBitLocalPackagesRoot, "linux-"+arch, packageName)
|
||||
return fmt.Errorf("install fluent-bit failed: local package missing for platform '%s', expected '%s'", platformKey, expectedPath)
|
||||
}
|
||||
return fmt.Errorf("install fluent-bit from local package failed: %w", err)
|
||||
}
|
||||
|
||||
binPath, err = this.lookupFluentBitBinPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if binPath == "" {
|
||||
return errors.New("fluent-bit binary not found after local package install")
|
||||
}
|
||||
|
||||
_, stderr, err := this.client.Exec(binPath + " --version")
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify fluent-bit version failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) installFluentBitFromLocalPackage(tempDir string, arch string, packageName string) error {
|
||||
packageDir := filepath.Join(Tea.Root, "deploy", "fluent-bit", fluentBitLocalPackagesRoot, "linux-"+arch)
|
||||
localPackagePath := filepath.Join(packageDir, packageName)
|
||||
if _, err := os.Stat(localPackagePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errFluentBitLocalPackageNotFound
|
||||
}
|
||||
return fmt.Errorf("check local package failed: %w", err)
|
||||
}
|
||||
|
||||
remotePackagePath := tempDir + "/" + filepath.Base(localPackagePath)
|
||||
if err := this.client.Copy(localPackagePath, remotePackagePath, 0644); err != nil {
|
||||
return fmt.Errorf("upload local package failed: %w", err)
|
||||
}
|
||||
|
||||
var installCmd string
|
||||
lowerName := strings.ToLower(localPackagePath)
|
||||
switch {
|
||||
case strings.HasSuffix(lowerName, ".deb"):
|
||||
installCmd = "dpkg -i " + shQuote(remotePackagePath)
|
||||
case strings.HasSuffix(lowerName, ".rpm"):
|
||||
installCmd = "rpm -Uvh --force " + shQuote(remotePackagePath) + " || rpm -ivh --force " + shQuote(remotePackagePath)
|
||||
case strings.HasSuffix(lowerName, ".tar.gz") || strings.HasSuffix(lowerName, ".tgz"):
|
||||
extractDir := tempDir + "/extract"
|
||||
installCmd = "rm -rf " + shQuote(extractDir) + "; mkdir -p " + shQuote(extractDir) + "; tar -xzf " + shQuote(remotePackagePath) + " -C " + shQuote(extractDir) + "; " +
|
||||
"bin=$(find " + shQuote(extractDir) + " -type f -name fluent-bit | head -n 1); " +
|
||||
"if [ -z \"$bin\" ]; then exit 3; fi; " +
|
||||
"mkdir -p /opt/fluent-bit/bin /usr/local/bin; " +
|
||||
"install -m 0755 \"$bin\" /opt/fluent-bit/bin/fluent-bit; " +
|
||||
"ln -sf /opt/fluent-bit/bin/fluent-bit /usr/local/bin/fluent-bit"
|
||||
default:
|
||||
return fmt.Errorf("unsupported local package format: %s", packageName)
|
||||
}
|
||||
|
||||
_, stderr, err := this.client.Exec(installCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("install fluent-bit local package '%s' failed: %w, stderr: %s", filepath.Base(localPackagePath), err, stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) detectRemotePlatformAndPackage() (platformKey string, packageName string, arch string, err error) {
|
||||
arch, err = this.detectRemoteLinuxArch()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
releaseData, stderr, err := this.client.Exec("cat /etc/os-release")
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("read /etc/os-release failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
if strings.TrimSpace(releaseData) == "" {
|
||||
return "", "", "", errors.New("/etc/os-release is empty")
|
||||
}
|
||||
|
||||
releaseMap := parseOSRelease(releaseData)
|
||||
id := strings.ToLower(strings.TrimSpace(releaseMap["ID"]))
|
||||
versionID := strings.TrimSpace(releaseMap["VERSION_ID"])
|
||||
|
||||
var distro string
|
||||
switch {
|
||||
case id == "ubuntu" && strings.HasPrefix(versionID, "22.04"):
|
||||
distro = "ubuntu22.04"
|
||||
case id == "amzn" && strings.HasPrefix(versionID, "2023"):
|
||||
distro = "amzn2023"
|
||||
default:
|
||||
return "", "", "", fmt.Errorf("unsupported linux platform id='%s' version='%s'", id, versionID)
|
||||
}
|
||||
|
||||
platformKey = distro + "-" + arch
|
||||
packageName, ok := fluentBitPackageFileMapping[platformKey]
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("no local package mapping for platform '%s'", platformKey)
|
||||
}
|
||||
return platformKey, packageName, arch, nil
|
||||
}
|
||||
|
||||
func parseOSRelease(content string) map[string]string {
|
||||
result := map[string]string{}
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
value = strings.Trim(value, "\"")
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) detectRemoteLinuxArch() (string, error) {
|
||||
stdout, stderr, err := this.client.Exec("uname -m")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("detect remote arch failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
|
||||
arch := strings.ToLower(strings.TrimSpace(stdout))
|
||||
switch arch {
|
||||
case "x86_64", "amd64":
|
||||
return "amd64", nil
|
||||
case "aarch64", "arm64":
|
||||
return "arm64", nil
|
||||
default:
|
||||
return arch, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) lookupFluentBitBinPath() (string, error) {
|
||||
stdout, stderr, err := this.client.Exec("if command -v fluent-bit >/dev/null 2>&1; then command -v fluent-bit; elif [ -x " + fluentBitDefaultBinPath + " ]; then echo " + fluentBitDefaultBinPath + "; fi")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lookup fluent-bit binary failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
return strings.TrimSpace(stdout), nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) readLocalParsersContent() (string, error) {
|
||||
parsersPath := filepath.Join(Tea.Root, "deploy", "fluent-bit", "parsers.conf")
|
||||
data, err := os.ReadFile(parsersPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read local parsers config failed: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) readManagedMeta() (*fluentBitManagedMeta, error) {
|
||||
exists, err := this.remoteFileExists(fluentBitManagedMetaFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
content, stderr, err := this.client.Exec("cat " + shQuote(fluentBitManagedMetaFile))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read fluent-bit managed metadata failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
meta := &fluentBitManagedMeta{}
|
||||
if err := json.Unmarshal([]byte(content), meta); err != nil {
|
||||
return nil, fmt.Errorf("decode fluent-bit managed metadata failed: %w", err)
|
||||
}
|
||||
meta.Roles = normalizeRoles(meta.Roles)
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func mergeManagedRoles(meta *fluentBitManagedMeta, role nodeconfigs.NodeRole) ([]string, error) {
|
||||
roleName, err := mapNodeRole(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleSet := map[string]struct{}{}
|
||||
if meta != nil {
|
||||
for _, r := range normalizeRoles(meta.Roles) {
|
||||
roleSet[r] = struct{}{}
|
||||
}
|
||||
}
|
||||
roleSet[roleName] = struct{}{}
|
||||
|
||||
roles := make([]string, 0, len(roleSet))
|
||||
for r := range roleSet {
|
||||
roles = append(roles, r)
|
||||
}
|
||||
sort.Strings(roles)
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
func mapNodeRole(role nodeconfigs.NodeRole) (string, error) {
|
||||
switch role {
|
||||
case nodeconfigs.NodeRoleNode:
|
||||
return fluentBitRoleNode, nil
|
||||
case nodeconfigs.NodeRoleDNS:
|
||||
return fluentBitRoleDNS, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported fluent-bit role '%s'", role)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRoles(rawRoles []string) []string {
|
||||
roleSet := map[string]struct{}{}
|
||||
for _, role := range rawRoles {
|
||||
role = strings.ToLower(strings.TrimSpace(role))
|
||||
if role != fluentBitRoleNode && role != fluentBitRoleDNS {
|
||||
continue
|
||||
}
|
||||
roleSet[role] = struct{}{}
|
||||
}
|
||||
|
||||
roles := make([]string, 0, len(roleSet))
|
||||
for role := range roleSet {
|
||||
roles = append(roles, role)
|
||||
}
|
||||
sort.Strings(roles)
|
||||
return roles
|
||||
}
|
||||
|
||||
func hasRole(roles []string, role string) bool {
|
||||
for _, one := range roles {
|
||||
if one == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) buildDesiredFluentBitConfig(roles []string) (*fluentBitDesiredConfig, error) {
|
||||
if len(roles) == 0 {
|
||||
return nil, errors.New("fluent-bit roles should not be empty")
|
||||
}
|
||||
|
||||
ch, err := models.SharedSysSettingDAO.ReadClickHouseConfig(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read clickhouse setting failed: %w", err)
|
||||
}
|
||||
if ch == nil {
|
||||
ch = &systemconfigs.ClickHouseSetting{}
|
||||
}
|
||||
if strings.TrimSpace(ch.Host) == "" {
|
||||
ch.Host = "127.0.0.1"
|
||||
}
|
||||
|
||||
ch.Scheme = strings.ToLower(strings.TrimSpace(ch.Scheme))
|
||||
if ch.Scheme == "" {
|
||||
ch.Scheme = "https"
|
||||
}
|
||||
if ch.Scheme != "http" && ch.Scheme != "https" {
|
||||
return nil, fmt.Errorf("unsupported clickhouse scheme '%s'", ch.Scheme)
|
||||
}
|
||||
|
||||
if ch.Port <= 0 {
|
||||
if ch.Scheme == "https" {
|
||||
ch.Port = 8443
|
||||
} else {
|
||||
ch.Port = 8443
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(ch.Database) == "" {
|
||||
ch.Database = "default"
|
||||
}
|
||||
if strings.TrimSpace(ch.User) == "" {
|
||||
ch.User = "default"
|
||||
}
|
||||
// 当前平台策略:后台固定跳过 ClickHouse TLS 证书校验,不暴露 ServerName 配置。
|
||||
ch.TLSSkipVerify = true
|
||||
ch.TLSServerName = ""
|
||||
|
||||
httpPathPattern := fluentBitHTTPPathPattern
|
||||
dnsPathPattern := fluentBitDNSPathPattern
|
||||
publicPolicyPath, err := this.readPublicAccessLogPolicyPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policyDir := dirFromPolicyPath(publicPolicyPath)
|
||||
if policyDir != "" {
|
||||
pattern := strings.TrimRight(policyDir, "/") + "/*.log"
|
||||
httpPathPattern = pattern
|
||||
dnsPathPattern = pattern
|
||||
}
|
||||
|
||||
return &fluentBitDesiredConfig{
|
||||
Roles: normalizeRoles(roles),
|
||||
ClickHouse: ch,
|
||||
HTTPPathPattern: httpPathPattern,
|
||||
DNSPathPattern: dnsPathPattern,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) readPublicAccessLogPolicyPath() (string, error) {
|
||||
policyId, err := models.SharedHTTPAccessLogPolicyDAO.FindCurrentPublicPolicyId(nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("find current public access log policy failed: %w", err)
|
||||
}
|
||||
if policyId <= 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
policy, err := models.SharedHTTPAccessLogPolicyDAO.FindEnabledHTTPAccessLogPolicy(nil, policyId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read public access log policy failed: %w", err)
|
||||
}
|
||||
if policy == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return strings.TrimSpace(models.ParseHTTPAccessLogPolicyFilePath(policy)), nil
|
||||
}
|
||||
|
||||
func dirFromPolicyPath(policyPath string) string {
|
||||
pathValue := strings.TrimSpace(policyPath)
|
||||
if pathValue == "" {
|
||||
return ""
|
||||
}
|
||||
pathValue = strings.ReplaceAll(pathValue, "\\", "/")
|
||||
dir := slashpath.Dir(pathValue)
|
||||
if dir == "." {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimRight(dir, "/")
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) applyManagedConfig(tempDir string, desired *fluentBitDesiredConfig, parserContent string, existingMeta *fluentBitManagedMeta) (bool, error) {
|
||||
mainExists, err := this.remoteFileExists(fluentBitMainConfigFile)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if mainExists && existingMeta == nil {
|
||||
containsMarker, err := this.remoteFileContains(fluentBitMainConfigFile, fluentBitManagedMarker)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !containsMarker {
|
||||
// Adopt unmanaged config by backing it up and replacing with managed config below.
|
||||
}
|
||||
}
|
||||
|
||||
configContent, err := renderManagedConfig(desired)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
envContent := renderManagedEnv(desired.ClickHouse)
|
||||
metaContent, newMeta, err := renderManagedMeta(desired, configContent, parserContent, envContent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
requiredFiles := []string{fluentBitMainConfigFile, fluentBitParsersFile, fluentBitManagedEnvFile, fluentBitManagedMetaFile}
|
||||
if existingMeta != nil && existingMeta.Hash == newMeta.Hash {
|
||||
allExists := true
|
||||
for _, file := range requiredFiles {
|
||||
exists, err := this.remoteFileExists(file)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !exists {
|
||||
allExists = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allExists {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if mainExists {
|
||||
backup := fluentBitMainConfigFile + ".bak." + strconv.FormatInt(time.Now().Unix(), 10)
|
||||
_, stderr, err := this.client.Exec("cp -f " + shQuote(fluentBitMainConfigFile) + " " + shQuote(backup))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("backup existing fluent-bit config failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := this.writeRemoteFileByTemp(tempDir, fluentBitMainConfigFile, configContent, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := this.writeRemoteFileByTemp(tempDir, fluentBitParsersFile, parserContent, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := this.writeRemoteFileByTemp(tempDir, fluentBitManagedEnvFile, envContent, 0600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := this.writeRemoteFileByTemp(tempDir, fluentBitManagedMetaFile, metaContent, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func renderManagedConfig(desired *fluentBitDesiredConfig) (string, error) {
|
||||
if desired == nil || desired.ClickHouse == nil {
|
||||
return "", errors.New("invalid fluent-bit desired config")
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(strings.TrimSpace(desired.ClickHouse.Scheme))
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return "", fmt.Errorf("unsupported clickhouse scheme '%s'", desired.ClickHouse.Scheme)
|
||||
}
|
||||
useTLS := scheme == "https"
|
||||
|
||||
insertHTTP := url.QueryEscape(fmt.Sprintf("INSERT INTO %s.logs_ingest FORMAT JSONEachRow", desired.ClickHouse.Database))
|
||||
insertDNS := url.QueryEscape(fmt.Sprintf("INSERT INTO %s.dns_logs_ingest FORMAT JSONEachRow", desired.ClickHouse.Database))
|
||||
|
||||
lines := []string{
|
||||
"# " + fluentBitManagedMarker,
|
||||
"[SERVICE]",
|
||||
" Flush 1",
|
||||
" Log_Level info",
|
||||
" Parsers_File " + fluentBitParsersFile,
|
||||
" storage.path " + fluentBitStorageDir,
|
||||
" storage.sync normal",
|
||||
" storage.checksum off",
|
||||
" storage.backlog.mem_limit 512MB",
|
||||
"",
|
||||
}
|
||||
|
||||
if hasRole(desired.Roles, fluentBitRoleNode) {
|
||||
lines = append(lines,
|
||||
"[INPUT]",
|
||||
" Name tail",
|
||||
" Path "+desired.HTTPPathPattern,
|
||||
" Tag app.http.logs",
|
||||
" Parser json",
|
||||
" Refresh_Interval 2",
|
||||
" Read_from_Head false",
|
||||
" DB /var/lib/fluent-bit/http-logs.db",
|
||||
" storage.type filesystem",
|
||||
" Mem_Buf_Limit 256MB",
|
||||
" Skip_Long_Lines On",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
if hasRole(desired.Roles, fluentBitRoleDNS) {
|
||||
lines = append(lines,
|
||||
"[INPUT]",
|
||||
" Name tail",
|
||||
" Path "+desired.DNSPathPattern,
|
||||
" Tag app.dns.logs",
|
||||
" Parser json",
|
||||
" Refresh_Interval 2",
|
||||
" Read_from_Head false",
|
||||
" DB /var/lib/fluent-bit/dns-logs.db",
|
||||
" storage.type filesystem",
|
||||
" Mem_Buf_Limit 256MB",
|
||||
" Skip_Long_Lines On",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
if hasRole(desired.Roles, fluentBitRoleNode) {
|
||||
lines = append(lines,
|
||||
"[OUTPUT]",
|
||||
" Name http",
|
||||
" Match app.http.logs",
|
||||
" Host "+desired.ClickHouse.Host,
|
||||
" Port "+strconv.Itoa(desired.ClickHouse.Port),
|
||||
" URI /?query="+insertHTTP,
|
||||
" Format json_lines",
|
||||
" http_user ${CH_USER}",
|
||||
" http_passwd ${CH_PASSWORD}",
|
||||
" json_date_key timestamp",
|
||||
" json_date_format epoch",
|
||||
" workers 2",
|
||||
" net.keepalive On",
|
||||
" Retry_Limit False",
|
||||
)
|
||||
if useTLS {
|
||||
lines = append(lines, " tls On")
|
||||
if desired.ClickHouse.TLSSkipVerify {
|
||||
lines = append(lines, " tls.verify Off")
|
||||
} else {
|
||||
lines = append(lines, " tls.verify On")
|
||||
}
|
||||
if strings.TrimSpace(desired.ClickHouse.TLSServerName) != "" {
|
||||
lines = append(lines, " tls.vhost "+strings.TrimSpace(desired.ClickHouse.TLSServerName))
|
||||
}
|
||||
}
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
if hasRole(desired.Roles, fluentBitRoleDNS) {
|
||||
lines = append(lines,
|
||||
"[OUTPUT]",
|
||||
" Name http",
|
||||
" Match app.dns.logs",
|
||||
" Host "+desired.ClickHouse.Host,
|
||||
" Port "+strconv.Itoa(desired.ClickHouse.Port),
|
||||
" URI /?query="+insertDNS,
|
||||
" Format json_lines",
|
||||
" http_user ${CH_USER}",
|
||||
" http_passwd ${CH_PASSWORD}",
|
||||
" json_date_key timestamp",
|
||||
" json_date_format epoch",
|
||||
" workers 2",
|
||||
" net.keepalive On",
|
||||
" Retry_Limit False",
|
||||
)
|
||||
if useTLS {
|
||||
lines = append(lines, " tls On")
|
||||
if desired.ClickHouse.TLSSkipVerify {
|
||||
lines = append(lines, " tls.verify Off")
|
||||
} else {
|
||||
lines = append(lines, " tls.verify On")
|
||||
}
|
||||
if strings.TrimSpace(desired.ClickHouse.TLSServerName) != "" {
|
||||
lines = append(lines, " tls.vhost "+strings.TrimSpace(desired.ClickHouse.TLSServerName))
|
||||
}
|
||||
}
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
|
||||
func renderManagedEnv(ch *systemconfigs.ClickHouseSetting) string {
|
||||
user := "default"
|
||||
password := ""
|
||||
if ch != nil {
|
||||
if strings.TrimSpace(ch.User) != "" {
|
||||
user = strings.TrimSpace(ch.User)
|
||||
}
|
||||
password = ch.Password
|
||||
}
|
||||
return "CH_USER=" + strconv.Quote(user) + "\n" +
|
||||
"CH_PASSWORD=" + strconv.Quote(password) + "\n"
|
||||
}
|
||||
|
||||
func renderManagedMeta(desired *fluentBitDesiredConfig, configContent string, parserContent string, envContent string) (string, *fluentBitManagedMeta, error) {
|
||||
hashInput := configContent + "\n---\n" + parserContent + "\n---\n" + envContent + "\n---\n" + strings.Join(desired.Roles, ",")
|
||||
hashBytes := sha256.Sum256([]byte(hashInput))
|
||||
hash := fmt.Sprintf("%x", hashBytes[:])
|
||||
|
||||
meta := &fluentBitManagedMeta{
|
||||
Roles: desired.Roles,
|
||||
Hash: hash,
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
SourceVersion: teaconst.Version,
|
||||
}
|
||||
data, err := json.MarshalIndent(meta, "", " ")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("encode fluent-bit managed metadata failed: %w", err)
|
||||
}
|
||||
return string(data) + "\n", meta, nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) copyLocalFileToRemote(tempDir string, localPath string, remotePath string, mode os.FileMode) error {
|
||||
tempFile := tempDir + "/" + filepath.Base(remotePath)
|
||||
if err := this.client.Copy(localPath, tempFile, mode); err != nil {
|
||||
return fmt.Errorf("upload fluent-bit file '%s' failed: %w", localPath, err)
|
||||
}
|
||||
_, stderr, err := this.client.Exec("cp -f " + shQuote(tempFile) + " " + shQuote(remotePath) + " && chmod " + fmt.Sprintf("%04o", mode) + " " + shQuote(remotePath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("install remote fluent-bit file '%s' failed: %w, stderr: %s", remotePath, err, stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) writeRemoteFileByTemp(tempDir string, remotePath string, content string, mode os.FileMode) error {
|
||||
tempFile := tempDir + "/" + filepath.Base(remotePath) + ".tmp"
|
||||
if _, err := this.client.WriteFile(tempFile, []byte(content)); err != nil {
|
||||
return fmt.Errorf("write temp fluent-bit file '%s' failed: %w", tempFile, err)
|
||||
}
|
||||
|
||||
_, stderr, err := this.client.Exec("cp -f " + shQuote(tempFile) + " " + shQuote(remotePath) + " && chmod " + fmt.Sprintf("%04o", mode) + " " + shQuote(remotePath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("write managed fluent-bit file '%s' failed: %w, stderr: %s", remotePath, err, stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) ensureFluentBitService(tempDir string, binPath string, configChanged bool) error {
|
||||
_, _, _ = this.client.Exec("if command -v systemctl >/dev/null 2>&1 && [ ! -f /etc/systemd/system/" + fluentBitServiceName + ".service ] && [ ! -f /lib/systemd/system/" + fluentBitServiceName + ".service ]; then " +
|
||||
"cat > /etc/systemd/system/" + fluentBitServiceName + ".service <<'EOF'\n" +
|
||||
"[Unit]\n" +
|
||||
"Description=Fluent Bit\n" +
|
||||
"After=network.target\n" +
|
||||
"\n" +
|
||||
"[Service]\n" +
|
||||
"ExecStart=" + binPath + " -c " + fluentBitMainConfigFile + "\n" +
|
||||
"Restart=always\n" +
|
||||
"RestartSec=5\n" +
|
||||
"\n" +
|
||||
"[Install]\n" +
|
||||
"WantedBy=multi-user.target\n" +
|
||||
"EOF\n" +
|
||||
"fi")
|
||||
|
||||
stdout, _, err := this.client.Exec("if command -v systemctl >/dev/null 2>&1; then echo 1; else echo 0; fi")
|
||||
if err != nil {
|
||||
return fmt.Errorf("check systemctl failed: %w", err)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(stdout) == "1" {
|
||||
dropInChanged, err := this.ensureServiceDropIn(tempDir, binPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
restartRequired := configChanged || dropInChanged
|
||||
_, stderr, err := this.client.Exec("systemctl daemon-reload; systemctl enable " + fluentBitServiceName + " >/dev/null 2>&1 || true; " +
|
||||
"if systemctl is-active " + fluentBitServiceName + " >/dev/null 2>&1; then " +
|
||||
"if [ \"" + boolToString(restartRequired) + "\" = \"1\" ]; then systemctl restart " + fluentBitServiceName + "; fi; " +
|
||||
"else systemctl start " + fluentBitServiceName + "; fi")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ensure fluent-bit service failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if configChanged {
|
||||
_, _, _ = this.client.Exec("pkill -f \"fluent-bit.*fluent-bit.conf\" >/dev/null 2>&1 || true")
|
||||
}
|
||||
|
||||
_, _, runningErr := this.client.Exec("pgrep -f \"fluent-bit.*fluent-bit.conf\" >/dev/null 2>&1")
|
||||
if runningErr != nil {
|
||||
startCmd := "set -a; [ -f " + shQuote(fluentBitManagedEnvFile) + " ] && . " + shQuote(fluentBitManagedEnvFile) + "; set +a; " +
|
||||
shQuote(binPath) + " -c " + shQuote(fluentBitMainConfigFile) + " >/dev/null 2>&1 &"
|
||||
_, stderr, err := this.client.Exec(startCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("start fluent-bit without systemd failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) ensureServiceDropIn(tempDir string, binPath string) (bool, error) {
|
||||
_, stderr, err := this.client.Exec("mkdir -p " + shQuote(fluentBitDropInDir))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("prepare fluent-bit drop-in dir failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
|
||||
content := "[Service]\n" +
|
||||
"EnvironmentFile=-" + fluentBitManagedEnvFile + "\n" +
|
||||
"ExecStart=\n" +
|
||||
"ExecStart=" + binPath + " -c " + fluentBitMainConfigFile + "\n"
|
||||
|
||||
existing, _, _ := this.client.Exec("if [ -f " + shQuote(fluentBitDropInFile) + " ]; then cat " + shQuote(fluentBitDropInFile) + "; fi")
|
||||
if existing == content {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := this.writeRemoteFileByTemp(tempDir, fluentBitDropInFile, content, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) remoteFileExists(path string) (bool, error) {
|
||||
stdout, stderr, err := this.client.Exec("if [ -f " + shQuote(path) + " ]; then echo 1; else echo 0; fi")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("check remote file '%s' failed: %w, stderr: %s", path, err, stderr)
|
||||
}
|
||||
return strings.TrimSpace(stdout) == "1", nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) remoteFileContains(path string, pattern string) (bool, error) {
|
||||
stdout, stderr, err := this.client.Exec("if grep -F " + shQuote(pattern) + " " + shQuote(path) + " >/dev/null 2>&1; then echo 1; else echo 0; fi")
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("check remote file content '%s' failed: %w, stderr: %s", path, err, stderr)
|
||||
}
|
||||
return strings.TrimSpace(stdout) == "1", nil
|
||||
}
|
||||
|
||||
func shQuote(value string) string {
|
||||
if value == "" {
|
||||
return "''"
|
||||
}
|
||||
return "'" + strings.ReplaceAll(value, "'", "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
func boolToString(v bool) string {
|
||||
if v {
|
||||
return "1"
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
1
EdgeAPI/internal/installers/helpers/README.md
Normal file
1
EdgeAPI/internal/installers/helpers/README.md
Normal file
@@ -0,0 +1 @@
|
||||
远程安装依赖文件,单独放在一个目录防止安装包过大
|
||||
95
EdgeAPI/internal/installers/helpers/unzip.go
Normal file
95
EdgeAPI/internal/installers/helpers/unzip.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Unzip struct {
|
||||
zipFile string
|
||||
targetDir string
|
||||
}
|
||||
|
||||
func NewUnzip(zipFile string, targetDir string) *Unzip {
|
||||
return &Unzip{
|
||||
zipFile: zipFile,
|
||||
targetDir: targetDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Unzip) Run() error {
|
||||
if len(this.zipFile) == 0 {
|
||||
return errors.New("zip file should not be empty")
|
||||
}
|
||||
if len(this.targetDir) == 0 {
|
||||
return errors.New("target dir should not be empty")
|
||||
}
|
||||
|
||||
reader, err := zip.OpenReader(this.zipFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = reader.Close()
|
||||
}()
|
||||
|
||||
for _, file := range reader.File {
|
||||
var info = file.FileInfo()
|
||||
var target = this.targetDir + "/" + file.Name
|
||||
|
||||
// 目录
|
||||
if info.IsDir() {
|
||||
stat, err := os.Stat(target)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
} else {
|
||||
err = os.MkdirAll(target, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
err = os.MkdirAll(target, info.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 文件
|
||||
err = func(file *zip.File, target string) error {
|
||||
fileReader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = fileReader.Close()
|
||||
}()
|
||||
|
||||
// remove old
|
||||
_ = os.Remove(target)
|
||||
|
||||
// create new
|
||||
fileWriter, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, file.FileInfo().Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = fileWriter.Close()
|
||||
}()
|
||||
|
||||
_, err = io.Copy(fileWriter, fileReader)
|
||||
return err
|
||||
}(file, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
17
EdgeAPI/internal/installers/helpers/unzip_test.go
Normal file
17
EdgeAPI/internal/installers/helpers/unzip_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package helpers_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/installers/helpers"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnzip_Run(t *testing.T) {
|
||||
var unzip = helpers.NewUnzip(Tea.Root+"/deploy/edge-node-v0.0.1.zip", Tea.Root+"/deploy/")
|
||||
err := unzip.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("OK")
|
||||
}
|
||||
281
EdgeAPI/internal/installers/installer_base.go
Normal file
281
EdgeAPI/internal/installers/installer_base.go
Normal file
@@ -0,0 +1,281 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/configutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
stringutil "github.com/iwind/TeaGo/utils/string"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BaseInstaller struct {
|
||||
client *SSHClient
|
||||
}
|
||||
|
||||
// Login 登录SSH服务
|
||||
func (this *BaseInstaller) Login(credentials *Credentials) error {
|
||||
var hostKeyCallback ssh.HostKeyCallback = nil
|
||||
|
||||
// 检查参数
|
||||
if len(credentials.Host) == 0 {
|
||||
return errors.New("'host' should not be empty")
|
||||
}
|
||||
if credentials.Port <= 0 {
|
||||
return errors.New("'port' should be greater than 0")
|
||||
}
|
||||
if len(credentials.Password) == 0 && len(credentials.PrivateKey) == 0 {
|
||||
return errors.New("require user 'password' or 'privateKey'")
|
||||
}
|
||||
|
||||
// 不使用known_hosts
|
||||
if hostKeyCallback == nil {
|
||||
hostKeyCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 认证
|
||||
var methods = []ssh.AuthMethod{}
|
||||
if credentials.Method == "user" {
|
||||
{
|
||||
var authMethod = ssh.Password(credentials.Password)
|
||||
methods = append(methods, authMethod)
|
||||
}
|
||||
|
||||
{
|
||||
authMethod := ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
||||
if len(questions) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
return []string{credentials.Password}, nil
|
||||
})
|
||||
methods = append(methods, authMethod)
|
||||
}
|
||||
} else if credentials.Method == "privateKey" {
|
||||
var signer ssh.Signer
|
||||
var err error
|
||||
if len(credentials.Passphrase) > 0 {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(credentials.PrivateKey), []byte(credentials.Passphrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(credentials.PrivateKey))
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse private key: %w", err)
|
||||
}
|
||||
authMethod := ssh.PublicKeys(signer)
|
||||
methods = append(methods, authMethod)
|
||||
} else {
|
||||
return errors.New("invalid method '" + credentials.Method + "'")
|
||||
}
|
||||
|
||||
// SSH客户端
|
||||
if len(credentials.Username) == 0 {
|
||||
credentials.Username = "root"
|
||||
}
|
||||
var config = &ssh.ClientConfig{
|
||||
User: credentials.Username,
|
||||
Auth: methods,
|
||||
HostKeyCallback: hostKeyCallback,
|
||||
Timeout: 5 * time.Second, // TODO 后期可以设置这个超时时间
|
||||
}
|
||||
|
||||
sshClient, err := ssh.Dial("tcp", configutils.QuoteIP(credentials.Host)+":"+strconv.Itoa(credentials.Port), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := NewSSHClient(sshClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if credentials.Sudo {
|
||||
client.Sudo(credentials.Password)
|
||||
}
|
||||
|
||||
this.client = client
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭SSH服务
|
||||
func (this *BaseInstaller) Close() error {
|
||||
if this.client != nil {
|
||||
return this.client.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupLatestInstaller 查找最新的版本的文件
|
||||
func (this *BaseInstaller) LookupLatestInstaller(filePrefix string) (string, error) {
|
||||
matches, err := filepath.Glob(Tea.Root + Tea.DS + "deploy" + Tea.DS + "*.zip")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pattern, err := regexp.Compile(filePrefix + `-v([\d.]+)\.zip`)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var lastVersion = ""
|
||||
var result = ""
|
||||
for _, match := range matches {
|
||||
var baseName = filepath.Base(match)
|
||||
if !pattern.MatchString(baseName) {
|
||||
continue
|
||||
}
|
||||
var m = pattern.FindStringSubmatch(baseName)
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
version := m[1]
|
||||
if len(lastVersion) == 0 || stringutil.VersionCompare(version, lastVersion) > 0 {
|
||||
lastVersion = version
|
||||
result = match
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LookupLatestInstallerForTarget 按目标系统优先选择安装包:
|
||||
// 1) Linux 且识别到发行版时,优先 filePrefix-{distroTag}-v*.zip(如 ubuntu22.04 / amzn2023)
|
||||
// 2) 回退 filePrefix-v*.zip(通用包)
|
||||
func (this *BaseInstaller) LookupLatestInstallerForTarget(filePrefix string, env *Env) (string, error) {
|
||||
if env != nil && env.OS == "linux" {
|
||||
distroTag, err := this.detectLinuxDistroTag()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(distroTag) > 0 {
|
||||
zipFile, err := this.LookupLatestInstaller(filePrefix + "-" + distroTag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(zipFile) > 0 {
|
||||
return zipFile, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.LookupLatestInstaller(filePrefix)
|
||||
}
|
||||
|
||||
// detectLinuxDistroTag 识别远端 Linux 发行版标签(用于选择发行版定制安装包)。
|
||||
// 当前支持:ubuntu22.04 / amzn2023;其他系统返回空字符串(回退通用包)。
|
||||
func (this *BaseInstaller) detectLinuxDistroTag() (string, error) {
|
||||
if this.client == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
releaseData, stderr, err := this.client.Exec("cat /etc/os-release")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read /etc/os-release failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
if len(strings.TrimSpace(releaseData)) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
releaseMap := parseOSRelease(releaseData)
|
||||
id := strings.ToLower(strings.TrimSpace(releaseMap["ID"]))
|
||||
versionID := strings.TrimSpace(releaseMap["VERSION_ID"])
|
||||
|
||||
switch {
|
||||
case id == "ubuntu" && strings.HasPrefix(versionID, "22.04"):
|
||||
return "ubuntu22.04", nil
|
||||
case id == "amzn" && strings.HasPrefix(versionID, "2023"):
|
||||
return "amzn2023", nil
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// InstallHelper 上传安装助手
|
||||
func (this *BaseInstaller) InstallHelper(targetDir string, role nodeconfigs.NodeRole) (env *Env, err error) {
|
||||
var uname = this.uname()
|
||||
|
||||
var osName string
|
||||
var archName string
|
||||
if strings.Contains(uname, "Darwin") {
|
||||
osName = "darwin"
|
||||
} else if strings.Contains(uname, "Linux") {
|
||||
osName = "linux"
|
||||
} else {
|
||||
// TODO 支持freebsd, aix ...
|
||||
return env, errors.New("installer not supported os '" + uname + "'")
|
||||
}
|
||||
|
||||
if strings.Contains(uname, "aarch64") || strings.Contains(uname, "armv8") {
|
||||
archName = "arm64"
|
||||
} else if strings.Contains(uname, "aarch64_be") {
|
||||
archName = "arm64be"
|
||||
} else if strings.Contains(uname, "mips64el") {
|
||||
archName = "mips64le"
|
||||
} else if strings.Contains(uname, "mips64") {
|
||||
archName = "mips64"
|
||||
} else if strings.Contains(uname, "x86_64") {
|
||||
archName = "amd64"
|
||||
} else {
|
||||
archName = "386"
|
||||
}
|
||||
|
||||
var exeName = "edge-installer-helper-" + osName + "-" + archName
|
||||
switch role {
|
||||
case nodeconfigs.NodeRoleDNS:
|
||||
exeName = "edge-installer-dns-helper-" + osName + "-" + archName
|
||||
}
|
||||
var exePath = Tea.Root + "/installers/" + exeName
|
||||
|
||||
var realHelperPath = ""
|
||||
|
||||
var firstCopyErr error
|
||||
for _, path := range []string{
|
||||
targetDir + "/" + exeName,
|
||||
this.client.UserHome() + "/" + exeName,
|
||||
"/tmp/" + exeName,
|
||||
} {
|
||||
err = this.client.Copy(exePath, path, 0777)
|
||||
if err != nil {
|
||||
if firstCopyErr == nil {
|
||||
firstCopyErr = err
|
||||
}
|
||||
} else {
|
||||
err = nil
|
||||
firstCopyErr = nil
|
||||
realHelperPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstCopyErr != nil {
|
||||
return env, errors.New("copy '" + exeName + "' to '" + targetDir + "' failed: " + firstCopyErr.Error())
|
||||
}
|
||||
|
||||
env = &Env{
|
||||
OS: osName,
|
||||
Arch: archName,
|
||||
HelperPath: realHelperPath,
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func (this *BaseInstaller) uname() (uname string) {
|
||||
var unameRetries = 3
|
||||
|
||||
for i := 0; i < unameRetries; i++ {
|
||||
for _, unameExe := range []string{"uname", "/bin/uname", "/usr/bin/uname"} {
|
||||
uname, _, _ = this.client.Exec(unameExe + " -a")
|
||||
if len(uname) > 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "x86_64 GNU/Linux"
|
||||
}
|
||||
20
EdgeAPI/internal/installers/installer_base_test.go
Normal file
20
EdgeAPI/internal/installers/installer_base_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
_ "github.com/iwind/TeaGo/bootstrap"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBaseInstaller_LookupLatest(t *testing.T) {
|
||||
installer := &BaseInstaller{}
|
||||
result, err := installer.LookupLatestInstaller("edge-node-linux-amd64")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
t.Log("not found")
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("result:", result)
|
||||
}
|
||||
14
EdgeAPI/internal/installers/installer_interface.go
Normal file
14
EdgeAPI/internal/installers/installer_interface.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package installers
|
||||
|
||||
import "github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
|
||||
type InstallerInterface interface {
|
||||
// 登录SSH服务
|
||||
Login(credentials *Credentials) error
|
||||
|
||||
// 安装
|
||||
Install(dir string, params interface{}, installStatus *models.NodeInstallStatus) error
|
||||
|
||||
// 关闭连接的SSH服务
|
||||
Close() error
|
||||
}
|
||||
172
EdgeAPI/internal/installers/installer_node.go
Normal file
172
EdgeAPI/internal/installers/installer_node.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type NodeInstaller struct {
|
||||
BaseInstaller
|
||||
}
|
||||
|
||||
func (this *NodeInstaller) Install(dir string, params interface{}, installStatus *models.NodeInstallStatus) error {
|
||||
if params == nil {
|
||||
return errors.New("'params' required for node installation")
|
||||
}
|
||||
nodeParams, ok := params.(*NodeParams)
|
||||
if !ok {
|
||||
return errors.New("'params' should be *NodeParams")
|
||||
}
|
||||
err := nodeParams.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("params validation: %w", err)
|
||||
}
|
||||
|
||||
// 检查目标目录是否存在
|
||||
_, err = this.client.Stat(dir)
|
||||
if err != nil {
|
||||
err = this.client.MkdirAll(dir)
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "CREATE_ROOT_DIRECTORY_FAILED"
|
||||
return fmt.Errorf("create directory '%s' failed: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 安装助手
|
||||
env, err := this.InstallHelper(dir, nodeconfigs.NodeRoleNode)
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "INSTALL_HELPER_FAILED"
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传安装文件
|
||||
var filePrefix = "edge-node-" + env.OS + "-" + env.Arch
|
||||
zipFile, err := this.LookupLatestInstallerForTarget(filePrefix, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(zipFile) == 0 {
|
||||
return errors.New("can not find installer file for " + env.OS + "/" + env.Arch + ", expected '" + filePrefix + "-v*.zip' or distro-specific '" + filePrefix + "-{ubuntu22.04|amzn2023}-v*.zip'")
|
||||
}
|
||||
var targetZip = ""
|
||||
var firstCopyErr error
|
||||
var zipName = filepath.Base(zipFile)
|
||||
for _, candidateTargetZip := range []string{
|
||||
dir + "/" + zipName,
|
||||
this.client.UserHome() + "/" + zipName,
|
||||
"/tmp/" + zipName,
|
||||
} {
|
||||
err = this.client.Copy(zipFile, candidateTargetZip, 0777)
|
||||
if err != nil {
|
||||
if firstCopyErr == nil {
|
||||
firstCopyErr = err
|
||||
}
|
||||
} else {
|
||||
err = nil
|
||||
firstCopyErr = nil
|
||||
targetZip = candidateTargetZip
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstCopyErr != nil {
|
||||
return fmt.Errorf("upload node file failed: %w", firstCopyErr)
|
||||
}
|
||||
|
||||
// 测试运行环境
|
||||
// 升级的节点暂时不列入测试
|
||||
if !nodeParams.IsUpgrading {
|
||||
_, stderr, err := this.client.Exec(env.HelperPath + " -cmd=test")
|
||||
if err != nil {
|
||||
return fmt.Errorf("test failed: %w", err)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("test failed: " + stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是升级则优雅停止先前的进程
|
||||
var exePath = dir + "/edge-node/bin/edge-node"
|
||||
if nodeParams.IsUpgrading {
|
||||
_, err = this.client.Stat(exePath)
|
||||
if err == nil {
|
||||
_, _, _ = this.client.Exec(exePath + " quit")
|
||||
|
||||
// 删除可执行文件防止冲突
|
||||
err = this.client.Remove(exePath)
|
||||
if err != nil && err != os.ErrNotExist {
|
||||
return fmt.Errorf("remove old file failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解压
|
||||
_, stderr, err := this.client.Exec(env.HelperPath + " -cmd=unzip -zip=\"" + targetZip + "\" -target=\"" + dir + "\"")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("unzip installer failed: " + stderr)
|
||||
}
|
||||
|
||||
// 修改配置文件
|
||||
{
|
||||
var configFile = dir + "/edge-node/configs/api_node.yaml"
|
||||
|
||||
// sudo之后我们需要修改配置目录才能写入文件
|
||||
if this.client.sudo {
|
||||
_, _, _ = this.client.Exec("chown " + this.client.User() + " " + filepath.Dir(configFile))
|
||||
}
|
||||
|
||||
var data = []byte(`rpc.endpoints: [ ${endpoints} ]
|
||||
nodeId: "${nodeId}"
|
||||
secret: "${nodeSecret}"`)
|
||||
|
||||
data = bytes.ReplaceAll(data, []byte("${endpoints}"), []byte(nodeParams.QuoteEndpoints()))
|
||||
data = bytes.ReplaceAll(data, []byte("${nodeId}"), []byte(nodeParams.NodeId))
|
||||
data = bytes.ReplaceAll(data, []byte("${nodeSecret}"), []byte(nodeParams.Secret))
|
||||
|
||||
_, err = this.client.WriteFile(configFile, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write '%s': %w", configFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 在线安装/更新 Fluent Bit(与边缘节点安装流程联动)
|
||||
err = this.SetupFluentBit(nodeconfigs.NodeRoleNode)
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "SETUP_FLUENT_BIT_FAILED"
|
||||
return fmt.Errorf("setup fluent-bit failed: %w", err)
|
||||
}
|
||||
|
||||
// 测试
|
||||
_, stderr, err = this.client.Exec(dir + "/edge-node/bin/edge-node test")
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "TEST_FAILED"
|
||||
return fmt.Errorf("test edge node failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
if regexp.MustCompile(`(?i)rpc`).MatchString(stderr) {
|
||||
installStatus.ErrorCode = "RPC_TEST_FAILED"
|
||||
}
|
||||
|
||||
return errors.New("test edge node failed: " + stderr)
|
||||
}
|
||||
|
||||
// 启动
|
||||
_, stderr, err = this.client.Exec(dir + "/edge-node/bin/edge-node start")
|
||||
if err != nil {
|
||||
return fmt.Errorf("start edge node failed: %w", err)
|
||||
}
|
||||
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("start edge node failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
EdgeAPI/internal/installers/installer_node_test.go
Normal file
38
EdgeAPI/internal/installers/installer_node_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNodeInstaller_Install(t *testing.T) {
|
||||
var installer InstallerInterface = &NodeInstaller{}
|
||||
err := installer.Login(&Credentials{
|
||||
Host: "192.168.2.30",
|
||||
Port: 22,
|
||||
Username: "root",
|
||||
Password: "123456",
|
||||
PrivateKey: "",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
defer func() {
|
||||
err := installer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 安装
|
||||
err = installer.Install("/opt/edge", &NodeParams{
|
||||
Endpoints: []string{"http://192.168.2.40:8003"},
|
||||
NodeId: "313fdb1b90d0a63c736f307b4d1ca358",
|
||||
Secret: "Pl3u5kYqBDZddp7raw6QfHiuGPRCWF54",
|
||||
}, &models.NodeInstallStatus{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
174
EdgeAPI/internal/installers/installer_ns_node_plus.go
Normal file
174
EdgeAPI/internal/installers/installer_ns_node_plus.go
Normal file
@@ -0,0 +1,174 @@
|
||||
//go:build plus
|
||||
|
||||
package installers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type NSNodeInstaller struct {
|
||||
BaseInstaller
|
||||
}
|
||||
|
||||
func (this *NSNodeInstaller) Install(dir string, params interface{}, installStatus *models.NodeInstallStatus) error {
|
||||
if params == nil {
|
||||
return errors.New("'params' required for node installation")
|
||||
}
|
||||
nodeParams, ok := params.(*NodeParams)
|
||||
if !ok {
|
||||
return errors.New("'params' should be *NodeParams")
|
||||
}
|
||||
err := nodeParams.Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("params validation: %w", err)
|
||||
}
|
||||
|
||||
// 检查目标目录是否存在
|
||||
_, err = this.client.Stat(dir)
|
||||
if err != nil {
|
||||
err = this.client.MkdirAll(dir)
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "CREATE_ROOT_DIRECTORY_FAILED"
|
||||
return fmt.Errorf("create directory '%s' failed: %w", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 安装助手
|
||||
env, err := this.InstallHelper(dir, nodeconfigs.NodeRoleDNS)
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "INSTALL_HELPER_FAILED"
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传安装文件
|
||||
filePrefix := "edge-dns-" + env.OS + "-" + env.Arch
|
||||
zipFile, err := this.LookupLatestInstallerForTarget(filePrefix, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(zipFile) == 0 {
|
||||
return errors.New("can not find installer file for " + env.OS + "/" + env.Arch + ", expected '" + filePrefix + "-v*.zip' or distro-specific '" + filePrefix + "-{ubuntu22.04|amzn2023}-v*.zip'")
|
||||
}
|
||||
var targetZip = ""
|
||||
var firstCopyErr error
|
||||
var zipName = filepath.Base(zipFile)
|
||||
for _, candidateTargetZip := range []string{
|
||||
dir + "/" + zipName,
|
||||
this.client.UserHome() + "/" + zipName,
|
||||
"/tmp/" + zipName,
|
||||
} {
|
||||
err = this.client.Copy(zipFile, candidateTargetZip, 0777)
|
||||
if err != nil {
|
||||
if firstCopyErr == nil {
|
||||
firstCopyErr = err
|
||||
}
|
||||
} else {
|
||||
err = nil
|
||||
firstCopyErr = nil
|
||||
targetZip = candidateTargetZip
|
||||
break
|
||||
}
|
||||
}
|
||||
if firstCopyErr != nil {
|
||||
return fmt.Errorf("upload node file failed: %w", firstCopyErr)
|
||||
}
|
||||
|
||||
// 测试运行环境
|
||||
// 升级的节点暂时不列入测试
|
||||
if !nodeParams.IsUpgrading {
|
||||
_, stderr, err := this.client.Exec(env.HelperPath + " -cmd=test")
|
||||
if err != nil {
|
||||
return fmt.Errorf("test failed: %w", err)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("test failed: " + stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是升级则优雅停止先前的进程
|
||||
exePath := dir + "/edge-dns/bin/edge-dns"
|
||||
if nodeParams.IsUpgrading {
|
||||
_, err = this.client.Stat(exePath)
|
||||
if err == nil {
|
||||
_, _, _ = this.client.Exec(exePath + " stop")
|
||||
|
||||
// 删除可执行文件防止冲突
|
||||
err = this.client.Remove(exePath)
|
||||
if err != nil && err != os.ErrNotExist {
|
||||
return fmt.Errorf("remove old file failed: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解压
|
||||
_, stderr, err := this.client.Exec(env.HelperPath + " -cmd=unzip -zip=\"" + targetZip + "\" -target=\"" + dir + "\"")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("unzip installer failed: " + stderr)
|
||||
}
|
||||
|
||||
// 修改配置文件
|
||||
{
|
||||
var configFile = dir + "/edge-dns/configs/api_dns.yaml"
|
||||
|
||||
// sudo之后我们需要修改配置目录才能写入文件
|
||||
if this.client.sudo {
|
||||
_, _, _ = this.client.Exec("chown " + this.client.User() + " " + filepath.Dir(configFile))
|
||||
}
|
||||
|
||||
var data = []byte(`rpc.endpoints: [ ${endpoints} ]
|
||||
nodeId: "${nodeId}"
|
||||
secret: "${nodeSecret}"`)
|
||||
|
||||
data = bytes.ReplaceAll(data, []byte("${endpoints}"), []byte(nodeParams.QuoteEndpoints()))
|
||||
data = bytes.ReplaceAll(data, []byte("${nodeId}"), []byte(nodeParams.NodeId))
|
||||
data = bytes.ReplaceAll(data, []byte("${nodeSecret}"), []byte(nodeParams.Secret))
|
||||
|
||||
_, err = this.client.WriteFile(configFile, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write '%s': %w", configFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 在线安装/更新 Fluent Bit(与 DNS 节点安装流程联动)
|
||||
err = this.SetupFluentBit(nodeconfigs.NodeRoleDNS)
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "SETUP_FLUENT_BIT_FAILED"
|
||||
return fmt.Errorf("setup fluent-bit failed: %w", err)
|
||||
}
|
||||
|
||||
// 测试
|
||||
_, stderr, err = this.client.Exec(dir + "/edge-dns/bin/edge-dns test")
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "TEST_FAILED"
|
||||
return fmt.Errorf("test edge node failed: %w, stderr: %s", err, stderr)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
if regexp.MustCompile(`(?i)rpc`).MatchString(stderr) {
|
||||
installStatus.ErrorCode = "RPC_TEST_FAILED"
|
||||
}
|
||||
|
||||
return errors.New("test edge dns node failed: " + stderr)
|
||||
}
|
||||
|
||||
// 启动
|
||||
_, stderr, err = this.client.Exec(dir + "/edge-dns/bin/edge-dns start")
|
||||
if err != nil {
|
||||
return fmt.Errorf("start edge dns failed: %w", err)
|
||||
}
|
||||
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("start edge dns failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
41
EdgeAPI/internal/installers/installer_ns_node_plus_test.go
Normal file
41
EdgeAPI/internal/installers/installer_ns_node_plus_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
//go:build plus
|
||||
|
||||
package installers
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDNSNodeInstaller_Install(t *testing.T) {
|
||||
var installer InstallerInterface = &NSNodeInstaller{}
|
||||
err := installer.Login(&Credentials{
|
||||
Host: "192.168.2.30",
|
||||
Port: 22,
|
||||
Username: "root",
|
||||
Password: "123456",
|
||||
PrivateKey: "",
|
||||
Method: "user",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 关闭连接
|
||||
defer func() {
|
||||
err := installer.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 安装
|
||||
err = installer.Install("/opt/edge", &NodeParams{
|
||||
Endpoints: []string{"http://192.168.2.40:8003"},
|
||||
NodeId: "b3f0690c793db5daaa666e89bd7b2301",
|
||||
Secret: "H6nbSzjN3tLYi0ecdtUeDpQdZZPjKL7S",
|
||||
}, &models.NodeInstallStatus{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
33
EdgeAPI/internal/installers/params_node.go
Normal file
33
EdgeAPI/internal/installers/params_node.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NodeParams struct {
|
||||
Endpoints []string
|
||||
NodeId string
|
||||
Secret string
|
||||
IsUpgrading bool // 是否为升级
|
||||
}
|
||||
|
||||
func (this *NodeParams) Validate() error {
|
||||
if len(this.Endpoints) == 0 {
|
||||
return errors.New("'endpoint' should not be empty")
|
||||
}
|
||||
if len(this.NodeId) == 0 {
|
||||
return errors.New("'nodeId' should not be empty")
|
||||
}
|
||||
if len(this.Secret) == 0 {
|
||||
return errors.New("'secret' should not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *NodeParams) QuoteEndpoints() string {
|
||||
if len(this.Endpoints) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "\"" + strings.Join(this.Endpoints, "\", \"") + "\""
|
||||
}
|
||||
585
EdgeAPI/internal/installers/queue_node.go
Normal file
585
EdgeAPI/internal/installers/queue_node.go
Normal file
@@ -0,0 +1,585 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"time"
|
||||
)
|
||||
|
||||
var sharedNodeQueue = NewNodeQueue()
|
||||
|
||||
type NodeQueue struct {
|
||||
}
|
||||
|
||||
func NewNodeQueue() *NodeQueue {
|
||||
return &NodeQueue{}
|
||||
}
|
||||
|
||||
func SharedNodeQueue() *NodeQueue {
|
||||
return sharedNodeQueue
|
||||
}
|
||||
|
||||
// InstallNodeProcess 安装边缘节点流程控制
|
||||
func (this *NodeQueue) InstallNodeProcess(nodeId int64, isUpgrading bool) error {
|
||||
var installStatus = models.NewNodeInstallStatus()
|
||||
installStatus.IsRunning = true
|
||||
installStatus.UpdatedAt = time.Now().Unix()
|
||||
|
||||
err := models.SharedNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
var ticker = utils.NewTicker(3 * time.Second)
|
||||
goman.New(func() {
|
||||
for ticker.Wait() {
|
||||
installStatus.UpdatedAt = time.Now().Unix()
|
||||
err := models.SharedNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
||||
if err != nil {
|
||||
logs.Println("[INSTALL]" + err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
})
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
// 开始安装
|
||||
err = this.InstallNode(nodeId, installStatus, isUpgrading)
|
||||
|
||||
// 安装结束
|
||||
installStatus.IsRunning = false
|
||||
installStatus.IsFinished = true
|
||||
if err != nil {
|
||||
installStatus.Error = err.Error()
|
||||
} else {
|
||||
installStatus.IsOk = true
|
||||
}
|
||||
err = models.SharedNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 修改为已安装
|
||||
if installStatus.IsOk {
|
||||
err = models.SharedNodeDAO.UpdateNodeIsInstalled(nil, nodeId, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallNode 安装边缘节点
|
||||
func (this *NodeQueue) InstallNode(nodeId int64, installStatus *models.NodeInstallStatus, isUpgrading bool) error {
|
||||
node, err := models.SharedNodeDAO.FindEnabledNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleNode, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var loginParams = &models.NodeLoginSSHParams{}
|
||||
if login != nil {
|
||||
sshLoginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sshLoginParams != nil {
|
||||
loginParams = sshLoginParams
|
||||
}
|
||||
}
|
||||
|
||||
if len(loginParams.Host) == 0 {
|
||||
// 查询节点IP
|
||||
ip, _, err := models.SharedNodeIPAddressDAO.FindFirstNodeAccessIPAddress(nil, nodeId, false, nodeconfigs.NodeRoleNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ip) > 0 {
|
||||
loginParams.Host = ip
|
||||
} else {
|
||||
installStatus.ErrorCode = "EMPTY_SSH_HOST"
|
||||
return errors.New("ssh host should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.Port <= 0 {
|
||||
// 从集群中读取
|
||||
sshParams, err := models.SharedNodeClusterDAO.FindClusterSSHParams(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sshParams != nil && sshParams.Port > 0 {
|
||||
loginParams.Port = sshParams.Port
|
||||
} else {
|
||||
installStatus.ErrorCode = "EMPTY_SSH_PORT"
|
||||
return errors.New("ssh port is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.GrantId == 0 {
|
||||
// 从集群中读取
|
||||
grantId, err := models.SharedNodeClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grantId == 0 {
|
||||
installStatus.ErrorCode = "EMPTY_GRANT"
|
||||
return errors.New("can not find node grant")
|
||||
}
|
||||
loginParams.GrantId = grantId
|
||||
}
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
installStatus.ErrorCode = "EMPTY_GRANT"
|
||||
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
// API终端
|
||||
apiNodes, err := models.SharedAPINodeDAO.FindAllEnabledAndOnAPINodes(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(apiNodes) == 0 {
|
||||
return errors.New("no available api nodes")
|
||||
}
|
||||
|
||||
apiEndpoints := []string{}
|
||||
for _, apiNode := range apiNodes {
|
||||
addrConfigs, err := apiNode.DecodeAccessAddrs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode api node access addresses failed: %w", err)
|
||||
}
|
||||
for _, addrConfig := range addrConfigs {
|
||||
apiEndpoints = append(apiEndpoints, addrConfig.FullAddresses()...)
|
||||
}
|
||||
}
|
||||
|
||||
params := &NodeParams{
|
||||
Endpoints: apiEndpoints,
|
||||
NodeId: node.UniqueId,
|
||||
Secret: node.Secret,
|
||||
IsUpgrading: isUpgrading,
|
||||
}
|
||||
|
||||
var installer = &NodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: loginParams.Host,
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "SSH_LOGIN_FAILED"
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
// 安装目录
|
||||
installDir := node.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
clusterId := node.ClusterId
|
||||
cluster, err := models.SharedNodeClusterDAO.FindEnabledNodeCluster(nil, int64(clusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cluster == nil {
|
||||
return errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
||||
}
|
||||
installDir = cluster.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
// 默认是 $登录用户/edge-node
|
||||
installDir = installer.client.UserHome() + "/edge-node"
|
||||
}
|
||||
}
|
||||
|
||||
err = installer.Install(installDir, params, installStatus)
|
||||
return err
|
||||
}
|
||||
|
||||
// StartNode 启动边缘节点
|
||||
func (this *NodeQueue) StartNode(nodeId int64) error {
|
||||
node, err := models.SharedNodeDAO.FindEnabledNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleNode, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if login == nil {
|
||||
return newGrantError("can not find node login information")
|
||||
}
|
||||
loginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return newGrantError(err.Error())
|
||||
}
|
||||
|
||||
if len(loginParams.Host) == 0 {
|
||||
// 查询节点IP
|
||||
ip, _, err := models.SharedNodeIPAddressDAO.FindFirstNodeAccessIPAddress(nil, nodeId, false, nodeconfigs.NodeRoleNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ip) > 0 {
|
||||
loginParams.Host = ip
|
||||
} else {
|
||||
return newGrantError("ssh host should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.Port <= 0 {
|
||||
// 从集群中读取
|
||||
sshParams, err := models.SharedNodeClusterDAO.FindClusterSSHParams(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sshParams != nil && sshParams.Port > 0 {
|
||||
loginParams.Port = sshParams.Port
|
||||
} else {
|
||||
return newGrantError("ssh port is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.GrantId == 0 {
|
||||
// 从集群中读取
|
||||
grantId, err := models.SharedNodeClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grantId == 0 {
|
||||
return newGrantError("can not find node grant")
|
||||
}
|
||||
loginParams.GrantId = grantId
|
||||
}
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
return newGrantError("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
var installer = &NodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: loginParams.Host,
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
// 检查命令是否存在
|
||||
exe, err := this.lookupNodeExe(node, installer.client)
|
||||
if err != nil {
|
||||
return errors.New("edge node was not installed correctly, can not find executable file")
|
||||
}
|
||||
if len(exe) == 0 {
|
||||
return errors.New("edge node was not installed correctly, can not find executable file")
|
||||
}
|
||||
|
||||
// 我们先尝试Systemd启动
|
||||
_, _, _ = installer.client.Exec("systemctl start edge-node")
|
||||
|
||||
// 执行start
|
||||
_, stderr, err := installer.client.Exec("sudo " + exe + " start")
|
||||
if err != nil {
|
||||
return fmt.Errorf("start failed: %w", err)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("start failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopNode 停止节点
|
||||
func (this *NodeQueue) StopNode(nodeId int64) error {
|
||||
node, err := models.SharedNodeDAO.FindEnabledNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleNode, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if login == nil {
|
||||
return errors.New("can not find node login information")
|
||||
}
|
||||
loginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(loginParams.Host) == 0 {
|
||||
// 查询节点IP
|
||||
ip, _, err := models.SharedNodeIPAddressDAO.FindFirstNodeAccessIPAddress(nil, nodeId, false, nodeconfigs.NodeRoleNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ip) > 0 {
|
||||
loginParams.Host = ip
|
||||
} else {
|
||||
return errors.New("ssh host should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.Port <= 0 {
|
||||
// 从集群中读取
|
||||
sshParams, err := models.SharedNodeClusterDAO.FindClusterSSHParams(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sshParams != nil && sshParams.Port > 0 {
|
||||
loginParams.Port = sshParams.Port
|
||||
} else {
|
||||
return errors.New("ssh port is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.GrantId == 0 {
|
||||
// 从集群中读取
|
||||
grantId, err := models.SharedNodeClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grantId == 0 {
|
||||
return errors.New("can not find node grant")
|
||||
}
|
||||
loginParams.GrantId = grantId
|
||||
}
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
var installer = &NodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: loginParams.Host,
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
// 检查命令是否存在
|
||||
exe, err := this.lookupNodeExe(node, installer.client)
|
||||
if err != nil {
|
||||
return errors.New("edge node was not installed correctly, can not find executable file")
|
||||
}
|
||||
if len(exe) == 0 {
|
||||
return errors.New("edge node was not installed correctly, can not find executable file")
|
||||
}
|
||||
|
||||
// 我们先尝试Systemd停止
|
||||
_, _, _ = installer.client.Exec("/usr/bin/systemctl stop edge-node")
|
||||
|
||||
// 执行stop
|
||||
_, stderr, err := installer.client.Exec(exe + " stop")
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop failed: %w", err)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("stop failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UninstallNode 卸载节点
|
||||
func (this *NodeQueue) UninstallNode(nodeId int64) error {
|
||||
node, err := models.SharedNodeDAO.FindEnabledNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleNode, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if login == nil {
|
||||
return errors.New("can not find node login information")
|
||||
}
|
||||
loginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(loginParams.Host) == 0 {
|
||||
// 查询节点IP
|
||||
ip, _, err := models.SharedNodeIPAddressDAO.FindFirstNodeAccessIPAddress(nil, nodeId, false, nodeconfigs.NodeRoleNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ip) > 0 {
|
||||
loginParams.Host = ip
|
||||
} else {
|
||||
return errors.New("ssh host should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.Port <= 0 {
|
||||
// 从集群中读取
|
||||
sshParams, err := models.SharedNodeClusterDAO.FindClusterSSHParams(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sshParams != nil && sshParams.Port > 0 {
|
||||
loginParams.Port = sshParams.Port
|
||||
} else {
|
||||
return errors.New("ssh port is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
if loginParams.GrantId == 0 {
|
||||
// 从集群中读取
|
||||
grantId, err := models.SharedNodeClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grantId == 0 {
|
||||
return errors.New("can not find node grant")
|
||||
}
|
||||
loginParams.GrantId = grantId
|
||||
}
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
var installer = &NodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: loginParams.Host,
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
// 检查命令是否存在
|
||||
exe, err := this.lookupNodeExe(node, installer.client)
|
||||
if err != nil {
|
||||
return errors.New("edge node was not installed correctly, can not find executable file")
|
||||
}
|
||||
if len(exe) == 0 {
|
||||
return errors.New("edge node was not installed correctly, can not find executable file")
|
||||
}
|
||||
|
||||
// 执行uninstall
|
||||
_, stderr, err := installer.client.Exec(exe + " uninstall")
|
||||
if err != nil {
|
||||
return fmt.Errorf("uninstall failed: %w", err)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("uninstall failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *NodeQueue) lookupNodeExe(node *models.Node, client *SSHClient) (string, error) {
|
||||
// 安装目录
|
||||
var nodeDirs = []string{}
|
||||
if len(node.InstallDir) > 0 {
|
||||
nodeDirs = append(nodeDirs, node.InstallDir)
|
||||
}
|
||||
var clusterId = node.ClusterId
|
||||
cluster, err := models.SharedNodeClusterDAO.FindEnabledNodeCluster(nil, int64(clusterId))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if cluster == nil {
|
||||
return "", errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
||||
}
|
||||
if len(cluster.InstallDir) > 0 {
|
||||
nodeDirs = append(nodeDirs, cluster.InstallDir)
|
||||
}
|
||||
|
||||
// 默认是 $登录用户/edge-node
|
||||
nodeDirs = append(nodeDirs, client.UserHome()+"/edge-node")
|
||||
|
||||
// edge-boot安装目录
|
||||
nodeDirs = append(nodeDirs, "/usr/local/goedge")
|
||||
|
||||
for _, dir := range nodeDirs {
|
||||
var path = dir + "/edge-node/bin/edge-node"
|
||||
_, err := client.sftp.Stat(path)
|
||||
if err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
19
EdgeAPI/internal/installers/queue_node_test.go
Normal file
19
EdgeAPI/internal/installers/queue_node_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestQueue_InstallNode(t *testing.T) {
|
||||
queue := NewNodeQueue()
|
||||
err := queue.InstallNodeProcess(16, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
t.Log("OK")
|
||||
|
||||
}
|
||||
421
EdgeAPI/internal/installers/queue_ns_node_plus.go
Normal file
421
EdgeAPI/internal/installers/queue_ns_node_plus.go
Normal file
@@ -0,0 +1,421 @@
|
||||
//go:build plus
|
||||
|
||||
package installers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/db/models"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/goman"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/numberutils"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/iwind/TeaGo/logs"
|
||||
"time"
|
||||
)
|
||||
|
||||
var sharedNSNodeQueue = NewNSNodeQueue()
|
||||
|
||||
type NSNodeQueue struct {
|
||||
}
|
||||
|
||||
func NewNSNodeQueue() *NSNodeQueue {
|
||||
return &NSNodeQueue{}
|
||||
}
|
||||
|
||||
func SharedNSNodeQueue() *NSNodeQueue {
|
||||
return sharedNSNodeQueue
|
||||
}
|
||||
|
||||
// InstallNodeProcess 安装边缘节点流程控制
|
||||
func (this *NSNodeQueue) InstallNodeProcess(nodeId int64, isUpgrading bool) error {
|
||||
installStatus := models.NewNodeInstallStatus()
|
||||
installStatus.IsRunning = true
|
||||
installStatus.UpdatedAt = time.Now().Unix()
|
||||
|
||||
err := models.SharedNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新时间
|
||||
ticker := utils.NewTicker(3 * time.Second)
|
||||
goman.New(func() {
|
||||
for ticker.Wait() {
|
||||
installStatus.UpdatedAt = time.Now().Unix()
|
||||
err := models.SharedNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
||||
if err != nil {
|
||||
logs.Println("[INSTALL]" + err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
})
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
// 开始安装
|
||||
err = this.InstallNode(nodeId, installStatus, isUpgrading)
|
||||
|
||||
// 安装结束
|
||||
installStatus.IsRunning = false
|
||||
installStatus.IsFinished = true
|
||||
if err != nil {
|
||||
installStatus.Error = err.Error()
|
||||
} else {
|
||||
installStatus.IsOk = true
|
||||
}
|
||||
err = models.SharedNSNodeDAO.UpdateNodeInstallStatus(nil, nodeId, installStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 修改为已安装
|
||||
if installStatus.IsOk {
|
||||
err = models.SharedNSNodeDAO.UpdateNodeIsInstalled(nil, nodeId, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallNode 安装边缘节点
|
||||
func (this *NSNodeQueue) InstallNode(nodeId int64, installStatus *models.NodeInstallStatus, isUpgrading bool) error {
|
||||
node, err := models.SharedNSNodeDAO.FindEnabledNSNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleDNS, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var loginParams = &models.NodeLoginSSHParams{}
|
||||
if login != nil {
|
||||
sshLoginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sshLoginParams != nil {
|
||||
loginParams = sshLoginParams
|
||||
}
|
||||
}
|
||||
|
||||
if len(loginParams.Host) == 0 {
|
||||
installStatus.ErrorCode = "EMPTY_SSH_HOST"
|
||||
return errors.New("ssh host should not be empty")
|
||||
}
|
||||
|
||||
if loginParams.Port <= 0 {
|
||||
installStatus.ErrorCode = "EMPTY_SSH_PORT"
|
||||
return errors.New("ssh port is invalid")
|
||||
}
|
||||
|
||||
if loginParams.GrantId == 0 {
|
||||
// 从集群中读取
|
||||
grantId, err := models.SharedNSClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grantId == 0 {
|
||||
installStatus.ErrorCode = "EMPTY_GRANT"
|
||||
return errors.New("can not find node grant")
|
||||
}
|
||||
loginParams.GrantId = grantId
|
||||
}
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
installStatus.ErrorCode = "EMPTY_GRANT"
|
||||
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
// API终端
|
||||
apiNodes, err := models.SharedAPINodeDAO.FindAllEnabledAndOnAPINodes(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(apiNodes) == 0 {
|
||||
return errors.New("no available api nodes")
|
||||
}
|
||||
|
||||
apiEndpoints := []string{}
|
||||
for _, apiNode := range apiNodes {
|
||||
addrConfigs, err := apiNode.DecodeAccessAddrs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode api node access addresses failed: %w", err)
|
||||
}
|
||||
for _, addrConfig := range addrConfigs {
|
||||
apiEndpoints = append(apiEndpoints, addrConfig.FullAddresses()...)
|
||||
}
|
||||
}
|
||||
|
||||
params := &NodeParams{
|
||||
Endpoints: apiEndpoints,
|
||||
NodeId: node.UniqueId,
|
||||
Secret: node.Secret,
|
||||
IsUpgrading: isUpgrading,
|
||||
}
|
||||
|
||||
installer := &NSNodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: loginParams.Host,
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
installStatus.ErrorCode = "SSH_LOGIN_FAILED"
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
// 安装目录
|
||||
installDir := node.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
clusterId := node.ClusterId
|
||||
cluster, err := models.SharedNSClusterDAO.FindEnabledNSCluster(nil, int64(clusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cluster == nil {
|
||||
return errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
||||
}
|
||||
installDir = cluster.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
// 默认是 $登录用户/edge-dns
|
||||
installDir = installer.client.UserHome() + "/edge-dns"
|
||||
}
|
||||
}
|
||||
|
||||
err = installer.Install(installDir, params, installStatus)
|
||||
return err
|
||||
}
|
||||
|
||||
// StartNode 启动边缘节点
|
||||
func (this *NSNodeQueue) StartNode(nodeId int64) error {
|
||||
node, err := models.SharedNSNodeDAO.FindEnabledNSNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleDNS, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if login == nil {
|
||||
return newGrantError("can not find node login information")
|
||||
}
|
||||
loginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(loginParams.Host) == 0 {
|
||||
return newGrantError("ssh host should not be empty")
|
||||
}
|
||||
|
||||
if loginParams.Port <= 0 {
|
||||
return newGrantError("ssh port is invalid")
|
||||
}
|
||||
|
||||
if loginParams.GrantId == 0 {
|
||||
// 从集群中读取
|
||||
grantId, err := models.SharedNSClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grantId == 0 {
|
||||
return newGrantError("can not find node grant")
|
||||
}
|
||||
loginParams.GrantId = grantId
|
||||
}
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
return newGrantError("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
installer := &NSNodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: loginParams.Host,
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
// 安装目录
|
||||
installDir := node.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
clusterId := node.ClusterId
|
||||
cluster, err := models.SharedNSClusterDAO.FindEnabledNSCluster(nil, int64(clusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cluster == nil {
|
||||
return errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
||||
}
|
||||
installDir = cluster.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
// 默认是 $登录用户/edge-dns
|
||||
installDir = installer.client.UserHome() + "/edge-dns"
|
||||
}
|
||||
}
|
||||
|
||||
// 检查命令是否存在
|
||||
exeFile := installDir + "/edge-dns/bin/edge-dns"
|
||||
_, err = installer.client.Stat(exeFile)
|
||||
if err != nil {
|
||||
return errors.New("edge node is not installed correctly, can not find executable file: " + exeFile)
|
||||
}
|
||||
|
||||
// 我们先尝试Systemd启动
|
||||
_, _, _ = installer.client.Exec("/usr/bin/systemctl start edge-dns")
|
||||
|
||||
_, stderr, err := installer.client.Exec(exeFile + " start")
|
||||
if err != nil {
|
||||
return fmt.Errorf("start failed: %w", err)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("start failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopNode 停止节点
|
||||
func (this *NSNodeQueue) StopNode(nodeId int64) error {
|
||||
node, err := models.SharedNSNodeDAO.FindEnabledNSNode(nil, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if node == nil {
|
||||
return errors.New("can not find node, ID:'" + numberutils.FormatInt64(nodeId) + "'")
|
||||
}
|
||||
|
||||
// 登录信息
|
||||
login, err := models.SharedNodeLoginDAO.FindEnabledNodeLoginWithNodeId(nil, nodeconfigs.NodeRoleDNS, nodeId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if login == nil {
|
||||
return errors.New("can not find node login information")
|
||||
}
|
||||
loginParams, err := login.DecodeSSHParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(loginParams.Host) == 0 {
|
||||
return errors.New("ssh host should not be empty")
|
||||
}
|
||||
|
||||
if loginParams.Port <= 0 {
|
||||
return errors.New("ssh port is invalid")
|
||||
}
|
||||
|
||||
if loginParams.GrantId == 0 {
|
||||
// 从集群中读取
|
||||
grantId, err := models.SharedNSClusterDAO.FindClusterGrantId(nil, int64(node.ClusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grantId == 0 {
|
||||
return errors.New("can not find node grant")
|
||||
}
|
||||
loginParams.GrantId = grantId
|
||||
}
|
||||
grant, err := models.SharedNodeGrantDAO.FindEnabledNodeGrant(nil, loginParams.GrantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if grant == nil {
|
||||
return errors.New("can not find user grant with id '" + numberutils.FormatInt64(loginParams.GrantId) + "'")
|
||||
}
|
||||
|
||||
installer := &NSNodeInstaller{}
|
||||
err = installer.Login(&Credentials{
|
||||
Host: loginParams.Host,
|
||||
Port: loginParams.Port,
|
||||
Username: grant.Username,
|
||||
Password: grant.Password,
|
||||
PrivateKey: grant.PrivateKey,
|
||||
Passphrase: grant.Passphrase,
|
||||
Method: grant.Method,
|
||||
Sudo: grant.Su == 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = installer.Close()
|
||||
}()
|
||||
|
||||
// 安装目录
|
||||
installDir := node.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
clusterId := node.ClusterId
|
||||
cluster, err := models.SharedNSClusterDAO.FindEnabledNSCluster(nil, int64(clusterId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cluster == nil {
|
||||
return errors.New("can not find cluster, ID:'" + fmt.Sprintf("%d", clusterId) + "'")
|
||||
}
|
||||
installDir = cluster.InstallDir
|
||||
if len(installDir) == 0 {
|
||||
// 默认是 $登录用户/edge-dns
|
||||
installDir = installer.client.UserHome() + "/edge-dns"
|
||||
}
|
||||
}
|
||||
|
||||
// 检查命令是否存在
|
||||
exeFile := installDir + "/edge-dns/bin/edge-dns"
|
||||
_, err = installer.client.Stat(exeFile)
|
||||
if err != nil {
|
||||
return errors.New("edge node is not installed correctly, can not find executable file: " + exeFile)
|
||||
}
|
||||
|
||||
// 我们先尝试Systemd停止
|
||||
_, _, _ = installer.client.Exec("/usr/bin/systemctl stop edge-dns")
|
||||
|
||||
_, stderr, err := installer.client.Exec(exeFile + " stop")
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop failed: %w", err)
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("stop failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
275
EdgeAPI/internal/installers/ssh_client.go
Normal file
275
EdgeAPI/internal/installers/ssh_client.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SSHClient struct {
|
||||
raw *ssh.Client
|
||||
sftp *sftp.Client
|
||||
|
||||
sudo bool
|
||||
sudoPassword string
|
||||
}
|
||||
|
||||
func NewSSHClient(raw *ssh.Client) (*SSHClient, error) {
|
||||
c := &SSHClient{
|
||||
raw: raw,
|
||||
}
|
||||
|
||||
sftpClient, err := sftp.NewClient(raw)
|
||||
if err != nil {
|
||||
_ = c.Close()
|
||||
return nil, err
|
||||
}
|
||||
c.sftp = sftpClient
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Sudo 设置使用Sudo
|
||||
func (this *SSHClient) Sudo(password string) {
|
||||
this.sudo = true
|
||||
this.sudoPassword = password
|
||||
}
|
||||
|
||||
// Exec 执行shell命令
|
||||
func (this *SSHClient) Exec(cmd string) (stdout string, stderr string, err error) {
|
||||
if this.raw.User() != "root" && this.sudo {
|
||||
return this.execSudo(cmd, this.sudoPassword)
|
||||
}
|
||||
|
||||
session, err := this.raw.NewSession()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = session.Close()
|
||||
}()
|
||||
|
||||
var stdoutBuf = &bytes.Buffer{}
|
||||
var stderrBuf = &bytes.Buffer{}
|
||||
session.Stdout = stdoutBuf
|
||||
session.Stderr = stderrBuf
|
||||
err = session.Run(cmd)
|
||||
if err != nil {
|
||||
return stdoutBuf.String(), stderrBuf.String(), err
|
||||
}
|
||||
return strings.TrimRight(stdoutBuf.String(), "\n"), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
// execSudo 使用sudo执行shell命令
|
||||
func (this *SSHClient) execSudo(cmd string, password string) (stdout string, stderr string, err error) {
|
||||
session, err := this.raw.NewSession()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = session.Close()
|
||||
}()
|
||||
|
||||
modes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0, // disable echo
|
||||
ssh.TTY_OP_ISPEED: 14400,
|
||||
ssh.TTY_OP_OSPEED: 14400,
|
||||
}
|
||||
|
||||
err = session.RequestPty("xterm", 80, 40, modes)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var stderrBuf = &bytes.Buffer{}
|
||||
session.Stderr = stderrBuf
|
||||
|
||||
pipeIn, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
pipeOut, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
var resultErr error
|
||||
var stdoutBuf = bytes.NewBuffer([]byte{})
|
||||
|
||||
go func() {
|
||||
var buf = make([]byte, 512)
|
||||
for {
|
||||
n, err := pipeOut.Read(buf)
|
||||
if n > 0 {
|
||||
if strings.Contains(string(buf[:n]), "[sudo] password for") {
|
||||
_, err = pipeIn.Write([]byte(password + "\n"))
|
||||
if err != nil {
|
||||
resultErr = err
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
stdoutBuf.Write(buf[:n])
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = session.Run("sudo " + cmd)
|
||||
|
||||
stdout = strings.TrimSpace(stdoutBuf.String())
|
||||
stderr = strings.TrimSpace(stderrBuf.String())
|
||||
|
||||
if err != nil {
|
||||
return stdout, stderr, err
|
||||
}
|
||||
|
||||
if resultErr != nil {
|
||||
return stdout, stderr, resultErr
|
||||
}
|
||||
return stdout, stderr, nil
|
||||
}
|
||||
|
||||
func (this *SSHClient) Listen(network string, addr string) (net.Listener, error) {
|
||||
return this.raw.Listen(network, addr)
|
||||
}
|
||||
|
||||
func (this *SSHClient) Dial(network string, addr string) (net.Conn, error) {
|
||||
return this.raw.Dial(network, addr)
|
||||
}
|
||||
|
||||
func (this *SSHClient) Close() error {
|
||||
if this.sftp != nil {
|
||||
_ = this.sftp.Close()
|
||||
}
|
||||
return this.raw.Close()
|
||||
}
|
||||
|
||||
func (this *SSHClient) OpenFile(path string, flags int) (*sftp.File, error) {
|
||||
return this.sftp.OpenFile(path, flags)
|
||||
}
|
||||
|
||||
func (this *SSHClient) Stat(path string) (os.FileInfo, error) {
|
||||
return this.sftp.Stat(path)
|
||||
}
|
||||
|
||||
func (this *SSHClient) Mkdir(path string) error {
|
||||
return this.sftp.Mkdir(path)
|
||||
}
|
||||
|
||||
func (this *SSHClient) MkdirAll(path string) error {
|
||||
err := this.sftp.MkdirAll(path)
|
||||
if err != nil && this.sudo {
|
||||
_, _, err2 := this.execSudo("mkdir -p "+path, this.sudoPassword)
|
||||
if err2 == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (this *SSHClient) Chmod(path string, mode os.FileMode) error {
|
||||
return this.sftp.Chmod(path, mode)
|
||||
}
|
||||
|
||||
// Copy 拷贝文件
|
||||
func (this *SSHClient) Copy(localPath string, remotePath string, mode os.FileMode) error {
|
||||
localFp, err := os.Open(localPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = localFp.Close()
|
||||
}()
|
||||
remoteFp, err := this.sftp.OpenFile(remotePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = remoteFp.Close()
|
||||
}()
|
||||
_, err = io.Copy(remoteFp, localFp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return this.Chmod(remotePath, mode)
|
||||
}
|
||||
|
||||
// NewSession 获取新Session
|
||||
func (this *SSHClient) NewSession() (*ssh.Session, error) {
|
||||
return this.raw.NewSession()
|
||||
}
|
||||
|
||||
// ReadFile 读取文件内容
|
||||
func (this *SSHClient) ReadFile(path string) ([]byte, error) {
|
||||
fp, err := this.sftp.OpenFile(path, 0444)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
buffer := bytes.NewBuffer([]byte{})
|
||||
_, err = io.Copy(buffer, fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
// WriteFile 写入文件内容
|
||||
func (this *SSHClient) WriteFile(path string, data []byte) (n int, err error) {
|
||||
fp, err := this.sftp.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
n, err = fp.Write(data)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove 删除文件
|
||||
func (this *SSHClient) Remove(path string) error {
|
||||
return this.sftp.Remove(path)
|
||||
}
|
||||
|
||||
// User 用户名
|
||||
func (this *SSHClient) User() string {
|
||||
return this.raw.User()
|
||||
}
|
||||
|
||||
// UserHome 用户地址
|
||||
func (this *SSHClient) UserHome() string {
|
||||
homeStdout, _, err := this.Exec("echo $HOME")
|
||||
if err != nil {
|
||||
return this.defaultUserHome()
|
||||
}
|
||||
|
||||
var home = strings.TrimSpace(homeStdout)
|
||||
if len(home) > 0 {
|
||||
return home
|
||||
}
|
||||
|
||||
return this.defaultUserHome()
|
||||
}
|
||||
|
||||
func (this *SSHClient) defaultUserHome() string {
|
||||
var user = this.raw.User()
|
||||
if user == "root" {
|
||||
return "/root"
|
||||
}
|
||||
return "/home/" + user
|
||||
}
|
||||
82
EdgeAPI/internal/installers/ssh_client_test.go
Normal file
82
EdgeAPI/internal/installers/ssh_client_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2021 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
|
||||
package installers
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func testSSHClient(t *testing.T, username string, password string) *SSHClient {
|
||||
methods := []ssh.AuthMethod{}
|
||||
{
|
||||
authMethod := ssh.Password(password)
|
||||
methods = append(methods, authMethod)
|
||||
}
|
||||
|
||||
{
|
||||
authMethod := ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) (answers []string, err error) {
|
||||
if len(questions) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
return []string{password}, nil
|
||||
})
|
||||
methods = append(methods, authMethod)
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: methods,
|
||||
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
|
||||
return nil
|
||||
},
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
sshClient, err := ssh.Dial("tcp", "192.168.2.31:22", config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client, err := NewSSHClient(sshClient)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func TestSSHClient_Home(t *testing.T) {
|
||||
var client = testSSHClient(t, "root", "123456")
|
||||
t.Log(client.UserHome())
|
||||
}
|
||||
|
||||
func TestSSHClient_Exec(t *testing.T) {
|
||||
var client = testSSHClient(t, "liuxiangchao", "123456")
|
||||
stdout, stderr, err := client.Exec("echo 'Hello'")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("stdout:", stdout, "stderr:", stderr)
|
||||
}
|
||||
|
||||
func TestSSHClient_SudoExec(t *testing.T) {
|
||||
var client = testSSHClient(t, "liuxiangchao", "123456")
|
||||
client.Sudo("123456")
|
||||
stdout, stderr, err := client.Exec("echo 'Hello'")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("stdout:", stdout, "stderr:", stderr)
|
||||
}
|
||||
|
||||
func TestSSHClient_SudoExec2(t *testing.T) {
|
||||
var client = testSSHClient(t, "liuxiangchao", "123456")
|
||||
client.Sudo("123456")
|
||||
stdout, stderr, err := client.Exec("/home/liuxiangchao/edge-node/edge-node/bin/edge-node start")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("stdout:", stdout, "stderr:", stderr)
|
||||
}
|
||||
90
EdgeAPI/internal/installers/upgrade_limiter.go
Normal file
90
EdgeAPI/internal/installers/upgrade_limiter.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package installers
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/sizes"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
UpgradeLimiterDuration = 10 // node key expire time, by seconds
|
||||
UpgradeLimiterConcurrent = 10 // 10 nodes
|
||||
UpgradeLimiterMaxBytesPerSecond = 5 * sizes.M // max bytes per second
|
||||
)
|
||||
|
||||
var SharedUpgradeLimiter = NewUpgradeLimiter()
|
||||
|
||||
// UpgradeLimiter 升级流量管理器
|
||||
type UpgradeLimiter struct {
|
||||
nodeMap map[string]int64 // key => timestamp
|
||||
|
||||
rateTimestamp int64
|
||||
rateBytes int64
|
||||
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
func NewUpgradeLimiter() *UpgradeLimiter {
|
||||
return &UpgradeLimiter{
|
||||
nodeMap: map[string]int64{},
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateNodeBytes 添加正在下载的节点流量
|
||||
func (this *UpgradeLimiter) UpdateNodeBytes(nodeType nodeconfigs.NodeRole, nodeId int64, bytes int64) {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
// 先清理
|
||||
var nowTime = time.Now().Unix()
|
||||
this.gc(nowTime)
|
||||
|
||||
// 添加
|
||||
var key = nodeType + "_" + types.String(nodeId)
|
||||
this.nodeMap[key] = nowTime
|
||||
|
||||
// 流量
|
||||
if this.rateTimestamp == nowTime {
|
||||
this.rateBytes += bytes
|
||||
} else {
|
||||
this.rateTimestamp = nowTime
|
||||
this.rateBytes = bytes
|
||||
}
|
||||
}
|
||||
|
||||
// CanUpgrade 检查是否有新的升级
|
||||
func (this *UpgradeLimiter) CanUpgrade() bool {
|
||||
this.locker.Lock()
|
||||
defer this.locker.Unlock()
|
||||
|
||||
var nowTime = time.Now().Unix()
|
||||
this.gc(nowTime)
|
||||
|
||||
// 限制并发节点数
|
||||
if len(this.nodeMap) >= UpgradeLimiterConcurrent {
|
||||
return false
|
||||
}
|
||||
|
||||
if this.rateTimestamp != nowTime {
|
||||
return true
|
||||
}
|
||||
|
||||
// 限制下载速度
|
||||
if this.rateBytes >= UpgradeLimiterMaxBytesPerSecond {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (this *UpgradeLimiter) gc(nowTime int64) {
|
||||
for nodeKey, timestamp := range this.nodeMap {
|
||||
if timestamp < nowTime-UpgradeLimiterDuration {
|
||||
delete(this.nodeMap, nodeKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
EdgeAPI/internal/installers/upgrade_limiter_test.go
Normal file
27
EdgeAPI/internal/installers/upgrade_limiter_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package installers_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/installers"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/utils/sizes"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewUpgradeLimiter(t *testing.T) {
|
||||
var limiter = installers.NewUpgradeLimiter()
|
||||
limiter.UpdateNodeBytes(nodeconfigs.NodeRoleNode, 1, 1)
|
||||
limiter.UpdateNodeBytes(nodeconfigs.NodeRoleNode, 2, 5*sizes.M)
|
||||
t.Log("limiter:", limiter)
|
||||
t.Log("canUpgrade:", limiter.CanUpgrade())
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
t.Log("canUpgrade:", limiter.CanUpgrade())
|
||||
t.Log("limiter:", limiter)
|
||||
limiter.UpdateNodeBytes(nodeconfigs.NodeRoleNode, 2, 4*sizes.M)
|
||||
t.Log("canUpgrade:", limiter.CanUpgrade())
|
||||
|
||||
t.Log("limiter:", limiter)
|
||||
}
|
||||
Reference in New Issue
Block a user