veyron/tools/mgmt/nodex: add commands for manipulating ACLs

Remaining functionality to manipulate ACLs on the node manager with
the nodex command.

Change-Id: Ib41d2a23ddad3748d422a9f50174016479436635
diff --git a/tools/mgmt/nodex/acl_impl.go b/tools/mgmt/nodex/acl_impl.go
index b4efc6a..384d0a7 100644
--- a/tools/mgmt/nodex/acl_impl.go
+++ b/tools/mgmt/nodex/acl_impl.go
@@ -7,7 +7,10 @@
 	"sort"
 
 	"veyron.io/veyron/veyron2/rt"
+	"veyron.io/veyron/veyron2/security"
 	"veyron.io/veyron/veyron2/services/mgmt/node"
+	"veyron.io/veyron/veyron2/services/security/access"
+	"veyron.io/veyron/veyron2/verror"
 
 	"veyron.io/veyron/veyron/lib/cmdline"
 )
@@ -50,30 +53,143 @@
 	// TODO(rjkroege): Update for custom labels.
 	output := make([]formattedACLEntry, 0)
 	for k, _ := range objACL.In {
-		output = append(output, formattedACLEntry{string(k), "in", objACL.In[k].String()})
+		output = append(output, formattedACLEntry{string(k), "", objACL.In[k].String()})
 	}
 	for k, _ := range objACL.NotIn {
-		output = append(output, formattedACLEntry{string(k), "nin", objACL.NotIn[k].String()})
+		output = append(output, formattedACLEntry{string(k), "!", objACL.NotIn[k].String()})
 	}
 
 	sort.Sort(byBlessing(output))
 
 	for _, e := range output {
-		fmt.Fprintf(cmd.Stdout(), "%s %s %s\n", e.blessing, e.inout, e.label)
+		fmt.Fprintf(cmd.Stdout(), "%s %s%s\n", e.blessing, e.inout, e.label)
 	}
 	return nil
 }
 
-// TODO(rjkroege): Implement the remaining sub-commands.
-// nodex acl set <target>  ([!]<label> <blessing>)...
+var cmdSet = &cmdline.Command{
+	Run:      runSet,
+	Name:     "set",
+	Short:    "Set ACLs for the given target.",
+	Long:     "Set ACLs for the given target",
+	ArgsName: "<node manager name>  (<blessing> [!]<label>)...",
+	ArgsLong: `
+<node manager name> can be a Vanadium name for a node manager,
+application installation or instance.
+
+<blessing> is a blessing pattern.
+
+<label> is a character sequence defining a set of rights: some subset
+of the defined standard Vanadium labels of XRWADM where X is resolve,
+R is read, W for write, A for admin, D for debug and M is for
+monitoring. By default, the combination of <blessing>, <label>
+replaces whatever entry is present in the ACL's In field for the
+<blessing> but it can instead be added to the NotIn field by prefacing
+<label> with a '!' character. Use the <label> of 0 to clear the label.
+
+For example: root/self !0 will clear the NotIn field for blessingroot/self.`,
+}
+
+type inAdditionTuple struct {
+	blessing security.BlessingPattern
+	ls       *security.LabelSet
+}
+
+type notInAdditionTuple struct {
+	blessing string
+	ls       *security.LabelSet
+}
+
+func runSet(cmd *cmdline.Command, args []string) error {
+	if got := len(args); !((got%2) == 1 && got >= 3) {
+		return cmd.UsageErrorf("set: incorrect number of arguments %d, must be 1 + 2n", got)
+	}
+
+	vanaName := args[0]
+	pairs := args[1:]
+
+	// Parse each pair and aggregate what should happen to all of them
+	notInDeletions := make([]string, 0)
+	inDeletions := make([]security.BlessingPattern, 0)
+	inAdditions := make([]inAdditionTuple, 0)
+	notInAdditions := make([]notInAdditionTuple, 0)
+
+	for i := 0; i < len(pairs); i += 2 {
+		blessing, label := pairs[i], pairs[i+1]
+		if label == "" || label == "!" {
+			return cmd.UsageErrorf("failed to parse LabelSet pair %s, %s", blessing, label)
+		}
+
+		switch {
+		case label == "!0":
+			notInDeletions = append(notInDeletions, blessing)
+		case label == "0":
+			inDeletions = append(inDeletions, security.BlessingPattern(blessing))
+		case label[0] == '!':
+			// e.g. !RW
+			ls := new(security.LabelSet)
+			if err := ls.FromString(label[1:]); err != nil {
+				return cmd.UsageErrorf("failed to parse LabelSet %s:  %v", label, err)
+			}
+			notInAdditions = append(notInAdditions, notInAdditionTuple{blessing, ls})
+		default:
+			// e.g. X
+			ls := new(security.LabelSet)
+			if err := ls.FromString(label); err != nil {
+				return fmt.Errorf("failed to parse LabelSet %s:  %v", label, err)
+			}
+			inAdditions = append(inAdditions, inAdditionTuple{security.BlessingPattern(blessing), ls})
+		}
+	}
+
+	// Set the ACLs on the specified name.
+	for {
+		objACL, etag, err := node.ApplicationClient(vanaName).GetACL(rt.R().NewContext())
+		if err != nil {
+			return cmd.UsageErrorf("GetACL(%s) failed: %v", vanaName, err)
+		}
+
+		// Insert into objACL
+		for _, b := range notInDeletions {
+			if _, contains := objACL.NotIn[b]; !contains {
+				fmt.Fprintf(cmd.Stderr(), "WARNING: ignoring attempt to remove non-existing NotIn ACL for %s\n", b)
+			}
+			delete(objACL.NotIn, b)
+		}
+
+		for _, b := range inDeletions {
+			if _, contains := objACL.In[b]; !contains {
+				fmt.Fprintf(cmd.Stderr(), "WARNING: ignoring attempt to remove non-existing In ACL for %s\n", b)
+			}
+			delete(objACL.In, b)
+		}
+
+		for _, b := range inAdditions {
+			objACL.In[b.blessing] = *b.ls
+		}
+
+		for _, b := range notInAdditions {
+			objACL.NotIn[b.blessing] = *b.ls
+		}
+
+		switch err := node.ApplicationClient(vanaName).SetACL(rt.R().NewContext(), objACL, etag); {
+		case err != nil && !verror.Is(err, access.ErrBadEtag):
+			return cmd.UsageErrorf("SetACL(%s) failed: %v", vanaName, err)
+		case err == nil:
+			return nil
+		}
+		fmt.Fprintf(cmd.Stderr(), "WARNING: trying again because of asynchronous change\n")
+	}
+	return nil
+}
 
 func aclRoot() *cmdline.Command {
 	return &cmdline.Command{
 		Name:  "acl",
-		Short: "Tool for creating associations between Vanadium blessings and a system account",
+		Short: "Tool for setting node manager ACLs",
 		Long: `
 The acl tool manages ACLs on the node manger, installations and instances.
 `,
-		Children: []*cmdline.Command{cmdGet},
+		Children: []*cmdline.Command{cmdGet, cmdSet},
 	}
 }
