github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/agent/tools/toolsdir.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package tools
     5  
     6  import (
     7  	"archive/tar"
     8  	"compress/gzip"
     9  	"crypto/sha256"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"path"
    16  	"strings"
    17  
    18  	"github.com/juju/errors"
    19  	"github.com/juju/utils/symlink"
    20  	"github.com/juju/version"
    21  
    22  	coretools "github.com/juju/juju/tools"
    23  )
    24  
    25  const (
    26  	dirPerm        = 0755
    27  	guiArchiveFile = "downloaded-gui.txt"
    28  	toolsFile      = "downloaded-tools.txt"
    29  )
    30  
    31  // SharedToolsDir returns the directory that is used to
    32  // store binaries for the given version of the juju tools
    33  // within the dataDir directory.
    34  func SharedToolsDir(dataDir string, vers version.Binary) string {
    35  	return path.Join(dataDir, "tools", vers.String())
    36  }
    37  
    38  // SharedGUIDir returns the directory that is used to store release archives
    39  // of the Juju GUI within the dataDir directory.
    40  func SharedGUIDir(dataDir string) string {
    41  	return path.Join(dataDir, "gui")
    42  }
    43  
    44  // ToolsDir returns the directory that is used/ to store binaries for
    45  // the tools used by the given agent within the given dataDir directory.
    46  // Conventionally it is a symbolic link to the actual tools directory.
    47  func ToolsDir(dataDir, agentName string) string {
    48  	//TODO(perrito666) ToolsDir and any other *Dir needs to take the
    49  	// agent series to use the right path, in this case, if filepath
    50  	// is used it ends up creating a bogus toolsdir when the client
    51  	// is in windows.
    52  	return path.Join(dataDir, "tools", agentName)
    53  }
    54  
    55  // UnpackTools reads a set of juju tools in gzipped tar-archive
    56  // format and unpacks them into the appropriate tools directory
    57  // within dataDir. If a valid tools directory already exists,
    58  // UnpackTools returns without error.
    59  func UnpackTools(dataDir string, tools *coretools.Tools, r io.Reader) (err error) {
    60  	// Unpack the gzip file and compute the checksum.
    61  	sha256hash := sha256.New()
    62  	zr, err := gzip.NewReader(io.TeeReader(r, sha256hash))
    63  	if err != nil {
    64  		return err
    65  	}
    66  	defer zr.Close()
    67  	f, err := ioutil.TempFile(os.TempDir(), "tools-tar")
    68  	if err != nil {
    69  		return err
    70  	}
    71  	_, err = io.Copy(f, zr)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	defer os.Remove(f.Name())
    76  	// TODO(wallyworld) - 2013-09-24 bug=1229512
    77  	// When we can ensure all tools records have valid checksums recorded,
    78  	// we can remove this test short circuit.
    79  	gzipSHA256 := fmt.Sprintf("%x", sha256hash.Sum(nil))
    80  	if tools.SHA256 != "" && tools.SHA256 != gzipSHA256 {
    81  		return fmt.Errorf("tarball sha256 mismatch, expected %s, got %s", tools.SHA256, gzipSHA256)
    82  	}
    83  
    84  	// Make a temporary directory in the tools directory,
    85  	// first ensuring that the tools directory exists.
    86  	toolsDir := path.Join(dataDir, "tools")
    87  	err = os.MkdirAll(toolsDir, dirPerm)
    88  	if err != nil {
    89  		return err
    90  	}
    91  	dir, err := ioutil.TempDir(toolsDir, "unpacking-")
    92  	if err != nil {
    93  		return err
    94  	}
    95  	defer removeAll(dir)
    96  
    97  	// Checksum matches, now reset the file and untar it.
    98  	_, err = f.Seek(0, 0)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	tr := tar.NewReader(f)
   103  	for {
   104  		hdr, err := tr.Next()
   105  		if err == io.EOF {
   106  			break
   107  		}
   108  		if err != nil {
   109  			return err
   110  		}
   111  		if strings.ContainsAny(hdr.Name, "/\\") {
   112  			return fmt.Errorf("bad name %q in tools archive", hdr.Name)
   113  		}
   114  		if hdr.Typeflag != tar.TypeReg {
   115  			return fmt.Errorf("bad file type %c in file %q in tools archive", hdr.Typeflag, hdr.Name)
   116  		}
   117  		name := path.Join(dir, hdr.Name)
   118  		if err := writeFile(name, os.FileMode(hdr.Mode&0777), tr); err != nil {
   119  			return errors.Annotatef(err, "tar extract %q failed", name)
   120  		}
   121  	}
   122  	toolsMetadataData, err := json.Marshal(tools)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	err = ioutil.WriteFile(path.Join(dir, toolsFile), []byte(toolsMetadataData), 0644)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	// The tempdir is created with 0700, so we need to make it more
   132  	// accessable for juju-run.
   133  	err = os.Chmod(dir, dirPerm)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	err = os.Rename(dir, SharedToolsDir(dataDir, tools.Version))
   139  	// If we've failed to rename the directory, it may be because
   140  	// the directory already exists - if ReadTools succeeds, we
   141  	// assume all's ok.
   142  	if err != nil {
   143  		if _, err := ReadTools(dataDir, tools.Version); err == nil {
   144  			return nil
   145  		}
   146  	}
   147  	return err
   148  }
   149  
   150  func removeAll(dir string) {
   151  	err := os.RemoveAll(dir)
   152  	if err == nil || os.IsNotExist(err) {
   153  		return
   154  	}
   155  	logger.Warningf("cannot remove %q: %v", dir, err)
   156  }
   157  
   158  func writeFile(name string, mode os.FileMode, r io.Reader) error {
   159  	f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	defer f.Close()
   164  	_, err = io.Copy(f, r)
   165  	return err
   166  }
   167  
   168  // ReadTools checks that the tools information for the given version exists
   169  // in the dataDir directory, and returns a Tools instance.
   170  // The tools information is json encoded in a text file, "downloaded-tools.txt".
   171  func ReadTools(dataDir string, vers version.Binary) (*coretools.Tools, error) {
   172  	dir := SharedToolsDir(dataDir, vers)
   173  	toolsData, err := ioutil.ReadFile(path.Join(dir, toolsFile))
   174  	if err != nil {
   175  		return nil, fmt.Errorf("cannot read tools metadata in tools directory: %v", err)
   176  	}
   177  	var tools coretools.Tools
   178  	if err := json.Unmarshal(toolsData, &tools); err != nil {
   179  		return nil, fmt.Errorf("invalid tools metadata in tools directory %q: %v", dir, err)
   180  	}
   181  	return &tools, nil
   182  }
   183  
   184  // ReadGUIArchive reads the GUI information from the dataDir directory.
   185  // The GUI information is JSON encoded in a text file, "downloaded-gui.txt".
   186  func ReadGUIArchive(dataDir string) (*coretools.GUIArchive, error) {
   187  	dir := SharedGUIDir(dataDir)
   188  	toolsData, err := ioutil.ReadFile(path.Join(dir, guiArchiveFile))
   189  	if err != nil {
   190  		if os.IsNotExist(err) {
   191  			return nil, errors.NotFoundf("GUI metadata")
   192  		}
   193  		return nil, fmt.Errorf("cannot read GUI metadata in tools directory: %v", err)
   194  	}
   195  	var gui coretools.GUIArchive
   196  	if err := json.Unmarshal(toolsData, &gui); err != nil {
   197  		return nil, fmt.Errorf("invalid GUI metadata in tools directory %q: %v", dir, err)
   198  	}
   199  	return &gui, nil
   200  }
   201  
   202  // ChangeAgentTools atomically replaces the agent-specific symlink
   203  // under dataDir so it points to the previously unpacked
   204  // version vers. It returns the new tools read.
   205  func ChangeAgentTools(dataDir string, agentName string, vers version.Binary) (*coretools.Tools, error) {
   206  	tools, err := ReadTools(dataDir, vers)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	// build absolute path to toolsDir. Windows implementation of symlink
   211  	// will check for the existance of the source file and error if it does
   212  	// not exists. This is a limitation of junction points (symlinks) on NTFS
   213  	toolPath := ToolsDir(dataDir, tools.Version.String())
   214  	toolsDir := ToolsDir(dataDir, agentName)
   215  
   216  	err = symlink.Replace(toolsDir, toolPath)
   217  	if err != nil {
   218  		return nil, fmt.Errorf("cannot replace tools directory: %s", err)
   219  	}
   220  	return tools, nil
   221  }