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
+}