veyron/services/mgmt/device: enable profiles statically

Start using device profiles in device manager, in the absence of a remote
Profile service: the set of 'known' profiles will be hardcoded for now.

Specifically:

- implement getKnownProfiles as a hardcoded list of profiles (for now, just
  linux-amd64)

- make use of the device's profile when fetching app envelopes (and verify in
  test that profiles are requested correctly during Match)

- export computeDeviceProfile and add command to deviced to dump the (internal
  representation of the) profile detected by the device for debugging/inspection

- add command to device tool to invoke remote Describe command on device manager
  (which returns the profile labels supported by the device.

Change-Id: I3e24021354edaeb90fb6efb11cee4330f63e8f86
diff --git a/services/mgmt/device/deviced/commands.go b/services/mgmt/device/deviced/commands.go
index 62d3108..1689b77 100644
--- a/services/mgmt/device/deviced/commands.go
+++ b/services/mgmt/device/deviced/commands.go
@@ -130,3 +130,20 @@
 	}
 	return nil
 }
+
+var cmdProfile = &cmdline.Command{
+	Run:   runProfile,
+	Name:  "profile",
+	Short: "Dumps profile for the device manager.",
+	Long:  "Prints the internal profile description for the device manager.",
+}
+
+func runProfile(cmd *cmdline.Command, _ []string) error {
+	spec, err := impl.ComputeDeviceProfile()
+	if err != nil {
+		vlog.Errorf("ComputeDeviceProfile failed: %v", err)
+		return err
+	}
+	fmt.Fprintf(cmd.Stdout(), "Profile: %#v\n", spec)
+	return nil
+}
diff --git a/services/mgmt/device/deviced/main.go b/services/mgmt/device/deviced/main.go
index 9b21878..710b33f 100644
--- a/services/mgmt/device/deviced/main.go
+++ b/services/mgmt/device/deviced/main.go
@@ -9,7 +9,7 @@
 		Long: `
 deviced can be used to launch, configure, or manage the device manager.
 `,
-		Children: []*cmdline.Command{cmdInstall, cmdUninstall, cmdStart, cmdStop},
+		Children: []*cmdline.Command{cmdInstall, cmdUninstall, cmdStart, cmdStop, cmdProfile},
 		Run:      runServer,
 	}
 	rootCmd.Main()
diff --git a/services/mgmt/device/impl/device_service.go b/services/mgmt/device/impl/device_service.go
index b67a496..e8c9617 100644
--- a/services/mgmt/device/impl/device_service.go
+++ b/services/mgmt/device/impl/device_service.go
@@ -144,31 +144,21 @@
 }
 
 func (*deviceService) Describe(ipc.ServerContext) (device.Description, error) {
-	empty := device.Description{}
-	deviceProfile, err := computeDeviceProfile()
-	if err != nil {
-		return empty, err
-	}
-	knownProfiles, err := getKnownProfiles()
-	if err != nil {
-		return empty, err
-	}
-	result := matchProfiles(deviceProfile, knownProfiles)
-	return result, nil
+	return describe()
 }
 
 func (*deviceService) IsRunnable(_ ipc.ServerContext, description binary.Description) (bool, error) {
-	deviceProfile, err := computeDeviceProfile()
+	deviceProfile, err := ComputeDeviceProfile()
 	if err != nil {
 		return false, err
 	}
-	binaryProfiles := make([]profile.Specification, 0)
+	binaryProfiles := make([]*profile.Specification, 0)
 	for name, _ := range description.Profiles {
 		profile, err := getProfile(name)
 		if err != nil {
 			return false, err
 		}
-		binaryProfiles = append(binaryProfiles, *profile)
+		binaryProfiles = append(binaryProfiles, profile)
 	}
 	result := matchProfiles(deviceProfile, binaryProfiles)
 	return len(result.Profiles) > 0, nil
diff --git a/services/mgmt/device/impl/mock_repo_test.go b/services/mgmt/device/impl/mock_repo_test.go
index 16fed19..4c2dd10 100644
--- a/services/mgmt/device/impl/mock_repo_test.go
+++ b/services/mgmt/device/impl/mock_repo_test.go
@@ -3,9 +3,11 @@
 import (
 	"crypto/md5"
 	"encoding/hex"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
+	"reflect"
 	"testing"
 
 	"v.io/core/veyron2/ipc"
@@ -62,8 +64,11 @@
 }
 
 // APPLICATION REPOSITORY INTERFACE IMPLEMENTATION
-func (i *arInvoker) Match(ipc.ServerContext, []string) (application.Envelope, error) {
+func (i *arInvoker) Match(_ ipc.ServerContext, profiles []string) (application.Envelope, error) {
 	vlog.VI(1).Infof("Match()")
+	if want := []string{"test-profile"}; !reflect.DeepEqual(profiles, want) {
+		return application.Envelope{}, fmt.Errorf("Expected profiles %v, got %v", want, profiles)
+	}
 	return i.envelope, nil
 }
 
diff --git a/services/mgmt/device/impl/only_for_test.go b/services/mgmt/device/impl/only_for_test.go
index 62a69cb..a0fc4a2 100644
--- a/services/mgmt/device/impl/only_for_test.go
+++ b/services/mgmt/device/impl/only_for_test.go
@@ -5,6 +5,7 @@
 	"os"
 	"path/filepath"
 
+	"v.io/core/veyron2/services/mgmt/device"
 	"v.io/core/veyron2/vlog"
 )
 
@@ -40,6 +41,10 @@
 		}
 	}
 	isSetuid = possiblyMockIsSetuid