diff --git a/tools/mgmt/nodex/acl_test.go b/tools/mgmt/nodex/acl_test.go
index ccd6d6b..9f4d581 100644
--- a/tools/mgmt/nodex/acl_test.go
+++ b/tools/mgmt/nodex/acl_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"bytes"
+	"fmt"
 	"reflect"
 	"strings"
 	"testing"
@@ -9,6 +10,8 @@
 	"veyron.io/veyron/veyron2/naming"
 	"veyron.io/veyron/veyron2/rt"
 	"veyron.io/veyron/veyron2/security"
+	"veyron.io/veyron/veyron2/services/security/access"
+	"veyron.io/veyron/veyron2/verror"
 )
 
 func TestACLGetCommand(t *testing.T) {
@@ -45,7 +48,7 @@
 	if err := cmd.Execute([]string{"acl", "get", nodeName}); err != nil {
 		t.Fatalf("%v, ouput: %v, error: %v", err)
 	}
-	if expected, got := "root/bob/... nin W\nroot/other in R\nroot/self/... in XRWADM", strings.TrimSpace(stdout.String()); got != expected {
+	if expected, got := "root/bob/... !W\nroot/other R\nroot/self/... XRWADM", strings.TrimSpace(stdout.String()); got != expected {
 		t.Fatalf("Unexpected output from get. Got %q, expected %q", got, expected)
 	}
 	if got, expected := tape.Play(), []interface{}{"GetACL"}; !reflect.DeepEqual(expected, got) {
@@ -54,3 +57,256 @@
 	tape.Rewind()
 	stdout.Reset()
 }
+
+func TestACLSetCommand(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(), "")
+
+	// Some tests to validate parse.
+	if err := cmd.Execute([]string{"acl", "set", nodeName}); err == nil {
+		t.Fatalf("failed to correctly detect insufficient parameters")
+	}
+	if expected, got := "ERROR: set: incorrect number of arguments 1, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	stderr.Reset()
+	stdout.Reset()
+	if err := cmd.Execute([]string{"acl", "set", nodeName, "foo"}); err == nil {
+		t.Fatalf("failed to correctly detect insufficient parameters")
+	}
+	if expected, got := "ERROR: set: incorrect number of arguments 2, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	stderr.Reset()
+	stdout.Reset()
+	if err := cmd.Execute([]string{"acl", "set", nodeName, "foo", "bar", "ohno"}); err == nil {
+		t.Fatalf("failed to correctly detect insufficient parameters")
+	}
+	if expected, got := "ERROR: set: incorrect number of arguments 4, must be 1 + 2n", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	stderr.Reset()
+	stdout.Reset()
+	if err := cmd.Execute([]string{"acl", "set", nodeName, "foo", "!"}); err == nil {
+		t.Fatalf("failed to detect invalid parameter")
+	}
+	if expected, got := "ERROR: failed to parse LabelSet pair foo, !", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, expected prefix %q", got, expected)
+	}
+
+	// Correct operation in the absence of errors.
+	stderr.Reset()
+	stdout.Reset()
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl: security.ACL{
+			In: map[security.BlessingPattern]security.LabelSet{
+				"root/self/...": security.AllLabels,
+				"root/other":    security.LabelSet(security.ReadLabel),
+			},
+			NotIn: map[string]security.LabelSet{
+				"root/bob": security.LabelSet(security.WriteLabel),
+			},
+		},
+		etag: "anEtagForToday",
+		err:  nil,
+	},
+		verror.Make(access.ErrBadEtag, fmt.Sprintf("etag mismatch in:%s vers:%s", "anEtagForToday", "anEtagForTomorrow")),
+		GetACLResponse{
+			acl: security.ACL{
+				In: map[security.BlessingPattern]security.LabelSet{
+					"root/self/...": security.AllLabels,
+					"root/other":    security.LabelSet(security.ReadLabel),
+				},
+				NotIn: map[string]security.LabelSet{
+					"root/bob":       security.LabelSet(security.WriteLabel),
+					"root/alice/cat": security.LabelSet(security.AdminLabel),
+				},
+			},
+			etag: "anEtagForTomorrow",
+			err:  nil,
+		},
+		nil,
+	})
+
+	if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/bad", "!XR", "root/vana/...", "RD", "root/other", "0", "root/bob", "!0"}); err != nil {
+		t.Fatalf("SetACL failed: %v", err)
+	}
+
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	if expected, got := "WARNING: trying again because of asynchronous change", strings.TrimSpace(stderr.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+
+	expected := []interface{}{
+		"GetACL",
+		SetACLStimulus{
+			fun: "SetACL",
+			acl: security.ACL{
+				In: map[security.BlessingPattern]security.LabelSet{
+					"root/self/...": security.AllLabels,
+					"root/vana/...": security.LabelSet(security.ReadLabel | security.DebugLabel),
+				},
+				NotIn: map[string]security.LabelSet{
+					"root/vana/bad": security.LabelSet(security.ResolveLabel | security.ReadLabel),
+				},
+			},
+			etag: "anEtagForToday",
+		},
+		"GetACL",
+		SetACLStimulus{
+			fun: "SetACL",
+			acl: security.ACL{
+				In: map[security.BlessingPattern]security.LabelSet{
+					"root/self/...": security.AllLabels,
+					"root/vana/...": security.LabelSet(security.ReadLabel | security.DebugLabel),
+				},
+				NotIn: map[string]security.LabelSet{
+					"root/alice/cat": security.LabelSet(security.AdminLabel),
+					"root/vana/bad":  security.LabelSet(security.ResolveLabel | security.ReadLabel),
+				},
+			},
+			etag: "anEtagForTomorrow",
+		},
+	}
+
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+
+	// GetACL fails.
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl:  security.ACL{In: nil, NotIn: nil},
+		etag: "anEtagForToday",
+		err:  verror.BadArgf("oops!"),
+	},
+	})
+
+	if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/bad", "!XR", "root/vana/...", "RD", "root/other", "0", "root/bob", "!0"}); err == nil {
+		t.Fatalf("GetACL RPC inside acl set command failed but error wrongly not detected")
+	}
+	if expected, got := "ERROR: GetACL("+nodeName+") failed: oops!", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, prefix %q", got, expected)
+	}
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	expected = []interface{}{
+		"GetACL",
+	}
+
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+
+	// SetACL fails with not a bad etag failure.
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl: security.ACL{
+			In: map[security.BlessingPattern]security.LabelSet{
+				"root/self/...": security.AllLabels,
+				"root/other":    security.LabelSet(security.ReadLabel),
+			},
+			NotIn: map[string]security.LabelSet{
+				"root/bob": security.LabelSet(security.WriteLabel),
+			},
+		},
+		etag: "anEtagForToday",
+		err:  nil,
+	},
+		verror.BadArgf("oops!"),
+	})
+
+	if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/bad", "!XR", "root/vana/...", "RD", "root/other", "0", "root/bob", "!0"}); err == nil {
+		t.Fatalf("SetACL should have failed: %v", err)
+	}
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	if expected, got := "ERROR: SetACL("+nodeName+") failed: oops!", strings.TrimSpace(stderr.String()); !strings.HasPrefix(got, expected) {
+		t.Fatalf("Unexpected output from list. Got %q, prefix %q", got, expected)
+	}
+
+	expected = []interface{}{
+		"GetACL",
+		SetACLStimulus{
+			fun: "SetACL",
+			acl: security.ACL{
+				In: map[security.BlessingPattern]security.LabelSet{
+					"root/self/...": security.AllLabels,
+					"root/vana/...": security.LabelSet(security.ReadLabel | security.DebugLabel),
+				},
+				NotIn: map[string]security.LabelSet{
+					"root/vana/bad": security.LabelSet(security.ResolveLabel | security.ReadLabel),
+				},
+			},
+			etag: "anEtagForToday",
+		},
+	}
+
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+
+	// Trying to delete non-existent items.
+	stderr.Reset()
+	stdout.Reset()
+	tape.SetResponses([]interface{}{GetACLResponse{
+		acl: security.ACL{
+			In:    map[security.BlessingPattern]security.LabelSet{},
+			NotIn: map[string]security.LabelSet{},
+		},
+		etag: "anEtagForToday",
+		err:  nil,
+	},
+		nil,
+	})
+
+	if err := cmd.Execute([]string{"acl", "set", nodeName, "root/vana/notin/missing", "!0", "root/vana/in/missing", "0"}); err != nil {
+		t.Fatalf("SetACL failed: %v", err)
+	}
+	if expected, got := "", strings.TrimSpace(stdout.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+	if expected, got := "WARNING: ignoring attempt to remove non-existing NotIn ACL for root/vana/notin/missing\nWARNING: ignoring attempt to remove non-existing In ACL for root/vana/in/missing", strings.TrimSpace(stderr.String()); got != expected {
+		t.Fatalf("Unexpected output from list. Got %q, expected %q", got, expected)
+	}
+
+	expected = []interface{}{
+		"GetACL",
+		SetACLStimulus{
+			fun:  "SetACL",
+			acl:  security.ACL{},
+			etag: "anEtagForToday",
+		},
+	}
+	if got := tape.Play(); !reflect.DeepEqual(expected, got) {
+		t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+	}
+	tape.Rewind()
+	stdout.Reset()
+	stderr.Reset()
+}
diff --git a/tools/mgmt/nodex/doc.go b/tools/mgmt/nodex/doc.go
index 7f2fa78..6b3ff6d 100644
--- a/tools/mgmt/nodex/doc.go
+++ b/tools/mgmt/nodex/doc.go
@@ -13,7 +13,10 @@
    associate   Tool for creating associations between Vanadium blessings and a
                system account
    claim       Claim the node.
