veyron/tools/associate: command line account association

A command line tool to assoicate system accounts with blessings
in a running node manager.

Change-Id: Idf76563de442e64e8caaf79999a0c2971497660a
diff --git a/tools/associate/doc.go b/tools/associate/doc.go
new file mode 100644
index 0000000..5be1076
--- /dev/null
+++ b/tools/associate/doc.go
@@ -0,0 +1,72 @@
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+The associate tool facilitates creating blessing to system account associations.
+
+Usage:
+   associate <command>
+
+The associate commands are:
+   list        Lists the account associations.
+   add         Associate the listed blessings with the specified system account
+   remove      Removes system accounts associated with the listed blessings.
+   help        Display help for commands or topics
+Run "associate help [command]" for command usage.
+
+The global flags are:
+   -alsologtostderr=true: log to standard error as well as files
+   -log_backtrace_at=:0: when logging hits line file:N, emit a stack trace
+   -log_dir=: if non-empty, write log files to this directory
+   -logtostderr=false: log to standard error instead of files
+   -max_stack_buf_size=4292608: max size in bytes of the buffer to use for logging stack traces
+   -stderrthreshold=2: logs at or above this threshold go to stderr
+   -v=0: log level for V logs
+   -vmodule=: comma-separated list of pattern=N settings for file-filtered logging
+   -vv=0: log level for V logs
+
+Associate List
+
+Lists all account associations 
+
+Usage:
+   associate list <nodemanager>.
+
+<nodemanager> is the name of the node manager to connect to.
+
+Associate Add
+
+Associate the listed blessings with the specified system account
+
+Usage:
+   associate add <nodemanager> <systemName> <blessing>...
+
+<identify specifier>... is a list of 1 or more identify specifications
+<systemName> is the name of an account holder on the local system
+<blessing>.. are the blessings to associate systemAccount with
+
+Associate Remove
+
+Removes system accounts associated with the listed blessings.
+
+Usage:
+   associate remove <nodemanager>  <blessing>...
+
+<nodemanager> is the node manager to connect to
+<blessing>... is a list of blessings.
+
+Associate Help
+
+Help with no args displays the usage of the parent command.
+Help with args displays the usage of the specified sub-command or help topic.
+"help ..." recursively displays help for all commands and topics.
+
+Usage:
+   associate help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The help flags are:
+   -style=text: The formatting style for help output, either "text" or "godoc".
+*/
+package main
diff --git a/tools/associate/impl.go b/tools/associate/impl.go
new file mode 100644
index 0000000..a60f649
--- /dev/null
+++ b/tools/associate/impl.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+	"fmt"
+	"time"
+
+	"veyron.io/veyron/veyron/lib/cmdline"
+
+	"veyron.io/veyron/veyron2/rt"
+	"veyron.io/veyron/veyron2/services/mgmt/node"
+)
+
+var cmdList = &cmdline.Command{
+	Run:      runList,
+	Name:     "list",
+	Short:    "Lists the account associations.",
+	Long:     "Lists all account associations.",
+	ArgsName: "<nodemanager>.",
+	ArgsLong: `
+<nodemanager> is the name of the node manager to connect to.`,
+}
+
+func runList(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("list: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+
+	ctx, cancel := rt.R().NewContext().WithTimeout(time.Minute)
+	defer cancel()
+	nodeStub, err := node.BindNode(args[0])
+	if err != nil {
+		return fmt.Errorf("BindNode(%s) failed: %v", args[0], err)
+	}
+	assocs, err := nodeStub.ListAssociations(ctx)
+	if err != nil {
+		return fmt.Errorf("ListAssociations failed: %v", err)
+	}
+
+	for _, a := range assocs {
+		fmt.Fprintf(cmd.Stdout(), "%s %s\n", a.IdentityName, a.AccountName)
+	}
+	return nil
+}
+
+var cmdAdd = &cmdline.Command{
+	Run:      runAdd,
+	Name:     "add",
+	Short:    "Add the listed blessings with the specified system account.",
+	Long:     "Add the listed blessings with the specified system account.",
+	ArgsName: "<nodemanager> <systemName> <blessing>...",
+	ArgsLong: `
+<nodemanager> is the name of the node manager to connect to.
+<systemName> is the name of an account holder on the local system.
+<blessing>.. are the blessings to associate systemAccount with.`,
+}
+
+func runAdd(cmd *cmdline.Command, args []string) error {
+	if expected, got := 3, len(args); got < expected {
+		return cmd.UsageErrorf("add: incorrect number of arguments, expected at least %d, got %d", expected, got)
+	}
+	ctx, cancel := rt.R().NewContext().WithTimeout(time.Minute)
+	defer cancel()
+	nodeStub, err := node.BindNode(args[0])
+	if err != nil {
+		return fmt.Errorf("BindNode(%s) failed: %v", args[0], err)
+	}
+	return nodeStub.AssociateAccount(ctx, args[2:], args[1])
+}
+
+var cmdRemove = &cmdline.Command{
+	Run:      runRemove,
+	Name:     "remove",
+	Short:    "Removes system accounts associated with the listed blessings.",
+	Long:     "Removes system accounts associated with the listed blessings.",
+	ArgsName: "<nodemanager>  <blessing>...",
+	ArgsLong: `
+<nodemanager> is the name of the node manager to connect to.
+<blessing>... is a list of blessings.`,
+}
+
+func runRemove(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); got < expected {
+		return cmd.UsageErrorf("remove: incorrect number of arguments, expected at least %d, got %d", expected, got)
+	}
+	ctx, cancel := rt.R().NewContext().WithTimeout(time.Minute)
+	defer cancel()
+	nodeStub, err := node.BindNode(args[0])
+	if err != nil {
+		return fmt.Errorf("BindNode(%s) failed: %v", args[0], err)
+	}
+
+	return nodeStub.AssociateAccount(ctx, args[1:], "")
+}
+
+func root() *cmdline.Command {
+	return &cmdline.Command{
+		Name:  "associate",
+		Short: "Tool for creating associations between Vanadium blessings and a system account",
+		Long: `
+The associate tool facilitates managing blessing to system account associations.
+`,
+		Children: []*cmdline.Command{cmdList, cmdAdd, cmdRemove},
+	}
+}
diff --git a/tools/associate/impl_test.go b/tools/associate/impl_test.go
new file mode 100644
index 0000000..7043fcf
--- /dev/null
+++ b/tools/associate/impl_test.go
@@ -0,0 +1,266 @@
+package main
+
+import (
+	"bytes"
+	"reflect"
+	"strings"
+	"testing"
+
+	"veyron.io/veyron/veyron2/security"
+	"veyron.io/veyron/veyron2/services/mgmt/binary"
+	"veyron.io/veyron/veyron2/services/mgmt/node"
+	"veyron.io/veyron/veyron2/services/mounttable"
+
+	"veyron.io/veyron/veyron/profiles"
+	"veyron.io/veyron/veyron2"
+	"veyron.io/veyron/veyron2/ipc"
+	"veyron.io/veyron/veyron2/naming"
+	"veyron.io/veyron/veyron2/rt"
+	"veyron.io/veyron/veyron2/vlog"
+)
+
+type mockNodeInvoker struct {
+	tape *Tape
+	t    *testing.T
+}
+
+type ListAssociationResponse struct {
+	na  []node.Association
+	err error
+}
+
+func (mni *mockNodeInvoker) ListAssociations(ipc.ServerContext) (associations []node.Association, err error) {
+	vlog.VI(2).Infof("ListAssociations() was called")
+
+	ir := mni.tape.Record("ListAssociations")
+	r := ir.(ListAssociationResponse)
+	return r.na, r.err
+}
+
+type AddAssociationStimulus struct {
+	fun           string
+	identityNames []string
+	accountName   string
+}
+
+func (i *mockNodeInvoker) AssociateAccount(call ipc.ServerContext, identityNames []string, accountName string) error {
+	ri := i.tape.Record(AddAssociationStimulus{"AssociateAccount", identityNames, accountName})
+	switch r := ri.(type) {
+	case nil:
+		return nil
+	case error:
+		return r
+	}
+	i.t.Fatalf("AssociateAccount (mock) response %v is of bad type", ri)
+	return nil
+}
+
+func (i *mockNodeInvoker) Claim(call ipc.ServerContext) error { return nil }
+func (*mockNodeInvoker) Describe(ipc.ServerContext) (node.Description, error) {
+	return node.Description{}, nil
+}
+func (*mockNodeInvoker) IsRunnable(_ ipc.ServerContext, description binary.Description) (bool, error) {
+	return false, nil
+}
+func (*mockNodeInvoker) Reset(call ipc.ServerContext, deadline uint64) error               { return nil }
+func (*mockNodeInvoker) Install(ipc.ServerContext, string) (string, error)                 { return "", nil }
+func (*mockNodeInvoker) Refresh(ipc.ServerContext) error                                   { return nil }
+func (*mockNodeInvoker) Restart(ipc.ServerContext) error                                   { return nil }
+func (*mockNodeInvoker) Resume(ipc.ServerContext) error                                    { return nil }
+func (i *mockNodeInvoker) Revert(call ipc.ServerContext) error                             { return nil }
+func (*mockNodeInvoker) Start(ipc.ServerContext) ([]string, error)                         { return []string{}, nil }
+func (*mockNodeInvoker) Stop(ipc.ServerContext, uint32) error                              { return nil }
+func (*mockNodeInvoker) Suspend(ipc.ServerContext) error                                   { return nil }
+func (*mockNodeInvoker) Uninstall(ipc.ServerContext) error                                 { return nil }
+func (i *mockNodeInvoker) Update(ipc.ServerContext) error                                  { return nil }
+func (*mockNodeInvoker) UpdateTo(ipc.ServerContext, string) error                          { return nil }
+func (i *mockNodeInvoker) SetACL(ipc.ServerContext, security.ACL, string) error { return nil }
+func (i *mockNodeInvoker) GetACL(ipc.ServerContext) (security.ACL, string, error) {
+	return security.ACL{}, "", nil
+}
+func (i *mockNodeInvoker) Glob(ctx ipc.ServerContext, pattern string, stream mounttable.GlobbableServiceGlobStream) error {
+	return nil
+}
+
+type dispatcher struct {
+	tape *Tape
+	t    *testing.T
+}
+
+func NewDispatcher(t *testing.T, tape *Tape) *dispatcher {
+	return &dispatcher{tape: tape, t: t}
+}
+
+func (d *dispatcher) Lookup(suffix, method string) (ipc.Invoker, security.Authorizer, error) {
+	invoker := ipc.ReflectInvoker(node.NewServerNode(&mockNodeInvoker{tape: d.tape, t: d.t}))
+	return invoker, nil, nil
+}
+
+func startServer(t *testing.T, r veyron2.Runtime, tape *Tape) (ipc.Server, naming.Endpoint, error) {
+	dispatcher := NewDispatcher(t, tape)
+	server, err := r.NewServer()
+	if err != nil {
+		t.Errorf("NewServer failed: %v", err)
+		return nil, nil, err
+	}
+	endpoint, err := server.Listen(profiles.LocalListenSpec)
+	if err != nil {
+		t.Errorf("Listen failed: %v", err)
+		stopServer(t, server)
+		return nil, nil, err
+	}
+	if err := server.Serve("", dispatcher); err != nil {
+		t.Errorf("Serve failed: %v", err)
+		stopServer(t, server)
+		return nil, nil, err
+	}
+	return server, endpoint, nil
+}
+
+func stopServer(t *testing.T, server ipc.Server) {
+	if err := server.Stop(); err != nil {
+		t.Errorf("server.Stop failed: %v", err)
+	}
+}
+
+func TestListCommand(t *testing.T) {
+	runtime := rt.Init()
+	tape := NewTape()
+	server, endpoint, err := startServer(t, runtime, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	nodeName := naming.JoinAddressName(endpoint.String(), "")
+
+	// Test the 'list' command.
+	tape.SetResponses([]interface{}{ListAssociationResponse{
+		na: []node.Association{
+			{
+				"root/self",
+				"alice_self_account",
+			},
+			{
+				"root/other",
+				"alice_other_account",
+			},
+		},
+		err: nil,
+	}})
+
+	if err := cmd.Execute([]string{"list", nodeName}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "root/self alice_self_account\nroot/other alice_other_account", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	if got, expected := tape.Play(), []interface{}{"ListAssociations"}; !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	// Test list with bad parameters.
+	if err := cmd.Execute([]string{"list", nodeName, "hello"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if got, expected := len(tape.Play()), 0; got != expected {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+}
+
+func TestAddCommand(t *testing.T) {
+	runtime := rt.Init()
+	tape := NewTape()
+	server, endpoint, err := startServer(t, runtime, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	nodeName := naming.JoinAddressName(endpoint.String(), "//myapp/1")
+
+	if err := cmd.Execute([]string{"add", "one"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if got, expected := len(tape.Play()), 0; got != expected {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	tape.SetResponses([]interface{}{nil})
+	if err := cmd.Execute([]string{"add", nodeName, "alice", "root/self"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	expected := []interface{}{
+		AddAssociationStimulus{"AssociateAccount", []string{"root/self"}, "alice"},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	tape.SetResponses([]interface{}{nil})
+	if err := cmd.Execute([]string{"add", nodeName, "alice", "root/other", "root/self"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	expected = []interface{}{
+		AddAssociationStimulus{"AssociateAccount", []string{"root/other", "root/self"}, "alice"},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+}
+
+func TestRemoveCommand(t *testing.T) {
+	runtime := rt.Init()
+	tape := NewTape()
+	server, endpoint, err := startServer(t, runtime, tape)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+
+	// Setup the command-line.
+	cmd := root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+	nodeName := naming.JoinAddressName(endpoint.String(), "//myapp/1")
+
+	if err := cmd.Execute([]string{"remove", "one"}); err == nil {
+		t.Fatalf("wrongly failed to receive a non-nil error.")
+	}
+	if got, expected := len(tape.Play()), 0; got != expected {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+
+	tape.SetResponses([]interface{}{nil})
+	if err := cmd.Execute([]string{"remove", nodeName, "root/self"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	expected := []interface{}{
+		AddAssociationStimulus{"AssociateAccount", []string{"root/self"}, ""},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("unexpected result. Got %v want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+}
diff --git a/tools/associate/main.go b/tools/associate/main.go
new file mode 100644
index 0000000..a99a159
--- /dev/null
+++ b/tools/associate/main.go
@@ -0,0 +1,21 @@
+// The following enables go generate to generate the doc.go file.
+// Things to look out for:
+// 1) go:generate evaluates double-quoted strings into a single argument.
+// 2) go:generate performs $NAME expansion, so the bash cmd can't contain '$'.
+// 3) We generate into a *.tmp file first, otherwise go run will pick up the
+//    initially empty *.go file, and fail.
+//
+//go:generate bash -c "{ echo -e '// This file was auto-generated via go generate.\n// DO NOT UPDATE MANUALLY\n\n/*' && veyron go run *.go help -style=godoc ... && echo -e '*/\npackage main'; } > ./doc.go.tmp && mv ./doc.go.tmp ./doc.go"
+
+package main
+
+import (
+	"veyron.io/veyron/veyron2/rt"
+
+	_ "veyron.io/veyron/veyron/profiles"
+)
+
+func main() {
+	defer rt.Init().Cleanup()
+	root().Main()
+}
diff --git a/tools/associate/mock_test.go b/tools/associate/mock_test.go
new file mode 100644
index 0000000..936fc45
--- /dev/null
+++ b/tools/associate/mock_test.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+	"fmt"
+)
+
+type Tape struct {
+	stimuli   []interface{}
+	responses []interface{}
+}
+
+func (r *Tape) Record(call interface{}) interface{} {
+	r.stimuli = append(r.stimuli, call)
+
+	if len(r.responses) < 1 {
+		return fmt.Errorf("Record(%#v) had no response", call)
+	}
+	resp := r.responses[0]
+	r.responses = r.responses[1:]
+	return resp
+}
+
+func (r *Tape) SetResponses(responses []interface{}) {
+	r.responses = responses
+}
+
+func (r *Tape) Rewind() {
+	r.stimuli = make([]interface{}, 0)
+	r.responses = make([]interface{}, 0)
+}
+
+func (r *Tape) Play() []interface{} {
+	return r.stimuli
+}
+
+func NewTape() *Tape {
+	tape := new(Tape)
+	tape.Rewind()
+	return tape
+}