Adding the various CLI commands

This commit is contained in:
Armon Dadgar 2013-12-31 13:06:33 -08:00
parent e0dabb3319
commit 5d3a3e41fe
12 changed files with 825 additions and 0 deletions

69
command/force_leave.go Normal file
View File

@ -0,0 +1,69 @@
package command
import (
"flag"
"fmt"
"github.com/mitchellh/cli"
"strings"
)
// ForceLeaveCommand is a Command implementation that tells a running Consul
// to force a member to enter the "left" state.
type ForceLeaveCommand struct {
Ui cli.Ui
}
func (c *ForceLeaveCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("join", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
nodes := cmdFlags.Args()
if len(nodes) != 1 {
c.Ui.Error("A node name must be specified to force leave.")
c.Ui.Error("")
c.Ui.Error(c.Help())
return 1
}
client, err := RPCClient(*rpcAddr)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
err = client.ForceLeave(nodes[0])
if err != nil {
c.Ui.Error(fmt.Sprintf("Error force leaving: %s", err))
return 1
}
return 0
}
func (c *ForceLeaveCommand) Synopsis() string {
return "Forces a member of the cluster to enter the \"left\" state"
}
func (c *ForceLeaveCommand) Help() string {
helpText := `
Usage: consul force-leave [options] name
Forces a member of a Consul cluster to enter the "left" state. Note
that if the member is still actually alive, it will eventually rejoin
the cluster. This command is most useful for cleaning out "failed" nodes
that are never coming back. If you do not force leave a failed node,
Consul will attempt to reconnect to those failed nodes for some period of
time before eventually reaping them.
Options:
-rpc-addr=127.0.0.1:7373 RPC address of the Consul agent.
`
return strings.TrimSpace(helpText)
}

View File

@ -0,0 +1,71 @@
package command
import (
"fmt"
"github.com/hashicorp/serf/serf"
"github.com/hashicorp/serf/testutil"
"github.com/mitchellh/cli"
"strings"
"testing"
"time"
)
func TestForceLeaveCommand_implements(t *testing.T) {
var _ cli.Command = &ForceLeaveCommand{}
}
func TestForceLeaveCommandRun(t *testing.T) {
a1 := testAgent(t)
a2 := testAgent(t)
defer a1.Shutdown()
defer a2.Shutdown()
addr := fmt.Sprintf("127.0.0.1:%d", a2.config.SerfLanPort)
_, err := a1.agent.JoinLAN([]string{addr})
if err != nil {
t.Fatalf("err: %s", err)
}
testutil.Yield()
// Forcibly shutdown a2 so that it appears "failed" in a1
a2.Shutdown()
time.Sleep(time.Second)
ui := new(cli.MockUi)
c := &ForceLeaveCommand{Ui: ui}
args := []string{
"-rpc-addr=" + a1.addr,
a2.config.NodeName,
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
m := a1.agent.LANMembers()
if len(m) != 2 {
t.Fatalf("should have 2 members: %#v", m)
}
if m[1].Status != serf.StatusLeft {
t.Fatalf("should be left: %#v", m[1])
}
}
func TestForceLeaveCommandRun_noAddrs(t *testing.T) {
ui := new(cli.MockUi)
c := &ForceLeaveCommand{Ui: ui}
args := []string{"-rpc-addr=foo"}
code := c.Run(args)
if code != 1 {
t.Fatalf("bad: %d", code)
}
if !strings.Contains(ui.ErrorWriter.String(), "node name") {
t.Fatalf("bad: %#v", ui.ErrorWriter.String())
}
}

70
command/join.go Normal file
View File

@ -0,0 +1,70 @@
package command
import (
"flag"
"fmt"
"github.com/mitchellh/cli"
"strings"
)
// JoinCommand is a Command implementation that tells a running Consul
// agent to join another.
type JoinCommand struct {
Ui cli.Ui
}
func (c *JoinCommand) Help() string {
helpText := `
Usage: consul join [options] address ...
Tells a running Consul agent (with "consul agent") to join the cluster
by specifying at least one existing member.
Options:
-rpc-addr=127.0.0.1:7373 RPC address of the Consul agent.
-wan Joins a server to another server in the WAN pool
`
return strings.TrimSpace(helpText)
}
func (c *JoinCommand) Run(args []string) int {
var wan bool
cmdFlags := flag.NewFlagSet("join", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.BoolVar(&wan, "wan", false, "wan")
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
addrs := cmdFlags.Args()
if len(addrs) == 0 {
c.Ui.Error("At least one address to join must be specified.")
c.Ui.Error("")
c.Ui.Error(c.Help())
return 1
}
client, err := RPCClient(*rpcAddr)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
n, err := client.Join(addrs, wan)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error joining the cluster: %s", err))
return 1
}
c.Ui.Output(fmt.Sprintf(
"Successfully joined cluster by contacting %d nodes.", n))
return 0
}
func (c *JoinCommand) Synopsis() string {
return "Tell Consul agent to join cluster"
}

74
command/join_test.go Normal file
View File

@ -0,0 +1,74 @@
package command
import (
"fmt"
"github.com/mitchellh/cli"
"strings"
"testing"
)
func TestJoinCommand_implements(t *testing.T) {
var _ cli.Command = &JoinCommand{}
}
func TestJoinCommandRun(t *testing.T) {
a1 := testAgent(t)
a2 := testAgent(t)
defer a1.Shutdown()
defer a2.Shutdown()
ui := new(cli.MockUi)
c := &JoinCommand{Ui: ui}
args := []string{
"-rpc-addr=" + a1.addr,
fmt.Sprintf("127.0.0.1:%d", a2.config.SerfLanPort),
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if len(a1.agent.LANMembers()) != 2 {
t.Fatalf("bad: %#v", a1.agent.LANMembers())
}
}
func TestJoinCommandRun_wan(t *testing.T) {
a1 := testAgent(t)
a2 := testAgent(t)
defer a1.Shutdown()
defer a2.Shutdown()
ui := new(cli.MockUi)
c := &JoinCommand{Ui: ui}
args := []string{
"-rpc-addr=" + a1.addr,
"-wan",
fmt.Sprintf("127.0.0.1:%d", a2.config.SerfWanPort),
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if len(a1.agent.WANMembers()) != 2 {
t.Fatalf("bad: %#v", a1.agent.WANMembers())
}
}
func TestJoinCommandRun_noAddrs(t *testing.T) {
ui := new(cli.MockUi)
c := &JoinCommand{Ui: ui}
args := []string{"-rpc-addr=foo"}
code := c.Run(args)
if code != 1 {
t.Fatalf("bad: %d", code)
}
if !strings.Contains(ui.ErrorWriter.String(), "one address") {
t.Fatalf("bad: %#v", ui.ErrorWriter.String())
}
}

46
command/keygen.go Normal file
View File

@ -0,0 +1,46 @@
package command
import (
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/mitchellh/cli"
"strings"
)
// KeygenCommand is a Command implementation that generates an encryption
// key for use in `consul agent`.
type KeygenCommand struct {
Ui cli.Ui
}
func (c *KeygenCommand) Run(_ []string) int {
key := make([]byte, 16)
n, err := rand.Reader.Read(key)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading random data: %s", err))
return 1
}
if n != 16 {
c.Ui.Error(fmt.Sprintf("Couldn't read enough entropy. Generate more entropy!"))
return 1
}
c.Ui.Output(base64.StdEncoding.EncodeToString(key))
return 0
}
func (c *KeygenCommand) Synopsis() string {
return "Generates a new encryption key"
}
func (c *KeygenCommand) Help() string {
helpText := `
Usage: consul keygen
Generates a new encryption key that can be used to configure the
agent to encrypt traffic. The output of this command is already
in the proper format that the agent expects.
`
return strings.TrimSpace(helpText)
}

30
command/keygen_test.go Normal file
View File

@ -0,0 +1,30 @@
package command
import (
"encoding/base64"
"github.com/mitchellh/cli"
"testing"
)
func TestKeygenCommand_implements(t *testing.T) {
var _ cli.Command = &KeygenCommand{}
}
func TestKeygenCommand(t *testing.T) {
ui := new(cli.MockUi)
c := &KeygenCommand{Ui: ui}
code := c.Run(nil)
if code != 0 {
t.Fatalf("bad: %d", code)
}
output := ui.OutputWriter.String()
result, err := base64.StdEncoding.DecodeString(output)
if err != nil {
t.Fatalf("err: %s", err)
}
if len(result) != 16 {
t.Fatalf("bad: %#v", result)
}
}

55
command/leave.go Normal file
View File

@ -0,0 +1,55 @@
package command
import (
"flag"
"fmt"
"github.com/mitchellh/cli"
"strings"
)
// LeaveCommand is a Command implementation that instructs
// the Consul agent to gracefully leave the cluster
type LeaveCommand struct {
Ui cli.Ui
}
func (c *LeaveCommand) Help() string {
helpText := `
Usage: consul leave
Causes the agent to gracefully leave the Consul cluster and shutdown.
Options:
-rpc-addr=127.0.0.1:7373 RPC address of the Consul agent.
`
return strings.TrimSpace(helpText)
}
func (c *LeaveCommand) Run(args []string) int {
cmdFlags := flag.NewFlagSet("leave", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
client, err := RPCClient(*rpcAddr)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
if err := client.Leave(); err != nil {
c.Ui.Error(fmt.Sprintf("Error leaving: %s", err))
return 1
}
c.Ui.Output("Graceful leave complete")
return 0
}
func (c *LeaveCommand) Synopsis() string {
return "Gracefully leaves the Consul cluster and shuts down"
}

29
command/leave_test.go Normal file
View File

@ -0,0 +1,29 @@
package command
import (
"github.com/mitchellh/cli"
"strings"
"testing"
)
func TestLeaveCommand_implements(t *testing.T) {
var _ cli.Command = &LeaveCommand{}
}
func TestLeaveCommandRun(t *testing.T) {
a1 := testAgent(t)
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &LeaveCommand{Ui: ui}
args := []string{"-rpc-addr=" + a1.addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), "leave complete") {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}

111
command/members.go Normal file
View File

@ -0,0 +1,111 @@
package command
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/agent"
"github.com/mitchellh/cli"
"net"
"regexp"
"strings"
)
// MembersCommand is a Command implementation that queries a running
// Consul agent what members are part of the cluster currently.
type MembersCommand struct {
Ui cli.Ui
}
func (c *MembersCommand) Help() string {
helpText := `
Usage: consul members [options]
Outputs the members of a running Consul agent.
Options:
-detailed Additional information such as protocol verions
will be shown.
-role=<regexp> If provided, output is filtered to only nodes matching
the regular expression for role
-rpc-addr=127.0.0.1:7373 RPC address of the Consul agent.
-status=<regexp> If provided, output is filtered to only nodes matching
the regular expression for status
-wan If the agent is in server mode, this can be used to return
the other peers in the WAN pool
`
return strings.TrimSpace(helpText)
}
func (c *MembersCommand) Run(args []string) int {
var detailed, wan bool
var roleFilter, statusFilter string
cmdFlags := flag.NewFlagSet("members", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
cmdFlags.BoolVar(&detailed, "detailed", false, "detailed output")
cmdFlags.BoolVar(&wan, "wan", false, "wan members")
cmdFlags.StringVar(&roleFilter, "role", ".*", "role filter")
cmdFlags.StringVar(&statusFilter, "status", ".*", "status filter")
rpcAddr := RPCAddrFlag(cmdFlags)
if err := cmdFlags.Parse(args); err != nil {
return 1
}
// Compile the regexp
roleRe, err := regexp.Compile(roleFilter)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to compile role regexp: %v", err))
return 1
}
statusRe, err := regexp.Compile(statusFilter)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to compile status regexp: %v", err))
return 1
}
client, err := RPCClient(*rpcAddr)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
defer client.Close()
var members []agent.Member
if wan {
members, err = client.WANMembers()
} else {
members, err = client.LANMembers()
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Error retrieving members: %s", err))
return 1
}
for _, member := range members {
// Skip the non-matching members
if !roleRe.MatchString(member.Role) || !statusRe.MatchString(member.Status) {
continue
}
addr := net.TCPAddr{IP: member.Addr, Port: int(member.Port)}
c.Ui.Output(fmt.Sprintf("%s %s %s %s",
member.Name, addr.String(), member.Status, member.Role))
if detailed {
c.Ui.Output(fmt.Sprintf(" Protocol Version: %d",
member.DelegateCur))
c.Ui.Output(fmt.Sprintf(" Available Protocol Range: [%d, %d]",
member.DelegateMin, member.DelegateMax))
}
}
return 0
}
func (c *MembersCommand) Synopsis() string {
return "Lists the members of a Consul cluster"
}

132
command/members_test.go Normal file
View File

@ -0,0 +1,132 @@
package command
import (
"fmt"
"github.com/mitchellh/cli"
"strings"
"testing"
)
func TestMembersCommand_implements(t *testing.T) {
var _ cli.Command = &MembersCommand{}
}
func TestMembersCommandRun(t *testing.T) {
a1 := testAgent(t)
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &MembersCommand{Ui: ui}
args := []string{"-rpc-addr=" + a1.addr}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), a1.config.NodeName) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}
func TestMembersCommandRun_WAN(t *testing.T) {
a1 := testAgent(t)
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &MembersCommand{Ui: ui}
args := []string{"-rpc-addr=" + a1.addr, "-wan"}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), fmt.Sprintf("%d", a1.config.SerfWanPort)) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}
func TestMembersCommandRun_statusFilter(t *testing.T) {
a1 := testAgent(t)
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &MembersCommand{Ui: ui}
args := []string{
"-rpc-addr=" + a1.addr,
"-status=a.*e",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), a1.config.NodeName) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}
func TestMembersCommandRun_statusFilter_failed(t *testing.T) {
a1 := testAgent(t)
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &MembersCommand{Ui: ui}
args := []string{
"-rpc-addr=" + a1.addr,
"-status=(fail|left)",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if strings.Contains(ui.OutputWriter.String(), a1.config.NodeName) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}
func TestMembersCommandRun_roleFilter(t *testing.T) {
a1 := testAgent(t)
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &MembersCommand{Ui: ui}
args := []string{
"-rpc-addr=" + a1.addr,
"-role=consul",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), a1.config.NodeName) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}
func TestMembersCommandRun_roleFilter_failed(t *testing.T) {
a1 := testAgent(t)
defer a1.Shutdown()
ui := new(cli.MockUi)
c := &MembersCommand{Ui: ui}
args := []string{
"-rpc-addr=" + a1.addr,
"-role=primary",
}
code := c.Run(args)
if code != 0 {
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
}
if strings.Contains(ui.OutputWriter.String(), a1.config.NodeName) {
t.Fatalf("bad: %#v", ui.OutputWriter.String())
}
}

