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)