-   instance    Subtool for managing application instances
+   stop        Stop the given application instance.
+   suspend     Suspend the given application instance.
+   resume      Resume the given application instance.
+   acl         Tool for setting node manager ACLs
    help        Display help for commands or topics
 Run "nodex help [command]" for command usage.
 
@@ -124,66 +127,75 @@
 <grant extension> is used to extend the default blessing of the current
 principal when blessing the app instance.
 
-Nodex Instance
-
-The instance tool permits controlling application instances.
-
-Usage:
-   nodex instance <command>
-
-The nodex instance commands are:
-   stop        Stop the given application instance.
-   suspend     Suspend the given application instance.
-   resume      Resume the given application instance.
-   refresh     Refresh the given application instance.
-   suspend     Restart the given application instance.
-
-Nodex Instance Stop
+Nodex Stop
 
 Stop the given application instance.
 
 Usage:
-   nodex instance stop <app instance> [<deadline>]
+   nodex stop <app instance>
 
 <app instance> is the veyron object name of the application instance to stop.
-<deadline> is the optional deadline to shut down by. If not provided, defaults
-to 5 seconds.
 
-Nodex Instance Suspend
+Nodex Suspend
 
 Suspend the given application instance.
 
 Usage:
-   nodex instance suspend <app instance>
+   nodex suspend <app instance>
 
