1.4.5.2
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
|
||||
}
|
||||
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")
|
||||
}
|
||||
230
EdgeAPI/internal/installers/installer_base.go
Normal file
230
EdgeAPI/internal/installers/installer_base.go
Normal file
@@ -0,0 +1,230 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
165
EdgeAPI/internal/installers/installer_node.go
Normal file
165
EdgeAPI/internal/installers/installer_node.go
Normal file
@@ -0,0 +1,165 @@
|
||||
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.LookupLatestInstaller(filePrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(zipFile) == 0 {
|
||||
return errors.New("can not find installer file for " + env.OS + "/" + env.Arch)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
167
EdgeAPI/internal/installers/installer_ns_node_plus.go
Normal file
167
EdgeAPI/internal/installers/installer_ns_node_plus.go
Normal file
@@ -0,0 +1,167 @@
|
||||
//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.LookupLatestInstaller(filePrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(zipFile) == 0 {
|
||||
return errors.New("can not find installer file for " + env.OS + "/" + env.Arch)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试
|
||||
_, 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