veyron/services/mgmt: Implement optional packages
This change implements optional packages in the mgmt services. The
binary repository now saves the media type and encoding of the objects,
e.g type=application/x-tar,encoding=gzip, or type=application/zip.
The node manager uses this information to download and install the
packages requested in the application envelopes.
TODO: Test the node manager implementation. This will require either using the
real binaryd, or significantly improving the mock binaryd.
Change-Id: Idc7d0ee7663ccd1816a271289de8f61403d8dcce
diff --git a/services/mgmt/lib/packages/packages.go b/services/mgmt/lib/packages/packages.go
new file mode 100644
index 0000000..57f9dde
--- /dev/null
+++ b/services/mgmt/lib/packages/packages.go
@@ -0,0 +1,192 @@
+// Package packages provides functionality to install ZIP and TAR packages.
+package packages
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "compress/bzip2"
+ "compress/gzip"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "veyron.io/veyron/veyron2/services/mgmt/repository"
+)
+
+const defaultType = "application/octet-stream"
+
+var typemap = map[string]repository.MediaInfo{
+ ".zip": repository.MediaInfo{Type: "application/zip"},
+ ".tar": repository.MediaInfo{Type: "application/x-tar"},
+ ".tgz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
+ ".tar.gz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
+ ".tbz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+ ".tb2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+ ".tbz": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+ ".tar.bz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
+}
+
+// MediaInfoForFileName returns the MediaInfo based on the file's extension.
+func MediaInfoForFileName(fileName string) repository.MediaInfo {
+ fileName = strings.ToLower(fileName)
+ for k, v := range typemap {
+ if strings.HasSuffix(fileName, k) {
+ return v
+ }
+ }
+ return repository.MediaInfo{Type: defaultType}
+}
+
+// Install installs a package in the given directory. If the package is a TAR or
+// ZIP archive, its content is extracted in the destination directory.
+// Otherwise, the package file itself is copied to the destination directory.
+func Install(pkgFile, dir string) error {
+ mediaInfo, err := LoadMediaInfo(pkgFile)
+ if err != nil {
+ return err
+ }
+ switch mediaInfo.Type {
+ case "application/x-tar":
+ return extractTar(pkgFile, mediaInfo, dir)
+ case "application/zip":
+ return extractZip(pkgFile, dir)
+ default:
+ return fmt.Errorf("unsupported media type: %v", mediaInfo.Type)
+ }
+}
+
+// LoadMediaInfo returns the MediaInfo for the given package file.
+func LoadMediaInfo(pkgFile string) (repository.MediaInfo, error) {
+ jInfo, err := ioutil.ReadFile(pkgFile + ".__info")
+ if err != nil {
+ return repository.MediaInfo{}, err
+ }
+ var info repository.MediaInfo
+ if err := json.Unmarshal(jInfo, &info); err != nil {
+ return repository.MediaInfo{}, err
+ }
+ return info, nil
+}
+
+// SaveMediaInfo saves the media info for a package.
+func SaveMediaInfo(pkgFile string, mediaInfo repository.MediaInfo) error {
+ jInfo, err := json.Marshal(mediaInfo)
+ if err != nil {
+ return err
+ }
+ infoFile := pkgFile + ".__info"
+ if err := ioutil.WriteFile(infoFile, jInfo, os.FileMode(0600)); err != nil {
+ return err
+ }
+ return nil
+}
+
+func extractZip(zipFile, installDir string) error {
+ zr, err := zip.OpenReader(zipFile)
+ if err != nil {
+ return err
+ }
+ for _, file := range zr.File {
+ fi := file.FileInfo()
+ name := filepath.Join(installDir, file.Name)
+ if !strings.HasPrefix(name, installDir) {
+ return fmt.Errorf("failed to extract file %q outside of install directory", file.Name)
+ }
+ if fi.IsDir() {
+ if err := os.MkdirAll(name, os.FileMode(fi.Mode()&0700)); err != nil && !os.IsExist(err) {
+ return err
+ }
+ continue
+ }
+ in, err := file.Open()
+ if err != nil {
+ return err
+ }
+ parentName := filepath.Dir(name)
+ if err := os.MkdirAll(parentName, os.FileMode(0700)); err != nil {
+ return err
+ }
+ out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(fi.Mode()&0700))
+ if err != nil {
+ in.Close()
+ return err
+ }
+ nbytes, err := io.Copy(out, in)
+ in.Close()
+ out.Close()
+ if err != nil {
+ return err
+ }
+ if nbytes != fi.Size() {
+ return fmt.Errorf("file size doesn't match for %q: %d != %d", fi.Name(), nbytes, fi.Size())
+ }
+ }
+ return nil
+}
+
+func extractTar(pkgFile string, mediaInfo repository.MediaInfo, installDir string) error {
+ f, err := os.Open(pkgFile)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ var reader io.Reader
+ switch enc := mediaInfo.Encoding; enc {
+ case "":
+ reader = f
+ case "gzip":
+ var err error
+ if reader, err = gzip.NewReader(f); err != nil {
+ return err
+ }
+ case "bzip2":
+ reader = bzip2.NewReader(f)
+ default:
+ return fmt.Errorf("unsupported encoding: %q", enc)
+ }
+
+ tr := tar.NewReader(reader)
+ for {
+ hdr, err := tr.Next()
+ if err == io.EOF {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ name := filepath.Join(installDir, hdr.Name)
+ if !strings.HasPrefix(name, installDir) {
+ return fmt.Errorf("failed to extract file %q outside of install directory", hdr.Name)
+ }
+ // Regular file
+ if hdr.Typeflag == tar.TypeReg {
+ out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(hdr.Mode&0700))
+ if err != nil {
+ return err
+ }
+ nbytes, err := io.Copy(out, tr)
+ out.Close()
+ if err != nil {
+ return err
+ }
+ if nbytes != hdr.Size {
+ return fmt.Errorf("file size doesn't match for %q: %d != %d", hdr.Name, nbytes, hdr.Size)
+ }
+ continue
+ }
+ // Directory
+ if hdr.Typeflag == tar.TypeDir {
+ if err := os.Mkdir(name, os.FileMode(hdr.Mode&0700)); err != nil && !os.IsExist(err) {
+ return err
+ }
+ continue
+ }
+ // Skip unsupported types
+ // TODO(rthellend): Consider adding support for Symlink.
+ }
+}