Skip to content

Commit

Permalink
pkg: add an underlying package hostpool to manage general host operation
Browse files Browse the repository at this point in the history
Signed-off-by: Allen Sun <[email protected]>
  • Loading branch information
allencloud committed Sep 16, 2022
1 parent cd61b99 commit dd591b0
Show file tree
Hide file tree
Showing 16 changed files with 1,453 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/BurntSushi/toml v1.0.0
github.com/Masterminds/semver/v3 v3.1.1
github.com/aliyun/alibaba-cloud-sdk-go v1.61.985
github.com/bramvdbogaerde/go-scp v1.2.0
github.com/cavaliergopher/grab/v3 v3.0.1
github.com/containers/buildah v1.25.0
github.com/containers/common v0.47.5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/bramvdbogaerde/go-scp v1.2.0 h1:mNF1lCXQ6jQcxCBBuc2g/CQwVy/4QONaoD5Aqg9r+Zg=
github.com/bramvdbogaerde/go-scp v1.2.0/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yPXQTPshACY8vQ0=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
Expand Down
115 changes: 115 additions & 0 deletions pkg/hostpool/host.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright © 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hostpool

import (
"fmt"
"net"
"strconv"

goscp "github.com/bramvdbogaerde/go-scp"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)

// Host contains both static and dynamic information of a host machine.
// Static part: the host config
// dynamic part, including ssh client and sftp client.
type Host struct {
config HostConfig

// sshClient is used to create ssh.Session.
// TODO: remove this and just make ssh.Session remain.
sshClient *ssh.Client
// sshSession is created by ssh.Client and used for command execution on specified host.
sshSession *ssh.Session
// sftpClient is used to file remote operation on specified host except scp operation.
sftpClient *sftp.Client
// scpClient is used to scp files between sealer node and all nodes.
scpClient *goscp.Client

// isLocal identifies that whether the initialized host is the sealer binary located node.
isLocal bool
}

// HostConfig is the host config, including IP, port, login credentials and so on.
type HostConfig struct {
// IP is the IP address of host.
// It supports both IPv4 and IPv6.
IP net.IP

// Port is the port config used by ssh to connect host
// The connecting operation will use port 22 if port is not set.
Port int

// Usually User will be root. If it is set a non-root user,
// then this non-root must has a sudo permission.
User string
Password string

// Encrypted means the password is encrypted.
// Password above should be decrypted first before being called.
Encrypted bool

// TODO: add PkFile support
// PkFile string
// PkPassword string
}

// Initialize setups ssh and sftp clients.
func (host *Host) Initialize() error {
config := &ssh.ClientConfig{
User: host.config.User,
Auth: []ssh.AuthMethod{
ssh.Password(host.config.Password),
},
HostKeyCallback: nil,
}

hostAddr := host.config.IP.String()
port := strconv.Itoa(host.config.Port)

// sshClient
sshClient, err := ssh.Dial("tcp", net.JoinHostPort(hostAddr, port), config)
if err != nil {
return fmt.Errorf("failed to create ssh client for host(%s): %v", hostAddr, err)
}
host.sshClient = sshClient

// sshSession
sshSession, err := sshClient.NewSession()
if err != nil {
return fmt.Errorf("failed to create ssh session for host(%s): %v", hostAddr, err)
}
host.sshSession = sshSession

// sftpClient
sftpClient, err := sftp.NewClient(sshClient, nil)
if err != nil {
return fmt.Errorf("failed to create sftp client for host(%s): %v", hostAddr, err)
}
host.sftpClient = sftpClient

// scpClient
scpClient, err := goscp.NewClientBySSH(sshClient)
if err != nil {
return fmt.Errorf("failed to create scp client for host(%s): %v", hostAddr, err)
}
host.scpClient = &scpClient

// TODO: set isLocal

return nil
}
72 changes: 72 additions & 0 deletions pkg/hostpool/host_pool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright © 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hostpool

import (
"fmt"
)

// HostPool is a host resource pool of sealer's cluster, including masters and nodes.
// While SEALER DEPLOYING NODE has no restrict relationship with masters nor nodes:
// 1. sealer deploying node could be a node which is no master nor node;
// 2. sealer deploying node could also be one of masters and nodes.
// Then deploying node is not included in HostPool.
type HostPool struct {
// host is a map:
// key has a type of string which is from net.Ip.String()
hosts map[string]*Host
}

// New initializes a brand new HostPool instance.
func New(hostConfigs []*HostConfig) (*HostPool, error) {
if len(hostConfigs) == 0 {
return nil, fmt.Errorf("input HostConfigs cannot be empty")
}
var hostPool HostPool
for _, hostConfig := range hostConfigs {
if _, OK := hostPool.hosts[hostConfig.IP.String()]; OK {
return nil, fmt.Errorf("there must not be duplicated host IP(%s) in cluster hosts", hostConfig.IP.String())
}
hostPool.hosts[hostConfig.IP.String()] = &Host{
config: HostConfig{
IP: hostConfig.IP,
Port: hostConfig.Port,
User: hostConfig.User,
Password: hostConfig.Password,
Encrypted: hostConfig.Encrypted,
},
}
}
return &hostPool, nil
}

