veyron/tools/mgmt/device: add install-local command

This allows a user to specify an app using a local binary and env/flags given on
the command-line, instead of having to use a remote application and binary
server. The implementation starts a short-lived app and binary server within the
command-line tool. Currently, this server listens on its own port (or proxy),
but eventually we should share the connection which the tool makes to the device
manager.

Change-Id: Iea0653f37584797ae09a5563ee9e7669f3bcf9e1
diff --git a/services/mgmt/device/impl/util.go b/services/mgmt/device/impl/util.go
index b98f4cc..596ef71 100644
--- a/services/mgmt/device/impl/util.go
+++ b/services/mgmt/device/impl/util.go
@@ -39,8 +39,6 @@
 
 func fetchEnvelope(ctx *context.T, origin string) (*application.Envelope, error) {
 	stub := repository.ApplicationClient(origin)
-	// TODO(jsimsa): Include logic that computes the set of supported
-	// profiles.
 	profilesSet, err := Describe()
 	if err != nil {
 		vlog.Errorf("Failed to obtain profile labels: %v", err)
diff --git a/tools/mgmt/device/doc.go b/tools/mgmt/device/doc.go
index a0e39e3..b760e71 100644
--- a/tools/mgmt/device/doc.go
+++ b/tools/mgmt/device/doc.go
@@ -8,16 +8,21 @@
    device <command>
 
 The device commands are:
-   install     Install the given application.
-   start       Start an instance of the given application.
-   associate   Tool for creating associations between Vanadium blessings and a
-               system account
-   claim       Claim the device.
-   stop        Stop the given application instance.
-   suspend     Suspend the given application instance.
-   resume      Resume the given application instance.
-   acl         Tool for setting device manager ACLs
-   help        Display help for commands or topics
+   install       Install the given application.
+   install-local Install the given application from the local system.
+   start         Start an instance of the given application.
+   associate     Tool for creating associations between Vanadium blessings and a
+                 system account
+   describe      Describe the device.
+   claim         Claim the device.
+   stop          Stop the given application instance.
+   suspend       Suspend the given application instance.
+   resume        Resume the given application instance.
+   revert        Revert the device manager or application
+   update        Update the device manager or application
+   debug         Debug the device.
+   acl           Tool for setting device manager ACLs
+   help          Display help for commands or topics
 Run "device help [command]" for command usage.
 
 The global flags are:
@@ -37,13 +42,28 @@
    log level for V logs
  -vanadium.i18n_catalogue=
    18n catalogue files to load, comma separated
+ -veyron.acl.file=map[]
+   specify an acl file as <name>:<aclfile>
+ -veyron.acl.literal=
+   explicitly specify the runtime acl as a JSON-encoded access.TaggedACLMap.
+   Overrides all --veyron.acl.file flags.
  -veyron.credentials=
    directory to use for storing security credentials
  -veyron.namespace.root=[/ns.dev.v.io:8101]
    local namespace root; can be repeated to provided multiple roots
+ -veyron.proxy=
+   object name of proxy service to use to export services across network
+   boundaries
+ -veyron.tcp.address=
+   address to listen on
+ -veyron.tcp.protocol=wsh
+   protocol to listen with
  -veyron.vtrace.cache_size=1024
    The number of vtrace traces to store in memory.
- -veyron.vtrace.dump_on_shutdown=false
+ -veyron.vtrace.collect_regexp=
+   Spans and annotations that match this regular expression will trigger trace
+   collection.
+ -veyron.vtrace.dump_on_shutdown=true
    If true, dump all stored traces on runtime shutdown.
  -veyron.vtrace.sample_rate=0
    Rate (from 0.0 to 1.0) to sample vtrace traces.
@@ -55,11 +75,29 @@
 Install the given application.
 
 Usage:
-   device install <device> <application>
+   device install <device> <application> [<config override>]
 
 <device> is the veyron object name of the device manager's app service.
+
 <application> is the veyron object name of the application.
 
+<config override> is an optional JSON-encoded device.Config object, of the form:
+   '{"flag1":"value1","flag2":"value2"}'.
+
+Device Install-Local
+
+Install the given application, specified using a local path.
+
+Usage:
+   device install-local <device> <title> [ENV=VAL ...] binary [--flag=val ...]
+
+<device> is the veyron object name of the device manager's app service.
+
+<title> is the app title.
+
+This is followed by an arbitrary number of environment variable settings, the
+local path for the binary to install, and arbitrary flag settings.
+
 Device Start
 
 Start an instance of the given application.
@@ -115,6 +153,15 @@
 <devicemanager> is the name of the device manager to connect to. <blessing>...
 is a list of blessings.
 
+Device Describe
+
+Describe the device.
+
+Usage:
+   device describe <device>
+
+<device> is the veyron object name of the device manager's device service.
+
 Device Claim
 
 Claim the device.
@@ -122,7 +169,7 @@
 Usage:
    device claim <device> <grant extension>
 
-<device> is the veyron object name of the device manager's app service.
+<device> is the veyron object name of the device manager's device service.
 
 <grant extension> is used to extend the default blessing of the current
 principal when blessing the app instance.
@@ -154,6 +201,35 @@
 
 <app instance> is the veyron object name of the application instance to resume.
 
+Device Revert
+
+Revert the device manager or application to its previous version
+
+Usage:
+   device revert <object>
+
+<object> is the veyron object name of the device manager or application
+installation to revert.
+
+Device Update
+
+Update the device manager or application
+
+Usage:
+   device update <object>
+
+<object> is the veyron object name of the device manager or application
+installation to update.
+
+Device Debug
+
+Debug the device.
+
+Usage:
+   device debug <device>
+
+<device> is the veyron object name of an app installation or instance.
+
 Device Acl
 
 The acl tool manages ACLs on the device manger, installations and instances.
diff --git a/tools/mgmt/device/impl/devicemanager_mock_test.go b/tools/mgmt/device/impl/devicemanager_mock_test.go
index 62cf94a..523c5e2 100644
--- a/tools/mgmt/device/impl/devicemanager_mock_test.go
+++ b/tools/mgmt/device/impl/devicemanager_mock_test.go
@@ -9,12 +9,14 @@
 	"v.io/core/veyron2/ipc"
 	"v.io/core/veyron2/naming"
 	"v.io/core/veyron2/security"
+	"v.io/core/veyron2/services/mgmt/application"
 	"v.io/core/veyron2/services/mgmt/binary"
 	"v.io/core/veyron2/services/mgmt/device"
+	"v.io/core/veyron2/services/mgmt/repository"
 	"v.io/core/veyron2/services/security/access"
 	"v.io/core/veyron2/vlog"
 
-	_ "v.io/core/veyron/profiles"
+	binlib "v.io/core/veyron/services/mgmt/lib/binary"
 )
 
 type mockDeviceInvoker struct {
@@ -77,9 +79,11 @@
 
 // Mock Install
 type InstallStimulus struct {
-	fun     string
-	appName string
-	config  device.Config
+	fun        string
+	appName    string
+	config     device.Config
+	envelope   application.Envelope
+	binarySize int64
 }
 
 type InstallResponse struct {
@@ -87,9 +91,38 @@
 	err   error
 }
 
+const (
+	// If provided with this app name, the mock device manager skips trying
+	// to fetch the envelope from the name.
+	appNameNoFetch = "skip-envelope-fetching"
+	// If provided with a fetcheable app name, the mock device manager sets
+	// the app name in the stimulus to this constant.
+	appNameAfterFetch = "envelope-fetched"
+	// The mock device manager sets the binary name in the envelope in the
+	// stimulus to this constant.
+	binaryNameAfterFetch = "binary-fetched"
+)
+
 func (mni *mockDeviceInvoker) Install(call ipc.ServerContext, appName string, config device.Config) (string, error) {
-	ir := mni.tape.Record(InstallStimulus{"Install", appName, config})
-	r := ir.(InstallResponse)
+	is := InstallStimulus{"Install", appName, config, application.Envelope{}, 0}
+	if appName != appNameNoFetch {
+		// Fetch the envelope and record it in the stimulus.
+		envelope, err := repository.ApplicationClient(appName).Match(call.Context(), []string{"test"})
+		if err != nil {
+			return "", err
+		}
+		binaryName := envelope.Binary
+		envelope.Binary = binaryNameAfterFetch
+		is.envelope = envelope
+		is.appName = appNameAfterFetch
+		// Fetch the binary and record its size in the stimulus.
+		data, _, err := binlib.Download(call.Context(), binaryName)
+		if err != nil {
+			return "", err
+		}
+		is.binarySize = int64(len(data))
+	}
+	r := mni.tape.Record(is).(InstallResponse)
 	return r.appId, r.err
 }
 
diff --git a/tools/mgmt/device/impl/impl_test.go b/tools/mgmt/device/impl/impl_test.go
index b841ee6..435d185 100644
--- a/tools/mgmt/device/impl/impl_test.go
+++ b/tools/mgmt/device/impl/impl_test.go
@@ -9,6 +9,7 @@
 	"testing"
 
 	"v.io/core/veyron2/naming"
+	"v.io/core/veyron2/services/mgmt/application"
 	"v.io/core/veyron2/services/mgmt/device"
 	verror "v.io/core/veyron2/verror2"
 
@@ -203,25 +204,25 @@
 			nil,
 		},
 		{
-			[]string{"install", deviceName, "myBestApp", "not-valid-json"},
+			[]string{"install", deviceName, appNameNoFetch, "not-valid-json"},
 			nil,
 			true,
 			nil,
 			nil,
 		},
 		{
-			[]string{"install", deviceName, "myBestApp"},
+			[]string{"install", deviceName, appNameNoFetch},
 			nil,
 			false,
 			InstallResponse{appId, nil},
-			InstallStimulus{"Install", "myBestApp", nil},
+			InstallStimulus{"Install", appNameNoFetch, nil, application.Envelope{}, 0},
 		},
 		{
-			[]string{"install", deviceName, "myBestApp"},
+			[]string{"install", deviceName, appNameNoFetch},
 			cfg,
 			false,
 			InstallResponse{appId, nil},
-			InstallStimulus{"Install", "myBestApp", cfg},
+			InstallStimulus{"Install", appNameNoFetch, cfg, application.Envelope{}, 0},
 		},
 	} {
 		tape.SetResponses([]interface{}{c.tapeResponse})
diff --git a/tools/mgmt/device/impl/local_install.go b/tools/mgmt/device/impl/local_install.go
new file mode 100644
index 0000000..a8b8fb9
--- /dev/null
+++ b/tools/mgmt/device/impl/local_install.go
@@ -0,0 +1,235 @@
+package impl
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	"v.io/core/veyron2"
+	"v.io/core/veyron2/context"
+	"v.io/core/veyron2/ipc"
+	"v.io/core/veyron2/naming"
+	"v.io/core/veyron2/security"
+	"v.io/core/veyron2/services/mgmt/application"
+	"v.io/core/veyron2/services/mgmt/binary"
+	"v.io/core/veyron2/services/mgmt/device"
+	"v.io/core/veyron2/services/mgmt/repository"
+	"v.io/core/veyron2/services/security/access"
+	"v.io/core/veyron2/uniqueid"
+	"v.io/lib/cmdline"
+)
+
+// TODO(caprita): Add a way to provide an origin for the app, so we can do
+// updates after it's been installed.
+
+var cmdInstallLocal = &cmdline.Command{
+	Run:      runInstallLocal,
+	Name:     "install-local",
+	Short:    "Install the given application from the local system.",
+	Long:     "Install the given application, specified using a local path.",
+	ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...]",
+	ArgsLong: `
+<device> is the veyron object name of the device manager's app service.
+
+<title> is the app title.
+
+This is followed by an arbitrary number of environment variable settings, the
+local path for the binary to install, and arbitrary flag settings.`,
+}
+
+type openAuthorizer struct{}
+
+func (openAuthorizer) Authorize(security.Context) error { return nil }
+
+type mapDispatcher map[string]interface{}
+
+func (d mapDispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
+	o, ok := d[suffix]
+	if !ok {
+		return nil, nil, fmt.Errorf("suffix %s not found", suffix)
+	}
+	// TODO(caprita): Do not open authorizer even for a short-lived server.
+	return o, &openAuthorizer{}, nil
+}
+
+func createServer(ctx *context.T, stderr io.Writer, objects map[string]interface{}) (string, func(), error) {
+	server, err := veyron2.NewServer(ctx)
+	if err != nil {
+		return "", nil, err
+	}
+	spec := veyron2.GetListenSpec(ctx)
+	endpoints, err := server.Listen(spec)
+	if err != nil {
+		return "", nil, err
+	}
+	var name string
+	if spec.Proxy != "" {
+		id, err := uniqueid.Random()
+		if err != nil {
+			return "", nil, err
+		}
+		name = id.String()
+	}
+	if err := server.ServeDispatcher(name, mapDispatcher(objects)); err != nil {
+		return "", nil, err
+	}
+	cleanup := func() {
+		if err := server.Stop(); err != nil {
+			fmt.Fprintf(stderr, "server.Stop failed: %v", err)
+		}
+	}
+	if name != "" {
+		// Send a name rooted in our namespace root rather than the
+		// relative name (in case the device manager uses a different
+		// namespace root).
+		//
+		// TODO(caprita): Avoid relying on a mounttable altogether, and
+		// instead pull out the proxied address and just send that.
+		nsRoots := veyron2.GetNamespace(ctx).Roots()
+		if len(nsRoots) > 0 {
+			name = naming.Join(nsRoots[0], name)
+		}
+		return name, cleanup, nil
+	}
+	if len(endpoints) == 0 {
+		return "", nil, fmt.Errorf("no endpoints")
+	}
+	return endpoints[0].Name(), cleanup, nil
+}
+
+var errNotImplemented = fmt.Errorf("method not implemented")
+
+type binaryInvoker string
+
+func (binaryInvoker) Create(ipc.ServerContext, int32, repository.MediaInfo) error {
+	return errNotImplemented
+}
+
+func (binaryInvoker) Delete(ipc.ServerContext) error {
+	return errNotImplemented
+}
+
+func (i binaryInvoker) Download(ctx repository.BinaryDownloadContext, _ int32) error {
+	fileName := string(i)
+	file, err := os.Open(fileName)
+	if err != nil {
+		return err
+	}
+	defer file.Close()
+	bufferLength := 4096
+	buffer := make([]byte, bufferLength)
+	sender := ctx.SendStream()
+	for {
+		n, err := file.Read(buffer)
+		switch err {
+		case io.EOF:
+			return nil
+		case nil:
+			if err := sender.Send(buffer[:n]); err != nil {
+				return err
+			}
+		default:
+			return err
+		}
+	}
+}
+
+func (binaryInvoker) DownloadURL(ipc.ServerContext) (string, int64, error) {
+	return "", 0, errNotImplemented
+}
+
+func (i binaryInvoker) Stat(ctx ipc.ServerContext) ([]binary.PartInfo, repository.MediaInfo, error) {
+	fileName := string(i)
+	h := md5.New()
+	bytes, err := ioutil.ReadFile(fileName)
+	if err != nil {
+		return []binary.PartInfo{}, repository.MediaInfo{}, err
+	}
+	h.Write(bytes)
+	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
+	return []binary.PartInfo{part}, repository.MediaInfo{Type: "application/octet-stream"}, nil
+}
+
+func (binaryInvoker) Upload(repository.BinaryUploadContext, int32) error {
+	return errNotImplemented
+}
+
+func (binaryInvoker) GetACL(ctx ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
+	return nil, "", errNotImplemented
+}
+
+func (binaryInvoker) SetACL(ctx ipc.ServerContext, acl access.TaggedACLMap, etag string) error {
+	return errNotImplemented
+}
+
+type envelopeInvoker application.Envelope
+
+func (i envelopeInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
+	return application.Envelope(i), nil
+}
+func (envelopeInvoker) GetACL(ipc.ServerContext) (acl access.TaggedACLMap, etag string, err error) {
+	return nil, "", errNotImplemented
+}
+
+func (envelopeInvoker) SetACL(ipc.ServerContext, access.TaggedACLMap, string) error {
+	return errNotImplemented
+}
+
+// runInstallLocal creates a new envelope on the fly from the provided
+// arguments, and then points the device manager back to itself for downloading
+// the app envelope and binary.
+//
+// It sets up an app and binary server that only lives for the duration of the
+// command, and listens on the profile's listen spec.  The caller should set the
+// --veyron.proxy if the machine running the command is not accessible from the
+// device manager.
+//
+// TODO(caprita/ashankar): We should use bi-directional streams to get this
+// working over the same connection that the command makes to the device
+// manager.
+func runInstallLocal(cmd *cmdline.Command, args []string) error {
+	if expectedMin, got := 2, len(args); got < expectedMin {
+		return cmd.UsageErrorf("install-local: incorrect number of arguments, expected at least %d, got %d", expectedMin, got)
+	}
+	deviceName, title := args[0], args[1]
+	args = args[2:]
+	envelope := application.Envelope{Title: title}
+	// Extract the environment settings, binary, and arguments.
+	firstNonEnv := len(args)
+	for i, arg := range args {
+		if strings.Index(arg, "=") <= 0 {
+			firstNonEnv = i
+			break
+		}
+	}
+	envelope.Env = args[:firstNonEnv]
+	args = args[firstNonEnv:]
+	if len(args) == 0 {
+		return cmd.UsageErrorf("install-local: missing binary")
+	}
+	binary := args[0]
+	envelope.Args = args[1:]
+	if _, err := os.Stat(binary); err != nil {
+		return fmt.Errorf("binary %v not found: %v", binary, err)
+	}
+	objects := map[string]interface{}{"binary": repository.BinaryServer(binaryInvoker(binary))}
+	name, cancel, err := createServer(gctx, cmd.Stderr(), objects)
+	if err != nil {
+		return fmt.Errorf("failed to create server: %v", err)
+	}
+	defer cancel()
+	envelope.Binary = naming.Join(name, "binary")
+
+	objects["application"] = repository.ApplicationServer(envelopeInvoker(envelope))
+	appName := naming.Join(name, "application")
+	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, nil)
+	if err != nil {
+		return fmt.Errorf("Install failed: %v", err)
+	}
+	fmt.Fprintf(cmd.Stdout(), "Successfully installed: %q\n", naming.Join(deviceName, appID))
+	return nil
+}
diff --git a/tools/mgmt/device/impl/local_install_test.go b/tools/mgmt/device/impl/local_install_test.go
new file mode 100644
index 0000000..7abb65f
--- /dev/null
+++ b/tools/mgmt/device/impl/local_install_test.go
@@ -0,0 +1,99 @@
+package impl_test
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"reflect"
+	"strings"
+	"testing"
+
+	"v.io/core/veyron2/naming"
+	"v.io/core/veyron2/services/mgmt/application"
+
+	"v.io/core/veyron/tools/mgmt/device/impl"
+)
+
+func TestInstallLocalCommand(t *testing.T) {
+	shutdown := initTest()
+	defer shutdown()
+
+	tape := NewTape()
+	server, endpoint, err := startServer(t, gctx, tape)
+	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)
+	deviceName := naming.JoinAddressName(endpoint.String(), "")
+	appTitle := "Appo di tutti Appi"
+	for i, c := range []struct {
+		args         []string
+		stderrSubstr string
+	}{
+		{
+			[]string{"install-local", deviceName}, "incorrect number of arguments",
+		},
+		{
+			[]string{"install-local", deviceName, appTitle}, "missing binary",
+		},
+		{
+			[]string{"install-local", deviceName, appTitle, "a=b"}, "missing binary",
+		},
+		{
+			[]string{"install-local", deviceName, appTitle, "foo"}, "binary foo not found",
+		},
+	} {
+		if err := cmd.Execute(c.args); err == nil {
+			t.Fatalf("test case %d: wrongly failed to receive a non-nil error.", i)
+		} else {
+			fmt.Fprintln(&stderr, "ERROR:", err)
+			if want, got := c.stderrSubstr, stderr.String(); !strings.Contains(got, want) {
+				t.Errorf("test case %d: %q not found in stderr: %q", i, want, got)
+			}
+		}
+		if got, expected := len(tape.Play()), 0; got != expected {
+			t.Errorf("test case %d: invalid call sequence. Got %v, want %v", got, expected)
+		}
+		tape.Rewind()
+		stdout.Reset()
+		stderr.Reset()
+	}
+	appId := "myBestAppID"
+	binary := os.Args[0]
+	fi, err := os.Stat(binary)
+	if err != nil {
+		t.Fatalf("Failed to stat %v: %v", binary, err)
+	}
+	binarySize := fi.Size()
+	for i, c := range []struct {
+		args         []string
+		expectedTape interface{}
+	}{
+		{
+			[]string{"install-local", deviceName, appTitle, binary},
+			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch}, binarySize},
+		},
+		{
+			[]string{"install-local", deviceName, appTitle, "ENV1=V1", "ENV2=V2", binary, "FLAG1=V1", "FLAG2=V2"},
+			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Env: []string{"ENV1=V1", "ENV2=V2"}, Args: []string{"FLAG1=V1", "FLAG2=V2"}}, binarySize},
+		},
+	} {
+		tape.SetResponses([]interface{}{InstallResponse{appId, nil}})
+		if err := cmd.Execute(c.args); err != nil {
+			t.Fatalf("test case %d: %v", i, err)
+		}
+		if expected, got := fmt.Sprintf("Successfully installed: %q", naming.Join(deviceName, appId)), strings.TrimSpace(stdout.String()); got != expected {
+			t.Fatalf("test case %d: Unexpected output from Install. Got %q, expected %q", i, got, expected)
+		}
+		if got, expected := tape.Play(), []interface{}{c.expectedTape}; !reflect.DeepEqual(expected, got) {
+			t.Errorf("test case %d: Invalid call sequence. Got %#v, want %#v", i, got, expected)
+		}
+		tape.Rewind()
+		stdout.Reset()
+		stderr.Reset()
+	}
+}
diff --git a/tools/mgmt/device/impl/root.go b/tools/mgmt/device/impl/root.go
index db04609..9c3f4ad 100644
--- a/tools/mgmt/device/impl/root.go
+++ b/tools/mgmt/device/impl/root.go
@@ -19,6 +19,6 @@
 		Long: `
 The device tool facilitates interaction with the veyron device manager.
 `,
-		Children: []*cmdline.Command{cmdInstall, cmdStart, associateRoot(), cmdDescribe, cmdClaim, cmdStop, cmdSuspend, cmdResume, cmdRevert, cmdUpdate, cmdDebug, aclRoot()},
+		Children: []*cmdline.Command{cmdInstall, cmdInstallLocal, cmdStart, associateRoot(), cmdDescribe, cmdClaim, cmdStop, cmdSuspend, cmdResume, cmdRevert, cmdUpdate, cmdDebug, aclRoot()},
 	}
 }
diff --git a/tools/mgmt/device/main.go b/tools/mgmt/device/main.go
index fc24f06..7d77330 100644
--- a/tools/mgmt/device/main.go
+++ b/tools/mgmt/device/main.go
@@ -8,7 +8,7 @@
 
 	"v.io/core/veyron2"
 
-	_ "v.io/core/veyron/profiles"
+	_ "v.io/core/veyron/profiles/static"
 	"v.io/core/veyron/tools/mgmt/device/impl"
 )