Skip to content

Commit dd591b0

Browse files
committed
pkg: add an underlying package hostpool to manage general host operation
Signed-off-by: Allen Sun <shlallen1990@gmail.com>
1 parent cd61b99 commit dd591b0

File tree

16 files changed

+1453
-0
lines changed

16 files changed

+1453
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/BurntSushi/toml v1.0.0
77
github.com/Masterminds/semver/v3 v3.1.1
88
github.com/aliyun/alibaba-cloud-sdk-go v1.61.985
9+
github.com/bramvdbogaerde/go-scp v1.2.0
910
github.com/cavaliergopher/grab/v3 v3.0.1
1011
github.com/containers/buildah v1.25.0
1112
github.com/containers/common v0.47.5

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6
266266
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
267267
github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
268268
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
269+
github.com/bramvdbogaerde/go-scp v1.2.0 h1:mNF1lCXQ6jQcxCBBuc2g/CQwVy/4QONaoD5Aqg9r+Zg=
270+
github.com/bramvdbogaerde/go-scp v1.2.0/go.mod h1:s4ZldBoRAOgUg8IrRP2Urmq5qqd2yPXQTPshACY8vQ0=
269271
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
270272
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
271273
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=

pkg/hostpool/host.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright © 2022 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package hostpool
16+
17+
import (
18+
"fmt"
19+
"net"
20+
"strconv"
21+
22+
goscp "github.com/bramvdbogaerde/go-scp"
23+
"github.com/pkg/sftp"
24+
"golang.org/x/crypto/ssh"
25+
)
26+
27+
// Host contains both static and dynamic information of a host machine.
28+
// Static part: the host config
29+
// dynamic part, including ssh client and sftp client.
30+
type Host struct {
31+
config HostConfig
32+
33+
// sshClient is used to create ssh.Session.
34+
// TODO: remove this and just make ssh.Session remain.
35+
sshClient *ssh.Client
36+
// sshSession is created by ssh.Client and used for command execution on specified host.
37+
sshSession *ssh.Session
38+
// sftpClient is used to file remote operation on specified host except scp operation.
39+
sftpClient *sftp.Client
40+
// scpClient is used to scp files between sealer node and all nodes.
41+
scpClient *goscp.Client
42+
43+
// isLocal identifies that whether the initialized host is the sealer binary located node.
44+
isLocal bool
45+
}
46+
47+
// HostConfig is the host config, including IP, port, login credentials and so on.
48+
type HostConfig struct {
49+
// IP is the IP address of host.
50+
// It supports both IPv4 and IPv6.
51+
IP net.IP
52+
53+
// Port is the port config used by ssh to connect host
54+
// The connecting operation will use port 22 if port is not set.
55+
Port int
56+
57+
// Usually User will be root. If it is set a non-root user,
58+
// then this non-root must has a sudo permission.
59+
User string
60+
Password string
61+
62+
// Encrypted means the password is encrypted.
63+
// Password above should be decrypted first before being called.
64+
Encrypted bool
65+
66+
// TODO: add PkFile support
67+
// PkFile string
68+
// PkPassword string
69+
}
70+
71+
// Initialize setups ssh and sftp clients.
72+
func (host *Host) Initialize() error {
73+
config := &ssh.ClientConfig{
74+
User: host.config.User,
75+
Auth: []ssh.AuthMethod{
76+
ssh.Password(host.config.Password),
77+
},
78+
HostKeyCallback: nil,
79+
}
80+
81+
hostAddr := host.config.IP.String()
82+
port := strconv.Itoa(host.config.Port)
83+
84+
// sshClient
85+
sshClient, err := ssh.Dial("tcp", net.JoinHostPort(hostAddr, port), config)
86+
if err != nil {
87+
return fmt.Errorf("failed to create ssh client for host(%s): %v", hostAddr, err)
88+
}
89+
host.sshClient = sshClient
90+
91+
// sshSession
92+
sshSession, err := sshClient.NewSession()
93+
if err != nil {
94+
return fmt.Errorf("failed to create ssh session for host(%s): %v", hostAddr, err)
95+
}
96+
host.sshSession = sshSession
97+
98+
// sftpClient
99+
sftpClient, err := sftp.NewClient(sshClient, nil)
100+
if err != nil {
101+
return fmt.Errorf("failed to create sftp client for host(%s): %v", hostAddr, err)
102+
}
103+
host.sftpClient = sftpClient
104+
105+
// scpClient
106+
scpClient, err := goscp.NewClientBySSH(sshClient)
107+
if err != nil {
108+
return fmt.Errorf("failed to create scp client for host(%s): %v", hostAddr, err)
109+
}
110+
host.scpClient = &scpClient
111+
112+
// TODO: set isLocal
113+
114+
return nil
115+
}