// Initialize helps HostPool to setup all attributes for each host,
// like scpClient, sshClient and so on.
func (hp *HostPool) Initialize() error {
for _, host := range hp.hosts {
if err := host.Initialize(); err != nil {
return fmt.Errorf("failed to initialize host in HostPool: %v", err)
}
}
return nil
}

// GetHost gets the detailed host connection instance via IP string as a key.
func (hp *HostPool) GetHost(ipStr string) (*Host, error) {
if host, exist := hp.hosts[ipStr]; exist {
return host, nil
}
return nil, fmt.Errorf("cannot get host connection in HostPool by key(%s)", ipStr)
}
95 changes: 95 additions & 0 deletions pkg/hostpool/scp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright © 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hostpool

import (
"context"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)

// CopyFile copies the contents of localFilePath to remote destination path.
// Both localFilePath and remotePath must be an absolute path.
//
// It must be executed in deploying node and towards the host instance.
func (host *Host) CopyToRemote(localFilePath string, remotePath string, permissions string) error {
if host.isLocal {
// TODO: add local file copy.
return fmt.Errorf("local file copy is not implemented")
}

f, err := os.Open(filepath.Clean(localFilePath))
if err != nil {
return err
}
defer f.Close()
return host.scpClient.CopyFromFile(context.Background(), *f, remotePath, permissions)
}

// CopyFile copies the contents of remotePath to local destination path.
// Both localFilePath and remotePath must be an absolute path.
//
// It must be executed in deploying node and towards the host instance.
func (host *Host) CopyFromRemote(localFilePath string, remotePath string) error {
if host.isLocal {
// TODO: add local file copy.
return fmt.Errorf("local file copy is not implemented")
}

f, err := os.Open(filepath.Clean(localFilePath))
if err != nil {
return err
}
defer f.Close()
return host.scpClient.CopyFromRemote(context.Background(), f, remotePath)
}

// CopyToRemoteDir copies the contents of local directory to remote destination directory.
// Both localFilePath and remotePath must be an absolute path.
//
// It must be executed in deploying node and towards the host instance.
func (host *Host) CopyToRemoteDir(localDir string, remoteDir string) error {
if host.isLocal {
// TODO: add local file copy.
return fmt.Errorf("local file copy is not implemented")
}

// get the localDir Directory name
fInfo, err := os.Lstat(localDir)
if err != nil {
return err
}
if !fInfo.IsDir() {
return fmt.Errorf("input localDir(%s) is not a directory when copying directory content", localDir)
}
dirName := fInfo.Name()

err = filepath.Walk(localDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
// Since localDir is an absolute path, then every passed path has a prefix of localDir,
// then the relative path is the input path trims localDir.
fileRelativePath := strings.TrimPrefix(path, localDir)
remotePath := filepath.Join(remoteDir, dirName, fileRelativePath)

return host.CopyToRemote(path, remotePath, info.Mode().String())
})

return err
}
71 changes: 71 additions & 0 deletions pkg/hostpool/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright © 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hostpool

import (
"bytes"
"fmt"
"os/exec"
)

// Output runs cmd on the remote host and returns its standard output.
// It must be executed in deploying node and towards the host instance.
func (host *Host) Output(cmd string) ([]byte, error) {
if host.isLocal {
return exec.Command(cmd).Output()
}
return host.sshSession.Output(cmd)
}

// CombinedOutput wraps the sshSession.CombinedOutput and does the same in both input and output.
// It must be executed in deploying node and towards the host instance.
func (host *Host) CombinedOutput(cmd string) ([]byte, error) {
if host.isLocal {
return exec.Command(cmd).CombinedOutput()
}
return host.sshSession.CombinedOutput(cmd)
}

// RunAndStderr runs a specified command and output stderr content.
// If command returns a nil, then no matter if there is content in session's stderr, just ignore stderr;
// If command return a non-nil, construct and return a new error with stderr content
// which may contains the exact error message.
//
// TODO: there is a potential issue that if much content is in stdout or stderr, and
// it may eventually cause the remote command to block.
//
// It must be executed in deploying node and towards the host instance.
func (host *Host) RunAndStderr(cmd string) ([]byte, error) {
var stdout, stderr bytes.Buffer
if host.isLocal {
localCmd := exec.Command(cmd)
localCmd.Stdout = &stdout
localCmd.Stderr = &stderr
if err := localCmd.Run(); err != nil {
return nil, fmt.Errorf("failed to exec cmd(%s) on host(%s): %s", cmd, host.config.IP, stderr.String())
}
return stdout.Bytes(), nil
}

host.sshSession.Stdout = &stdout
host.sshSession.Stderr = &stderr
if err := host.sshSession.Run(cmd); err != nil {
return nil, fmt.Errorf("failed to exec cmd(%s) on host(%s): %s", cmd, host.config.IP, stderr.String())
}

return stdout.Bytes(), nil
}

// TODO: Do we need asynchronously output stdout and stderr?
15 changes: 15 additions & 0 deletions pkg/hostpool/sftp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright © 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hostpool
2 changes: 2 additions & 0 deletions vendor/github.com/bramvdbogaerde/go-scp/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit dd591b0

Please sign in to comment.