launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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/errgo/errgo"
    19  
    20  	coretools "launchpad.net/juju-core/tools"
    21  	"launchpad.net/juju-core/version"
    22  )
    23  
    24  const toolsFile = "downloaded-tools.txt"
    25  
    26  // SharedToolsDir returns the directory that is used to
    27  // store binaries for the given version of the juju tools
    28  // within the dataDir directory.
    29  func SharedToolsDir(dataDir string, vers version.Binary) string {
    30  	return path.Join(dataDir, "tools", vers.String())
    31  }
    32  
    33  // ToolsDir returns the directory that is used/ to store binaries for
    34  // the tools used by the given agent within the given dataDir directory.
    35  // Conventionally it is a symbolic link to the actual tools directory.
    36  func ToolsDir(dataDir, agentName string) string {
    37  	return path.Join(dataDir, "tools", agentName)
    38  }
    39  
    40  // UnpackTools reads a set of juju tools in gzipped tar-archive
    41  // format and unpacks them into the appropriate tools directory
    42  // within dataDir. If a valid tools directory already exists,
    43  // UnpackTools returns without error.
    44  func UnpackTools(dataDir string, tools *coretools.Tools, r io.Reader) (err error) {
    45  	// Unpack the gzip file and compute the checksum.
    46  	sha256hash := sha256.New()
    47  	zr, err := gzip.NewReader(io.TeeReader(r, sha256hash))
    48  	if err != nil {
    49  		return err
    50  	}
    51  	defer zr.Close()
    52  	f, err := ioutil.TempFile(os.TempDir(), "tools-tar")
    53  	if err != nil {
    54  		return err
    55  	}
    56  	_, err = io.Copy(f, zr)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	defer os.Remove(f.Name())
    61  	// TODO(wallyworld) - 2013-09-24 bug=1229512
    62  	// When we can ensure all tools records have valid checksums recorded,
    63  	// we can remove this test short circuit.
    64  	gzipSHA256 := fmt.Sprintf("%x", sha256hash.Sum(nil))
    65  	if tools.SHA256 != "" && tools.SHA256 != gzipSHA256 {
    66  		return fmt.Errorf("tarball sha256 mismatch, expected %s, got %s", tools.SHA256, gzipSHA256)
    67  	}
    68  
    69  	// Make a temporary directory in the tools directory,
    70  	// first ensuring that the tools directory exists.
    71  	toolsDir := path.Join(dataDir, "tools")
    72  	err = os.MkdirAll(toolsDir, 0755)
    73  	if err != nil {
    74  		return err
    75  	}
    76  	dir, err := ioutil.TempDir(toolsDir, "unpacking-")
    77  	if err != nil {
    78  		return err
    79  	}
    80  	defer removeAll(dir)
    81  
    82  	// Checksum matches, now reset the file and untar it.
    83  	_, err = f.Seek(0, 0)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	tr := tar.NewReader(f)
    88  	for {
    89  		hdr, err := tr.Next()
    90  		if err == io.EOF {
    91  			break
    92  		}
    93  		if err != nil {
    94  			return err
    95  		}
    96  		if strings.ContainsAny(hdr.Name, "/\\") {
    97  			return fmt.Errorf("bad name %q in tools archive", hdr.Name)
    98  		}
    99  		if hdr.Typeflag != tar.TypeReg {
   100  			return fmt.Errorf("bad file type %c in file %q in tools archive", hdr.Typeflag, hdr.Name)
   101  		}
   102  		name := path.Join(dir, hdr.Name)
   103  		if err := writeFile(name, os.FileMode(hdr.Mode&0777), tr); err != nil {
   104  			return errgo.Annotatef(err, "tar extract %q failed", name)
   105  		}
   106  	}
   107  	toolsMetadataData, err := json.Marshal(tools)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	err = ioutil.WriteFile(path.Join(dir, toolsFile), []byte(toolsMetadataData), 0644)
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	err = os.Rename(dir, SharedToolsDir(dataDir, tools.Version))
   117  	// If we've failed to rename the directory, it may be because
   118  	// the directory already exists - if ReadTools succeeds, we
   119  	// assume all's ok.
   120  	if err != nil {
   121  		if _, err := ReadTools(dataDir, tools.Version); err == nil {
   122  			return nil
   123  		}
   124  	}
   125  	return err
   126  }
   127  
   128  func removeAll(dir string) {
   129  	err := os.RemoveAll(dir)
   130  	if err == nil || os.IsNotExist(err) {
   131  		return
   132  	}
   133  	logger.Warningf("cannot remove %q: %v", dir, err)
   134  }
   135  
   136  func writeFile(name string, mode os.FileMode, r io.Reader) error {
   137  	f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	defer f.Close()
   142  	_, err = io.Copy(f, r)
   143  	return err
   144  }
   145  
   146  // ReadTools checks that the tools information for the given version exists
   147  // in the dataDir directory, and returns a Tools instance.
   148  // The tools information is json encoded in a text file, "downloaded-tools.txt".
   149  func ReadTools(dataDir string, vers version.Binary) (*coretools.Tools, error) {
   150  	dir := SharedToolsDir(dataDir, vers)
   151  	toolsData, err := ioutil.ReadFile(path.Join(dir, toolsFile))
   152  	if err != nil {
   153  		return nil, fmt.Errorf("cannot read tools metadata in tools directory: %v", err)
   154  	}
   155  	var tools coretools.Tools
   156  	if err := json.Unmarshal(toolsData, &tools); err != nil {
   157  		return nil, fmt.Errorf("invalid tools metadata in tools directory %q: %v", dir, err)
   158  	}
   159  	return &tools, nil
   160  }
   161  
   162  // ChangeAgentTools atomically replaces the agent-specific symlink
   163  // under dataDir so it points to the previously unpacked
   164  // version vers. It returns the new tools read.
   165  func ChangeAgentTools(dataDir string, agentName string, vers version.Binary) (*coretools.Tools, error) {
   166  	tools, err := ReadTools(dataDir, vers)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  	tmpName := ToolsDir(dataDir, "tmplink-"+agentName)
   171  	err = os.Symlink(tools.Version.String(), tmpName)
   172  	if err != nil {
   173  		return nil, fmt.Errorf("cannot create tools symlink: %v", err)
   174  	}
   175  	err = os.Rename(tmpName, ToolsDir(dataDir, agentName))
   176  	if err != nil {
   177  		return nil, fmt.Errorf("cannot update tools symlink: %v", err)
   178  	}
   179  	return tools, nil
   180  }