veyron/services/mgmt/device/impl: add Debug RPC method for sundry state dumps
The way we currently debug the device manager is by poking around the directory
tree that the device manager sets up on the local filesystem. Sometimes, this is
not convenient (e.g. when we're remote), and other times the information we want
is not available on the filesystem (e.g. the credentials for app instances, when
using the security agent).
This CL adds the new Debug method to the Application interface in the VDL.
The corresponding implementation supports app installations and instances for
now, and prints an assortment of stuff about the respective installations and
instances.
Change-Id: I84dd88ff0324f103590651fdfca3252f17d37da7
diff --git a/services/mgmt/device/impl/app_service.go b/services/mgmt/device/impl/app_service.go
index 1a49e46..2d40769 100644
--- a/services/mgmt/device/impl/app_service.go
+++ b/services/mgmt/device/impl/app_service.go
@@ -108,6 +108,7 @@
// refine that later.
import (
+ "bytes"
"crypto/md5"
"crypto/rand"
"encoding/base64"
@@ -124,6 +125,7 @@
"reflect"
"strconv"
"strings"
+ "text/template"
"time"
"v.io/core/veyron2"
@@ -465,6 +467,29 @@
return installationDir, nil
}
+// agentPrincipal creates a Principal backed by the given agent connection,
+// taking ownership of the connection. The returned cancel function is to be
+// called when the Principal is no longer in use.
+func agentPrincipal(ctx *context.T, conn *os.File) (security.Principal, func(), error) {
+ agentctx, cancel := context.WithCancel(ctx)
+ var err error
+ if agentctx, _, err = veyron2.SetNewStreamManager(agentctx); err != nil {
+ cancel()
+ conn.Close()
+ return nil, nil, err
+ }
+ p, err := agent.NewAgentPrincipal(agentctx, int(conn.Fd()), veyron2.GetClient(agentctx))
+ if err != nil {
+ cancel()
+ conn.Close()
+ return nil, nil, err
+ }
+ // conn will be closed when the connection to the agent is shut down, as
+ // a result of cancel shutting down the stream manager. No need to
+ // explicitly call conn.Close() with cancel.
+ return p, cancel, nil
+}
+
// setupPrincipal sets up the instance's principal, with the right blessings.
func setupPrincipal(ctx *context.T, instanceDir, versionDir string, call ipc.ServerContext, securityAgent *securityAgentState, info *instanceInfo) error {
var p security.Principal
@@ -476,19 +501,12 @@
vlog.Errorf("NewPrincipal() failed %v", err)
return verror2.Make(ErrOperationFailed, nil)
}
- agentctx, cancel := context.WithCancel(ctx)
- if agentctx, _, err = veyron2.SetNewStreamManager(agentctx); err != nil {
- cancel()
- conn.Close()
- vlog.Errorf("SetNewStreamManager failed: %v", err)
+ var cancel func()
+ if p, cancel, err = agentPrincipal(ctx, conn); err != nil {
+ vlog.Errorf("agentPrincipal failed: %v", err)
return verror2.Make(ErrOperationFailed, nil)
}
defer cancel()
- if p, err = agent.NewAgentPrincipal(agentctx, int(conn.Fd()), veyron2.GetClient(agentctx)); err != nil {
- conn.Close()
- vlog.Errorf("NewAgentPrincipal() failed: %v", err)
- return verror2.Make(ErrOperationFailed, nil)
- }
info.SecurityAgentHandle = handle
// conn will be closed when the connection to the agent is shut
// down, as a result of cancel() shutting down the stream
@@ -1248,3 +1266,142 @@
}
return i.locks.GetPathACL(ctx.LocalPrincipal(), path.Join(dir, "acls"))
}
+
+func (i *appService) Debug(ctx ipc.ServerContext) (string, error) {
+ switch len(i.suffix) {
+ case 2:
+ return i.installationDebug(ctx)
+ case 3:
+ return i.instanceDebug(ctx)
+ default:
+ return "", verror2.Make(ErrInvalidSuffix, nil)
+ }
+}
+
+func (i *appService) installationDebug(ctx ipc.ServerContext) (string, error) {
+ const installationDebug = `Installation dir: {{.InstallationDir}}
+
+Origin: {{.Origin}}
+
+Envelope: {{printf "%+v" .Envelope}}
+
+Config: {{printf "%+v" .Config}}
+`
+ installationDebugTemplate, err := template.New("installation-debug").Parse(installationDebug)
+ if err != nil {
+ return "", err
+ }
+
+ installationDir, err := i.installationDir()
+ if err != nil {
+ return "", err
+ }
+ debugInfo := struct {
+ InstallationDir, Origin string
+ Envelope *application.Envelope
+ Config device.Config
+ }{}
+ debugInfo.InstallationDir = installationDir
+
+ if origin, err := loadOrigin(installationDir); err != nil {
+ return "", err
+ } else {
+ debugInfo.Origin = origin
+ }
+
+ currLink := filepath.Join(installationDir, "current")
+ if envelope, err := loadEnvelope(currLink); err != nil {
+ return "", err
+ } else {
+ debugInfo.Envelope = envelope
+ }
+
+ if config, err := loadConfig(installationDir); err != nil {
+ return "", err
+ } else {
+ debugInfo.Config = config
+ }
+
+ var buf bytes.Buffer
+ if err := installationDebugTemplate.Execute(&buf, debugInfo); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+
+}
+
+func (i *appService) instanceDebug(ctx ipc.ServerContext) (string, error) {
+ const instanceDebug = `Instance dir: {{.InstanceDir}}
+
+System name / start system name: {{.SystemName}} / {{.StartSystemName}}
+
+Cmd: {{printf "%+v" .Cmd}}
+
+Info: {{printf "%+v" .Info}}
+
+Principal: {{.PrincipalType}}
+Public Key: {{.Principal.PublicKey}}
+Blessing Store: {{.Principal.BlessingStore.DebugString}}
+Roots: {{.Principal.Roots.DebugString}}
+`
+ instanceDebugTemplate, err := template.New("instance-debug").Parse(instanceDebug)
+ if err != nil {
+ return "", err
+ }
+
+ instanceDir, err := i.instanceDir()
+ if err != nil {
+ return "", err
+ }
+ debugInfo := struct {
+ InstanceDir, SystemName, StartSystemName string
+ Cmd *exec.Cmd
+ Info *instanceInfo
+ Principal security.Principal
+ PrincipalType string
+ }{}
+ debugInfo.InstanceDir = instanceDir
+
+ debugInfo.SystemName = suidHelper.usernameForPrincipal(ctx, i.uat)
+ if startSystemName, err := readSystemNameForInstance(instanceDir); err != nil {
+ return "", err
+ } else {
+ debugInfo.StartSystemName = startSystemName
+ }
+ if cmd, err := genCmd(instanceDir, i.config.Helper, debugInfo.SystemName, veyron2.GetNamespace(ctx.Context()).Roots()); err != nil {
+ return "", err
+ } else {
+ debugInfo.Cmd = cmd
+ }
+ if info, err := loadInstanceInfo(instanceDir); err != nil {
+ return "", err
+ } else {
+ debugInfo.Info = info
+ }
+
+ if sa := i.securityAgent; sa != nil {
+ file, err := sa.keyMgrAgent.NewConnection(debugInfo.Info.SecurityAgentHandle)
+ if err != nil {
+ vlog.Errorf("NewConnection(%v) failed: %v", debugInfo.Info.SecurityAgentHandle, err)
+ return "", err
+ }
+ var cancel func()
+ if debugInfo.Principal, cancel, err = agentPrincipal(ctx.Context(), file); err != nil {
+ return "", err
+ }
+ defer cancel()
+ debugInfo.PrincipalType = "Agent-based"
+ } else {
+ credentialsDir := filepath.Join(instanceDir, "credentials")
+ var err error
+ if debugInfo.Principal, err = vsecurity.LoadPersistentPrincipal(credentialsDir, nil); err != nil {
+ return "", err
+ }
+ debugInfo.PrincipalType = fmt.Sprintf("Credentials dir-based (%v)", credentialsDir)
+ }
+ var buf bytes.Buffer
+ if err := instanceDebugTemplate.Execute(&buf, debugInfo); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+}
diff --git a/services/mgmt/device/impl/device_service.go b/services/mgmt/device/impl/device_service.go
index d53a10d..ce54b96 100644
--- a/services/mgmt/device/impl/device_service.go
+++ b/services/mgmt/device/impl/device_service.go
@@ -535,3 +535,7 @@
}
return s.uat.AllBlessingSystemAssociations()
}
+
+func (*deviceService) Debug(ipc.ServerContext) (string, error) {
+ return "Not implemented", nil
+}
diff --git a/services/mgmt/device/impl/impl_test.go b/services/mgmt/device/impl/impl_test.go
index 4a6fee9..7dd1c06 100644
--- a/services/mgmt/device/impl/impl_test.go
+++ b/services/mgmt/device/impl/impl_test.go
@@ -593,6 +593,19 @@
// Install the app. The config-specified flag value for testFlagName
// should override the value specified in the envelope above.
appID := installApp(t, ctx, device.Config{testFlagName: "flag-val-install"})
+ installationDebug := debug(t, ctx, appID)
+ // We spot-check a couple pieces of information we expect in the debug
+ // output.
+ // TODO(caprita): Is there a way to verify more without adding brittle
+ // logic that assumes too much about the format? This may be one
+ // argument in favor of making the output of Debug a struct instead of
+ // free-form string.
+ if !strings.Contains(installationDebug, "Origin: ar") {
+ t.Fatalf("debug response doesn't contain expected info: %v", installationDebug)
+ }
+ if !strings.Contains(installationDebug, "Config: map[random_test_flag:flag-val-install]") {
+ t.Fatalf("debug response doesn't contain expected info: %v", installationDebug)
+ }
// Start requires the caller to grant a blessing for the app instance.
if _, err := startAppImpl(t, ctx, appID, ""); err == nil || !verror.Is(err, impl.ErrInvalidBlessing.ID) {
@@ -602,6 +615,11 @@
// Start an instance of the app.
instance1ID := startApp(t, ctx, appID)
+ instanceDebug := debug(t, ctx, appID, instance1ID)
+ if !strings.Contains(instanceDebug, "Blessing Store: Default blessings: test-principal/forapp/google naps") {
+ t.Fatalf("debug response doesn't contain expected info: %v", instanceDebug)
+ }
+
// Wait until the app pings us that it's ready.
verifyPingArgs(t, pingCh, userName(t), "flag-val-install", "env-val-envelope")
diff --git a/services/mgmt/device/impl/util_test.go b/services/mgmt/device/impl/util_test.go
index 0ac22f6..732ca47 100644
--- a/services/mgmt/device/impl/util_test.go
+++ b/services/mgmt/device/impl/util_test.go
@@ -241,6 +241,14 @@
}
}
+func debug(t *testing.T, ctx *context.T, nameComponents ...string) string {
+ dbg, err := appStub(nameComponents...).Debug(ctx)
+ if err != nil {
+ t.Fatalf(testutil.FormatLogLine(2, "Debug(%v) failed: %v", nameComponents, err))
+ }
+ return dbg
+}
+
// Code to make Association lists sortable.
type byIdentity []device.Association
diff --git a/tools/mgmt/device/impl/devicemanager_mock_test.go b/tools/mgmt/device/impl/devicemanager_mock_test.go
index 3777043..62cf94a 100644
--- a/tools/mgmt/device/impl/devicemanager_mock_test.go
+++ b/tools/mgmt/device/impl/devicemanager_mock_test.go
@@ -68,9 +68,11 @@
func (*mockDeviceInvoker) Describe(ipc.ServerContext) (device.Description, error) {
return device.Description{}, nil
}
+
func (*mockDeviceInvoker) IsRunnable(_ ipc.ServerContext, description binary.Description) (bool, error) {
return false, nil
}
+
func (*mockDeviceInvoker) Reset(call ipc.ServerContext, deadline uint64) error { return nil }
// Mock Install
@@ -92,11 +94,13 @@
}
func (*mockDeviceInvoker) Refresh(ipc.ServerContext) error { return nil }
+
func (*mockDeviceInvoker) Restart(ipc.ServerContext) error { return nil }
func (mni *mockDeviceInvoker) Resume(_ ipc.ServerContext) error {
return mni.simpleCore("Resume", "Resume")
}
+
func (i *mockDeviceInvoker) Revert(call ipc.ServerContext) error { return nil }
type StartResponse struct {
@@ -122,8 +126,11 @@
func (mni *mockDeviceInvoker) Suspend(_ ipc.ServerContext) error {
return mni.simpleCore("Suspend", "Suspend")
}
-func (*mockDeviceInvoker) Uninstall(ipc.ServerContext) error { return nil }
-func (i *mockDeviceInvoker) Update(ipc.ServerContext) error { return nil }
+
+func (*mockDeviceInvoker) Uninstall(ipc.ServerContext) error { return nil }
+
+func (i *mockDeviceInvoker) Update(ipc.ServerContext) error { return nil }
+
func (*mockDeviceInvoker) UpdateTo(ipc.ServerContext, string) error { return nil }
// Mock ACL getting and setting
@@ -149,6 +156,12 @@
return r.acl, r.etag, r.err
}
+func (mni *mockDeviceInvoker) Debug(ipc.ServerContext) (string, error) {
+ ir := mni.tape.Record("Debug")
+ r := ir.(string)
+ return r, nil
+}
+
type dispatcher struct {
tape *Tape
t *testing.T
diff --git a/tools/mgmt/device/impl/impl.go b/tools/mgmt/device/impl/impl.go
index 3a60782..6f469cd 100644
--- a/tools/mgmt/device/impl/impl.go
+++ b/tools/mgmt/device/impl/impl.go
@@ -94,7 +94,7 @@
Long: "Claim the device.",
ArgsName: "<device> <grant extension>",
ArgsLong: `
-<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.`,
@@ -120,7 +120,7 @@
Long: "Describe the device.",
ArgsName: "<device>",
ArgsLong: `
-<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.`,
}
func runDescribe(cmd *cmdline.Command, args []string) error {
@@ -135,3 +135,26 @@
}
return nil
}
+
+var cmdDebug = &cmdline.Command{
+ Run: runDebug,
+ Name: "debug",
+ Short: "Debug the device.",
+ Long: "Debug the device.",
+ ArgsName: "<device>",
+ ArgsLong: `
+<device> is the veyron object name of an app installation or instance.`,
+}
+
+func runDebug(cmd *cmdline.Command, args []string) error {
+ if expected, got := 1, len(args); expected != got {
+ return cmd.UsageErrorf("debug: incorrect number of arguments, expected %d, got %d", expected, got)
+ }
+ deviceName := args[0]
+ if description, err := device.DeviceClient(deviceName).Debug(gctx); err != nil {
+ return fmt.Errorf("Debug failed: %v", err)
+ } else {
+ fmt.Fprintf(cmd.Stdout(), "%v\n", description)
+ }
+ return nil
+}
diff --git a/tools/mgmt/device/impl/impl_test.go b/tools/mgmt/device/impl/impl_test.go
index dc53e15..b841ee6 100644
--- a/tools/mgmt/device/impl/impl_test.go
+++ b/tools/mgmt/device/impl/impl_test.go
@@ -418,3 +418,31 @@
stdout.Reset()
stderr.Reset()
}
+
+func TestDebugCommand(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)
+ appName := naming.JoinAddressName(endpoint.String(), "")
+
+ debugMessage := "the secrets of the universe, revealed"
+ tape.SetResponses([]interface{}{debugMessage})
+ if err := cmd.Execute([]string{"debug", appName}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if expected, got := debugMessage, strings.TrimSpace(stdout.String()); got != expected {
+ t.Fatalf("Unexpected output from debug. Got %q, expected %q", got, expected)
+ }
+ if got, expected := tape.Play(), []interface{}{"Debug"}; !reflect.DeepEqual(expected, got) {
+ t.Errorf("invalid call sequence. Got %v, want %v", got, expected)
+ }
+}
diff --git a/tools/mgmt/device/impl/root.go b/tools/mgmt/device/impl/root.go
index 9228247..5902799 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, aclRoot()},
+ Children: []*cmdline.Command{cmdInstall, cmdStart, associateRoot(), cmdDescribe, cmdClaim, cmdStop, cmdSuspend, cmdResume, cmdDebug, aclRoot()},
}
}