veyron/services/mounttable: Add a command-line tool.

Add a command-line tool to interact with mounttable servers. All the server
methods are supported.

Change-Id: Iebc80cf24826ce774851fbcd595a046ea3ddff81
diff --git a/services/mounttable/mounttable/impl/impl.go b/services/mounttable/mounttable/impl/impl.go
new file mode 100644
index 0000000..b878c68
--- /dev/null
+++ b/services/mounttable/mounttable/impl/impl.go
@@ -0,0 +1,158 @@
+package impl
+
+import (
+	"fmt"
+	"io"
+	"time"
+
+	"veyron/lib/cmdline"
+
+	"veyron2/services/mounttable"
+)
+
+var cmdGlob = &cmdline.Command{
+	Run:      runGlob,
+	Name:     "glob",
+	Short:    "returns all matching entries in the mount table",
+	Long:     "returns all matching entries in the mount table",
+	ArgsName: "<mount name> <pattern>",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.
+<pattern> is a glob pattern that is matched against all the entries below the
+specified mount name.
+`,
+}
+
+func runGlob(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.Errorf("glob: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	c, err := mounttable.BindMountTable(args[0])
+	if err != nil {
+		return fmt.Errorf("bind error: %v", err)
+	}
+	stream, err := c.Glob(args[1])
+	if err != nil {
+		return err
+	}
+	for {
+		buf, err := stream.Recv()
+		if err != nil {
+			if err == io.EOF {
+				break
+			}
+			return fmt.Errorf("recv error: %v", err)
+		}
+		fmt.Fprint(cmd.Stdout(), buf.Name)
+		for _, s := range buf.Servers {
+			fmt.Fprintf(cmd.Stdout(), " %s (TTL %s)", s.Server, time.Duration(s.TTL)*time.Second)
+		}
+		fmt.Fprintln(cmd.Stdout())
+	}
+	err = stream.Finish()
+	if err != nil {
+		return fmt.Errorf("finish error: %v", err)
+	}
+	return nil
+}
+
+var cmdMount = &cmdline.Command{
+	Run:      runMount,
+	Name:     "mount",
+	Short:    "Mounts a server <name> onto a mount table",
+	Long:     "Mounts a server <name> onto a mount table",
+	ArgsName: "<mount name> <name> <ttl>",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.
+<name> is the rooted veyron name of the server.
+<ttl> is the TTL of the new entry. It is a decimal number followed by a unit
+suffix (s, m, h). A value of 0s represents an infinite duration.
+`,
+}
+
+func runMount(cmd *cmdline.Command, args []string) error {
+	if expected, got := 3, len(args); expected != got {
+		return cmd.Errorf("mount: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	c, err := mounttable.BindMountTable(args[0])
+	if err != nil {
+		return fmt.Errorf("bind error: %v", err)
+	}
+	ttl, err := time.ParseDuration(args[2])
+	if err != nil {
+		return fmt.Errorf("TTL parse error: %v", err)
+	}
+	err = c.Mount(args[1], uint32(ttl.Seconds()))
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintln(cmd.Stdout(), "Name mounted successfully.")
+	return nil
+}
+
+var cmdUnmount = &cmdline.Command{
+	Run:      runUnmount,
+	Name:     "unmount",
+	Short:    "removes server <name> from the mount table",
+	Long:     "removes server <name> from the mount table",
+	ArgsName: "<mount name> <name>",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.
+<name> is the rooted veyron name of the server.
+`,
+}
+
+func runUnmount(cmd *cmdline.Command, args []string) error {
+	if expected, got := 2, len(args); expected != got {
+		return cmd.Errorf("unmount: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	c, err := mounttable.BindMountTable(args[0])
+	if err != nil {
+		return fmt.Errorf("bind error: %v", err)
+	}
+	err = c.Unmount(args[1])
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintln(cmd.Stdout(), "Name unmounted successfully.")
+	return nil
+}
+
+var cmdResolveStep = &cmdline.Command{
+	Run:      runResolveStep,
+	Name:     "resolvestep",
+	Short:    "takes the next step in resolving a name.",
+	Long:     "takes the next step in resolving a name.",
+	ArgsName: "<mount name>",
+	ArgsLong: `
+<mount name> is a mount name on a mount table.
+`,
+}
+
+func runResolveStep(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.Errorf("mount: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	c, err := mounttable.BindMountTable(args[0])
+	if err != nil {
+		return fmt.Errorf("bind error: %v", err)
+	}
+	servers, suffix, err := c.ResolveStep()
+	if err != nil {
+		return err
+	}
+
+	fmt.Fprintf(cmd.Stdout(), "Servers: %v Suffix: %q\n", servers, suffix)
+	return nil
+}
+
+func Root() *cmdline.Command {
+	return &cmdline.Command{
+		Name:     "mounttable",
+		Short:    "Command-line tool for interacting with a Veyron mount table",
+		Long:     "Command-line tool for interacting with a Veyron mount table",
+		Children: []*cmdline.Command{cmdGlob, cmdMount, cmdUnmount, cmdResolveStep},
+	}
+}
diff --git a/services/mounttable/mounttable/impl/impl_test.go b/services/mounttable/mounttable/impl/impl_test.go
new file mode 100644
index 0000000..fdd5125
--- /dev/null
+++ b/services/mounttable/mounttable/impl/impl_test.go
@@ -0,0 +1,127 @@
+package impl_test
+
+import (
+	"bytes"
+	"strings"
+	"testing"
+
+	"veyron/services/mounttable/mounttable/impl"
+
+	"veyron2"
+	"veyron2/ipc"
+	"veyron2/naming"
+	"veyron2/rt"
+	"veyron2/security"
+	"veyron2/services/mounttable"
+	"veyron2/vlog"
+)
+
+type server struct {
+	suffix string
+}
+
+func (s *server) Glob(_ ipc.Context, pattern string, stream mounttable.GlobableServiceGlobStream) error {
+	vlog.VI(2).Infof("Glob() was called. suffix=%v pattern=%q", s.suffix, pattern)
+	stream.Send(mounttable.MountEntry{"name1", []mounttable.MountedServer{{"server1", 123}}})
+	stream.Send(mounttable.MountEntry{"name2", []mounttable.MountedServer{{"server2", 456}, {"server3", 789}}})
+	return nil
+}
+
+func (s *server) Mount(_ ipc.Context, server string, ttl uint32) error {
+	vlog.VI(2).Infof("Mount() was called. suffix=%v server=%q ttl=%d", s.suffix, server, ttl)
+	return nil
+}
+
+func (s *server) Unmount(_ ipc.Context, server string) error {
+	vlog.VI(2).Infof("Unmount() was called. suffix=%v server=%q", s.suffix, server)
+	return nil
+}
+
+func (s *server) ResolveStep(ipc.Context) (servers []mounttable.MountedServer, suffix string, err error) {
+	vlog.VI(2).Infof("ResolveStep() was called. suffix=%v", s.suffix)
+	servers = []mounttable.MountedServer{{"server1", 123}}
+	suffix = s.suffix
+	return
+}
+
+type dispatcher struct {
+}
+
+func (d *dispatcher) Lookup(suffix string) (ipc.Invoker, security.Authorizer, error) {
+	invoker := ipc.ReflectInvoker(mounttable.NewServerMountTable(&server{suffix: suffix}))
+	return invoker, nil, nil
+}
+
+func startServer(t *testing.T, r veyron2.Runtime) (ipc.Server, naming.Endpoint, error) {
+	dispatcher := new(dispatcher)
+	server, err := r.NewServer()
+	if err != nil {
+		t.Errorf("NewServer failed: %v", err)
+		return nil, nil, err
+	}
+	if err := server.Register("", dispatcher); err != nil {
+		t.Errorf("Register failed: %v", err)
+		return nil, nil, err
+	}
+	endpoint, err := server.Listen("tcp", "localhost:0")
+	if err != nil {
+		t.Errorf("Listen failed: %v", err)
+		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 TestMountTableClient(t *testing.T) {
+	runtime := rt.Init()
+	server, endpoint, err := startServer(t, runtime)
+	if err != nil {
+		return
+	}
+	defer stopServer(t, server)
+	// Setup the command-line.
+	cmd := impl.Root()
+	var stdout, stderr bytes.Buffer
+	cmd.Init(nil, &stdout, &stderr)
+
+	// Test the 'glob' command.
+	if err := cmd.Execute([]string{"glob", naming.JoinAddressNameFixed(endpoint.String(), ""), "*"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "name1 server1 (TTL 2m3s)\nname2 server2 (TTL 7m36s) server3 (TTL 13m9s)", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'mount' command.
+	if err := cmd.Execute([]string{"mount", naming.JoinAddressNameFixed(endpoint.String(), ""), "server", "123s"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Name mounted successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'unmount' command.
+	if err := cmd.Execute([]string{"unmount", naming.JoinAddressNameFixed(endpoint.String(), ""), "server"}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := "Name unmounted successfully.", strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+
+	// Test the 'resolvestep' command.
+	if err := cmd.Execute([]string{"resolvestep", naming.JoinAddressNameFixed(endpoint.String(), "name")}); err != nil {
+		t.Fatalf("%v", err)
+	}
+	if expected, got := `Servers: [{server1 123}] Suffix: "name"`, strings.TrimSpace(stdout.String()); got != expected {
+		t.Errorf("Got %q, expected %q", got, expected)
+	}
+	stdout.Reset()
+}
diff --git a/services/mounttable/mounttable/main.go b/services/mounttable/mounttable/main.go
new file mode 100644
index 0000000..d62d991
--- /dev/null
+++ b/services/mounttable/mounttable/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"veyron/services/mounttable/mounttable/impl"
+
+	"veyron2/rt"
+)
+
+func main() {
+	r := rt.Init()
+	defer r.Shutdown()
+
+	impl.Root().Main()
+}