github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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 20 coretools "github.com/juju/juju/tools" 21 "github.com/juju/juju/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 errors.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 // The tempdir is created with 0700, so we need to make it more 117 // accessable for juju-run. 118 err = os.Chmod(dir, 0755) 119 if err != nil { 120 return err 121 } 122 123 err = os.Rename(dir, SharedToolsDir(dataDir, tools.Version)) 124 // If we've failed to rename the directory, it may be because 125 // the directory already exists - if ReadTools succeeds, we 126 // assume all's ok. 127 if err != nil { 128 if _, err := ReadTools(dataDir, tools.Version); err == nil { 129 return nil 130 } 131 } 132 return err 133 } 134 135 func removeAll(dir string) { 136 err := os.RemoveAll(dir) 137 if err == nil || os.IsNotExist(err) { 138 return 139 } 140 logger.Warningf("cannot remove %q: %v", dir, err) 141 } 142 143 func writeFile(name string, mode os.FileMode, r io.Reader) error { 144 f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) 145 if err != nil { 146 return err 147 } 148 defer f.Close() 149 _, err = io.Copy(f, r) 150 return err 151 } 152 153 // ReadTools checks that the tools information for the given version exists 154 // in the dataDir directory, and returns a Tools instance. 155 // The tools information is json encoded in a text file, "downloaded-tools.txt". 156 func ReadTools(dataDir string, vers version.Binary) (*coretools.Tools, error) { 157 dir := SharedToolsDir(dataDir, vers) 158 toolsData, err := ioutil.ReadFile(path.Join(dir, toolsFile)) 159 if err != nil { 160 return nil, fmt.Errorf("cannot read tools metadata in tools directory: %v", err) 161 } 162 var tools coretools.Tools 163 if err := json.Unmarshal(toolsData, &tools); err != nil { 164 return nil, fmt.Errorf("invalid tools metadata in tools directory %q: %v", dir, err) 165 } 166 return &tools, nil 167 } 168 169 // ChangeAgentTools atomically replaces the agent-specific symlink 170 // under dataDir so it points to the previously unpacked 171 // version vers. It returns the new tools read. 172 func ChangeAgentTools(dataDir string, agentName string, vers version.Binary) (*coretools.Tools, error) { 173 tools, err := ReadTools(dataDir, vers) 174 if err != nil { 175 return nil, err 176 } 177 tmpName := ToolsDir(dataDir, "tmplink-"+agentName) 178 err = os.Symlink(tools.Version.String(), tmpName) 179 if err != nil { 180 return nil, fmt.Errorf("cannot create tools symlink: %v", err) 181 } 182 err = os.Rename(tmpName, ToolsDir(dataDir, agentName)) 183 if err != nil { 184 return nil, fmt.Errorf("cannot update tools symlink: %v", err) 185 } 186 return tools, nil 187 }