阶段性提交

This commit is contained in:
GoEdgeLab
2020-09-06 16:19:54 +08:00
parent f8049b3739
commit 84868c1a0b
37 changed files with 3134 additions and 91 deletions

View File

@@ -0,0 +1,9 @@
package installers
type Credentials struct {
Host string
Port int
Username string
Password string
PrivateKey string
}

View File

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

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

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,12 @@
package installers
type InstallerInterface interface {
// 登录SSH服务
Login(credentials *Credentials) error
// 安装
Install(dir string, params interface{}) error
// 关闭连接的SSH服务
Close() error
}

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

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

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

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