Skip to content

Commit 9f28e04

Browse files
committed
Initial commit
Initial commit of basic working version. Need to cleanup code in a few places, and document, and test.
0 parents  commit 9f28e04

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+11466
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.idea/*
2+
.imi

README.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Consul DSH
2+
==========
3+
4+
`go get -u github.com/grubernaut/cdsh`
5+
6+
The remote shell `exec` for consul services.
7+
8+
### Quick How-to
9+
10+
```
11+
cdsh --server <consul-server-address> --service <consul-service> --user <username> '<bash command to execute>'
12+
```
13+
14+
**Note:**
15+
Be sure that you're actually able to SSH to the target consul service. (SSH Keys, Host key verification, etc)
16+
17+
### Options
18+
19+
* `--server` / `-s`: Consul server to query against (Required)
20+
* `--service` / `-S`: Target service to run command against
21+
* `--user` / `-u`: Remote user to connect as. If unspecified, defaults to current local user
22+
23+
More options are likely to come in the future, such as `--tag` `--token`, `--node`, etc...
24+
25+
If `--service` is left unspecified, `cdsh` will return a list of all available services. This will likely be changed
26+
in the future to a separate flag entirely.
27+
28+
### Doesn't Consul have a native `exec` command?
29+
30+
While consul _does_ have a native `exec` command, there are a couple issues with using `consul exec`.
31+
32+
> Agents are informed about the new job using the event system, which propagates messages via the gossip protocol. As a
33+
> result, delivery is best-effort, and there is no guarantee of execution.
34+
> ...
35+
> While events are purely gossip driven, remote execution relies on the KV store as a message broker.
36+
37+
Using Consul K/V as a message broker _definitely_ has it's benefits, but there are often times in daily operations work,
38+
where a user might want to know immediately if they cannot connect to a service.
39+
40+
However, the main concern with using `consul exec` is as follows:
41+
42+
> Verbose output warning: use care to make sure that your command does not produce a large volume of output. Writes to
43+
> the KV store for this output go through the Consul servers and the Raft consensus algorithm, so having a large number
44+
> of nodes in the cluster flow a large amount of data through the KV store could make the cluster unavailable.
45+
46+
There are quite a few times where I've needed to fetch fairly verbose log output from N-servers, and pipe the output to
47+
a file for further analysis. Using DSH completely abstracts the command output from Consul K/V, allowing the user to run
48+
fairly verbose commands without worry.

cdsh

7.78 MB
Binary file not shown.

flags.go

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import "gopkg.in/urfave/cli.v2"
4+
5+
func setFlags() []cli.Flag {
6+
return []cli.Flag{
7+
&cli.BoolFlag{
8+
Name: "verbose",
9+
Value: false,
10+
Aliases: []string{"v"},
11+
Usage: "Verbose output",
12+
},
13+
&cli.StringFlag{
14+
Name: "server",
15+
Value: "",
16+
Aliases: []string{"s"},
17+
Usage: "Consul Server Address",
18+
},
19+
&cli.StringFlag{
20+
Name: "service",
21+
Value: "",
22+
Aliases: []string{"S"},
23+
Usage: "Target Consul Service",
24+
},
25+
&cli.StringFlag{
26+
Name: "user",
27+
Value: "",
28+
Aliases: []string{"u"},
29+
Usage: "Remote User to connect as",
30+
},
31+
}
32+
}

main.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"gopkg.in/urfave/cli.v2"
7+
)
8+
9+
func main() {
10+
cli.VersionFlag = &cli.BoolFlag{
11+
Name: "version",
12+
Usage: "print the version",
13+
Aliases: []string{"V"},
14+
Value: false,
15+
}
16+
17+
app := &cli.App{
18+
Name: "cdsh",
19+
Usage: "Consul DSH",
20+
Flags: setFlags(),
21+
Action: func(c *cli.Context) error {
22+
return run(c)
23+
},
24+
}
25+
app.Run(os.Args)
26+
}

run.go

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package main
2+
3+
import (
4+
"container/list"
5+
"fmt"
6+
7+
"os"
8+
9+
"github.com/grubernaut/gdsh/dsh"
10+
consul "github.com/hashicorp/consul/api"
11+
"gopkg.in/urfave/cli.v2"
12+
)
13+
14+
func run(c *cli.Context) error {
15+
// If a service isn't requested, exit
16+
if c.NArg() < 1 {
17+
fmt.Printf("Error: Remote command not specified\n")
18+
return cli.ShowAppHelp(c)
19+
}
20+
21+
// Create ExecOpts
22+
opts := defaultDSHConfig()
23+
// Set opts.Verbose output
24+
opts.Verbose = false
25+
if c.Bool("verbose") {
26+
fmt.Printf("Verbose flag on\n")
27+
opts.Verbose = true
28+
}
29+
30+
// Find Consul server, env-var takes priority
31+
consulServer := c.String("server")
32+
if os.Getenv("CONSUL_SERVER") != "" {
33+
consulServer = os.Getenv("CONSUL_SERVER")
34+
}
35+
// Can't be empty, we need servers
36+
if consulServer == "" {
37+
fmt.Printf("Error: consul-server not supplied\n")
38+
return cli.ShowAppHelp(c)
39+
}
40+
41+
// Create a consul client
42+
client, err := consulClient(consulServer)
43+
if err != nil {
44+
return cli.Exit(fmt.Sprintf(
45+
"Error creating consul agent: %s\n", err,
46+
), 1)
47+
}
48+
49+
// Parse requested service, if empty return a list of available services
50+
service := c.String("service")
51+
if service == "" {
52+
fmt.Printf("No service specified. Available services:\n")
53+
avail, err := consulServices(client)
54+
if err != nil {
55+
return cli.Exit(fmt.Sprintf(
56+
"Error querying Consul services: %s\n", err,
57+
), 1)
58+
}
59+
for k := range avail {
60+
fmt.Printf("%s\n", k)
61+
}
62+
return nil
63+
}
64+
65+
// Add consul services to linked list
66+
machineList, err := populateList(client, service, c.String("user"))
67+
if err != nil {
68+
return cli.Exit(fmt.Sprintf(
69+
"Error populating DSH machine list: %s\n", err,
70+
), 1)
71+
}
72+
opts.MachineList = machineList
73+
74+
// Set remote commands to all trailing args
75+
for _, v := range c.Args().Slice() {
76+
// Initialize remote command
77+
if opts.RemoteCommand == "" && v != "" {
78+
opts.RemoteCommand = v
79+
continue
80+
}
81+
opts.RemoteCommand = fmt.Sprintf("%s %s", opts.RemoteCommand, v)
82+
}
83+
84+
// Execute DSH!
85+
if err := opts.Execute(); err != nil {
86+
return cli.Exit(fmt.Sprintf("Error executing: %s", err), 1)
87+
}
88+
return nil
89+
}
90+
91+
// Default GDSH config
92+
// TODO: Make these configurable
93+
func defaultDSHConfig() dsh.ExecOpts {
94+
opts := dsh.ExecOpts{
95+
ConcurrentShell: true,
96+
RemoteShell: "ssh",
97+
ShowNames: true,
98+
}
99+
return opts
100+
}
101+
102+
// Returns all available consul services
103+
func consulServices(client *consul.Client) (map[string][]string, error) {
104+
// Create catalog
105+
catalog := client.Catalog()
106+
services, _, err := catalog.Services(nil)
107+
if err != nil {
108+
return nil, err
109+
}
110+
return services, nil
111+
}
112+
113+
// Returns a Consul Client
114+
func consulClient(server string) (*consul.Client, error) {
115+
// Create Consul Client
116+
config := consul.DefaultConfig()
117+
config.Address = server
118+
return consul.NewClient(config)
119+
}
120+
121+
// Populates doubly linked machine list, with a list of requested consul services's addresses
122+
func populateList(client *consul.Client, service string, user string) (*list.List, error) {
123+
// Create consul agent
124+
agent := client.Agent()
125+
services, err := agent.Services()
126+
if err != nil {
127+
return nil, fmt.Errorf("Error querying consul services: %s", err)
128+
}
129+
130+
serviceList := list.New()
131+
for _, v := range services {
132+
if v.Service == service {
133+
remoteAddr := v.Address
134+
if user != "" {
135+
remoteAddr = fmt.Sprintf("%s@%s", user, remoteAddr)
136+
}
137+
addList(serviceList, remoteAddr)
138+
}
139+
}
140+
141+
return serviceList, nil
142+
}
143+
144+
// Populates linked list with a supplied string element, ensuring no duplicates or nil values are stored
145+
func addList(llist *list.List, elem string) {
146+
if elem == "" {
147+
return
148+
}
149+
if llist.Len() == 0 {
150+
llist.PushFront(elem)
151+
return
152+
}
153+
// Verify no items match currently
154+
for e := llist.Front(); e != nil; e = e.Next() {
155+
if e.Value == elem {
156+
return
157+
}
158+
}
159+
llist.PushBack(elem)
160+
}

vendor/github.com/grubernaut/gdsh/LICENSE.MD

+21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)