pkg/hostpool/host_pool.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright © 2022 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package hostpool
16+
17+
import (
18+
"fmt"
19+
)
20+
21+
// HostPool is a host resource pool of sealer's cluster, including masters and nodes.
22+
// While SEALER DEPLOYING NODE has no restrict relationship with masters nor nodes:
23+
// 1. sealer deploying node could be a node which is no master nor node;
24+
// 2. sealer deploying node could also be one of masters and nodes.
25+
// Then deploying node is not included in HostPool.
26+
type HostPool struct {
27+
// host is a map:
28+
// key has a type of string which is from net.Ip.String()
29+
hosts map[string]*Host
30+
}
31+
32+
// New initializes a brand new HostPool instance.
33+
func New(hostConfigs []*HostConfig) (*HostPool, error) {
34+
if len(hostConfigs) == 0 {
35+
return nil, fmt.Errorf("input HostConfigs cannot be empty")
36+
}
37+
var hostPool HostPool
38+
for _, hostConfig := range hostConfigs {
39+
if _, OK := hostPool.hosts[hostConfig.IP.String()]; OK {
40+
return nil, fmt.Errorf("there must not be duplicated host IP(%s) in cluster hosts", hostConfig.IP.String())
41+
}
42+
hostPool.hosts[hostConfig.IP.String()] = &Host{
43+
config: HostConfig{
44+
IP: hostConfig.IP,
45+
Port: hostConfig.Port,
46+
User: hostConfig.User,
47+
Password: hostConfig.Password,
48+
Encrypted: hostConfig.Encrypted,
49+
},
50+
}
51+
}
52+
return &hostPool, nil
53+
}
54+
55+
// Initialize helps HostPool to setup all attributes for each host,
56+
// like scpClient, sshClient and so on.
57+
func (hp *HostPool) Initialize() error {
58+
for _, host := range hp.hosts {
59+
if err := host.Initialize(); err != nil {
60+
return fmt.Errorf("failed to initialize host in HostPool: %v", err)
61+
}
62+
}
63+
return nil
64+
}
65+
66+
// GetHost gets the detailed host connection instance via IP string as a key.
67+
func (hp *HostPool) GetHost(ipStr string) (*Host, error) {
68+
if host, exist := hp.hosts[ipStr]; exist {
69+
return host, nil
70+
}
71+
return nil, fmt.Errorf("cannot get host connection in HostPool by key(%s)", ipStr)
72+
}

