Merge "veyron/tools/mgmt/nminstall: implement fetching binaries from local path."
diff --git a/lib/exec/parent.go b/lib/exec/parent.go
index a3aebec..b0d995d 100644
--- a/lib/exec/parent.go
+++ b/lib/exec/parent.go
@@ -66,7 +66,6 @@
// NewParentHandle creates a ParentHandle for the child process represented by
// an instance of exec.Cmd.
func NewParentHandle(c *exec.Cmd, opts ...ParentHandleOpt) *ParentHandle {
-
cfg, secret := NewConfig(), ""
tk := timekeeper.RealTime()
for _, opt := range opts {
@@ -92,7 +91,18 @@
// Start starts the child process, sharing a secret with it and
// setting up a communication channel over which to read its status.
func (p *ParentHandle) Start() error {
- p.c.Env = append(p.c.Env, VersionVariable+"="+version1)
+ // Make sure that there are no instances of the VersionVariable
+ // already in the environment (which can happen when a subprocess
+ // creates a subprocess etc)
+ nenv := make([]string, 0, len(p.c.Env)+1)
+ for _, e := range p.c.Env {
+ if strings.HasPrefix(e, VersionVariable+"=") {
+ continue
+ }
+ nenv = append(nenv, e)
+ }
+ p.c.Env = append(nenv, VersionVariable+"="+version1)
+
// Create anonymous pipe for communicating data between the child
// and the parent.
dataRead, dataWrite, err := os.Pipe()
diff --git a/lib/flags/flags.go b/lib/flags/flags.go
index f6c2ef1..102ae14 100644
--- a/lib/flags/flags.go
+++ b/lib/flags/flags.go
@@ -11,11 +11,11 @@
type FlagGroup int
const (
- // Essential identifies the flags and associated environment variables
- // required by all Vanadium processes. Namely:
+ // Runtime identifies the flags and associated environment variables
+ // used by the Vanadium process runtime. Namely:
// --veyron.namespace.root (which may be repeated to supply multiple values)
// --veyron.credentials
- Essential FlagGroup = iota
+ Runtime FlagGroup = iota
// Listen identifies the flags typically required to configure
// ipc.ListenSpec. Namely:
// --veyron.tcp.protocol
@@ -44,8 +44,8 @@
return nil
}
-// EssentialFlags contains the values of the Essential flag group.
-type EssentialFlags struct {
+// RuntimeFlags contains the values of the Runtime flag group.
+type RuntimeFlags struct {
// NamespaceRoots may be initialized by NAMESPACE_ROOT* enivornment
// variables as well as --veyron.namespace.root. The command line
// will override the environment.
@@ -65,10 +65,10 @@
ListenProxy string
}
-// createAndRegisterEssentialFlags creates and registers the EssentialFlags
+// createAndRegisterRuntimeFlags creates and registers the RuntimeFlags
// group with the supplied flag.FlagSet.
-func createAndRegisterEssentialFlags(fs *flag.FlagSet) *EssentialFlags {
- f := &EssentialFlags{}
+func createAndRegisterRuntimeFlags(fs *flag.FlagSet) *RuntimeFlags {
+ f := &RuntimeFlags{}
fs.Var(&f.namespaceRootsFlag, "veyron.namespace.root", "local namespace root; can be repeated to provided multiple roots")
fs.StringVar(&f.Credentials, "veyron.credentials", "", "directory to use for storing security credentials")
return f
@@ -88,14 +88,16 @@
// CreateAndRegister creates a new set of flag groups as specified by the
// supplied flag group parameters and registers them with the supplied
-// flag.Flagset. The Essential flag group is always included.
+// flag.Flagset.
func CreateAndRegister(fs *flag.FlagSet, groups ...FlagGroup) *Flags {
+ if len(groups) == 0 {
+ return nil
+ }
f := &Flags{FlagSet: fs, groups: make(map[FlagGroup]interface{})}
- f.groups[Essential] = createAndRegisterEssentialFlags(fs)
- for _, s := range groups {
- switch s {
- case Essential:
- // do nothing, always included
+ for _, g := range groups {
+ switch g {
+ case Runtime:
+ f.groups[Runtime] = createAndRegisterRuntimeFlags(fs)
case Listen:
f.groups[Listen] = createAndRegisterListenFlags(fs)
}
@@ -103,10 +105,13 @@
return f
}
-// EssentialFlags returns the Essential flag subset stored in its Flags
+// RuntimeFlags returns the Runtime flag subset stored in its Flags
// instance.
-func (f *Flags) EssentialFlags() EssentialFlags {
- from := f.groups[Essential].(*EssentialFlags)
+func (f *Flags) RuntimeFlags() RuntimeFlags {
+ if p := f.groups[Runtime]; p == nil {
+ return RuntimeFlags{}
+ }
+ from := f.groups[Runtime].(*RuntimeFlags)
to := *from
to.NamespaceRoots = make([]string, len(from.NamespaceRoots))
copy(to.NamespaceRoots, from.NamespaceRoots)
@@ -138,7 +143,7 @@
// legacyEnvInit provides support for the legacy NAMESPACE_ROOT? and
// VEYRON_CREDENTIALS env vars.
-func (es *EssentialFlags) legacyEnvInit() {
+func (es *RuntimeFlags) legacyEnvInit() {
for _, ev := range os.Environ() {
p := strings.SplitN(ev, "=", 2)
if len(p) != 2 {
@@ -156,15 +161,22 @@
// Parse parses the supplied args, as per flag.Parse
func (f *Flags) Parse(args []string) error {
- f.groups[Essential].(*EssentialFlags).legacyEnvInit()
+ hasrt := f.groups[Runtime] != nil
+ if hasrt {
+ f.groups[Runtime].(*RuntimeFlags).legacyEnvInit()
+ }
+
// TODO(cnicolaou): implement a single env var 'VANADIUM_OPTS'
// that can be used to specify any command line.
if err := f.FlagSet.Parse(args); err != nil {
return err
}
- essential := f.groups[Essential].(*EssentialFlags)
- if len(essential.namespaceRootsFlag.roots) > 0 {
- essential.NamespaceRoots = essential.namespaceRootsFlag.roots
+
+ if hasrt {
+ runtime := f.groups[Runtime].(*RuntimeFlags)
+ if len(runtime.namespaceRootsFlag.roots) > 0 {
+ runtime.NamespaceRoots = runtime.namespaceRootsFlag.roots
+ }
}
return nil
}
diff --git a/lib/flags/flags_test.go b/lib/flags/flags_test.go
index e8ca273..8d10c8a 100644
--- a/lib/flags/flags_test.go
+++ b/lib/flags/flags_test.go
@@ -11,25 +11,32 @@
)
func TestFlags(t *testing.T) {
- fl := flags.CreateAndRegister(flag.NewFlagSet("test", flag.ContinueOnError))
+ fs := flag.NewFlagSet("test", flag.ContinueOnError)
+ if flags.CreateAndRegister(fs) != nil {
+ t.Errorf("should have failed")
+ }
+ fl := flags.CreateAndRegister(fs, flags.Runtime)
+ if fl == nil {
+ t.Fatalf("should have returned a non-nil value")
+ }
creds := "creddir"
roots := []string{"ab:cd:ef"}
args := []string{"--veyron.credentials=" + creds, "--veyron.namespace.root=" + roots[0]}
fl.Parse(args)
- es := fl.EssentialFlags()
- if got, want := es.NamespaceRoots, roots; !reflect.DeepEqual(got, want) {
+ rtf := fl.RuntimeFlags()
+ if got, want := rtf.NamespaceRoots, roots; !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
- if got, want := es.Credentials, creds; !reflect.DeepEqual(got, want) {
+ if got, want := rtf.Credentials, creds; !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
if got, want := fl.HasGroup(flags.Listen), false; got != want {
t.Errorf("got %t, want %t", got, want)
}
// Make sure we have a deep copy.
- es.NamespaceRoots[0] = "oooh"
- es = fl.EssentialFlags()
- if got, want := es.NamespaceRoots, roots; !reflect.DeepEqual(got, want) {
+ rtf.NamespaceRoots[0] = "oooh"
+ rtf = fl.RuntimeFlags()
+ if got, want := rtf.NamespaceRoots, roots; !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
@@ -37,7 +44,7 @@
func TestFlagError(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
fs.SetOutput(ioutil.Discard)
- fl := flags.CreateAndRegister(fs)
+ fl := flags.CreateAndRegister(fs, flags.Runtime)
addr := "192.168.10.1:0"
args := []string{"--xxxveyron.tcp.address=" + addr, "not an arg"}
err := fl.Parse(args)
@@ -50,7 +57,7 @@
}
func TestFlagsGroups(t *testing.T) {
- fl := flags.CreateAndRegister(flag.NewFlagSet("test", flag.ContinueOnError), flags.Listen)
+ fl := flags.CreateAndRegister(flag.NewFlagSet("test", flag.ContinueOnError), flags.Runtime, flags.Listen)
if got, want := fl.HasGroup(flags.Listen), true; got != want {
t.Errorf("got %t, want %t", got, want)
}
@@ -59,7 +66,7 @@
args := []string{"--veyron.tcp.address=" + addr, "--veyron.namespace.root=" + roots[0]}
fl.Parse(args)
lf := fl.ListenFlags()
- if got, want := fl.EssentialFlags().NamespaceRoots, roots; !reflect.DeepEqual(got, want) {
+ if got, want := fl.RuntimeFlags().NamespaceRoots, roots; !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
if got, want := lf.ListenAddress.String(), addr; got != want {
@@ -81,39 +88,41 @@
defer os.Setenv(rootEnvVar0, oldroot0)
os.Setenv(credEnvVar, "bar")
- fl := flags.CreateAndRegister(flag.NewFlagSet("test", flag.ContinueOnError))
+ fl := flags.CreateAndRegister(flag.NewFlagSet("test", flag.ContinueOnError), flags.Runtime)
if err := fl.Parse([]string{}); err != nil {
t.Fatalf("unexpected error: %s", err)
}
- es := fl.EssentialFlags()
- if got, want := es.Credentials, "bar"; got != want {
+ rtf := fl.RuntimeFlags()
+ if got, want := rtf.Credentials, "bar"; got != want {
t.Errorf("got %q, want %q", got, want)
}
if err := fl.Parse([]string{"--veyron.credentials=baz"}); err != nil {
t.Fatalf("unexpected error: %s", err)
}
- es = fl.EssentialFlags()
- if got, want := es.Credentials, "baz"; got != want {
+ rtf = fl.RuntimeFlags()
+ if got, want := rtf.Credentials, "baz"; got != want {
t.Errorf("got %q, want %q", got, want)
}
os.Setenv(rootEnvVar, "a:1")
os.Setenv(rootEnvVar0, "a:2")
- fl = flags.CreateAndRegister(flag.NewFlagSet("test", flag.ContinueOnError))
+ fl = flags.CreateAndRegister(flag.NewFlagSet("test", flag.ContinueOnError), flags.Runtime)
if err := fl.Parse([]string{}); err != nil {
t.Fatalf("unexpected error: %s", err)
}
- es = fl.EssentialFlags()
- if got, want := es.NamespaceRoots, []string{"a:1", "a:2"}; !reflect.DeepEqual(got, want) {
+ rtf = fl.RuntimeFlags()
+ if got, want := rtf.NamespaceRoots, []string{"a:1", "a:2"}; !reflect.DeepEqual(got, want) {
t.Errorf("got %q, want %q", got, want)
}
- if err := fl.Parse([]string{"--veyron.namespace.root=b:1", "--veyron.namespace.root=b:2", "--veyron.namespace.root=b:3"}); err != nil {
+ if err := fl.Parse([]string{"--veyron.namespace.root=b:1", "--veyron.namespace.root=b:2", "--veyron.namespace.root=b:3", "--veyron.credentials=b:4"}); err != nil {
t.Fatalf("unexpected error: %s", err)
}
- es = fl.EssentialFlags()
- if got, want := es.NamespaceRoots, []string{"b:1", "b:2", "b:3"}; !reflect.DeepEqual(got, want) {
+ rtf = fl.RuntimeFlags()
+ if got, want := rtf.NamespaceRoots, []string{"b:1", "b:2", "b:3"}; !reflect.DeepEqual(got, want) {
t.Errorf("got %q, want %q", got, want)
}
-
+ if got, want := rtf.Credentials, "b:4"; got != want {
+ t.Errorf("got %q, want %q", got, want)
+ }
}
diff --git a/lib/modules/exec.go b/lib/modules/exec.go
index e81d38b..26c8b61 100644
--- a/lib/modules/exec.go
+++ b/lib/modules/exec.go
@@ -101,7 +101,16 @@
newargs := []string{os.Args[0]}
newargs = append(newargs, testFlags()...)
newargs = append(newargs, args...)
- return newargs, append(env, eh.entryPoint)
+ // Be careful to remove any existing ShellEntryPoint env vars. This
+ // can happen when subprocesses run other subprocesses etc.
+ cleaned := make([]string, 0, len(env)+1)
+ for _, e := range env {
+ if strings.HasPrefix(e, ShellEntryPoint+"=") {
+ continue
+ }
+ cleaned = append(cleaned, e)
+ }
+ return newargs, append(cleaned, eh.entryPoint)
}
func (eh *execHandle) start(sh *Shell, env []string, args ...string) (Handle, error) {
@@ -136,8 +145,8 @@
if err := handle.Start(); err != nil {
return nil, err
}
- // TODO(cnicolaou): make this timeout configurable
- err = handle.WaitForReady(time.Minute)
+ vlog.VI(1).Infof("Started: %q, pid %d", eh.name, cmd.Process.Pid)
+ err = handle.WaitForReady(sh.startTimeout)
return eh, err
}
@@ -223,11 +232,17 @@
}
func (child *childRegistrar) dispatch() error {
- ch, _ := vexec.GetChildHandle()
+ ch, err := vexec.GetChildHandle()
+ if err != nil {
+ // This is for debugging only. It's perfectly reasonable for this
+ // error to occur if the process is started by a means other
+ // than the exec library.
+ vlog.VI(1).Infof("failed to get child handle: %s", err)
+ }
+
// Only signal that the child is ready or failed if we successfully get
// a child handle. We most likely failed to get a child handle
// because the subprocess was run directly from the command line.
-
command := os.Getenv(ShellEntryPoint)
if len(command) == 0 {
err := fmt.Errorf("Failed to find entrypoint %q", ShellEntryPoint)
diff --git a/lib/modules/shell.go b/lib/modules/shell.go
index 6919bd3..1bf3e61 100644
--- a/lib/modules/shell.go
+++ b/lib/modules/shell.go
@@ -47,17 +47,19 @@
"os"
"strings"
"sync"
+ "time"
"veyron.io/veyron/veyron2/vlog"
)
// Shell represents the context within which commands are run.
type Shell struct {
- mu sync.Mutex
- env map[string]string
- cmds map[string]*commandDesc
- handles map[Handle]struct{}
- credDir string
+ mu sync.Mutex
+ env map[string]string
+ cmds map[string]*commandDesc
+ handles map[Handle]struct{}
+ credDir string
+ startTimeout time.Duration
}
type commandDesc struct {
@@ -90,9 +92,10 @@
// TODO(cnicolaou): should create a new identity if one doesn't
// already exist
sh := &Shell{
- env: make(map[string]string),
- cmds: make(map[string]*commandDesc),
- handles: make(map[Handle]struct{}),
+ env: make(map[string]string),
+ cmds: make(map[string]*commandDesc),
+ handles: make(map[Handle]struct{}),
+ startTimeout: time.Minute,
}
if flag.Lookup("test.run") != nil && os.Getenv("VEYRON_CREDENTIALS") == "" {
if err := sh.CreateAndUseNewCredentials(); err != nil {
@@ -181,7 +184,9 @@
// ones override the Shell and the Shell ones override the OS ones.
//
// The Shell tracks all of the Handles that it creates so that it can shut
-// them down when asked to.
+// them down when asked to. The returned Handle may be non-nil even when an
+// error is returned, in which case it may be used to retrieve any output
+// from the failed command.
//
// Commands may have already been registered with the Shell using AddFunction
// or AddSubprocess, but if not, they will treated as subprocess commands
@@ -196,7 +201,9 @@
expanded := append([]string{name}, sh.expand(args...)...)
h, err := cmd.factory().start(sh, cenv, expanded...)
if err != nil {
- return nil, err
+ // If the error is a timeout, then h can be used to recover
+ // any output from the process.
+ return h, err
}
sh.mu.Lock()
sh.handles[h] = struct{}{}
@@ -214,6 +221,11 @@
return cmd
}
+// SetStartTimeout sets the timeout for starting subcommands.
+func (sh *Shell) SetStartTimeout(d time.Duration) {
+ sh.startTimeout = d
+}
+
// CommandEnvelope returns the command line and environment that would be
// used for running the subprocess or function if it were started with the
// specifed arguments.
diff --git a/runtimes/google/rt/rt.go b/runtimes/google/rt/rt.go
index d106b7f..6de8860 100644
--- a/runtimes/google/rt/rt.go
+++ b/runtimes/google/rt/rt.go
@@ -5,7 +5,6 @@
"fmt"
"os"
"path/filepath"
- "strings"
"sync"
"veyron.io/veyron/veyron2"
@@ -17,6 +16,7 @@
"veyron.io/veyron/veyron2/rt"
"veyron.io/veyron/veyron/lib/exec"
+ "veyron.io/veyron/veyron/lib/flags"
_ "veyron.io/veyron/veyron/lib/stats/sysstats"
"veyron.io/veyron/veyron/runtimes/google/naming/namespace"
"veyron.io/veyron/veyron2/options"
@@ -40,6 +40,7 @@
store security.PublicIDStore
client ipc.Client
mgmt *mgmtImpl
+ flags flags.RuntimeFlags
nServers int // GUARDED_BY(mu)
cleaningUp bool // GUARDED_BY(mu)
@@ -49,14 +50,21 @@
var _ veyron2.Runtime = (*vrt)(nil)
+var flagsOnce sync.Once
+var runtimeFlags *flags.Flags
+
func init() {
rt.RegisterRuntime(veyron2.GoogleRuntimeName, New)
+ runtimeFlags = flags.CreateAndRegister(flag.CommandLine, flags.Runtime)
}
// Implements veyron2/rt.New
func New(opts ...veyron2.ROpt) (veyron2.Runtime, error) {
rt := &vrt{mgmt: new(mgmtImpl), lang: i18n.LangIDFromEnv(), program: filepath.Base(os.Args[0])}
- flag.Parse()
+ flagsOnce.Do(func() {
+ runtimeFlags.Parse(os.Args[1:])
+ })
+ rt.flags = runtimeFlags.RuntimeFlags()
nsRoots := []string{}
for _, o := range opts {
switch v := o.(type) {
@@ -66,8 +74,6 @@
rt.id = v.PrivateID
case options.Profile:
rt.profile = v.Profile
- case options.NamespaceRoots:
- nsRoots = v
case options.RuntimeName:
if v != "google" && v != "" {
return nil, fmt.Errorf("%q is the wrong name for this runtime", v)
@@ -88,20 +94,7 @@
vlog.VI(1).Infof("Using profile %q", rt.profile.Name())
}
- if len(nsRoots) == 0 {
- for _, ev := range os.Environ() {
- p := strings.SplitN(ev, "=", 2)
- if len(p) != 2 {
- continue
- }
- k, v := p[0], p[1]
- if strings.HasPrefix(k, "NAMESPACE_ROOT") {
- nsRoots = append(nsRoots, v)
- }
- }
- }
-
- if ns, err := namespace.New(rt, nsRoots...); err != nil {
+ if ns, err := namespace.New(rt, rt.flags.NamespaceRoots...); err != nil {
return nil, fmt.Errorf("Couldn't create mount table: %v", err)
} else {
rt.ns = ns
@@ -113,7 +106,7 @@
return nil, fmt.Errorf("failed to create stream manager: %s", err)
}
- if err := rt.initSecurity(); err != nil {
+ if err := rt.initSecurity(rt.flags.Credentials); err != nil {
return nil, fmt.Errorf("failed to init sercurity: %s", err)
}
diff --git a/runtimes/google/rt/rt_test.go b/runtimes/google/rt/rt_test.go
index cb1832e..44b28fe 100644
--- a/runtimes/google/rt/rt_test.go
+++ b/runtimes/google/rt/rt_test.go
@@ -10,7 +10,6 @@
"testing"
"time"
- "veyron.io/veyron/veyron2"
"veyron.io/veyron/veyron2/naming"
"veyron.io/veyron/veyron2/options"
"veyron.io/veyron/veyron2/rt"
@@ -20,7 +19,6 @@
"veyron.io/veyron/veyron/lib/expect"
"veyron.io/veyron/veyron/lib/modules"
"veyron.io/veyron/veyron/lib/testutil"
- irt "veyron.io/veyron/veyron/runtimes/google/rt"
vsecurity "veyron.io/veyron/veyron/security"
)
@@ -28,6 +26,10 @@
local security.Principal
}
+// Environment variable pointing to a directory where information about a
+// principal (private key, blessing store, blessing roots etc.) is stored.
+const veyronCredentialsEnvVar = "VEYRON_CREDENTIALS"
+
func (*context) Method() string { return "" }
func (*context) Name() string { return "" }
func (*context) Suffix() string { return "" }
@@ -44,6 +46,9 @@
func init() {
testutil.Init()
modules.RegisterChild("child", "", child)
+ modules.RegisterChild("principal", "", principal)
+ modules.RegisterChild("mutate", "", mutatePrincipal)
+ modules.RegisterChild("runner", "", runner)
}
func TestHelperProcess(t *testing.T) {
@@ -71,7 +76,7 @@
vlog.Infof("%s\n", r.Logger())
fmt.Fprintf(stdout, "%s\n", r.Logger())
modules.WaitForEOF(stdin)
- fmt.Printf("done\n")
+ fmt.Fprintf(stdout, "done\n")
return nil
}
@@ -99,67 +104,221 @@
h.Shutdown(os.Stderr, os.Stderr)
}
-func TestInitPrincipal(t *testing.T) {
- newRT := func() veyron2.Runtime {
- r, err := rt.New(profileOpt)
- if err != nil {
- t.Fatalf("rt.New failed: %v", err)
- }
- return r
+func validatePrincipal(p security.Principal) error {
+ if p == nil {
+ return fmt.Errorf("nil principal")
}
- testPrincipal := func(r veyron2.Runtime) security.Principal {
- p := r.Principal()
- if p == nil {
- t.Fatalf("rt.Principal() returned nil")
- }
- blessings := p.BlessingStore().Default()
- if blessings == nil {
- t.Fatalf("rt.Principal().BlessingStore().Default() returned nil")
+ blessings := p.BlessingStore().Default()
+ if blessings == nil {
+ return fmt.Errorf("rt.Principal().BlessingStore().Default() returned nil")
- }
- if n := len(blessings.ForContext(&context{local: p})); n != 1 {
- t.Fatalf("rt.Principal().BlessingStore().Default() returned Blessing %v with %d recognized blessings, want exactly one recognized blessing", blessings, n)
- }
- return p
}
- origCredentialsDir := os.Getenv(irt.VeyronCredentialsEnvVar)
- defer os.Setenv(irt.VeyronCredentialsEnvVar, origCredentialsDir)
-
- // Test that even with VEYRON_CREDENTIALS unset the runtime's Principal
- // is correctly initialized.
- if err := os.Setenv(irt.VeyronCredentialsEnvVar, ""); err != nil {
- t.Fatal(err)
+ if n := len(blessings.ForContext(&context{local: p})); n != 1 {
+ fmt.Errorf("rt.Principal().BlessingStore().Default() returned Blessing %v with %d recognized blessings, want exactly one recognized blessing", blessings, n)
}
- testPrincipal(newRT())
+ return nil
+}
- // Test that with VEYRON_CREDENTIALS set the runtime's Principal is correctly
- // initialized.
- credentials, err := ioutil.TempDir("", "credentials")
+func tmpDir(t *testing.T) string {
+ dir, err := ioutil.TempDir("", "rt_test_dir")
if err != nil {
- t.Fatal(err)
+ t.Fatalf("unexpected error: %s", err)
}
- defer os.RemoveAll(credentials)
- if err := os.Setenv(irt.VeyronCredentialsEnvVar, credentials); err != nil {
- t.Fatal(err)
+ return dir
+}
+
+func principal(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+ r := rt.Init()
+ err := validatePrincipal(r.Principal())
+ fmt.Fprintf(stdout, "ERROR=%v\n", err)
+ fmt.Fprintf(stdout, "PUBKEY=%s\n", r.Principal().PublicKey())
+ modules.WaitForEOF(stdin)
+ return nil
+}
+
+// Runner runs a principal as a subprocess and reports back with its
+// own security info and it's childs.
+func runner(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+ r := rt.Init()
+ err := validatePrincipal(r.Principal())
+ fmt.Fprintf(stdout, "RUNNER_ERROR=%v\n", err)
+ fmt.Fprintf(stdout, "RUNNER_PUBKEY=%s\n", r.Principal().PublicKey())
+ if err != nil {
+ return err
}
- p := testPrincipal(newRT())
+ sh := modules.NewShell("principal")
+ defer sh.Cleanup(os.Stderr, os.Stderr)
+ h, err := sh.Start("principal", nil, args[1:]...)
+ if err != nil {
+ return err
+ }
+ s := expect.NewSession(nil, h.Stdout(), 1*time.Second) // time.Minute)
+ fmt.Fprintf(stdout, s.ReadLine()+"\n")
+ fmt.Fprintf(stdout, s.ReadLine()+"\n")
+ modules.WaitForEOF(stdin)
+ return nil
+}
+
+func mutatePrincipal(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+ r := rt.Init()
+
+ rtPrincipal := r.Principal()
+ err := validatePrincipal(rtPrincipal)
+ fmt.Fprintf(stdout, "ERROR=%v\n", err)
// Mutate the roots and store of this principal.
- blessing, err := p.BlessSelf("irrelevant")
+ blessing, err := rtPrincipal.BlessSelf("irrelevant")
if err != nil {
- t.Fatal(err)
+ return err
}
- if _, err := p.BlessingStore().Set(blessing, security.AllPrincipals); err != nil {
- t.Fatal(err)
+ if _, err := rtPrincipal.BlessingStore().Set(blessing, security.AllPrincipals); err != nil {
+ return err
}
- if err := p.AddToRoots(blessing); err != nil {
+ if err := rtPrincipal.AddToRoots(blessing); err != nil {
+ return err
+ }
+ newRT, err := rt.New(profileOpt)
+ if err != nil {
+ return fmt.Errorf("rt.New failed: %v", err)
+ }
+ // Test that the same principal gets initialized on creating a new runtime
+ // from the same credentials directory.
+ if got := newRT.Principal(); !reflect.DeepEqual(got, rtPrincipal) {
+ return fmt.Errorf("Initialized Principal: %v, expected: %v", got.PublicKey(), rtPrincipal.PublicKey())
+ }
+ fmt.Fprintf(stdout, "PUBKEY=%s\n", newRT.Principal().PublicKey())
+ modules.WaitForEOF(stdin)
+ return nil
+}
+
+func createCredentialsInDir(t *testing.T, dir string) security.Principal {
+ principal, err := vsecurity.CreatePersistentPrincipal(dir, nil)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ vsecurity.InitDefaultBlessings(principal, "test")
+ return principal
+}
+
+func TestPrincipalInheritance(t *testing.T) {
+ sh := modules.NewShell("principal", "runner")
+ defer func() {
+ sh.Cleanup(os.Stdout, os.Stderr)
+ }()
+
+ // Test that the child inherits the parent's credentials correctly.
+ // The running test process may or may not have a credentials directory set
+ // up so we have to use a 'runner' process to ensure the correct setup.
+ cdir := tmpDir(t)
+ defer os.RemoveAll(cdir)
+
+ principal := createCredentialsInDir(t, cdir)
+
+ // directory supplied by the environment.
+ credEnv := []string{veyronCredentialsEnvVar + "=" + cdir}
+
+ h, err := sh.Start("runner", credEnv)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ s := expect.NewSession(t, h.Stdout(), 2*time.Second) //time.Minute)
+ runnerErr := s.ExpectVar("RUNNER_ERROR")
+ runnerPubKey := s.ExpectVar("RUNNER_PUBKEY")
+ principalErr := s.ExpectVar("ERROR")
+ principalPubKey := s.ExpectVar("PUBKEY")
+ if err := s.Error(); err != nil {
+ t.Fatalf("failed to read input from children: %s", err)
+ }
+ h.Shutdown(os.Stdout, os.Stderr)
+ if runnerErr != "<nil>" || principalErr != "<nil>" {
+ t.Fatalf("unexpected error: runner %q, principal %q", runnerErr, principalErr)
+ }
+ pubKey := principal.PublicKey().String()
+ if runnerPubKey != pubKey || principalPubKey != pubKey {
+ t.Fatalf("unexpected pubkeys: expected %s: runner %s, principal %s",
+ pubKey, runnerPubKey, principalPubKey)
+ }
+
+}
+
+func TestPrincipalInit(t *testing.T) {
+ // Collet the process' public key and error status
+ collect := func(sh *modules.Shell, cmd string, env []string, args ...string) (string, error) {
+ h, err := sh.Start(cmd, env, args...)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ s := expect.NewSession(t, h.Stdout(), time.Minute)
+ s.SetVerbosity(testing.Verbose())
+ errstr := s.ExpectVar("ERROR")
+ pubkey := s.ExpectVar("PUBKEY")
+ if errstr != "<nil>" {
+ return pubkey, fmt.Errorf("%s", errstr)
+ }
+ return pubkey, nil
+ }
+
+ // A credentials directory may, or may, not have been already specified.
+ // Either way, we want to use our own, so we set it aside and use our own.
+ origCredentialsDir := os.Getenv(veyronCredentialsEnvVar)
+ defer os.Setenv(veyronCredentialsEnvVar, origCredentialsDir)
+
+ // Test that with VEYRON_CREDENTIALS unset the runtime's Principal
+ // is correctly initialized.
+ if err := os.Setenv(veyronCredentialsEnvVar, ""); err != nil {
t.Fatal(err)
}
- // Test that the same principal gets initialized on creating a new runtime
- // from the same credentials directory.
- if got := newRT().Principal(); !reflect.DeepEqual(got, p) {
- t.Fatalf("Initialized Principal: %v, expected: %v", got.PublicKey(), p.PublicKey())
+ sh := modules.NewShell("principal")
+ defer sh.Cleanup(os.Stderr, os.Stderr)
+
+ pubkey, err := collect(sh, "principal", nil)
+ if err != nil {
+ t.Fatalf("child failed to create+validate principal: %v", err)
+ }
+ if len(pubkey) == 0 {
+ t.Fatalf("child failed to return a public key")
+ }
+
+ // Test specifying credentials via VEYRON_CREDENTIALS
+ cdir1 := tmpDir(t)
+ defer os.RemoveAll(cdir1)
+ principal := createCredentialsInDir(t, cdir1)
+ // directory supplied by the environment.
+ credEnv := []string{veyronCredentialsEnvVar + "=" + cdir1}
+
+ pubkey, err = collect(sh, "principal", credEnv)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ }
+
+ if got, want := pubkey, principal.PublicKey().String(); got != want {
+ t.Errorf("got %q, want %q", got, want)
+ }
+
+ // Test specifying credentials via the command line and that the
+ // comand line overrides the environment
+ cdir2 := tmpDir(t)
+ defer os.RemoveAll(cdir2)
+ clPrincipal := createCredentialsInDir(t, cdir2)
+
+ pubkey, err = collect(sh, "principal", credEnv, "--veyron.credentials="+cdir2)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ }
+
+ if got, want := pubkey, clPrincipal.PublicKey().String(); got != want {
+ t.Errorf("got %q, want %q", got, want)
+ }
+
+ // Mutate the roots and store of the principal in the child process.
+ pubkey, err = collect(sh, "mutate", credEnv, "--veyron.credentials="+cdir2)
+ if err != nil {
+ t.Errorf("unexpected error: %s", err)
+ }
+
+ if got, want := pubkey, clPrincipal.PublicKey().String(); got != want {
+ t.Errorf("got %q, want %q", got, want)
}
}
diff --git a/runtimes/google/rt/security.go b/runtimes/google/rt/security.go
index 0863c04a..aa673d4 100644
--- a/runtimes/google/rt/security.go
+++ b/runtimes/google/rt/security.go
@@ -15,10 +15,6 @@
"veyron.io/veyron/veyron2/vlog"
)
-// Environment variable pointing to a directory where information about a principal
-// (private key, blessing store, blessing roots etc.) is stored.
-const VeyronCredentialsEnvVar = "VEYRON_CREDENTIALS"
-
func (rt *vrt) Principal() security.Principal {
return rt.principal
}
@@ -35,31 +31,33 @@
return rt.store
}
-func (rt *vrt) initSecurity() error {
+func (rt *vrt) initSecurity(credentials string) error {
if err := rt.initOldSecurity(); err != nil {
return err
}
- if err := rt.initPrincipal(); err != nil {
+ if err := rt.initPrincipal(credentials); err != nil {
return fmt.Errorf("principal initialization failed: %v", err)
}
return nil
}
-func (rt *vrt) initPrincipal() error {
+func (rt *vrt) initPrincipal(credentials string) error {
if rt.principal != nil {
return nil
}
var err error
+ // TODO(cnicolaou,ashankar,ribrdb): this should be supplied via
+ // the exec.GetChildHandle call.
if len(os.Getenv(agent.FdVarName)) > 0 {
rt.principal, err = rt.connectToAgent()
return err
- } else if dir := os.Getenv(VeyronCredentialsEnvVar); len(dir) > 0 {
+ } else if len(credentials) > 0 {
// TODO(ataly, ashankar): If multiple runtimes are getting
// initialized at the same time from the same VEYRON_CREDENTIALS
// we will need some kind of locking for the credential files.
- if rt.principal, err = vsecurity.LoadPersistentPrincipal(dir, nil); err != nil {
+ if rt.principal, err = vsecurity.LoadPersistentPrincipal(credentials, nil); err != nil {
if os.IsNotExist(err) {
- if rt.principal, err = vsecurity.CreatePersistentPrincipal(dir, nil); err != nil {
+ if rt.principal, err = vsecurity.CreatePersistentPrincipal(credentials, nil); err != nil {
return err
}
return vsecurity.InitDefaultBlessings(rt.principal, defaultBlessingName())
diff --git a/security/agent/agentd/main.go b/security/agent/agentd/main.go
index b3341cb..423e146 100644
--- a/security/agent/agentd/main.go
+++ b/security/agent/agentd/main.go
@@ -1,11 +1,12 @@
package main
import (
- "code.google.com/p/gopass"
+ "code.google.com/p/go.crypto/ssh/terminal"
"flag"
"fmt"
"os"
"os/exec"
+ "os/signal"
"syscall"
_ "veyron.io/veyron/veyron/profiles"
vsecurity "veyron.io/veyron/veyron/security"
@@ -89,11 +90,11 @@
func handleDoesNotExist(dir string) (security.Principal, error) {
fmt.Println("Private key file does not exist. Creating new private key...")
- pass, err := gopass.GetPass("Enter passphrase (entering nothing will store unecrypted): ")
+ pass, err := getPassword("Enter passphrase (entering nothing will store unecrypted): ")
if err != nil {
return nil, fmt.Errorf("failed to read passphrase: %v", err)
}
- p, err := vsecurity.CreatePersistentPrincipal(dir, []byte(pass))
+ p, err := vsecurity.CreatePersistentPrincipal(dir, pass)
if err != nil {
return nil, err
}
@@ -103,9 +104,40 @@
func handlePassphrase(dir string) (security.Principal, error) {
fmt.Println("Private key file is encrypted. Please enter passphrase.")
- pass, err := gopass.GetPass("Enter passphrase: ")
+ pass, err := getPassword("Enter passphrase: ")
if err != nil {
return nil, fmt.Errorf("failed to read passphrase: %v", err)
}
- return vsecurity.LoadPersistentPrincipal(dir, []byte(pass))
+ return vsecurity.LoadPersistentPrincipal(dir, pass)
+}
+
+func getPassword(prompt string) ([]byte, error) {
+ fmt.Printf(prompt)
+ stop := make(chan bool)
+ defer close(stop)
+ state, err := terminal.GetState(int(os.Stdin.Fd()))
+ if err != nil {
+ return nil, err
+ }
+ go catchTerminationSignals(stop, state)
+ return terminal.ReadPassword(int(os.Stdin.Fd()))
+}
+
+// catchTerminationSignals catches signals to allow us to turn terminal echo back on.
+func catchTerminationSignals(stop <-chan bool, state *terminal.State) {
+ var successErrno syscall.Errno
+ sig := make(chan os.Signal, 4)
+ // Catch the blockable termination signals.
+ signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGHUP)
+ select {
+ case <-sig:
+ // Start on new line in terminal.
+ fmt.Printf("\n")
+ if err := terminal.Restore(int(os.Stdin.Fd()), state); err != successErrno {
+ vlog.Errorf("Failed to restore terminal state (%v), you words may not show up when you type, enter 'stty echo' to fix this.", err)
+ }
+ os.Exit(-1)
+ case <-stop:
+ signal.Stop(sig)
+ }
}
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
+}