-<app instance> is the veyron object name of the application instance to stop.
+<app instance> is the veyron object name of the application instance to suspend.
 
-Nodex Instance Resume
+Nodex Resume
 
 Resume the given application instance.
 
 Usage:
-   nodex instance resume <app instance>
+   nodex resume <app instance>
 
-<app instance> is the veyron object name of the application instance to stop.
+<app instance> is the veyron object name of the application instance to resume.
 
-Nodex Instance Refresh
+Nodex Acl
 
-Refresh the given application instance.
+The acl tool manages ACLs on the node manger, installations and instances.
 
 Usage:
-   nodex instance refresh <app instance>
+   nodex acl <command>
 
-<app instance> is the veyron object name of the application instance to stop.
+The nodex acl commands are:
+   get         Get ACLs for the given target.
+   set         Set ACLs for the given target.
 
-Nodex Instance Suspend
+Nodex Acl Get
 
-Restart the given application instance.
+Get ACLs for the given target.
 
 Usage:
-   nodex instance suspend <app instance>
+   nodex acl get <node manager name>
 
-<app instance> is the veyron object name of the application instance to stop.
+<node manager name> can be a Vanadium name for a node manager, application
+installation or instance.
+
+Nodex Acl Set
+
+Set ACLs for the given target
+
+Usage:
+   nodex acl set <node manager name>  (<blessing> [!]<label>)...
+
+<node manager name> can be a Vanadium name for a node manager, application
+installation or instance.
+
+<blessing> is a blessing pattern.
+
+<label> is a character sequence defining a set of rights: some subset of the
+defined standard Vanadium labels of XRWADM where X is resolve, R is read, W for
+write, A for admin, D for debug and M is for monitoring. By default, the
+combination of <blessing>, <label> replaces whatever entry is present in the
+ACL's In field for the <blessing> but it can instead be added to the NotIn field
+by prefacing <label> with a '!' character. Use the <label> of 0 to clear the
+label.
+
+For example: root/self !0 will clear the NotIn field for blessingroot/self.
 
 Nodex Help
 
