Merge "device manager test"
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)
+}