veyron/tools/mgmt/device/impl: allow packages to be specified for local-install.

This cl adds the ability to provide package files or directories on the
command-line for the local-install device tool.

Change-Id: Ie6b59ed0408e51cfffb55bd97fbe7c96b5d9a30f
diff --git a/tools/mgmt/device/impl/devicemanager_mock_test.go b/tools/mgmt/device/impl/devicemanager_mock_test.go
index d0da827..254d370 100644
--- a/tools/mgmt/device/impl/devicemanager_mock_test.go
+++ b/tools/mgmt/device/impl/devicemanager_mock_test.go
@@ -1,7 +1,11 @@
 package impl_test
 
 import (
+	"fmt"
+	"io/ioutil"
 	"log"
+	"os"
+	"path/filepath"
 	"testing"
 
 	"v.io/core/veyron2"
@@ -17,6 +21,7 @@
 	"v.io/core/veyron2/vlog"
 
 	binlib "v.io/core/veyron/services/mgmt/lib/binary"
+	"v.io/core/veyron/services/mgmt/lib/packages"
 )
 
 type mockDeviceInvoker struct {
@@ -79,11 +84,14 @@
 
 // Mock Install
 type InstallStimulus struct {
-	fun        string
-	appName    string
-	config     device.Config
-	envelope   application.Envelope
-	binarySize int64
+	fun      string
+	appName  string
+	config   device.Config
+	envelope application.Envelope
+	// files holds a map from file or package name to file or package size.
+	// The app binary has the key "binary". Each of the packages will have
+	// the key "package/<package name>".
+	files map[string]int64
 }
 
 type InstallResponse struct {
@@ -103,8 +111,28 @@
 	binaryNameAfterFetch = "binary-fetched"
 )
 
+func packageSize(pkgPath string) int64 {
+	info, err := os.Stat(pkgPath)
+	if err != nil {
+		return -1
+	}
+	if info.IsDir() {
+		infos, err := ioutil.ReadDir(pkgPath)
+		if err != nil {
+			return -1
+		}
+		var size int64
+		for _, i := range infos {
+			size += i.Size()
+		}
+		return size
+	} else {
+		return info.Size()
+	}
+}
+
 func (mni *mockDeviceInvoker) Install(call ipc.ServerContext, appName string, config device.Config) (string, error) {
-	is := InstallStimulus{"Install", appName, config, application.Envelope{}, 0}
+	is := InstallStimulus{"Install", appName, config, application.Envelope{}, nil}
 	if appName != appNameNoFetch {
 		// Fetch the envelope and record it in the stimulus.
 		envelope, err := repository.ApplicationClient(appName).Match(call.Context(), []string{"test"})
@@ -113,14 +141,38 @@
 		}
 		binaryName := envelope.Binary
 		envelope.Binary = binaryNameAfterFetch
-		is.envelope = envelope
 		is.appName = appNameAfterFetch
+		is.files = make(map[string]int64)
 		// Fetch the binary and record its size in the stimulus.
-		data, _, err := binlib.Download(call.Context(), binaryName)
+		data, mediaInfo, err := binlib.Download(call.Context(), binaryName)
 		if err != nil {
 			return "", err
 		}
-		is.binarySize = int64(len(data))
+		is.files["binary"] = int64(len(data))
+		if mediaInfo.Type != "application/octet-stream" {
+			return "", fmt.Errorf("unexpected media type: %v", mediaInfo)
+		}
+		// Iterate over the packages, download them, compute the size of
+		// the file(s) that make up each package, and record that in the
+		// stimulus.
+		for pkgLocalName, pkgVON := range envelope.Packages {
+			dir, err := ioutil.TempDir("", "package")
+			if err != nil {
+				return "", fmt.Errorf("failed to create temp package dir: %v", err)
+			}
+			defer os.RemoveAll(dir)
+			tmpFile := filepath.Join(dir, pkgLocalName)
+			if err := binlib.DownloadToFile(call.Context(), pkgVON, tmpFile); err != nil {
+				return "", fmt.Errorf("DownloadToFile failed: %v", err)
+			}
+			dst := filepath.Join(dir, "install")
+			if err := packages.Install(tmpFile, dst); err != nil {
+				return "", fmt.Errorf("packages.Install failed: %v", err)
+			}
+			is.files[naming.Join("packages", pkgLocalName)] = packageSize(dst)
+		}
+		envelope.Packages = nil
+		is.envelope = envelope
 	}
 	r := mni.tape.Record(is).(InstallResponse)
 	return r.appId, r.err
diff --git a/tools/mgmt/device/impl/impl_test.go b/tools/mgmt/device/impl/impl_test.go
index 09e0a83..3a14a76 100644
--- a/tools/mgmt/device/impl/impl_test.go
+++ b/tools/mgmt/device/impl/impl_test.go
@@ -215,14 +215,14 @@
 			nil,
 			false,
 			InstallResponse{appId, nil},
-			InstallStimulus{"Install", appNameNoFetch, nil, application.Envelope{}, 0},
+			InstallStimulus{"Install", appNameNoFetch, nil, application.Envelope{}, nil},
 		},
 		{
 			[]string{deviceName, appNameNoFetch},
 			cfg,
 			false,
 			InstallResponse{appId, nil},
-			InstallStimulus{"Install", appNameNoFetch, cfg, application.Envelope{}, 0},
+			InstallStimulus{"Install", appNameNoFetch, cfg, application.Envelope{}, nil},
 		},
 	} {
 		tape.SetResponses([]interface{}{c.tapeResponse})
diff --git a/tools/mgmt/device/impl/local_install.go b/tools/mgmt/device/impl/local_install.go
index 1598559..b6a926d 100644
--- a/tools/mgmt/device/impl/local_install.go
+++ b/tools/mgmt/device/impl/local_install.go
@@ -7,6 +7,7 @@
 	"io"
 	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strings"
 
 	"v.io/core/veyron2"
@@ -20,6 +21,8 @@
 	"v.io/core/veyron2/services/mgmt/repository"
 	"v.io/core/veyron2/services/security/access"
 	"v.io/core/veyron2/uniqueid"
+
+	"v.io/core/veyron/services/mgmt/lib/packages"
 	"v.io/lib/cmdline"
 )
 