diff --git a/tools/mgmt/nodex/impl_test.go b/tools/mgmt/nodex/impl_test.go
index 3eb47c7..ce92e31 100644
--- a/tools/mgmt/nodex/impl_test.go
+++ b/tools/mgmt/nodex/impl_test.go
@@ -3,20 +3,21 @@
 import (
 	"bytes"
 	"fmt"
+	"log"
 	"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/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/security"
+	"veyron.io/veyron/veyron2/services/mgmt/binary"
+	"veyron.io/veyron/veyron2/services/mgmt/node"
 	"veyron.io/veyron/veyron2/vlog"
+
+	"veyron.io/veyron/veyron/profiles"
 )
 
 type mockNodeInvoker struct {
@@ -53,7 +54,7 @@
 	case error:
 		return r
 	}
-	i.t.Fatalf("AssociateAccount (mock) response %v is of bad type", ri)
+	log.Fatalf("AssociateAccount (mock) response %v is of bad type", ri)
 	return nil
 }
 
@@ -101,7 +102,24 @@
 	err  error
 }
 
-func (mni *mockNodeInvoker) SetACL(ipc.ServerContext, security.ACL, string) error { return nil }
+type SetACLStimulus struct {
+	fun  string
+	acl  security.ACL
+	etag string
+}
+
+func (mni *mockNodeInvoker) SetACL(_ ipc.ServerContext, acl security.ACL, etag string) error {
+	ri := mni.tape.Record(SetACLStimulus{"SetACL", acl, etag})
+	switch r := ri.(type) {
+	case nil:
+		return nil
+	case error:
+		return r
+	}
+	log.Fatalf("AssociateAccount (mock) response %v is of bad type\n", ri)
+	return nil
+}
+
 func (mni *mockNodeInvoker) GetACL(ipc.ServerContext) (security.ACL, string, error) {
 	ir := mni.tape.Record("GetACL")
 	r := ir.(GetACLResponse)
diff --git a/tools/mgmt/nodex/main.go b/tools/mgmt/nodex/main.go
index a99a159..b3c5b7b 100644
--- a/tools/mgmt/nodex/main.go
+++ b/tools/mgmt/nodex/main.go
@@ -4,6 +4,8 @@
 // 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.
+// TODO(rjkroege): The below fails because _test.go files are present. Fix this.
+//  An alternative in Sam/Acme: Edit /\/\*/+1, /\*\//-1 <./nodex help -style'='godoc ...
 //
 //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"