forked from coreos/fleet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ssh.go
287 lines (241 loc) · 7.93 KB
/
ssh.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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
// Copyright 2014 CoreOS, 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,
// 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 main
import (
"errors"
"fmt"
"net"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"github.com/spf13/cobra"
// "github.com/coreos/fleet/client"
"github.com/coreos/fleet/machine"
"github.com/coreos/fleet/pkg"
"github.com/coreos/fleet/ssh"
)
var (
flagMachine string
flagUnit string
flagSSHAgentForwarding bool
)
var cmdSSH = &cobra.Command{
Use: "ssh [-A|--forward-agent] [--ssh-port=N] [--machine|--unit] {MACHINE|UNIT}",
Short: "Open interactive shell on a machine in the cluster",
Long: `Open an interactive shell on a specific machine in the cluster or on the machine
where the specified unit is located.
fleetctl tries to detect whether your first argument is a machine or a unit.
To skip this check use the --machine or --unit flags.
Open a shell on a machine:
fleetctl ssh 2444264c-eac2-4eff-a490-32d5e5e4af24
Open a shell from your laptop, to the machine running a specific unit, using a
cluster member as a bastion host:
fleetctl --tunnel 10.10.10.10 ssh foo.service
Open a shell on a machine and forward the authentication agent connection:
fleetctl ssh --forward-agent 2444264c-eac2-4eff-a490-32d5e5e4af24
Tip: Create an alias for --tunnel.
- Add "alias fleetctl=fleetctl --tunnel 10.10.10.10" to your bash profile.
- Now you can run all fleet commands locally.
This command does not work with global units.`,
Run: runWrapper(runSSH),
}
func init() {
cmdFleet.AddCommand(cmdSSH)
cmdSSH.Flags().StringVar(&flagMachine, "machine", "", "Open SSH connection to a specific machine.")
cmdSSH.Flags().StringVar(&flagUnit, "unit", "", "Open SSH connection to machine running provided unit.")
cmdSSH.Flags().BoolVar(&flagSSHAgentForwarding, "forward-agent", false, "Forward local ssh-agent to target machine.")
cmdSSH.Flags().BoolVar(&flagSSHAgentForwarding, "A", false, "Shorthand for --forward-agent")
cmdSSH.Flags().IntVar(&sharedFlags.SSHPort, "ssh-port", 22, "Connect to remote hosts over SSH using this TCP port.")
}
func runSSH(cCmd *cobra.Command, args []string) (exit int) {
if flagUnit != "" && flagMachine != "" {
stderr("Both machine and unit flags provided, please specify only one.")
return 1
}
var err error
var addr string
switch {
case flagMachine != "":
addr, _, err = findAddressInMachineList(flagMachine)
case flagUnit != "":
addr, _, err = findAddressInRunningUnits(flagUnit)
default:
addr, err = globalMachineLookup(args)
// trim machine/unit name from args
if len(args) > 0 {
args = args[1:]
}
}
if err != nil {
stderr("Unable to proceed: %v", err)
return 1
}
if addr == "" {
stderr("Could not determine address of machine.")
return 1
}
addr = findSSHPort(cCmd, addr)
args = pkg.TrimToDashes(args)
var sshClient *ssh.SSHForwardingClient
timeout := getSSHTimeoutFlag(cCmd)
if tun := getTunnelFlag(cCmd); tun != "" {
sshClient, err = ssh.NewTunnelledSSHClient(globalFlags.SSHUserName, tun, addr, getChecker(cCmd), flagSSHAgentForwarding, timeout)
} else {
sshClient, err = ssh.NewSSHClient(globalFlags.SSHUserName, addr, getChecker(cCmd), flagSSHAgentForwarding, timeout)
}
if err != nil {
stderr("Failed building SSH client: %v", err)
return 1
}
defer sshClient.Close()
if len(args) > 0 {
cmd := strings.Join(args, " ")
err, exit = ssh.Execute(sshClient, cmd)
if err != nil {
stderr("Failed running command over SSH: %v", err)
}
} else {
if err := ssh.Shell(sshClient); err != nil {
stderr("Failed opening shell over SSH: %v", err)
exit = 1
}
}
return
}
func findSSHPort(cCmd *cobra.Command, addr string) string {
SSHPort, _ := cCmd.Flags().GetInt("ssh-port")
if SSHPort != 22 && !strings.Contains(addr, ":") {
return net.JoinHostPort(addr, strconv.Itoa(SSHPort))
} else {
return addr
}
}
func globalMachineLookup(args []string) (string, error) {
if len(args) == 0 {
return "", errors.New("one machine or unit must be provided")
}
lookup := args[0]
machineAddr, machineOk, _ := findAddressInMachineList(lookup)
unitAddr, unitOk, _ := findAddressInRunningUnits(lookup)
switch {
case machineOk && unitOk:
return "", fmt.Errorf("ambiguous argument, both machine and unit found for `%s`.\nPlease use flag `-m` or `-u` to refine the search", lookup)
case machineOk:
return machineAddr, nil
case unitOk:
return unitAddr, nil
}
return "", fmt.Errorf("could not find matching unit or machine")
}
func findAddressInMachineList(lookup string) (string, bool, error) {
states, err := cAPI.Machines()
if err != nil {
return "", false, err
}
var match *machine.MachineState
for i := range states {
machState := states[i]
if !strings.HasPrefix(machState.ID, lookup) {
continue
}
if match != nil {
return "", false, fmt.Errorf("found more than one machine")
}
match = &machState
}
if match == nil {
return "", false, fmt.Errorf("machine does not exist")
}
return match.PublicIP, true, nil
}
func findAddressInRunningUnits(name string) (string, bool, error) {
name = unitNameMangle(name)
u, err := cAPI.Unit(name)
if err != nil {
return "", false, err
} else if u == nil {
return "", false, fmt.Errorf("unit does not exist")
} else if suToGlobal(*u) {
return "", false, fmt.Errorf("global units unsupported")
}
m := cachedMachineState(u.MachineID)
if m != nil && m.PublicIP != "" {
return m.PublicIP, true, nil
}
return "", false, nil
}
// runCommand will attempt to run a command on a given machine. It will attempt
// to SSH to the machine if it is identified as being remote.
func runCommand(cCmd *cobra.Command, machID string, cmd string, args ...string) (retcode int) {
var err error
if machine.IsLocalMachineID(machID) {
err, retcode = runLocalCommand(cmd, args...)
if err != nil {
stderr("Error running local command: %v", err)
}
} else {
ms, err := machineState(machID)
if err != nil || ms == nil {
stderr("Error getting machine IP: %v", err)
} else {
addr := findSSHPort(cCmd, ms.PublicIP)
err, retcode = runRemoteCommand(cCmd, addr, cmd, args...)
if err != nil {
stderr("Error running remote command: %v", err)
}
}
}
return
}
// runLocalCommand runs the given command locally and returns any error encountered and the exit code of the command
func runLocalCommand(cmd string, args ...string) (error, int) {
osCmd := exec.Command(cmd, args...)
osCmd.Stderr = os.Stderr
osCmd.Stdout = os.Stdout
osCmd.Start()
err := osCmd.Wait()
if err != nil {
// Get the command's exit status if we can
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
return nil, status.ExitStatus()
}
}
// Otherwise, generic command error
return err, -1
}
return nil, 0
}
// runRemoteCommand runs the given command over SSH on the given IP, and returns
// any error encountered and the exit status of the command
func runRemoteCommand(cCmd *cobra.Command, addr string, cmd string, args ...string) (err error, exit int) {
var sshClient *ssh.SSHForwardingClient
timeout := getSSHTimeoutFlag(cCmd)
if tun := getTunnelFlag(cCmd); tun != "" {
sshClient, err = ssh.NewTunnelledSSHClient(globalFlags.SSHUserName, tun, addr, getChecker(cCmd), false, timeout)
} else {
sshClient, err = ssh.NewSSHClient(globalFlags.SSHUserName, addr, getChecker(cCmd), false, timeout)
}
if err != nil {
return err, -1
}
cmdargs := cmd
for _, arg := range args {
cmdargs += fmt.Sprintf(" %q", arg)
}
defer sshClient.Close()
return ssh.Execute(sshClient, cmdargs)
}