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"
)