forked from coreos/fleet
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ssh.go
242 lines (208 loc) · 6.54 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
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
log "github.com/coreos/fleet/Godeps/_workspace/src/github.com/golang/glog"
"github.com/coreos/fleet/machine"
"github.com/coreos/fleet/pkg"
"github.com/coreos/fleet/ssh"
)
var (
flagMachine string
flagUnit string
flagSSHAgentForwarding bool
cmdSSH = &Command{
Name: "ssh",
Summary: "Open interactive shell on a machine in the cluster",
Usage: "[-A|--forward-agent] [--machine|--unit] {MACHINE|UNIT}",
Description: `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.`,
Run: runSSH,
}
)
func init() {
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")
}
func runSSH(args []string) (exit int) {
if flagUnit != "" && flagMachine != "" {
fmt.Fprintln(os.Stderr, "Both machine and unit flags provided, please specify only one.")
return 1
}
var err error
var addr string
switch {
case flagMachine != "":
addr, _ = findAddressInMachineList(flagMachine)
case flagUnit != "":
addr, _ = findAddressInRunningUnits(flagUnit)
default:
addr, err = globalMachineLookup(args)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
// trim machine/unit name from args
if len(args) > 0 {
args = args[1:]
}
}
if addr == "" {
fmt.Fprintln(os.Stderr, "Requested machine could not be found.")
return 1
}
args = pkg.TrimToDashes(args)
var sshClient *ssh.SSHForwardingClient
if tun := getTunnelFlag(); tun != "" {
sshClient, err = ssh.NewTunnelledSSHClient("core", tun, addr, getChecker(), flagSSHAgentForwarding)
} else {
sshClient, err = ssh.NewSSHClient("core", addr, getChecker(), flagSSHAgentForwarding)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Failed building SSH client: %v\n", err)
return 1
}
defer sshClient.Close()
if len(args) > 0 {
cmd := strings.Join(args, " ")
err, exit = ssh.Execute(sshClient, cmd)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed running command over SSH: %v\n", err)
}
} else {
if err := ssh.Shell(sshClient); err != nil {
fmt.Fprintf(os.Stderr, "Failed opening shell over SSH: %v\n", err)
exit = 1
}
}
return
}
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 "", nil
}
func findAddressInMachineList(lookup string) (string, bool) {
states, err := cAPI.Machines()
if err != nil {
log.V(1).Infof("Unable to retrieve list of active machines from the Registry: %v", err)
return "", false
}
var match *machine.MachineState
for i := range states {
machState := states[i]
if !strings.HasPrefix(machState.ID, lookup) {
continue
} else if match != nil {
fmt.Fprintln(os.Stderr, "Found more than one Machine, be more specific.")
os.Exit(1)
}
match = &machState
}
if match == nil {
return "", false
}
return match.PublicIP, true
}
func findAddressInRunningUnits(jobName string) (string, bool) {
name := unitNameMangle(jobName)
j, err := cAPI.Job(name)
if err != nil {
log.V(1).Infof("Unable to retrieve Job(%s) from Repository: %v", name, err)
}
if j == nil || j.UnitState == nil {
return "", false
}
m := cachedMachineState(j.UnitState.MachineID)
if m != nil && m.PublicIP != "" {
return m.PublicIP, true
}
return "", false
}
// 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(cmd string, machID string) (retcode int) {
var err error
if machine.IsLocalMachineID(machID) {
err, retcode = runLocalCommand(cmd)
if err != nil {
fmt.Printf("Error running local command: %v\n", err)
}
} else {
ms, err := machineState(machID)
if err != nil || ms == nil {
fmt.Printf("Error getting machine IP: %v\n", err)
} else {
err, retcode = runRemoteCommand(cmd, ms.PublicIP)
if err != nil {
fmt.Printf("Error running remote command: %v\n", err)
}
}
}
return
}
// runLocalCommand runs the given command locally and returns any error encountered and the exit code of the command
func runLocalCommand(cmd string) (error, int) {
cmdSlice := strings.Split(cmd, " ")
osCmd := exec.Command(cmdSlice[0], cmdSlice[1:]...)
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(cmd string, addr string) (err error, exit int) {
var sshClient *ssh.SSHForwardingClient
if tun := getTunnelFlag(); tun != "" {
sshClient, err = ssh.NewTunnelledSSHClient("core", tun, addr, getChecker(), false)
} else {
sshClient, err = ssh.NewSSHClient("core", addr, getChecker(), false)
}
if err != nil {
return err, -1
}
defer sshClient.Close()
return ssh.Execute(sshClient, cmd)
}