github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 gzipSHA256 := fmt.Sprintf("%x", sha256hash.Sum(nil)) 77 if tools.SHA256 != gzipSHA256 { 78 return fmt.Errorf("tarball sha256 mismatch, expected %s, got %s", tools.SHA256, gzipSHA256) 79 } 80 81 // Make a temporary directory in the tools directory, 82 // first ensuring that the tools directory exists. 83 toolsDir := path.Join(dataDir, "tools") 84 err = os.MkdirAll(toolsDir, dirPerm) 85 if err != nil { 86 return err 87 } 88 dir, err := ioutil.TempDir(toolsDir, "unpacking-") 89 if err != nil { 90 return err 91 } 92 defer removeAll(dir) 93 94 // Checksum matches, now reset the file and untar it. 95 _, err = f.Seek(0, 0) 96 if err != nil { 97 return err 98 } 99 tr := tar.NewReader(f) 100 for { 101 hdr, err := tr.Next() 102 if err == io.EOF { 103 break 104 } 105 if err != nil { 106 return err 107 } 108 if strings.ContainsAny(hdr.Name, "/\\") { 109 return fmt.Errorf("bad name %q in tools archive", hdr.Name) 110 } 111 if hdr.Typeflag != tar.TypeReg { 112 return fmt.Errorf("bad file type %c in file %q in tools archive", hdr.Typeflag, hdr.Name) 113 } 114 name := path.Join(dir, hdr.Name) 115 if err := writeFile(name, os.FileMode(hdr.Mode&0777), tr); err != nil { 116 return errors.Annotatef(err, "tar extract %q failed", name) 117 } 118 } 119 toolsMetadataData, err := json.Marshal(tools) 120 if err != nil { 121 return err 122 } 123 err = ioutil.WriteFile(path.Join(dir, toolsFile), []byte(toolsMetadataData), 0644) 124 if err != nil { 125 return err 126 } 127 128 // The tempdir is created with 0700, so we need to make it more 129 // accessable for juju-run. 130 err = os.Chmod(dir, dirPerm) 131 if err != nil { 132 return err 133 } 134 135 err = os.Rename(dir, SharedToolsDir(dataDir, tools.Version)) 136 // If we've failed to rename the directory, it may be because 137 // the directory already exists - if ReadTools succeeds, we 138 // assume all's ok. 139 if err != nil { 140 if _, err := ReadTools(dataDir, tools.Version); err == nil { 141 return nil 142 } 143 } 144 return err 145 } 146 147 func removeAll(dir string) { 148 err := os.RemoveAll(dir) 149 if err == nil || os.IsNotExist(err) { 150 return 151 } 152 logger.Errorf("cannot remove %q: %v", dir, err) 153 } 154 155 func writeFile(name string, mode os.FileMode, r io.Reader) error { 156 f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) 157 if err != nil { 158 return err 159 } 160 defer f.Close() 161 _, err = io.Copy(f, r) 162 return err 163 } 164 165 // ReadTools checks that the tools information for the given version exists 166 // in the dataDir directory, and returns a Tools instance. 167 // The tools information is json encoded in a text file, "downloaded-tools.txt". 168 func ReadTools(dataDir string, vers version.Binary) (*coretools.Tools, error) { 169 dir := SharedToolsDir(dataDir, vers) 170 toolsData, err := ioutil.ReadFile(path.Join(dir, toolsFile)) 171 if err != nil { 172 return nil, fmt.Errorf("cannot read tools metadata in tools directory: %v", err) 173 } 174 var tools coretools.Tools 175 if err := json.Unmarshal(toolsData, &tools); err != nil { 176 return nil, fmt.Errorf("invalid tools metadata in tools directory %q: %v", dir, err) 177 } 178 return &tools, nil 179 } 180 181 // ReadGUIArchive reads the GUI information from the dataDir directory. 182 // The GUI information is JSON encoded in a text file, "downloaded-gui.txt". 183 func ReadGUIArchive(dataDir string) (*coretools.GUIArchive, error) { 184 dir := SharedGUIDir(dataDir) 185 toolsData, err := ioutil.ReadFile(path.Join(dir, guiArchiveFile)) 186 if err != nil { 187 if os.IsNotExist(err) { 188 return nil, errors.NotFoundf("GUI metadata") 189 } 190 return nil, fmt.Errorf("cannot read GUI metadata in tools directory: %v", err) 191 } 192 var gui coretools.GUIArchive 193 if err := json.Unmarshal(toolsData, &gui); err != nil { 194 return nil, fmt.Errorf("invalid GUI metadata in tools directory %q: %v", dir, err) 195 } 196 return &gui, nil 197 } 198 199 // ChangeAgentTools atomically replaces the agent-specific symlink 200 // under dataDir so it points to the previously unpacked 201 // version vers. It returns the new tools read. 202 func ChangeAgentTools(dataDir string, agentName string, vers version.Binary) (*coretools.Tools, error) { 203 tools, err := ReadTools(dataDir, vers) 204 if err != nil { 205 return nil, err 206 } 207 // build absolute path to toolsDir. Windows implementation of symlink 208 // will check for the existance of the source file and error if it does 209 // not exists. This is a limitation of junction points (symlinks) on NTFS 210 toolPath := ToolsDir(dataDir, tools.Version.String()) 211 toolsDir := ToolsDir(dataDir, agentName) 212 213 err = symlink.Replace(toolsDir, toolPath) 214 if err != nil { 215 return nil, fmt.Errorf("cannot replace tools directory: %s", err) 216 } 217 return tools, nil 218 }