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