101
command/util_test.go Normal file
View File

@ -0,0 +1,101 @@
package command
import (
"fmt"
"github.com/hashicorp/consul/command/agent"
"github.com/hashicorp/consul/consul"
"io"
"io/ioutil"
"math/rand"
"net"
"os"
"sync/atomic"
"testing"
"time"
)
var offset uint64
func init() {
// Seed the random number generator
rand.Seed(time.Now().UnixNano())
}
type agentWrapper struct {
dir string
config *agent.Config
agent *agent.Agent
rpc *agent.AgentRPC
addr string
}
func (a *agentWrapper) Shutdown() {
a.rpc.Shutdown()
a.agent.Shutdown()
os.RemoveAll(a.dir)
}
func testAgent(t *testing.T) *agentWrapper {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %s", err)
}
lw := agent.NewLogWriter(512)
mult := io.MultiWriter(os.Stderr, lw)
conf := nextConfig()
dir, err := ioutil.TempDir("", "agent")
if err != nil {
t.Fatalf(fmt.Sprintf("err: %v", err))
}
conf.DataDir = dir
a, err := agent.Create(conf, lw)
if err != nil {
os.RemoveAll(dir)
t.Fatalf(fmt.Sprintf("err: %v", err))
}
rpc := agent.NewAgentRPC(a, l, mult, lw)
return &agentWrapper{
dir: dir,
config: conf,
agent: a,
rpc: rpc,
addr: l.Addr().String(),
}
}
func nextConfig() *agent.Config {
idx := atomic.AddUint64(&offset, 1)
conf := agent.DefaultConfig()
conf.Bootstrap = true
conf.Datacenter = "dc1"
conf.NodeName = fmt.Sprintf("Node %d", idx)
conf.HTTPAddr = fmt.Sprintf("127.0.0.1:%d", 10000+10*idx)
conf.RPCAddr = fmt.Sprintf("127.0.0.1:%d", 10100+10*idx)
conf.SerfBindAddr = "127.0.0.1"
conf.SerfLanPort = int(10201 + 10*idx)
conf.SerfWanPort = int(10202 + 10*idx)
conf.Server = true
conf.ServerAddr = fmt.Sprintf("127.0.0.1:%d", 10300+10*idx)
cons := consul.DefaultConfig()
conf.ConsulConfig = cons
cons.SerfLANConfig.MemberlistConfig.ProbeTimeout = 100 * time.Millisecond
cons.SerfLANConfig.MemberlistConfig.ProbeInterval = 100 * time.Millisecond
cons.SerfLANConfig.MemberlistConfig.GossipInterval = 100 * time.Millisecond
cons.SerfWANConfig.MemberlistConfig.ProbeTimeout = 100 * time.Millisecond
cons.SerfWANConfig.MemberlistConfig.ProbeInterval = 100 * time.Millisecond
cons.SerfWANConfig.MemberlistConfig.GossipInterval = 100 * time.Millisecond
cons.RaftConfig.HeartbeatTimeout = 40 * time.Millisecond
cons.RaftConfig.ElectionTimeout = 40 * time.Millisecond
return conf
}

View File

@ -22,6 +22,43 @@ func init() {
}, nil
},
"force-leave": func() (cli.Command, error) {
return &command.ForceLeaveCommand{
Ui: ui,
}, nil
},
"join": func() (cli.Command, error) {
return &command.JoinCommand{
Ui: ui,
}, nil
},
"keygen": func() (cli.Command, error) {
return &command.KeygenCommand{
Ui: ui,
}, nil
},
"leave": func() (cli.Command, error) {
return &command.LeaveCommand{
Ui: ui,
}, nil
},
"members": func() (cli.Command, error) {
return &command.MembersCommand{
Ui: ui,
}, nil
},
"monitor": func() (cli.Command, error) {
return &command.MonitorCommand{
ShutdownCh: makeShutdownCh(),
Ui: ui,
}, nil
},
"version": func() (cli.Command, error) {
return &command.VersionCommand{
Revision: GitCommit,