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 }