device manager test

The test publishes and runs syncbased using the device manager
and verifies that the running instance of syncbased has
appropriate blessings.

The test is mostly copied from the original device manager test:
v.io/x/ref/services/device/mgmt_v23_test.go

Change-Id: Iece44d85c0b9203adca3e2065ee1bb0b4207cef7
diff --git a/v23/syncbase/mgmt_v23_test.go b/v23/syncbase/mgmt_v23_test.go
new file mode 100644
index 0000000..0384c7e
--- /dev/null
+++ b/v23/syncbase/mgmt_v23_test.go
@@ -0,0 +1,305 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package syncbase_test
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"time"
+
+	_ "v.io/x/ref/runtime/factories/generic"
+	"v.io/x/ref/test/v23tests"
+)
+
+//go:generate v23 test generate
+
+var (
+	hostname   string
+	errTimeout = errors.New("timeout")
+)
+
+func init() {
+	name, err := os.Hostname()
+	if err != nil {
+		panic(fmt.Sprintf("Hostname() failed: %v", err))
+	}
+	hostname = name
+}
+
+// V23TestDeviceManager publishes and runs syncbased using the device manager
+// and verifies that the running instance of syncbased has appropriate
+// blessings.
+func V23TestDeviceManager(i *v23tests.T) {
+	defer fmt.Fprintf(os.Stderr, "--------------- SHUTDOWN ---------------\n")
+	var (
+		workDir       = i.NewTempDir("")
+		binStagingDir = mkSubdir(i, workDir, "bin")
+		dmInstallDir  = filepath.Join(workDir, "dm")
+
+		// Most vanadium command-line utilities will be run by a
+		// principal that has "root/u/alice" as its blessing.
+		// (Where "root" comes from i.Principal().BlessingStore().Default()).
+		// Create those credentials and options to use to setup the
+		// binaries with them.
+		aliceCreds, _ = i.Shell().NewChildCredentials("u/alice")
+		aliceOpts     = i.Shell().DefaultStartOpts().ExternalProgram().WithCustomCredentials(aliceCreds)
+
+		// Build all the command-line tools and set them up to run as alice.
+		// applicationd/binaryd servers will be run by alice too.
+		// TODO: applicationd/binaryd should run as a separate "service" role, as
+		// alice is just a user.
+		namespaceBin    = i.BuildV23Pkg("v.io/x/ref/cmd/namespace").WithStartOpts(aliceOpts)
+		deviceBin       = i.BuildV23Pkg("v.io/x/ref/services/device/device").WithStartOpts(aliceOpts)
+		binarydBin      = i.BuildV23Pkg("v.io/x/ref/services/binary/binaryd").WithStartOpts(aliceOpts)
+		applicationdBin = i.BuildV23Pkg("v.io/x/ref/services/application/applicationd").WithStartOpts(aliceOpts)
+		syncbasedBin    = i.BuildV23Pkg("v.io/syncbase/x/ref/services/syncbase/syncbased")
+
+		// The devicex script is not provided with any credentials, it
+		// will generate its own.  This means that on "devicex start"
+		// the device will have no useful credentials and until "device
+		// claim" is invoked (as alice), it will just sit around
+		// waiting to be claimed.
+		//
+		// Other binaries, like applicationd and binaryd will be run by alice.
+		deviceScriptPath = filepath.Join(os.Getenv("V23_ROOT"), "release", "go", "src", "v.io", "x", "ref", "services", "device", "devicex")
+		deviceScript     = i.BinaryFromPath(deviceScriptPath).WithEnv("V23_DEVICE_DIR=" + dmInstallDir)
+
+		mtName = "devices/" + hostname // Name under which the device manager will publish itself.
+	)
+
+	// We also need some tools running with different sets of credentials...
+
+	// Administration tasks will be performed with a blessing that represents a corporate
+	// administrator (which is usually a role account)
+	adminCreds, err := i.Shell().NewChildCredentials("r/admin")
+	if err != nil {
+		i.Fatalf("generating admin creds: %v", err)
+	}
+	adminOpts := i.Shell().DefaultStartOpts().ExternalProgram().WithCustomCredentials(adminCreds)
+	adminDeviceBin := deviceBin.WithStartOpts(adminOpts)
+	debugBin := i.BuildV23Pkg("v.io/x/ref/services/debug/debug").WithStartOpts(adminOpts)
+
+	// A special set of credentials will be used to give two blessings to the device manager
+	// when claiming it -- one blessing will be from the corporate administrator role who owns
+	// the machine, and the other will be a manufacturer blessing. (This is a hack until
+	// there's a way to separately supply a manufacturer blessing. Eventually, the claim
+	// would really be done by the administrator, and the administrator's blessing would get
+	// added to the manufacturer's blessing, which would already be present.)
+	claimCreds, err := i.Shell().AddToChildCredentials(adminCreds, "m/orange/zphone5/ime-i007")
+	if err != nil {
+		i.Fatalf("adding the mfr blessing to admin creds: %v", err)
+	}
+	claimOpts := i.Shell().DefaultStartOpts().ExternalProgram().WithCustomCredentials(claimCreds)
+	claimDeviceBin := deviceBin.WithStartOpts(claimOpts)
+
+	// Another set of credentials be used to represent the application publisher, who
+	// signs and pushes binaries
+	pubCreds, err := i.Shell().NewChildCredentials("a/rovio")
+	if err != nil {
+		i.Fatalf("generating publisher creds: %v", err)
+	}
+	pubOpts := i.Shell().DefaultStartOpts().ExternalProgram().WithCustomCredentials(pubCreds)
+	pubDeviceBin := deviceBin.WithStartOpts(pubOpts)
+	applicationBin := i.BuildV23Pkg("v.io/x/ref/services/application/application").WithStartOpts(pubOpts)
+
+	v23tests.RunRootMT(i, "--v23.tcp.address=127.0.0.1:0")
+	buildAndCopyBinaries(
+		i,
+		binStagingDir,
+		"v.io/x/ref/services/device/deviced",
+		"v.io/x/ref/services/agent/agentd",
+		"v.io/x/ref/services/device/suidhelper",
+		"v.io/x/ref/services/device/inithelper")
+
+	appDName := "applications"
+	devicedAppName := filepath.Join(appDName, "deviced", "test")
+
+	deviceScript.Start(
+		"install",
+		binStagingDir,
+		"--single_user",
+		"--origin="+devicedAppName,
+		"--",
+		"--v23.tcp.address=127.0.0.1:0",
+		"--neighborhood-name="+fmt.Sprintf("%s-%d-%d", hostname, os.Getpid(), rand.Int()),
+	).WaitOrDie(os.Stdout, os.Stderr)
+	deviceScript.Start("start").WaitOrDie(os.Stdout, os.Stderr)
+	// Grab the endpoint for the claimable service from the device manager's
+	// log.
+	dmLog := filepath.Join(dmInstallDir, "dmroot/device-manager/logs/deviced.INFO")
+	var claimableEP string
+	expiry := time.Now().Add(30 * time.Second)
+	for {
+		if time.Now().After(expiry) {
+			i.Fatalf("Timed out looking for claimable endpoint in %v", dmLog)
+		}
+		startLog, err := ioutil.ReadFile(dmLog)
+		if err != nil {
+			i.Logf("Couldn't read log %v: %v", dmLog, err)
+			time.Sleep(time.Second)
+			continue
+		}
+		re := regexp.MustCompile(`Unclaimed device manager \((.*)\)`)
+		matches := re.FindSubmatch(startLog)
+		if len(matches) == 0 {
+			i.Logf("Couldn't find match in %v [%v]", dmLog, startLog)
+			time.Sleep(time.Second)
+			continue
+		}
+		if len(matches) != 2 {
+			i.Fatalf("Wrong match in %v (%d) %v", dmLog, len(matches), string(matches[0]))
+		}
+		claimableEP = string(matches[1])
+		break
+	}
+	// Claim the device as "root/u/alice/myworkstation".
+	claimDeviceBin.Start("claim", claimableEP, "myworkstation")
+
+	resolve := func(name string) string {
+		resolver := func() (interface{}, error) {
+			// Use Start, rather than Run, since it's ok for 'namespace resolve'
+			// to fail with 'name doesn't exist'
+			inv := namespaceBin.Start("resolve", name)
+			// Cleanup after ourselves to avoid leaving a ton of invocations
+			// lying around which obscure logging output.
+			defer inv.Wait(nil, os.Stderr)
+			if r := strings.TrimRight(inv.Output(), "\n"); len(r) > 0 {
+				return r, nil
+			}
+			return nil, nil
+		}
+		return i.WaitFor(resolver, 100*time.Millisecond, time.Minute).(string)
+	}
+
+	// Wait for the device manager to publish its mount table entry.
+	resolve(mtName)
+	adminDeviceBin.Run("acl", "set", mtName+"/devmgr/device", "root/u/alice", "Read,Resolve,Write")
+
+	// Verify the device's default blessing is as expected.
+	mfrBlessing := "root/m/orange/zphone5/ime-i007/myworkstation"
+	ownerBlessing := "root/r/admin/myworkstation"
+	inv := debugBin.Start("stats", "read", mtName+"/devmgr/__debug/stats/security/principal/*/blessingstore")
+	inv.ExpectSetEventuallyRE(".*Default Blessings[ ]+" + mfrBlessing + "," + ownerBlessing)
+
+	// Get the device's profile, which should be set to non-empty string
+	inv = adminDeviceBin.Start("describe", mtName+"/devmgr/device")
+
+	parts := inv.ExpectRE(`{Profiles:map\[(.*):{}\]}`, 1)
+	expectOneMatch := func(parts [][]string) string {
+		if len(parts) != 1 || len(parts[0]) != 2 {
+			loc := v23tests.Caller(1)
+			i.Fatalf("%s: failed to match profile: %#v", loc, parts)
+		}
+		return parts[0][1]
+	}
+	deviceProfile := expectOneMatch(parts)
+	if len(deviceProfile) == 0 {
+		i.Fatalf("failed to get profile")
+	}
+
+	// Start a binaryd server that will serve the binary for the test
+	// application to be installed on the device.
+	binarydName := "binaries"
+	binarydBin.Start(
+		"--name=binaries",
+		"--root-dir="+filepath.Join(workDir, "binstore"),
+		"--v23.tcp.address=127.0.0.1:0",
+		"--http=127.0.0.1:0")
+	// Allow publishers to update binaries
+	deviceBin.Run("acl", "set", binarydName, "root/a", "Write")
+
+	// Start an applicationd server that will serve the application
+	// envelope for the test application to be installed on the device.
+	applicationdBin.Start(
+		"--name="+appDName,
+		"--store="+mkSubdir(i, workDir, "appstore"),
+		"--v23.tcp.address=127.0.0.1:0",
+	)
+	// Allow publishers to create and update envelopes
+	deviceBin.Run("acl", "set", appDName, "root/a", "Read,Write,Resolve")
+
+	syncbasedName := appDName + "/syncbased/0"
+	syncbasedBinName := binarydName + "/syncbased"
+	syncbasedEnvelopeFilename := filepath.Join(workDir, "syncbased.envelope")
+	syncbasedEnvelope := fmt.Sprintf("{\"Title\":\"syncbased\","+
+		"\"Args\":[\"-v=0\", \"--name=syncbased\", \"--root-dir=%s\", \"--v23.tcp.address=127.0.0.1:0\"]}",
+		mkSubdir(i, workDir, "syncbased"))
+	ioutil.WriteFile(syncbasedEnvelopeFilename, []byte(syncbasedEnvelope), 0666)
+	defer os.Remove(syncbasedEnvelopeFilename)
+
+	output := applicationBin.Run("put", syncbasedName, deviceProfile, syncbasedEnvelopeFilename)
+	if got, want := output, "Application envelope added successfully."; got != want {
+		i.Fatalf("got %q, want %q", got, want)
+	}
+
+	// Publish the app.
+	pubDeviceBin.Start("publish", "-from", filepath.Dir(syncbasedBin.Path()), "-readers", "root/r/admin", "syncbased").WaitOrDie(os.Stdout, os.Stderr)
+	if got := namespaceBin.Run("glob", syncbasedBinName); len(got) == 0 {
+		i.Fatalf("glob failed for %q", syncbasedBinName)
+	}
+
+	// Install the app on the device.
+	inv = deviceBin.Start("install", mtName+"/devmgr/apps", syncbasedName)
+	installationName := inv.ReadLine()
+	if installationName == "" {
+		i.Fatalf("got empty installation name from install")
+	}
+
+	// Verify that the installation shows up when globbing the device manager.
+	output = namespaceBin.Run("glob", mtName+"/devmgr/apps/syncbased/*")
+	if got, want := output, installationName; got != want {
+		i.Fatalf("got %q, want %q", got, want)
+	}
+
+	// Start an instance of the app, granting it blessing extension syncbased.
+	inv = deviceBin.Start("instantiate", installationName, "syncbased")
+	instanceName := inv.ReadLine()
+	if instanceName == "" {
+		i.Fatalf("got empty instance name from new")
+	}
+	deviceBin.Start("run", instanceName).Wait(os.Stdout, os.Stderr)
+
+	resolve(mtName + "/syncbased")
+
+	// Verify that the instance shows up when globbing the device manager.
+	output = namespaceBin.Run("glob", mtName+"/devmgr/apps/syncbased/*/*")
+	if got, want := output, instanceName; got != want {
+		i.Fatalf("got %q, want %q", got, want)
+	}
+
+	// Verify the app's blessings. We check the default blessing, as well as the
+	// "..." blessing, which should be the default blessing plus a publisher blessing.
+	userBlessing := "root/u/alice/syncbased"
+	pubBlessing := "root/a/rovio/apps/published/syncbased"
+	// Just to remind:
+	// mfrBlessing   = "root/m/orange/zphone5/ime-i007/myworkstation"
+	// ownerBlessing = "root/r/admin/myworkstation"
+	appBlessing := mfrBlessing + "/a/" + pubBlessing + "," + ownerBlessing + "/a/" + pubBlessing
+	inv = debugBin.Start("stats", "read", instanceName+"/stats/security/principal/*/blessingstore")
+	inv.ExpectSetEventuallyRE(".*Default Blessings[ ]+"+userBlessing+"$", "[.][.][.][ ]+"+userBlessing+","+appBlessing)
+}
+
+func buildAndCopyBinaries(i *v23tests.T, destinationDir string, packages ...string) {
+	var args []string
+	for _, pkg := range packages {
+		args = append(args, i.BuildGoPkg(pkg).Path())
+	}
+	args = append(args, destinationDir)
+	i.BinaryFromPath("/bin/cp").Start(args...).WaitOrDie(os.Stdout, os.Stderr)
+}
+
+func mkSubdir(i *v23tests.T, parent, child string) string {
+	dir := filepath.Join(parent, child)
+	if err := os.Mkdir(dir, 0755); err != nil {
+		i.Fatalf("failed to create %q: %v", dir, err)
+	}
+	return dir
+}
diff --git a/v23/syncbase/v23_test.go b/v23/syncbase/v23_test.go
index 914c0a2..c075d3f 100644
--- a/v23/syncbase/v23_test.go
+++ b/v23/syncbase/v23_test.go
@@ -32,3 +32,7 @@
 func TestV23ServiceRestart(t *testing.T) {
 	v23tests.RunTest(t, V23TestServiceRestart)
 }
+
+func TestV23DeviceManager(t *testing.T) {
+	v23tests.RunTest(t, V23TestDeviceManager)
+}