services/device/internal/starter: don't try publishing before claim

Most likely, the publish will fail since we don't have credentials, with the
effect that we hang for 30 seconds stuck in publish retry limbo.

This change rips out the local device mounttable pre-claim, and disables
publishing of the claimable service.  Instead, the endpoint of the claimable
service will be returned from starter.Start.

The device unit test is updated to reflect these changes -- in particular, we
manually mount the claimable service's endpoint into the global MT for discovery
by the test fixtures.  This ends up also simplifying the post-claim 'wait for
remount' logic.

The integration test is also updated -- we now grab the claimable endpoint from
the device manager's log.

Change-Id: I2426090be009e1f9c4b875aaf152c19bde8fa96a
diff --git a/services/device/deviced/server.go b/services/device/deviced/server.go
index 26e38d1..61e8773 100644
--- a/services/device/deviced/server.go
+++ b/services/device/deviced/server.go
@@ -124,7 +124,7 @@
 	// method that calls Stop on the app cycle manager (e.g. the Stop RPC)
 	// will precipitate an immediate process exit.
 	shutdownChan := signals.ShutdownOnSignals(ctx)
-	stop, err := starter.Start(ctx, starter.Args{Namespace: ns, Device: dev, MountGlobalNamespaceInLocalNamespace: true, Proxy: proxy})
+	_, stop, err := starter.Start(ctx, starter.Args{Namespace: ns, Device: dev, MountGlobalNamespaceInLocalNamespace: true, Proxy: proxy})
 	if err != nil {
 		return err
 	}
