This commit is contained in:
unknown
2026-02-04 20:27:13 +08:00
commit 3b042d1dad
9410 changed files with 1488147 additions and 0 deletions

View 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
}

View 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
}

View 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
}
}

View 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
}

View 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)
}

View File

@@ -0,0 +1,7 @@
package installers
type Env struct {
OS string
Arch string
HelperPath string
}

View 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
}

View File

@@ -0,0 +1 @@
远程安装依赖文件,单独放在一个目录防止安装包过大

View 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
}

View 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")
}

View 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"
}

View 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)
}

View 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
}

View 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
}

View 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)
}
}

View 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
}

View 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)
}
}

View 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, "\", \"") + "\""
}

View 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
}

View 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")
}

View 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
}

View 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
}

View 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)
}

View 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)
}
}
}

View 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)
}