pkg/hostpool/scp.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright © 2022 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package hostpool
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"io/fs"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
)
25+
26+
// CopyFile copies the contents of localFilePath to remote destination path.
27+
// Both localFilePath and remotePath must be an absolute path.
28+
//
29+
// It must be executed in deploying node and towards the host instance.
30+
func (host *Host) CopyToRemote(localFilePath string, remotePath string, permissions string) error {
31+
if host.isLocal {
32+
// TODO: add local file copy.
33+
return fmt.Errorf("local file copy is not implemented")
34+
}
35+
36+
f, err := os.Open(filepath.Clean(localFilePath))
37+
if err != nil {
38+
return err
39+
}
40+
defer f.Close()
41+
return host.scpClient.CopyFromFile(context.Background(), *f, remotePath, permissions)
42+
}
43+
44+
// CopyFile copies the contents of remotePath to local destination path.
45+
// Both localFilePath and remotePath must be an absolute path.
46+
//
47+
// It must be executed in deploying node and towards the host instance.
48+
func (host *Host) CopyFromRemote(localFilePath string, remotePath string) error {
49+
if host.isLocal {
50+
// TODO: add local file copy.
51+
return fmt.Errorf("local file copy is not implemented")
52+
}
53+
54+
f, err := os.Open(filepath.Clean(localFilePath))
55+
if err != nil {
56+
return err
57+
}
58+
defer f.Close()
59+
return host.scpClient.CopyFromRemote(context.Background(), f, remotePath)
60+
}
61+
62+
// CopyToRemoteDir copies the contents of local directory to remote destination directory.
63+
// Both localFilePath and remotePath must be an absolute path.
64+
//
65+
// It must be executed in deploying node and towards the host instance.
66+
func (host *Host) CopyToRemoteDir(localDir string, remoteDir string) error {
67+
if host.isLocal {
68+
// TODO: add local file copy.
69+
return fmt.Errorf("local file copy is not implemented")
70+
}
71+
72+
// get the localDir Directory name
73+
fInfo, err := os.Lstat(localDir)
74+
if err != nil {
75+
return err
76+
}
77+
if !fInfo.IsDir() {
78+
return fmt.Errorf("input localDir(%s) is not a directory when copying directory content", localDir)
79+
}
80+
dirName := fInfo.Name()
81+
82+
err = filepath.Walk(localDir, func(path string, info fs.FileInfo, err error) error {
83+
if err != nil {
84+
return err
85+
}
86+
// Since localDir is an absolute path, then every passed path has a prefix of localDir,
87+
// then the relative path is the input path trims localDir.
88+
fileRelativePath := strings.TrimPrefix(path, localDir)
89+
remotePath := filepath.Join(remoteDir, dirName, fileRelativePath)
90+
91+
return host.CopyToRemote(path, remotePath, info.Mode().String())
92+
})
93+
94+
return err
95+
}

pkg/hostpool/session.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright © 2022 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package hostpool
16+
17+
import (
18+
"bytes"
19+
"fmt"
20+
"os/exec"
21+
)
22+
23+
// Output runs cmd on the remote host and returns its standard output.
24+
// It must be executed in deploying node and towards the host instance.
25+
func (host *Host) Output(cmd string) ([]byte, error) {
26+
if host.isLocal {
27+
return exec.Command(cmd).Output()
28+
}
29+
return host.sshSession.Output(cmd)
30+
}
31+
32+
// CombinedOutput wraps the sshSession.CombinedOutput and does the same in both input and output.
33+
// It must be executed in deploying node and towards the host instance.
34+
func (host *Host) CombinedOutput(cmd string) ([]byte, error) {
35+
if host.isLocal {
36+
return exec.Command(cmd).CombinedOutput()
37+
}
38+
return host.sshSession.CombinedOutput(cmd)
39+
}
40+
41+
// RunAndStderr runs a specified command and output stderr content.
42+
// If command returns a nil, then no matter if there is content in session's stderr, just ignore stderr;
43+
// If command return a non-nil, construct and return a new error with stderr content
44+
// which may contains the exact error message.
45+
//
46+
// TODO: there is a potential issue that if much content is in stdout or stderr, and
47+
// it may eventually cause the remote command to block.
48+
//
49+
// It must be executed in deploying node and towards the host instance.
50+
func (host *Host) RunAndStderr(cmd string) ([]byte, error) {
51+
var stdout, stderr bytes.Buffer
52+
if host.isLocal {
53+
localCmd := exec.Command(cmd)
54+
localCmd.Stdout = &stdout
55+
localCmd.Stderr = &stderr
56+
if err := localCmd.Run(); err != nil {
57+
return nil, fmt.Errorf("failed to exec cmd(%s) on host(%s): %s", cmd, host.config.IP, stderr.String())
58+
}
59+
return stdout.Bytes(), nil
60+
}
61+
62+
host.sshSession.Stdout = &stdout
63+
host.sshSession.Stderr = &stderr
64+
if err := host.sshSession.Run(cmd); err != nil {
65+
return nil, fmt.Errorf("failed to exec cmd(%s) on host(%s): %s", cmd, host.config.IP, stderr.String())
66+
}
67+
68+
return stdout.Bytes(), nil
69+
}
70+
71+
// TODO: Do we need asynchronously output stdout and stderr?

pkg/hostpool/sftp.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright © 2022 Alibaba Group Holding Ltd.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package hostpool

vendor/github.com/bramvdbogaerde/go-scp/.gitignore

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)