mirror of
https://github.com/TeaOSLab/EdgeAPI.git
synced 2025-11-03 23:20:26 +08:00
阶段性提交
This commit is contained in:
9
internal/installers/credentials.go
Normal file
9
internal/installers/credentials.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package installers
|
||||
|
||||
type Credentials struct {
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
PrivateKey string
|
||||
}
|
||||
7
internal/installers/env.go
Normal file
7
internal/installers/env.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package installers
|
||||
|
||||
type Env struct {
|
||||
OS string
|
||||
Arch string
|
||||
HelperName string
|
||||
}
|
||||
168
internal/installers/installer_base.go
Normal file
168
internal/installers/installer_base.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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
|
||||
}
|
||||
|
||||
// 登录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
|
||||
}
|
||||
}
|
||||
|
||||
// 认证
|
||||
methods := []ssh.AuthMethod{}
|
||||
if len(credentials.Password) > 0 {
|
||||
{
|
||||
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 {
|
||||
signer, err := ssh.ParsePrivateKey([]byte(credentials.PrivateKey))
|
||||
if err != nil {
|
||||
return errors.New("parse private key: " + err.Error())
|
||||
}
|
||||
authMethod := ssh.PublicKeys(signer)
|
||||
methods = append(methods, authMethod)
|
||||
}
|
||||
|
||||
// SSH客户端
|
||||
config := &ssh.ClientConfig{
|
||||
User: credentials.Username,
|
||||
Auth: methods,
|
||||
HostKeyCallback: hostKeyCallback,
|
||||
Timeout: 5 * time.Second, // TODO 后期可以设置这个超时时间
|
||||
}
|
||||
|
||||
sshClient, err := ssh.Dial("tcp", credentials.Host+":"+strconv.Itoa(credentials.Port), config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := NewSSHClient(sshClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
this.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
// 关闭SSH服务
|
||||
func (this *BaseInstaller) Close() error {
|
||||
if this.client != nil {
|
||||
return this.client.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 查找最新的版本的文件
|
||||
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
|
||||
}
|
||||
|
||||
lastVersion := ""
|
||||
result := ""
|
||||
for _, match := range matches {
|
||||
baseName := filepath.Base(match)
|
||||
if !pattern.MatchString(baseName) {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 上传安装助手
|
||||
func (this *BaseInstaller) InstallHelper(targetDir string) (env *Env, err error) {
|
||||
uname, _, err := this.client.Exec("uname -a")
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
|
||||
osName := ""
|
||||
archName := ""
|
||||
if strings.Index(uname, "Darwin") > 0 {
|
||||
osName = "darwin"
|
||||
} else if strings.Index(uname, "Linux") >= 0 {
|
||||
osName = "linux"
|
||||
} else {
|
||||
// TODO 支持freebsd, aix ...
|
||||
return env, errors.New("installer not supported os '" + uname + "'")
|
||||
}
|
||||
|
||||
if strings.Index(uname, "x86_64") > 0 {
|
||||
archName = "amd64"
|
||||
} else {
|
||||
// TODO 支持ARM和MIPS等架构
|
||||
archName = "386"
|
||||
}
|
||||
|
||||
exeName := "installer-helper-" + osName + "-" + archName
|
||||
exePath := Tea.Root + "/installers/" + exeName
|
||||
|
||||
err = this.client.Copy(exePath, targetDir+"/"+exeName, 0777)
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
|
||||
env = &Env{
|
||||
OS: osName,
|
||||
Arch: archName,
|
||||
HelperName: exeName,
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
20
internal/installers/installer_base_test.go
Normal file
20
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)
|
||||
}
|
||||
12
internal/installers/installer_interface.go
Normal file
12
internal/installers/installer_interface.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package installers
|
||||
|
||||
type InstallerInterface interface {
|
||||
// 登录SSH服务
|
||||
Login(credentials *Credentials) error
|
||||
|
||||
// 安装
|
||||
Install(dir string, params interface{}) error
|
||||
|
||||
// 关闭连接的SSH服务
|
||||
Close() error
|
||||
}
|
||||
86
internal/installers/installer_node.go
Normal file
86
internal/installers/installer_node.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package installers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type NodeInstaller struct {
|
||||
BaseInstaller
|
||||
}
|
||||
|
||||
func (this *NodeInstaller) Install(dir string, params interface{}) 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 errors.New("params validation: " + err.Error())
|
||||
}
|
||||
|
||||
// 安装助手
|
||||
env, err := this.InstallHelper(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传安装文件
|
||||
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)
|
||||
}
|
||||
targetZip := dir + "/" + filepath.Base(zipFile)
|
||||
err = this.client.Copy(zipFile, targetZip, 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解压
|
||||
_, stderr, err := this.client.Exec(dir + "/" + env.HelperName + " -cmd=unzip -zip=\"" + targetZip + "\" -target=\"" + dir + "\"")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("unzip installer failed: " + stderr)
|
||||
}
|
||||
|
||||
// 修改配置文件
|
||||
{
|
||||
templateFile := dir + "/edge-node/configs/api.template.yaml"
|
||||
configFile := dir + "/edge-node/configs/api.yaml"
|
||||
data, err := this.client.ReadFile(templateFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = bytes.ReplaceAll(data, []byte("${endpoint}"), []byte(nodeParams.Endpoint))
|
||||
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 errors.New("write 'configs/api.yaml': " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 启动
|
||||
_, stderr, err = this.client.Exec(dir + "/edge-node/bin/edge-node start")
|
||||
if err != nil {
|
||||
return errors.New("start edge node failed: " + err.Error())
|
||||
}
|
||||
|
||||
if len(stderr) > 0 {
|
||||
return errors.New("start edge node failed: " + stderr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
internal/installers/installer_node_test.go
Normal file
35
internal/installers/installer_node_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package installers
|
||||
|
||||
import "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{
|
||||
Endpoint: "192.168.2.40:8003",
|
||||
NodeId: "313fdb1b90d0a63c736f307b4d1ca358",
|
||||
Secret: "Pl3u5kYqBDZddp7raw6QfHiuGPRCWF54",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
22
internal/installers/params_node.go
Normal file
22
internal/installers/params_node.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package installers
|
||||
|
||||
import "errors"
|
||||
|
||||
type NodeParams struct {
|
||||
Endpoint string
|
||||
NodeId string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func (this *NodeParams) Validate() error {
|
||||
if len(this.Endpoint) == 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
|
||||
}
|
||||
149
internal/installers/ssh_client.go
Normal file
149
internal/installers/ssh_client.go
Normal file
@@ -0,0 +1,149 @@
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 执行shell命令
|
||||
func (this *SSHClient) Exec(cmd string) (stdout string, stderr string, err error) {
|
||||
session, err := this.raw.NewSession()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = session.Close()
|
||||
}()
|
||||
|
||||
stdoutBuf := bytes.NewBuffer([]byte{})
|
||||
stderrBuf := bytes.NewBuffer([]byte{})
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return this.sftp.MkdirAll(path)
|
||||
}
|
||||
|
||||
func (this *SSHClient) Chmod(path string, mode os.FileMode) error {
|
||||
return this.sftp.Chmod(path, mode)
|
||||
}
|
||||
|
||||
// 拷贝文件
|
||||
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)
|
||||
}
|
||||
|
||||
// 获取新Session
|
||||
func (this *SSHClient) NewSession() (*ssh.Session, error) {
|
||||
return this.raw.NewSession()
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
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
|
||||
}
|
||||
|
||||
// 写入文件内容
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user