+
+	describe = func() (descr device.Description, err error) {
+		return device.Description{Profiles: map[string]struct{}{"test-profile": struct{}{}}}, nil
+	}
 }
 
 func possiblyMockIsSetuid(fileStat os.FileInfo) bool {
diff --git a/services/mgmt/device/impl/profile.go b/services/mgmt/device/impl/profile.go
index 275fc3d..4e784af 100644
--- a/services/mgmt/device/impl/profile.go
+++ b/services/mgmt/device/impl/profile.go
@@ -13,13 +13,13 @@
 	"v.io/core/veyron2/services/mgmt/device"
 )
 
-// computeDeviceProfile generates a description of the runtime
+// ComputeDeviceProfile generates a description of the runtime
 // environment (supported file format, OS, architecture, libraries) of
 // the host device.
 //
 // TODO(jsimsa): Avoid computing the host device description from
 // scratch if a recent cached copy exists.
-func computeDeviceProfile() (*profile.Specification, error) {
+func ComputeDeviceProfile() (*profile.Specification, error) {
 	result := profile.Specification{}
 
 	// Find out what the supported file format, operating system, and
@@ -52,8 +52,8 @@
 	switch runtime.GOOS {
 	case "linux":
 		// For Linux, we identify what dynamically linked libraries are
-		// install by parsing the output of "ldconfig -p".
-		command := exec.Command("ldconfig", "-p")
+		// installed by parsing the output of "ldconfig -p".
+		command := exec.Command("/sbin/ldconfig", "-p")
 		output, err := command.CombinedOutput()
 		if err != nil {
 			return nil, err
@@ -101,32 +101,43 @@
 // TODO(jsimsa): Avoid retrieving the list of known profiles from a
 // remote server if a recent cached copy exists.
 func getProfile(name string) (*profile.Specification, error) {
+	profiles, err := getKnownProfiles()
+	if err != nil {
+		return nil, err
+	}
+	for _, p := range profiles {
+		if p.Label == name {
+			return p, nil
+		}
+	}
+	return nil, nil
+
 	// TODO(jsimsa): This function assumes the existence of a profile
 	// server from which the profiles can be retrieved. The profile
 	// server is a work in progress. When it exists, the commented out
 	// code below should work.
-	var profile profile.Specification
 	/*
-			client, err := r.NewClient()
-			if err != nil {
-				vlog.Errorf("NewClient() failed: %v", err)
-				return nil, err
-			}
-			defer client.Close()
-		  server := // TODO
-			method := "Specification"
-			inputs := make([]interface{}, 0)
-			call, err := client.StartCall(server + "/" + name, method, inputs)
-			if err != nil {
-				vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server + "/" + name, method, inputs, err)
-				return nil, err
-			}
-			if err := call.Finish(&profiles); err != nil {
-				vlog.Errorf("Finish(%v) failed: %v\n", &profiles, err)
-				return nil, err
-			}
+		var profile profile.Specification
+				client, err := r.NewClient()
+				if err != nil {
+					vlog.Errorf("NewClient() failed: %v", err)
+					return nil, err
+				}
+				defer client.Close()
+			  server := // TODO
+				method := "Specification"
+				inputs := make([]interface{}, 0)
+				call, err := client.StartCall(server + "/" + name, method, inputs)
+				if err != nil {
+					vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server + "/" + name, method, inputs, err)
+					return nil, err
+				}
+				if err := call.Finish(&profiles); err != nil {
+					vlog.Errorf("Finish(%v) failed: %v\n", &profiles, err)
+					return nil, err
+				}
+		return &profile, nil
 	*/
-	return &profile, nil
 }
 
 // getKnownProfiles gets a list of description for all publicly known
@@ -134,39 +145,51 @@
 //
 // TODO(jsimsa): Avoid retrieving the list of known profiles from a
 // remote server if a recent cached copy exists.
-func getKnownProfiles() ([]profile.Specification, error) {
+func getKnownProfiles() ([]*profile.Specification, error) {
+	return []*profile.Specification{
+		{
+			Label:       "linux-amd64",
+			Description: "",
+			Arch:        build.AMD64,
+			OS:          build.Linux,
+			Format:      build.ELF,
+		},
+		// TODO(caprita): Add profiles for Mac, Pi, etc.
+	}, nil
+
 	// TODO(jsimsa): This function assumes the existence of a profile
 	// server from which a list of known profiles can be retrieved. The
 	// profile server is a work in progress. When it exists, the
 	// commented out code below should work.
-	knownProfiles := make([]profile.Specification, 0)
+
 	/*
-			client, err := r.NewClient()
-			if err != nil {
-				vlog.Errorf("NewClient() failed: %v\n", err)
-				return nil, err
-			}
-			defer client.Close()
-		  server := // TODO
-			method := "List"
-			inputs := make([]interface{}, 0)
-			call, err := client.StartCall(server, method, inputs)
-			if err != nil {
-				vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err)
-				return nil, err
-			}
-			if err := call.Finish(&knownProfiles); err != nil {
-				vlog.Errorf("Finish(&knownProfile) failed: %v\n", err)
-				return nil, err
-			}
+		knownProfiles := make([]profile.Specification, 0)
+				client, err := r.NewClient()
+				if err != nil {
+					vlog.Errorf("NewClient() failed: %v\n", err)
+					return nil, err
+				}
+				defer client.Close()
+			  server := // TODO
+				method := "List"
+				inputs := make([]interface{}, 0)
+				call, err := client.StartCall(server, method, inputs)
+				if err != nil {
+					vlog.Errorf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err)
+					return nil, err
+				}
+				if err := call.Finish(&knownProfiles); err != nil {
+					vlog.Errorf("Finish(&knownProfile) failed: %v\n", err)
+					return nil, err
+				}
+		return knownProfiles, nil
 	*/
-	return knownProfiles, nil
 }
 
 // matchProfiles inputs a profile that describes the host device and a
 // set of publicly known profiles and outputs a device description that
 // identifies the publicly known profiles supported by the host device.
-func matchProfiles(p *profile.Specification, known []profile.Specification) device.Description {
+func matchProfiles(p *profile.Specification, known []*profile.Specification) device.Description {
 	result := device.Description{Profiles: make(map[string]struct{})}
 loop:
 	for _, profile := range known {
@@ -189,3 +212,29 @@
 	}
 	return result
 }
+
+// describe returns a Description containing the profile that matches the
+// current device.  It's declared as a variable so we can override it for
+// testing.
+var describe = func() (device.Description, error) {
+	empty := device.Description{}
+	deviceProfile, err := ComputeDeviceProfile()
+	if err != nil {
+		return empty, err
+	}
+	knownProfiles, err := getKnownProfiles()
+	if err != nil {
+		return empty, err
+	}
+	result := matchProfiles(deviceProfile, knownProfiles)
+	if len(result.Profiles) == 0 {
+		// For now, return "unknown" as the profile, if no known profile
+		// matches the device's profile.
+		//
+		// TODO(caprita): Get rid of this crutch once we have profiles
+		// defined for our supported systems; for now it helps us make
+		// the integration test work on e.g. Mac.
+		result.Profiles["unknown"] = struct{}{}
+	}
+	return result, nil
+}
diff --git a/services/mgmt/device/impl/util.go b/services/mgmt/device/impl/util.go
index 50d892e..09fa70a 100644
--- a/services/mgmt/device/impl/util.go
+++ b/services/mgmt/device/impl/util.go
@@ -41,7 +41,15 @@
 	stub := repository.ApplicationClient(origin)
 	// TODO(jsimsa): Include logic that computes the set of supported
 	// profiles.
-	profiles := []string{"test"}
+	profilesSet, err := describe()
+	if err != nil {
+		vlog.Errorf("Failed to obtain profile labels: %v", err)
+		return nil, verror2.Make(ErrOperationFailed, ctx)
+	}
+	var profiles []string
+	for label := range profilesSet.Profiles {
+		profiles = append(profiles, label)
+	}
 	envelope, err := stub.Match(ctx, profiles)
 	if err != nil {
 		vlog.Errorf("Match(%v) failed: %v", profiles, err)
diff --git a/services/mgmt/profile/profile.vdl b/services/mgmt/profile/profile.vdl
index 1e12ec8..63240ec 100644
--- a/services/mgmt/profile/profile.vdl
+++ b/services/mgmt/profile/profile.vdl
@@ -17,17 +17,17 @@
 // Specification is how we represent a profile internally. It should
 // provide enough information to allow matching of binaries to devices.
 type Specification struct {
-	// Arch is the target hardware architecture of the profile.
-	Arch        build.Architecture
+	// Label is a human-friendly concise label for the profile,
+	// e.g. "linux-media".
+	Label       string
 	// Description is a human-friendly description of the profile.
 	Description string
+	// Arch is the target hardware architecture of the profile.
+	Arch        build.Architecture
+	// OS is the target operating system of the profile.
+	OS          build.OperatingSystem
 	// Format is the file format supported by the profile.
 	Format      build.Format
 	// Libraries is a set of libraries the profile requires.
 	Libraries   set[Library]
-	// Label is a human-friendly concise label for the profile,
-	// e.g. "linux-media".
-	Label       string
-	// OS is the target operating system of the profile.
-	OS          build.OperatingSystem
 }
diff --git a/services/mgmt/profile/profile.vdl.go b/services/mgmt/profile/profile.vdl.go
index 8b50064..6db310d 100644
--- a/services/mgmt/profile/profile.vdl.go
+++ b/services/mgmt/profile/profile.vdl.go
@@ -30,19 +30,19 @@
 // Specification is how we represent a profile internally. It should
 // provide enough information to allow matching of binaries to devices.
 type Specification struct {
-	// Arch is the target hardware architecture of the profile.
-	Arch build.Architecture
+	// Label is a human-friendly concise label for the profile,
+	// e.g. "linux-media".
+	Label string
 	// Description is a human-friendly description of the profile.
 	Description string
+	// Arch is the target hardware architecture of the profile.
+	Arch build.Architecture
+	// OS is the target operating system of the profile.
+	OS build.OperatingSystem
 	// Format is the file format supported by the profile.
 	Format build.Format
 	// Libraries is a set of libraries the profile requires.
 	Libraries map[Library]struct{}
-	// Label is a human-friendly concise label for the profile,
-	// e.g. "linux-media".
-	Label string
-	// OS is the target operating system of the profile.
-	OS build.OperatingSystem
 }
 
 func (Specification) __VDLReflect(struct {
diff --git a/services/mgmt/profile/profiled/test.sh b/services/mgmt/profile/profiled/test.sh
index 756916d..685dfee 100755
--- a/services/mgmt/profile/profiled/test.sh
+++ b/services/mgmt/profile/profiled/test.sh
@@ -55,7 +55,7 @@
   OUTPUT=$(shell::tmp_file)
   "${PROFILE_BIN}" specification "${PROFILE}" | tee "${OUTPUT}" || shell_test::fail "line ${LINENO}: 'spec' failed"
   GOT=$(cat "${OUTPUT}")
-  WANT='profile.Specification{Arch:"amd64", Description:"Example profile to test the profile manager implementation.", Format:"ELF", Libraries:map[profile.Library]struct {}{profile.Library{Name:"foo", MajorVersion:"1", MinorVersion:"0"}:struct {}{}}, Label:"example", OS:"linux"}'
+  WANT='profile.Specification{Label:"example", Description:"Example profile to test the profile manager implementation.", Arch:"amd64", OS:"linux", Format:"ELF", Libraries:map[profile.Library]struct {}{profile.Library{Name:"foo", MajorVersion:"1", MinorVersion:"0"}:struct {}{}}}'
   shell_test::assert_eq "${GOT}" "${WANT}" "${LINENO}"
 
   # Remove the profile.
diff --git a/services/mgmt/repository/repository.vdl.go b/services/mgmt/repository/repository.vdl.go
index 75842f2..36401ea 100644
--- a/services/mgmt/repository/repository.vdl.go
+++ b/services/mgmt/repository/repository.vdl.go
@@ -586,21 +586,21 @@
 	}
 
 	result.TypeDefs = []__vdlutil.Any{
-		__wiretype.NamedPrimitiveType{Type: 0x3, Name: "v.io/core/veyron2/services/mgmt/build.Architecture", Tags: []string(nil)}, __wiretype.NamedPrimitiveType{Type: 0x3, Name: "v.io/core/veyron2/services/mgmt/build.Format", Tags: []string(nil)}, __wiretype.StructType{
+		__wiretype.NamedPrimitiveType{Type: 0x3, Name: "v.io/core/veyron2/services/mgmt/build.Architecture", Tags: []string(nil)}, __wiretype.NamedPrimitiveType{Type: 0x3, Name: "v.io/core/veyron2/services/mgmt/build.OperatingSystem", Tags: []string(nil)}, __wiretype.NamedPrimitiveType{Type: 0x3, Name: "v.io/core/veyron2/services/mgmt/build.Format", Tags: []string(nil)}, __wiretype.StructType{
 			[]__wiretype.FieldType{
 				__wiretype.FieldType{Type: 0x3, Name: "Name"},
 				__wiretype.FieldType{Type: 0x3, Name: "MajorVersion"},
 				__wiretype.FieldType{Type: 0x3, Name: "MinorVersion"},
 			},
 			"v.io/core/veyron/services/mgmt/profile.Library", []string(nil)},
-		__wiretype.MapType{Key: 0x43, Elem: 0x2, Name: "", Tags: []string(nil)}, __wiretype.NamedPrimitiveType{Type: 0x3, Name: "v.io/core/veyron2/services/mgmt/build.OperatingSystem", Tags: []string(nil)}, __wiretype.StructType{
+		__wiretype.MapType{Key: 0x44, Elem: 0x2, Name: "", Tags: []string(nil)}, __wiretype.StructType{
 			[]__wiretype.FieldType{
-				__wiretype.FieldType{Type: 0x41, Name: "Arch"},
-				__wiretype.FieldType{Type: 0x3, Name: "Description"},
-				__wiretype.FieldType{Type: 0x42, Name: "Format"},
-				__wiretype.FieldType{Type: 0x44, Name: "Libraries"},
 				__wiretype.FieldType{Type: 0x3, Name: "Label"},
-				__wiretype.FieldType{Type: 0x45, Name: "OS"},
+				__wiretype.FieldType{Type: 0x3, Name: "Description"},
+				__wiretype.FieldType{Type: 0x41, Name: "Arch"},
+				__wiretype.FieldType{Type: 0x42, Name: "OS"},
+				__wiretype.FieldType{Type: 0x43, Name: "Format"},
+				__wiretype.FieldType{Type: 0x45, Name: "Libraries"},
 			},
 			"v.io/core/veyron/services/mgmt/profile.Specification", []string(nil)},
 		__wiretype.NamedPrimitiveType{Type: 0x1, Name: "error", Tags: []string(nil)}}
diff --git a/tools/mgmt/device/impl.go b/tools/mgmt/device/impl.go
index fced994..9a6aaf9 100644
--- a/tools/mgmt/device/impl.go
+++ b/tools/mgmt/device/impl.go
@@ -102,6 +102,29 @@
 	return nil
 }
 
+var cmdDescribe = &cmdline.Command{
+	Run:      runDescribe,
+	Name:     "describe",
+	Short:    "Describe the device.",
+	Long:     "Describe the device.",
+	ArgsName: "<device>",
+	ArgsLong: `
+<device> is the veyron object name of the device manager's app service.`,
+}
+
+func runDescribe(cmd *cmdline.Command, args []string) error {
+	if expected, got := 1, len(args); expected != got {
+		return cmd.UsageErrorf("describe: incorrect number of arguments, expected %d, got %d", expected, got)
+	}
+	deviceName := args[0]
+	if description, err := device.DeviceClient(deviceName).Describe(gctx); err != nil {
+		return fmt.Errorf("Describe failed: %v", err)
+	} else {
+		fmt.Fprintf(cmd.Stdout(), "%+v\n", description)
+	}
+	return nil
+}
+
 func root() *cmdline.Command {
 	return &cmdline.Command{
 		Name:  "device",
@@ -109,6 +132,6 @@
 		Long: `
 The device tool facilitates interaction with the veyron device manager.
 `,
-		Children: []*cmdline.Command{cmdInstall, cmdStart, associateRoot(), cmdClaim, cmdStop, cmdSuspend, cmdResume, aclRoot()},
+		Children: []*cmdline.Command{cmdInstall, cmdStart, associateRoot(), cmdDescribe, cmdClaim, cmdStop, cmdSuspend, cmdResume, aclRoot()},
 	}
 }
diff --git a/tools/mgmt/test.sh b/tools/mgmt/test.sh
index 432bedf..a0fb910 100755
--- a/tools/mgmt/test.sh
+++ b/tools/mgmt/test.sh
@@ -123,6 +123,9 @@
   shell_test::assert_eq "$("${DEBUG_BIN}" stats read "${DM_NAME}/__debug/stats/security/principal/blessingstore" | head -1 | sed -e 's/^.*Default blessings: '//)" \
     "alice/myworkstation" "${LINENO}"
 
+  # Get the device's profile.
+  local -r DEVICE_PROFILE=$("${DEVICE_BIN}" describe "${DM_NAME}/device" | sed -r 's/\{Profiles:map\[(.+):.*/\1/g')
+
   # Start a binary server under the blessing "alice/myworkstation/binaryd" so that
   # the device ("alice/myworkstation") can talk to it.
   local -r BINARYD_NAME="binaryd"
@@ -150,10 +153,10 @@
   local -r SAMPLE_APP_NAME="${APPLICATIOND_NAME}/testapp/v0"
   local -r APP_PUBLISH_NAME="testbinaryd"
   echo "{\"Title\":\"BINARYD\", \"Args\":[\"--name=${APP_PUBLISH_NAME}\", \"--root_dir=./binstore\", \"--veyron.tcp.address=127.0.0.1:0\"], \"Binary\":\"${SAMPLE_APP_BIN_NAME}\", \"Env\":[]}" > ./app.envelope && \
-    "${APPLICATION_BIN}" put "${SAMPLE_APP_NAME}" test ./app.envelope && rm ./app.envelope
+    "${APPLICATION_BIN}" put "${SAMPLE_APP_NAME}" "${DEVICE_PROFILE}" ./app.envelope && rm ./app.envelope
 
   # Verify that the envelope we uploaded shows up with glob.
-  shell_test::assert_eq "$("${APPLICATION_BIN}" match "${SAMPLE_APP_NAME}" test | grep Title | sed -e 's/^.*"Title": "'// | sed -e 's/",//')" \
+  shell_test::assert_eq "$("${APPLICATION_BIN}" match "${SAMPLE_APP_NAME}" "${DEVICE_PROFILE}" | grep Title | sed -e 's/^.*"Title": "'// | sed -e 's/",//')" \
     "BINARYD" "${LINENO}"
 
   # Install the app on the device.