Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkg: add an underlying package hostpool to manage general host operation #1713

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
93 changes: 93 additions & 0 deletions pkg/hostpool/scp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// 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
}
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
}
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