diff --git a/services/device/internal/impl/debug_perms_test.go b/services/device/internal/impl/debug_perms_test.go
index 5179c07..f8a14f9 100644
--- a/services/device/internal/impl/debug_perms_test.go
+++ b/services/device/internal/impl/debug_perms_test.go
@@ -45,7 +45,7 @@
 	// Set up the device manager.
 	dmh := servicetest.RunCommand(t, sh, nil, deviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
 	pingCh, cleanup := setupPingServer(t, ctx)
@@ -209,7 +209,7 @@
 	hjCtx := ctxWithNewPrincipal(t, selfCtx, idp, "hackerjoe")
 
 	// Bob claims the device manager.
-	claimDevice(t, bobCtx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, bobCtx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create some globbing test vectors.
 	dmGlobtests := []globTestVector{
diff --git a/services/device/internal/impl/impl_test.go b/services/device/internal/impl/impl_test.go
index 4b2cdf4..1109c71 100644
--- a/services/device/internal/impl/impl_test.go
+++ b/services/device/internal/impl/impl_test.go
@@ -174,7 +174,7 @@
 	// method that calls Stop on the app cycle manager (e.g. the Stop RPC)
 	// will precipitate an immediate process exit.
 	shutdownChan := signals.ShutdownOnSignals(ctx)
-	stop, err := starter.Start(ctx, starter.Args{
+	claimableName, stop, err := starter.Start(ctx, starter.Args{
 		Namespace: starter.NamespaceArgs{
 			ListenSpec: rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}},
 		},
@@ -196,6 +196,15 @@
 		return err
 	}
 	defer stop()
+	// Update the namespace roots to remove the server blessing from the
+	// endpoints.  This is needed to be able to publish into the 'global'
+	// mounttable before we have compatible credentials.
+	ctx, err = setNamespaceRootsForUnclaimedDevice(ctx)
+	if err != nil {
+		return err
+	}
+	// Manually mount the claimable service in the 'global' mounttable.
+	v23.GetNamespace(ctx).Mount(ctx, "claimable", claimableName, 0)
 	fmt.Fprintf(stdout, "ready:%d\n", os.Getpid())
 
 	<-shutdownChan
@@ -414,9 +423,9 @@
 	}()
 
 	servicetest.ReadPID(t, dmh)
+	resolve(t, ctx, "claimable", 1)
 	// Brand new device manager must be claimed first.
-	claimDevice(t, ctx, "factoryDM", "mydevice", noPairingToken)
-
+	claimDevice(t, ctx, "claimable", "factoryDM", "mydevice", noPairingToken)
 	// Simulate an invalid envelope in the application repository.
 	*envelope = envelopeFromShell(sh, dmPauseBeforeStopEnv, deviceManagerCmd, "bogus", dmArgs...)
 
@@ -651,7 +660,7 @@
 	// don't worry about its application envelope and current link.
 	dmh := servicetest.RunCommand(t, sh, nil, deviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
 	pingCh, cleanup := setupPingServer(t, ctx)
@@ -980,9 +989,9 @@
 	//installAppExpectError(t, octx, impl.ErrUnclaimedDevice.ID)
 
 	// Claim the device with an incorrect pairing token should fail.
-	claimDeviceExpectError(t, claimantCtx, "dm", "mydevice", "badtoken", impl.ErrInvalidPairingToken.ID)
+	claimDeviceExpectError(t, claimantCtx, "claimable", "mydevice", "badtoken", impl.ErrInvalidPairingToken.ID)
 	// But succeed with a valid pairing token
-	claimDevice(t, claimantCtx, "dm", "mydevice", pairingToken)
+	claimDevice(t, claimantCtx, "claimable", "dm", "mydevice", pairingToken)
 
 	// Installation should succeed since claimantRT is now the "owner" of
 	// the devicemanager.
@@ -1054,13 +1063,12 @@
 	*envelope = envelopeFromShell(sh, nil, appCmd, "google naps")
 
 	// On an unclaimed device manager, there will be no AccessLists.
-	deviceStub := device.DeviceClient("dm/device")
-	if _, _, err := deviceStub.GetPermissions(selfCtx); err == nil {
+	if _, _, err := device.DeviceClient("claimable").GetPermissions(selfCtx); err == nil {
 		t.Fatalf("GetPermissions should have failed but didn't.")
 	}
 
 	// Claim the devicemanager as "root/self/mydevice"
-	claimDevice(t, selfCtx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, selfCtx, "claimable", "dm", "mydevice", noPairingToken)
 	expectedAccessList := make(access.Permissions)
 	for _, tag := range access.AllTypicalTags() {
 		expectedAccessList[string(tag)] = access.AccessList{In: []security.BlessingPattern{"root/$", "root/self/$", "root/self/mydevice/$"}}
@@ -1073,6 +1081,7 @@
 	// manager version.
 	md5hash := md5.Sum(b.Bytes())
 	expectedVersion := hex.EncodeToString(md5hash[:])
+	deviceStub := device.DeviceClient("dm/device")
 	perms, version, err := deviceStub.GetPermissions(selfCtx)
 	if err != nil {
 		t.Fatal(err)
@@ -1151,7 +1160,7 @@
 	}
 	dms := expect.NewSession(t, stdout, servicetest.ExpectTimeout)
 	servicetest.ReadPID(t, dms)
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 	revertDeviceExpectError(t, ctx, "dm", impl.ErrUpdateNoOp.ID) // No previous version available.
 
 	// Stop the device manager.
@@ -1208,7 +1217,7 @@
 	*envelope = envelopeFromShell(sh, nil, appCmd, "google naps", "appV1")
 
 	// Device must be claimed before applications can be installed.
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 	// Install the app.
 	appID := installApp(t, ctx)
 	install1ID := path.Base(appID)
@@ -1363,7 +1372,7 @@
 		},
 	}
 	// Device must be claimed before apps can be installed.
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 	// Install the app.
 	appID := installApp(t, ctx, packages)
 
@@ -1452,17 +1461,17 @@
 	defer syscall.Kill(pid, syscall.SIGINT)
 	defer verifyNoRunningProcesses(t)
 
-	deviceStub := device.DeviceClient("dm/device")
 	// Attempt to list associations on the device manager without having
 	// claimed it.
-	if list, err := deviceStub.ListAssociations(otherCtx); err == nil {
+	if list, err := device.DeviceClient("claimable").ListAssociations(otherCtx); err == nil {
 		t.Fatalf("ListAssociations should fail on unclaimed device manager but did not: (%v, %v)", list, err)
 	}
 
 	// self claims the device manager.
-	claimDevice(t, selfCtx, "dm", "alice", noPairingToken)
+	claimDevice(t, selfCtx, "claimable", "dm", "alice", noPairingToken)
 
 	vlog.VI(2).Info("Verify that associations start out empty.")
+	deviceStub := device.DeviceClient("dm/device")
 	listAndVerifyAssociations(t, selfCtx, deviceStub, []device.Association(nil))
 
 	if err := deviceStub.AssociateAccount(selfCtx, []string{"root/self", "root/other"}, "alice_system_account"); err != nil {
@@ -1552,7 +1561,7 @@
 	defer syscall.Kill(pid, syscall.SIGINT)
 	defer verifyNoRunningProcesses(t)
 	// Claim the devicemanager with selfCtx as root/self/alice
-	claimDevice(t, selfCtx, "dm", "alice", noPairingToken)
+	claimDevice(t, selfCtx, "claimable", "dm", "alice", noPairingToken)
 
 	deviceStub := device.DeviceClient("dm/device")
 
@@ -1733,7 +1742,7 @@
 	dmh := servicetest.RunCommand(t, sh, nil, deviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	publisher, err := v23.GetPrincipal(ctx).BlessSelf("publisher")
 	if err != nil {
diff --git a/services/device/internal/impl/instance_reaping_test.go b/services/device/internal/impl/instance_reaping_test.go
index b8b70dd..2ff35fa 100644
--- a/services/device/internal/impl/instance_reaping_test.go
+++ b/services/device/internal/impl/instance_reaping_test.go
@@ -30,7 +30,7 @@
 	// don't worry about its application envelope and current link.
 	dmh := servicetest.RunCommand(t, sh, nil, deviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
 	pingCh, cleanup := setupPingServer(t, ctx)
@@ -110,7 +110,7 @@
 
 	dmh := servicetest.RunCommand(t, sh, dmEnv, deviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
-	claimDevice(t, ctx, "dm", "mydevice", noPairingToken)
+	claimDevice(t, ctx, "claimable", "dm", "mydevice", noPairingToken)
 
 	// Create the local server that the app uses to let us know it's ready.
 	pingCh, cleanup := setupPingServer(t, ctx)
diff --git a/services/device/internal/impl/util_test.go b/services/device/internal/impl/util_test.go
index 909a58a..c043873 100644
--- a/services/device/internal/impl/util_test.go
+++ b/services/device/internal/impl/util_test.go
@@ -94,25 +94,24 @@
 	return device.DeviceClient(deviceName)
 }
 
-func claimDevice(t *testing.T, ctx *context.T, name, extension, pairingToken string) {
+func claimDevice(t *testing.T, ctx *context.T, claimableName, deviceName, extension, pairingToken string) {
 	// Setup blessings to be granted to the claimed device
 	g := &granter{extension: extension}
 	s := options.SkipServerEndpointAuthorization{}
 	// Call the Claim RPC: Skip server authorization because the unclaimed
 	// device presents nothing that can be used to recognize it.
-	if err := device.ClaimableClient(name).Claim(ctx, pairingToken, g, s); err != nil {
-		t.Fatalf(testutil.FormatLogLine(2, "%q.Claim(%q) failed: %v [%v]", name, pairingToken, verror.ErrorID(err), err))
+	if err := device.ClaimableClient(claimableName).Claim(ctx, pairingToken, g, s); err != nil {
+		t.Fatalf(testutil.FormatLogLine(2, "%q.Claim(%q) failed: %v [%v]", claimableName, pairingToken, verror.ErrorID(err), err))
 	}
 	// Wait for the device to remount itself with the device service after
 	// being claimed.
-	// (Detected by the next claim failing with an error other than
-	// AlreadyClaimed)
 	start := time.Now()
 	for {
-		if err := device.ClaimableClient(name).Claim(ctx, pairingToken, g, s); verror.ErrorID(err) != impl.ErrDeviceAlreadyClaimed.ID {
+		_, err := v23.GetNamespace(ctx).Resolve(ctx, deviceName)
+		if err == nil {
 			return
 		}
-		vlog.VI(4).Infof("Claimable server at %q has not stopped yet", name)
+		vlog.VI(4).Infof("Resolve(%q) failed: %v", err)
 		time.Sleep(time.Millisecond)
 		if elapsed := time.Since(start); elapsed > time.Minute {
 			t.Fatalf("Device hasn't remounted itself in %v since it was claimed", elapsed)
@@ -630,3 +629,24 @@
 		t.Errorf("device manager incorrectly terminating with child processes still running")
 	}
 }
+
+func setNamespaceRootsForUnclaimedDevice(ctx *context.T) (*context.T, error) {
+	origroots := v23.GetNamespace(ctx).Roots()
+	roots := make([]string, len(origroots))
+	for i, orig := range origroots {
+		addr, suffix := naming.SplitAddressName(orig)
+		origep, err := v23.NewEndpoint(addr)
+		if err != nil {
+			return nil, err
+		}
+		ep := naming.FormatEndpoint(
+			origep.Addr().Network(),
+			origep.Addr().String(),
+			origep.RoutingID(),
+			naming.ServesMountTable(origep.ServesMountTable()))
+		roots[i] = naming.JoinAddressName(ep, suffix)
+	}
+	vlog.Infof("Changing namespace roots from %v to %v", origroots, roots)
+	ctx, _, err := v23.WithNewNamespace(ctx, roots...)
+	return ctx, err
+}
diff --git a/services/device/internal/starter/starter.go b/services/device/internal/starter/starter.go
index 646beff..4ad1dac 100644
--- a/services/device/internal/starter/starter.go
+++ b/services/device/internal/starter/starter.go
@@ -32,14 +32,14 @@
 const pkgPath = "v.io/x/ref/services/device/internal/starter"
 
 var (
-	errCantSaveInfo       = verror.Register(pkgPath+".errCantSaveInfo", verror.NoRetry, "{1:}{2:} failed to save info{:_}")
-	errBadPort            = verror.Register(pkgPath+".errBadPort", verror.NoRetry, "{1:}{2:} invalid port{:_}")
-	errCantCreateProxy    = verror.Register(pkgPath+".errCantCreateProxy", verror.NoRetry, "{1:}{2:} Failed to create proxy{:_}")
-	errCantCreateEndpoint = verror.Register(pkgPath+".errCantCreateEndpoint", verror.NoRetry, "{1:}{2:} failed to create endpoint from namespace root {3}{:_}")
+	errCantSaveInfo      = verror.Register(pkgPath+".errCantSaveInfo", verror.NoRetry, "{1:}{2:} failed to save info{:_}")
+	errBadPort           = verror.Register(pkgPath+".errBadPort", verror.NoRetry, "{1:}{2:} invalid port{:_}")
+	errCantCreateProxy   = verror.Register(pkgPath+".errCantCreateProxy", verror.NoRetry, "{1:}{2:} Failed to create proxy{:_}")
+	errNoEndpointToClaim = verror.Register(pkgPath+".errNoEndpointToClaim", verror.NoRetry, "{1:}{2:} failed to find an endpoint for claiming{:_}")
 )
 
 type NamespaceArgs struct {
-	Name            string         // Name to publish the mounttable service under.
+	Name            string         // Name to publish the mounttable service under (after claiming).
 	ListenSpec      rpc.ListenSpec // ListenSpec for the server.
 	PermissionsFile string         // Path to the Permissions file used by the mounttable.
 	PersistenceDir  string         // Path to the directory holding persistent acls.
@@ -50,7 +50,7 @@
 }
 
 type DeviceArgs struct {
-	Name            string         // Name to publish the device service under.
+	Name            string         // Name to publish the device service under (after claiming).
 	ListenSpec      rpc.ListenSpec // ListenSpec for the device server.
 	ConfigState     *config.State  // Configuration for the device.
 	TestMode        bool           // Whether the device is running in test mode or not.
@@ -81,12 +81,13 @@
 
 // Start creates servers for the mounttable and device services and links them together.
 //
-// Returns the callback to be invoked to shutdown the services on success, or
-// an error on failure.
-func Start(ctx *context.T, args Args) (func(), error) {
+// Returns the object name for the claimable service (empty if already claimed),
+// a callback to be invoked to shutdown the services on success, or an error on
+// failure.
+func Start(ctx *context.T, args Args) (string, func(), error) {
 	// Is this binary compatible with the state on disk?
 	if err := impl.CheckCompatibility(args.Device.ConfigState.Root); err != nil {
-		return nil, err
+		return "", nil, err
 	}
 	// In test mode, we skip writing the info file to disk, and we skip
 	// attempting to start the claimable service: the device must have been
@@ -94,7 +95,8 @@
 	// NewClaimableDispatcher needlessly prints a perms signature
 	// verification error to the logs.
 	if args.Device.TestMode {
-		return startClaimedDevice(ctx, args)
+		cleanup, err := startClaimedDevice(ctx, args)
+		return "", cleanup, err
 	}
 
 	// TODO(caprita): use some mechanism (a file lock or presence of entry
@@ -104,7 +106,7 @@
 		Pid: os.Getpid(),
 	}
 	if err := impl.SaveManagerInfo(filepath.Join(args.Device.ConfigState.Root, "device-manager"), mi); err != nil {
-		return nil, verror.New(errCantSaveInfo, ctx, err)
+		return "", nil, verror.New(errCantSaveInfo, ctx, err)
 	}
 
 	// If the device has not yet been claimed, start the mounttable and
@@ -115,26 +117,23 @@
 	if claimable == nil {
 		// Device has already been claimed, bypass claimable service
 		// stage.
-		return startClaimedDevice(ctx, args)
+		cleanup, err := startClaimedDevice(ctx, args)
+		return "", cleanup, err
 	}
-	stopClaimable, err := startClaimableDevice(ctx, claimable, args)
+	epName, stopClaimable, err := startClaimableDevice(ctx, claimable, args)
 	if err != nil {
-		return nil, err
+		return "", nil, err
 	}
 	stop := make(chan struct{})
 	stopped := make(chan struct{})
 	go waitToBeClaimedAndStartClaimedDevice(ctx, stopClaimable, claimed, stop, stopped, args)
-	return func() {
+	return epName, func() {
 		close(stop)
 		<-stopped
 	}, nil
 }
 
-func startClaimableDevice(ctx *context.T, dispatcher rpc.Dispatcher, args Args) (func(), error) {
-	ctx, err := setNamespaceRootsForUnclaimedDevice(ctx)
-	if err != nil {
-		return nil, err
-	}
+func startClaimableDevice(ctx *context.T, dispatcher rpc.Dispatcher, args Args) (string, func(), error) {
 	// TODO(caprita,ashankar): We create a context with a new stream manager
 	// that we can cancel once the device has been claimed. This gets around
 	// the following issue: if we publish the claimable server to the local
@@ -147,43 +146,37 @@
 	// gets confused trying to reuse the old connection and doesn't attempt
 	// to create a new connection).  We should get to the bottom of it.
 	ctx, cancel := context.WithCancel(ctx)
+	var err error
 	if ctx, err = v23.WithNewStreamManager(ctx); err != nil {
 		cancel()
-		return nil, err
-	}
-	mtName, stopMT, err := startMounttable(ctx, args.Namespace)
-	if err != nil {
-		cancel()
-		return nil, err
+		return "", nil, err
 	}
 	server, err := v23.NewServer(ctx)
 	if err != nil {
-		stopMT()
 		cancel()
-		return nil, err
+		return "", nil, err
 	}
 	shutdown := func() {
 		vlog.Infof("Stopping claimable server...")
 		server.Stop()
 		vlog.Infof("Stopped claimable server.")
-		stopMT()
 		cancel()
 	}
 	endpoints, err := server.Listen(args.Device.ListenSpec)
 	if err != nil {
 		shutdown()
-		return nil, err
+		return "", nil, err
 	}
-	claimableServerName := args.Device.name(mtName)
-	if err := server.ServeDispatcher(claimableServerName, dispatcher); err != nil {
+	if err := server.ServeDispatcher("", dispatcher); err != nil {
 		shutdown()
-		return nil, err
+		return "", nil, err
 	}
 	publicKey, err := v23.GetPrincipal(ctx).PublicKey().MarshalBinary()
 	if err != nil {
 		shutdown()
-		return nil, err
+		return "", nil, err
 	}
+	var epName string
 	if args.Device.ListenSpec.Proxy != "" {
 		for {
 			p := server.Status().Proxies
@@ -192,13 +185,19 @@
 				time.Sleep(time.Second)
 				continue
 			}
-			vlog.Infof("Proxied address: %s", p[0].Endpoint.Name())
+			epName = p[0].Endpoint.Name()
+			vlog.Infof("Proxied address: %s", epName)
 			break
 		}
+	} else {
+		if len(endpoints) == 0 {
+			return "", nil, verror.New(errNoEndpointToClaim, ctx, err)
+		}
+		epName = endpoints[0].Name()
 	}
-	vlog.Infof("Unclaimed device manager (%v) published as %v with public_key: %s", endpoints[0].Name(), claimableServerName, base64.URLEncoding.EncodeToString(publicKey))
+	vlog.Infof("Unclaimed device manager (%v) with public_key: %s", epName, base64.URLEncoding.EncodeToString(publicKey))
 	vlog.FlushLog()
-	return shutdown, nil
+	return epName, shutdown, nil
 }
 
 func waitToBeClaimedAndStartClaimedDevice(ctx *context.T, stopClaimable func(), claimed, stop <-chan struct{}, stopped chan<- struct{}, args Args) {
@@ -369,43 +368,3 @@
 		}(root)
 	}
 }
-
-// Unclaimed devices typically have Principals that recognize no other
-// authoritative public keys than their own. As a result, they will fail to
-// authorize any other services.
-//
-// With no information to authenticate or authorize peers (including the
-// mounttable at the namespace root), this unclaimed device manager will be
-// unable to make any outgoing RPCs.
-//
-// As a workaround, reconfigure it to "authorize any root mounttable" by
-// removing references to the expected blessings of the namespace root.  This
-// will allow the unclaimed device manager to mount itself.
-//
-// TODO(ashankar,caprita): The more secure fix would be to ensure that an
-// unclaimed device is configured to recognize the blessings presented by the
-// mounttable it is configured to talk to. Of course, if the root mounttable is
-// "discovered" as opposed to "configured", then this new device will have to
-// return to either not mounting itself (and being claimed via some discovery
-// protocol like mdns or bluetooth) or ignoring the blessings of the namespace
-// root.
-func setNamespaceRootsForUnclaimedDevice(ctx *context.T) (*context.T, error) {
-	origroots := v23.GetNamespace(ctx).Roots()
-	roots := make([]string, len(origroots))
-	for i, orig := range origroots {
-		addr, suffix := naming.SplitAddressName(orig)
-		origep, err := v23.NewEndpoint(addr)
-		if err != nil {
-			return nil, verror.New(errCantCreateEndpoint, ctx, orig, err)
-		}
-		ep := naming.FormatEndpoint(
-			origep.Addr().Network(),
-			origep.Addr().String(),
-			origep.RoutingID(),
-			naming.ServesMountTable(origep.ServesMountTable()))
-		roots[i] = naming.JoinAddressName(ep, suffix)
-	}
-	vlog.Infof("Changing namespace roots from %v to %v", origroots, roots)
-	ctx, _, err := v23.WithNewNamespace(ctx, roots...)
-	return ctx, err
-}
diff --git a/services/device/mgmt_v23_test.go b/services/device/mgmt_v23_test.go
index 1b09af6..b5b9e29 100644
--- a/services/device/mgmt_v23_test.go
+++ b/services/device/mgmt_v23_test.go
@@ -170,6 +170,36 @@
 
 	deviceScript.Start(deviceScriptArguments...).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/alice/myworkstation".
+	deviceBin.Start("claim", claimableEP, "myworkstation")
 
 	resolve := func(name string) string {
 		resolver := func() (interface{}, error) {
@@ -186,32 +216,10 @@
 		}
 		return i.WaitFor(resolver, 100*time.Millisecond, time.Minute).(string)
 	}
+
+	// Wait for the device manager to publish its mount table entry.
 	mtEP := resolve(mtName)
 
-	// Verify that device manager's mounttable is published under the expected
-	// name (hostname).
-	if got := namespaceBin.Run("glob", mtName); len(got) == 0 {
-		i.Fatalf("glob failed for %q", mtName)
-	}
-
-	// Claim the device as "root/alice/myworkstation".
-	deviceBin.Start("claim", mtName+"/devmgr/device", "myworkstation")
-
-	resolveChange := func(name, old string) string {
-		resolver := func() (interface{}, error) {
-			inv := namespaceBin.Start("resolve", name)
-			defer inv.Wait(nil, os.Stderr)
-			if r := strings.TrimRight(inv.Output(), "\n"); len(r) > 0 && r != old {
-				return r, nil
-			}
-			return nil, nil
-		}
-		return i.WaitFor(resolver, 100*time.Millisecond, time.Minute).(string)
-	}
-
-	// Wait for the device manager to update its mount table entry.
-	mtEP = resolveChange(mtName, mtEP)
-
 	if withSuid {
 		deviceBin.Start("associate", "add", mtName+"/devmgr/device", appUserFlag, "root/alice")
 
@@ -355,6 +363,17 @@
 
 	// Update the device manager.
 	deviceBin.Run("update", mtName+"/devmgr/device")
+	resolveChange := func(name, old string) string {
+		resolver := func() (interface{}, error) {
+			inv := namespaceBin.Start("resolve", name)
+			defer inv.Wait(nil, os.Stderr)
+			if r := strings.TrimRight(inv.Output(), "\n"); len(r) > 0 && r != old {
+				return r, nil
+			}
+			return nil, nil
+		}
+		return i.WaitFor(resolver, 100*time.Millisecond, time.Minute).(string)
+	}
 	mtEP = resolveChange(mtName, mtEP)
 
 	// Verify that device manager's mounttable is still published under the