-
Notifications
You must be signed in to change notification settings - Fork 312
/
executor.go
187 lines (163 loc) · 5.33 KB
/
executor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// Copyright 2020 PingCAP, Inc.
//
// 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,
// See the License for the specific language governing permissions and
// limitations under the License.
package executor
import (
"fmt"
"net"
"os"
"strings"
"time"
"github.com/joomcode/errorx"
"github.com/pingcap/errors"
"github.com/pingcap/failpoint"
"github.com/pingcap/tiup/pkg/localdata"
)
// SSHType represent the type of the chanel used by ssh
type SSHType string
var (
errNS = errorx.NewNamespace("executor")
// SSHTypeBuiltin is the type of easy ssh executor
SSHTypeBuiltin SSHType = "builtin"
// SSHTypeSystem is the type of host ssh client
SSHTypeSystem SSHType = "system"
// SSHTypeNone is the type of local executor (no ssh will be used)
SSHTypeNone SSHType = "none"
executeDefaultTimeout = time.Second * 60
// This command will be execute once the NativeSSHExecutor is created.
// It's used to predict if the connection can establish success in the future.
// Its main purpose is to avoid sshpass hang when user speficied a wrong prompt.
connectionTestCommand = "echo connection test, if killed, check the password prompt"
// SSH authorized_keys file
defaultSSHAuthorizedKeys = "~/.ssh/authorized_keys"
)
// Executor is the executor interface for TiUP, all tasks will in the end
// be passed to a executor and then be actually performed.
type Executor interface {
// Execute run the command, then return it's stdout and stderr
// NOTE: stdin is not supported as it seems we don't need it (for now). If
// at some point in the future we need to pass stdin to a command, we'll
// need to refactor this function and its implementations.
// If the cmd can't quit in timeout, it will return error, the default timeout is 60 seconds.
Execute(cmd string, sudo bool, timeout ...time.Duration) (stdout []byte, stderr []byte, err error)
// Transfer copies files from or to a target
Transfer(src string, dst string, download bool) error
}
// New create a new Executor
func New(etype SSHType, sudo bool, c SSHConfig) (Executor, error) {
if etype == "" {
etype = SSHTypeBuiltin
}
// Used in integration testing, to check if native ssh client is really used when it need to be.
failpoint.Inject("assertNativeSSH", func() {
// XXX: We call system executor 'native' by mistake in commit f1142b1
// this should be fixed after we remove --native-ssh flag
if etype != SSHTypeSystem {
msg := fmt.Sprintf(
"native ssh client should be used in this case, os.Args: %s, %s = %s",
os.Args, localdata.EnvNameNativeSSHClient, os.Getenv(localdata.EnvNameNativeSSHClient),
)
panic(msg)
}
})
// set default values
if c.Port <= 0 {
c.Port = 22
}
if c.Timeout == 0 {
c.Timeout = time.Second * 5 // default timeout is 5 sec
}
switch etype {
case SSHTypeBuiltin:
e := &EasySSHExecutor{
Locale: "C",
Sudo: sudo,
}
e.initialize(c)
return e, nil
case SSHTypeSystem:
e := &NativeSSHExecutor{
Config: &c,
Locale: "C",
Sudo: sudo,
}
if c.Password != "" || (c.KeyFile != "" && c.Passphrase != "") {
_, _, e.ConnectionTestResult = e.Execute(connectionTestCommand, false, executeDefaultTimeout)
}
return e, nil
case SSHTypeNone:
if err := checkLocalIP(c.Host); err != nil {
return nil, err
}
e := &Local{
Config: &c,
Sudo: sudo,
Locale: "C",
}
return e, nil
default:
return nil, fmt.Errorf("unregistered executor: %s", etype)
}
}
func checkLocalIP(ip string) error {
ifaces, err := net.Interfaces()
if err != nil {
return errors.AddStack(err)
}
foundIps := []string{}
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
switch v := addr.(type) {
case *net.IPNet:
if ip == v.IP.String() {
return nil
}
foundIps = append(foundIps, v.IP.String())
case *net.IPAddr:
if ip == v.IP.String() {
return nil
}
foundIps = append(foundIps, v.IP.String())
}
}
}
return fmt.Errorf("address %s not found in all interfaces, found ips: %s", ip, strings.Join(foundIps, ","))
}
// FindSSHAuthorizedKeysFile finds the correct path of SSH authorized keys file
func FindSSHAuthorizedKeysFile(exec Executor) string {
// detect if custom path of authorized keys file is set
// NOTE: we do not yet support:
// - custom config for user (~/.ssh/config)
// - sshd started with custom config (other than /etc/ssh/sshd_config)
// - ssh server implementations other than OpenSSH (such as dropbear)
sshAuthorizedKeys := defaultSSHAuthorizedKeys
cmd := "grep -Ev '^\\s*#|^\\s*$' /etc/ssh/sshd_config"
stdout, _, _ := exec.Execute(cmd, true) // error ignored as we have default value
for _, line := range strings.Split(string(stdout), "\n") {
if !strings.Contains(line, "AuthorizedKeysFile") {
continue
}
fields := strings.Fields(line)
if len(fields) >= 2 {
sshAuthorizedKeys = fields[1]
break
}
}
if !strings.HasPrefix(sshAuthorizedKeys, "/") && !strings.HasPrefix(sshAuthorizedKeys, "~") {
sshAuthorizedKeys = fmt.Sprintf("~/%s", sshAuthorizedKeys)
}
return sshAuthorizedKeys
}