@@ -28,15 +31,16 @@
 	Name:     "install-local",
 	Short:    "Install the given application from the local system.",
 	Long:     "Install the given application, specified using a local path.",
-	ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...]",
+	ArgsName: "<device> <title> [ENV=VAL ...] binary [--flag=val ...] [PACKAGES path ...]",
 	ArgsLong: `
 <device> is the veyron object name of the device manager's app service.
 
 <title> is the app title.
 
 This is followed by an arbitrary number of environment variable settings, the
-local path for the binary to install, and arbitrary flag settings.`,
-}
+local path for the binary to install, and arbitrary flag settings and args.
+Optionally, this can be followed by 'PACKAGES' and a list of local files and
+directories to be installed as packages for the app`}
 
 func init() {
 	cmdInstallLocal.Flags.Var(&configOverride, "config", "JSON-encoded device.Config object, of the form: '{\"flag1\":\"value1\",\"flag2\":\"value2\"}'")
@@ -152,7 +156,7 @@
 	}
 	h.Write(bytes)
 	part := binary.PartInfo{Checksum: hex.EncodeToString(h.Sum(nil)), Size: int64(len(bytes))}
-	return []binary.PartInfo{part}, repository.MediaInfo{Type: "application/octet-stream"}, nil
+	return []binary.PartInfo{part}, packages.MediaInfoForFileName(fileName), nil
 }
 
 func (binaryInvoker) Upload(repository.BinaryUploadContext, int32) error {
@@ -213,7 +217,17 @@
 		return cmd.UsageErrorf("install-local: missing binary")
 	}
 	binary := args[0]
-	envelope.Args = args[1:]
+	args = args[1:]
+	firstNonArg, firstPackage := len(args), len(args)
+	for i, arg := range args {
+		if arg == "PACKAGES" {
+			firstNonArg = i
+			firstPackage = i + 1
+			break
+		}
+	}
+	envelope.Args = args[:firstNonArg]
+	pkgs := args[firstPackage:]
 	if _, err := os.Stat(binary); err != nil {
 		return fmt.Errorf("binary %v not found: %v", binary, err)
 	}
@@ -225,6 +239,43 @@
 	defer cancel()
 	envelope.Binary = naming.Join(name, "binary")
 
+	// For each package dir/file specified in the arguments list, set up an
+	// object in the binary service to serve that package, and add the
+	// object name to the envelope's Packages map.
+	var tmpZipDir string
+	for _, p := range pkgs {
+		if envelope.Packages == nil {
+			envelope.Packages = make(map[string]string)
+		}
+		info, err := os.Stat(p)
+		if os.IsNotExist(err) {
+			return fmt.Errorf("%v not found: %v", p, err)
+		} else if err != nil {
+			return fmt.Errorf("Stat(%v) failed: %v", p, err)
+		}
+		pkgName := naming.Join("packages", info.Name())
+		if _, ok := objects[pkgName]; ok {
+			return fmt.Errorf("can't have more than one package with name %v", info.Name())
+		}
+		fileName := p
+		// Directory packages first get zip'ped.
+		if info.IsDir() {
+			if tmpZipDir == "" {
+				tmpZipDir, err = ioutil.TempDir("", "packages")
+				if err != nil {
+					return fmt.Errorf("failed to create a temp dir for zip packages: %v", err)
+				}
+				defer os.RemoveAll(tmpZipDir)
+			}
+			fileName = filepath.Join(tmpZipDir, info.Name()+".zip")
+			if err := packages.CreateZip(fileName, p); err != nil {
+				return err
+			}
+		}
+		objects[pkgName] = repository.BinaryServer(binaryInvoker(fileName))
+		envelope.Packages[info.Name()] = naming.Join(name, pkgName)
+	}
+
 	objects["application"] = repository.ApplicationServer(envelopeInvoker(envelope))
 	appName := naming.Join(name, "application")
 	appID, err := device.ApplicationClient(deviceName).Install(gctx, appName, device.Config(configOverride))
diff --git a/tools/mgmt/device/impl/local_install_test.go b/tools/mgmt/device/impl/local_install_test.go
index 1029ec1..4c8b5b5 100644
--- a/tools/mgmt/device/impl/local_install_test.go
+++ b/tools/mgmt/device/impl/local_install_test.go
@@ -4,7 +4,9 @@
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"reflect"
 	"strings"
 	"testing"
@@ -17,6 +19,12 @@
 	"v.io/core/veyron/tools/mgmt/device/impl"
 )
 
+func createFile(t *testing.T, path string, contents string) {
+	if err := ioutil.WriteFile(path, []byte(contents), 0700); err != nil {
+		t.Fatalf("Failed to create %v: %v", path, err)
+	}
+}
+
 func TestInstallLocalCommand(t *testing.T) {
 	shutdown := initTest()
 	defer shutdown()
@@ -32,7 +40,13 @@
 	var stdout, stderr bytes.Buffer
 	cmd.Init(nil, &stdout, &stderr)
 	deviceName := naming.JoinAddressName(endpoint.String(), "")
-	appTitle := "Appo di tutti Appi"
+	const appTitle = "Appo di tutti Appi"
+	binary := os.Args[0]
+	fi, err := os.Stat(binary)
+	if err != nil {
+		t.Fatalf("Failed to stat %v: %v", binary, err)
+	}
+	binarySize := fi.Size()
 	for i, c := range []struct {
 		args         []string
 		stderrSubstr string
@@ -49,6 +63,9 @@
 		{
 			[]string{deviceName, appTitle, "foo"}, "binary foo not found",
 		},
+		{
+			[]string{deviceName, appTitle, binary, "PACKAGES", "foo"}, "foo not found",
+		},
 	} {
 		c.args = append([]string{"install-local"}, c.args...)
 		if err := cmd.Execute(c.args); err == nil {
@@ -66,16 +83,27 @@
 		stdout.Reset()
 		stderr.Reset()
 	}
-	appId := "myBestAppID"
-	binary := os.Args[0]
-	fi, err := os.Stat(binary)
-	if err != nil {
-		t.Fatalf("Failed to stat %v: %v", binary, err)
-	}
 	emptySig := security.Signature{Purpose: []uint8{}, Hash: "", R: []uint8{}, S: []uint8{}}
 	emptyBlessings := security.WireBlessings{}
-	binarySize := fi.Size()
 	cfg := device.Config{"someflag": "somevalue"}
+
+	testPackagesDir, err := ioutil.TempDir("", "testdir")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(testPackagesDir)
+	pkgFile1 := filepath.Join(testPackagesDir, "file1.txt")
+	createFile(t, pkgFile1, "1234567")
+	pkgFile2 := filepath.Join(testPackagesDir, "file2")
+	createFile(t, pkgFile2, string([]byte{0x01, 0x02, 0x03, 0x04}))
+	pkgDir := filepath.Join(testPackagesDir, "dir")
+	if err := os.Mkdir(pkgDir, 0700); err != nil {
+		t.Fatalf("Failed to create dir: %v", err)
+	}
+	createFile(t, filepath.Join(pkgDir, "f1"), "123")
+	createFile(t, filepath.Join(pkgDir, "f2"), "456")
+	createFile(t, filepath.Join(pkgDir, "f3"), "7890")
+
 	for i, c := range []struct {
 		args         []string
 		config       device.Config
@@ -84,19 +112,68 @@
 		{
 			[]string{deviceName, appTitle, binary},
 			nil,
-			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Signature: emptySig, Publisher: emptyBlessings}, binarySize},
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				nil,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+				},
+				map[string]int64{"binary": binarySize}},
 		},
 		{
 			[]string{deviceName, appTitle, binary},
 			cfg,
-			InstallStimulus{"Install", appNameAfterFetch, cfg, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Signature: emptySig, Publisher: emptyBlessings}, binarySize},
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				cfg,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+				},
+				map[string]int64{"binary": binarySize}},
 		},
 		{
 			[]string{deviceName, appTitle, "ENV1=V1", "ENV2=V2", binary, "FLAG1=V1", "FLAG2=V2"},
 			nil,
-			InstallStimulus{"Install", appNameAfterFetch, nil, application.Envelope{Title: appTitle, Binary: binaryNameAfterFetch, Signature: emptySig, Publisher: emptyBlessings, Env: []string{"ENV1=V1", "ENV2=V2"}, Args: []string{"FLAG1=V1", "FLAG2=V2"}}, binarySize},
+			InstallStimulus{
+				"Install",
+				appNameAfterFetch,
+				nil,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+					Env:       []string{"ENV1=V1", "ENV2=V2"},
+					Args:      []string{"FLAG1=V1", "FLAG2=V2"},
+				},
+				map[string]int64{"binary": binarySize}},
+		},
+		{
+			[]string{deviceName, appTitle, "ENV=V", binary, "FLAG=V", "PACKAGES", pkgFile1, pkgFile2, pkgDir},
+			nil,
+			InstallStimulus{"Install",
+				appNameAfterFetch,
+				nil,
+				application.Envelope{
+					Title:     appTitle,
+					Binary:    binaryNameAfterFetch,
+					Signature: emptySig,
+					Publisher: emptyBlessings,
+					Env:       []string{"ENV=V"},
+					Args:      []string{"FLAG=V"},
+				},
+				map[string]int64{"binary": binarySize, "packages/file1.txt": 7, "packages/file2": 4, "packages/dir": 10}},
 		},
 	} {
+		const appId = "myBestAppID"
 		tape.SetResponses([]interface{}{InstallResponse{appId, nil}})
 		if c.config != nil {
 			jsonConfig, err := json.Marshal(c.config)